def test_no_index_for_foreignkey(self): """ MySQL on InnoDB already creates indexes automatically for foreign keys. (#14180). An index should be created if db_constraint=False (#26171). """ storage = connection.introspection.get_storage_engine( connection.cursor(), ArticleTranslation._meta.db_table ) if storage != "InnoDB": self.skip("This test only applies to the InnoDB storage engine") index_sql = [str(statement) for statement in connection.schema_editor()._model_indexes_sql(ArticleTranslation)] self.assertEqual(index_sql, [ 'CREATE INDEX `indexes_articletranslation_article_no_constraint_id_d6c0806b` ' 'ON `indexes_articletranslation` (`article_no_constraint_id`)' ]) # The index also shouldn't be created if the ForeignKey is added after # the model was created. field_created = False try: with connection.schema_editor() as editor: new_field = ForeignKey(Article, CASCADE) new_field.set_attributes_from_name('new_foreign_key') editor.add_field(ArticleTranslation, new_field) field_created = True self.assertEqual([str(statement) for statement in editor.deferred_sql], [ 'ALTER TABLE `indexes_articletranslation` ' 'ADD CONSTRAINT `indexes_articletrans_new_foreign_key_id_d27a9146_fk_indexes_a` ' 'FOREIGN KEY (`new_foreign_key_id`) REFERENCES `indexes_article` (`id`)' ]) finally: if field_created: with connection.schema_editor() as editor: editor.remove_field(ArticleTranslation, new_field)
def test_spgist_parameters(self): index_name = 'integer_array_spgist_fillfactor' index = SpGistIndex(fields=['field'], name=index_name, fillfactor=80) with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) constraints = self.get_constraints(CharFieldModel._meta.db_table) self.assertEqual(constraints[index_name]['type'], SpGistIndex.suffix) self.assertEqual(constraints[index_name]['options'], ['fillfactor=80']) with connection.schema_editor() as editor: editor.remove_index(CharFieldModel, index) self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
def test_detect_soft_applied_add_field_manytomanyfield(self): """ executor.detect_soft_applied() detects ManyToManyField tables from an AddField operation. This checks the case of AddField in a migration with other operations (0001) and the case of AddField in its own migration (0002). """ tables = [ # from 0001 "migrations_project", "migrations_task", "migrations_project_tasks", # from 0002 "migrations_task_projects", ] executor = MigrationExecutor(connection) # Create the tables for 0001 but make it look like the migration hasn't # been applied. executor.migrate([("migrations", "0001_initial")]) executor.migrate([("migrations", None)], fake=True) for table in tables[:3]: self.assertTableExists(table) # Table detection sees 0001 is applied but not 0002. migration = executor.loader.get_migration("migrations", "0001_initial") self.assertIs(executor.detect_soft_applied(None, migration)[0], True) migration = executor.loader.get_migration("migrations", "0002_initial") self.assertIs(executor.detect_soft_applied(None, migration)[0], False) # Create the tables for both migrations but make it look like neither # has been applied. executor.loader.build_graph() executor.migrate([("migrations", "0001_initial")], fake=True) executor.migrate([("migrations", "0002_initial")]) executor.loader.build_graph() executor.migrate([("migrations", None)], fake=True) # Table detection sees 0002 is applied. migration = executor.loader.get_migration("migrations", "0002_initial") self.assertIs(executor.detect_soft_applied(None, migration)[0], True) # Leave the tables for 0001 except the many-to-many table. That missing # table should cause detect_soft_applied() to return False. with connection.schema_editor() as editor: for table in tables[2:]: editor.execute(editor.sql_delete_table % {"table": table}) migration = executor.loader.get_migration("migrations", "0001_initial") self.assertIs(executor.detect_soft_applied(None, migration)[0], False) # Cleanup by removing the remaining tables. with connection.schema_editor() as editor: for table in tables[:2]: editor.execute(editor.sql_delete_table % {"table": table}) for table in tables: self.assertTableNotExists(table)
def test_brin_index(self): index_name = 'char_field_model_field_brin' index = BrinIndex(fields=['field'], name=index_name, pages_per_range=4) with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) constraints = self.get_constraints(CharFieldModel._meta.db_table) self.assertEqual(constraints[index_name]['type'], BrinIndex.suffix) self.assertEqual(constraints[index_name]['options'], ['pages_per_range=4']) with connection.schema_editor() as editor: editor.remove_index(CharFieldModel, index) self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
def test_gin_fastupdate(self): index_name = 'integer_array_gin_fastupdate' index = GinIndex(fields=['field'], name=index_name, fastupdate=False) with connection.schema_editor() as editor: editor.add_index(IntegerArrayModel, index) constraints = self.get_constraints(IntegerArrayModel._meta.db_table) self.assertEqual(constraints[index_name]['type'], 'gin') self.assertEqual(constraints[index_name]['options'], ['fastupdate=off']) with connection.schema_editor() as editor: editor.remove_index(IntegerArrayModel, index) self.assertNotIn( index_name, self.get_constraints(IntegerArrayModel._meta.db_table))
def test_brin_parameters(self): index_name = 'char_field_brin_params' index = BrinIndex(fields=['field'], name=index_name, autosummarize=True) with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) constraints = self.get_constraints(CharFieldModel._meta.db_table) self.assertEqual(constraints[index_name]['type'], BrinIndex.suffix) self.assertEqual(constraints[index_name]['options'], ['autosummarize=on']) with connection.schema_editor() as editor: editor.remove_index(CharFieldModel, index) self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
def test_alter_id_type_with_fk(self): try: executor = MigrationExecutor(connection) self.assertTableNotExists("author_app_author") self.assertTableNotExists("book_app_book") # Apply initial migrations executor.migrate([ ("author_app", "0001_initial"), ("book_app", "0001_initial"), ]) self.assertTableExists("author_app_author") self.assertTableExists("book_app_book") # Rebuild the graph to reflect the new DB state executor.loader.build_graph() # Apply PK type alteration executor.migrate([("author_app", "0002_alter_id")]) # Rebuild the graph to reflect the new DB state executor.loader.build_graph() finally: # We can't simply unapply the migrations here because there is no # implicit cast from VARCHAR to INT on the database level. with connection.schema_editor() as editor: editor.execute(editor.sql_delete_table % {"table": "book_app_book"}) editor.execute(editor.sql_delete_table % {"table": "author_app_author"}) self.assertTableNotExists("author_app_author") self.assertTableNotExists("book_app_book") executor.migrate([("author_app", None)], fake=True)
def test_gin_parameters(self): index_name = 'integer_array_gin_params' index = GinIndex(fields=['field'], name=index_name, fastupdate=True, gin_pending_list_limit=64) with connection.schema_editor() as editor: editor.add_index(IntegerArrayModel, index) constraints = self.get_constraints(IntegerArrayModel._meta.db_table) self.assertEqual(constraints[index_name]['type'], 'gin') self.assertEqual(constraints[index_name]['options'], ['gin_pending_list_limit=64', 'fastupdate=on']) with connection.schema_editor() as editor: editor.remove_index(IntegerArrayModel, index) self.assertNotIn( index_name, self.get_constraints(IntegerArrayModel._meta.db_table))
def test_gin_index(self): # Ensure the table is there and doesn't have an index. self.assertNotIn( 'field', self.get_constraints(IntegerArrayModel._meta.db_table)) # Add the index index_name = 'integer_array_model_field_gin' index = GinIndex(fields=['field'], name=index_name) with connection.schema_editor() as editor: editor.add_index(IntegerArrayModel, index) constraints = self.get_constraints(IntegerArrayModel._meta.db_table) # Check gin index was added self.assertEqual(constraints[index_name]['type'], GinIndex.suffix) # Drop the index with connection.schema_editor() as editor: editor.remove_index(IntegerArrayModel, index) self.assertNotIn( index_name, self.get_constraints(IntegerArrayModel._meta.db_table))
def test_spgist_index(self): # Ensure the table is there and doesn't have an index. self.assertNotIn('field', self.get_constraints(CharFieldModel._meta.db_table)) # Add the index. index_name = 'char_field_model_field_spgist' index = SpGistIndex(fields=['field'], name=index_name) with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) constraints = self.get_constraints(CharFieldModel._meta.db_table) # The index was added. self.assertEqual(constraints[index_name]['type'], SpGistIndex.suffix) # Drop the index. with connection.schema_editor() as editor: editor.remove_index(CharFieldModel, index) self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
def _test_procedure(self, procedure_sql, params, param_types, kparams=None): with connection.cursor() as cursor: cursor.execute(procedure_sql) # Use a new cursor because in MySQL a procedure can't be used in the # same cursor in which it was created. with connection.cursor() as cursor: cursor.callproc('test_procedure', params, kparams) with connection.schema_editor() as editor: editor.remove_procedure('test_procedure', param_types)
def test_create_index_ignores_opclasses(self): index = Index( name='test_ops_class', fields=['headline'], opclasses=['varchar_pattern_ops'], ) with connection.schema_editor() as editor: # This would error if opclasses weren't ignored. editor.add_index(IndexedArticle2, index)
def test_text_indexes(self): """Test creation of PostgreSQL-specific text indexes (#12234)""" from .models import IndexedArticle index_sql = [str(statement) for statement in connection.schema_editor()._model_indexes_sql(IndexedArticle)] self.assertEqual(len(index_sql), 5) self.assertIn('("headline" varchar_pattern_ops)', index_sql[1]) self.assertIn('("body" text_pattern_ops)', index_sql[3]) # unique=True and db_index=True should only create the varchar-specific # index (#19441). self.assertIn('("slug" varchar_pattern_ops)', index_sql[4])
def test_quote_value(self): editor = connection.schema_editor() tested_values = [ ('string', "'string'"), (42, '42'), (1.754, '1.754'), (False, '0'), ] for value, expected in tested_values: with self.subTest(value=value): self.assertEqual(editor.quote_value(value), expected)
def test_index_name_hash(self): """ Index names should be deterministic. """ with connection.schema_editor() as editor: index_name = editor._create_index_name( table_name=Article._meta.db_table, column_names=("c1",), suffix="123", ) self.assertEqual(index_name, "indexes_article_c1_a52bd80b123")
def test_index_together(self): editor = connection.schema_editor() index_sql = [str(statement) for statement in editor._model_indexes_sql(Article)] self.assertEqual(len(index_sql), 1) # Ensure the index name is properly quoted self.assertIn( connection.ops.quote_name( editor._create_index_name(Article._meta.db_table, ['headline', 'pub_date'], suffix='_idx') ), index_sql[0] )
def test_ops_class(self): index = Index( name='test_ops_class', fields=['headline'], opclasses=['varchar_pattern_ops'], ) with connection.schema_editor() as editor: editor.add_index(IndexedArticle2, index) with editor.connection.cursor() as cursor: cursor.execute(self.get_opclass_query % 'test_ops_class') self.assertEqual(cursor.fetchall(), [('varchar_pattern_ops', 'test_ops_class')])
def test_brin_index_not_supported(self): index_name = 'brin_index_exception' index = BrinIndex(fields=['field'], name=index_name) with self.assertRaisesMessage(NotSupportedError, 'BRIN indexes require PostgreSQL 9.5+.'): with mock.patch( 'djmodels.db.connection.features.has_brin_index_support', False): with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
def test_extra_args(self): editor = connection.schema_editor(collect_sql=True) sql = 'SELECT * FROM foo WHERE id in (%s, %s)' params = [42, 1337] with self.assertLogs('djmodels.db.backends.schema', 'DEBUG') as cm: editor.execute(sql, params) self.assertEqual(cm.records[0].sql, sql) self.assertEqual(cm.records[0].params, params) self.assertEqual( cm.records[0].getMessage(), 'SELECT * FROM foo WHERE id in (%s, %s); (params [42, 1337])', )
def test_get_sequences_manually_created_index(self): with connection.cursor() as cursor: with connection.schema_editor() as editor: editor._drop_identity(Person._meta.db_table, 'id') seqs = connection.introspection.get_sequences( cursor, Person._meta.db_table, Person._meta.local_fields) self.assertEqual(seqs, [{ 'table': Person._meta.db_table, 'column': 'id' }]) # Recreate model, because adding identity is impossible. editor.delete_model(Person) editor.create_model(Person)
def test_get_relations(self): with connection.cursor() as cursor: relations = connection.introspection.get_relations( cursor, Article._meta.db_table) # That's {field_name: (field_name_other_table, other_table)} expected_relations = { 'reporter_id': ('id', Reporter._meta.db_table), 'response_to_id': ('id', Article._meta.db_table), } self.assertEqual(relations, expected_relations) # Removing a field shouldn't disturb get_relations (#17785) body = Article._meta.get_field('body') with connection.schema_editor() as editor: editor.remove_field(Article, body) with connection.cursor() as cursor: relations = connection.introspection.get_relations( cursor, Article._meta.db_table) with connection.schema_editor() as editor: editor.add_field(Article, body) self.assertEqual(relations, expected_relations)
def test_table_rename_inside_atomic_block(self): """ NotImplementedError is raised when a table rename is attempted inside an atomic block. """ msg = ( "Renaming the 'backends_author' table while in a transaction is " "not supported on SQLite because it would break referential " "integrity. Try adding `atomic = False` to the Migration class." ) with self.assertRaisesMessage(NotSupportedError, msg): with connection.schema_editor(atomic=True) as editor: editor.alter_db_table(Author, "backends_author", "renamed_table")
def test_gin_parameters_exception(self): index_name = 'gin_options_exception' index = GinIndex(fields=['field'], name=index_name, gin_pending_list_limit=64) msg = 'GIN option gin_pending_list_limit requires PostgreSQL 9.5+.' with self.assertRaisesMessage(NotSupportedError, msg): with mock.patch( 'djmodels.db.connection.features.has_gin_pending_list_limit', False): with connection.schema_editor() as editor: editor.add_index(IntegerArrayModel, index) self.assertNotIn( index_name, self.get_constraints(IntegerArrayModel._meta.db_table))
def test_ops_class_multiple_columns(self): index = Index( name='test_ops_class_multiple', fields=['headline', 'body'], opclasses=['varchar_pattern_ops', 'text_pattern_ops'], ) with connection.schema_editor() as editor: editor.add_index(IndexedArticle2, index) with editor.connection.cursor() as cursor: cursor.execute(self.get_opclass_query % 'test_ops_class_multiple') expected_ops_classes = ( ('varchar_pattern_ops', 'test_ops_class_multiple'), ('text_pattern_ops', 'test_ops_class_multiple'), ) self.assertCountEqual(cursor.fetchall(), expected_ops_classes)
def test_autoincrement(self): """ auto_increment fields are created with the AUTOINCREMENT keyword in order to be monotonically increasing (#10164). """ with connection.schema_editor(collect_sql=True) as editor: editor.create_model(Square) statements = editor.collected_sql match = re.search('"id" ([^,]+),', statements[0]) self.assertIsNotNone(match) self.assertEqual( 'integer NOT NULL PRIMARY KEY AUTOINCREMENT', match.group(1), 'Wrong SQL used to create an auto-increment column on SQLite' )
def _test_create_model(self, app_label, should_run): """ CreateModel honors multi-db settings. """ operation = migrations.CreateModel( "Pony", [("id", models.AutoField(primary_key=True))], ) # Test the state alteration project_state = ProjectState() new_state = project_state.clone() operation.state_forwards(app_label, new_state) # Test the database alteration self.assertTableNotExists("%s_pony" % app_label) with connection.schema_editor() as editor: operation.database_forwards(app_label, editor, project_state, new_state) if should_run: self.assertTableExists("%s_pony" % app_label) else: self.assertTableNotExists("%s_pony" % app_label) # And test reversal with connection.schema_editor() as editor: operation.database_backwards(app_label, editor, new_state, project_state) self.assertTableNotExists("%s_pony" % app_label)
def test_field_rename_inside_atomic_block(self): """ NotImplementedError is raised when a model field rename is attempted inside an atomic block. """ new_field = CharField(max_length=255, unique=True) new_field.set_attributes_from_name('renamed') msg = ( "Renaming the 'backends_author'.'name' column while in a " "transaction is not supported on SQLite because it would break " "referential integrity. Try adding `atomic = False` to the " "Migration class." ) with self.assertRaisesMessage(NotSupportedError, msg): with connection.schema_editor(atomic=True) as editor: editor.alter_field(Author, Author._meta.get_field('name'), new_field)
def alter_gis_model(self, migration_class, model_name, field_name, blank=False, field_class=None, field_class_kwargs=None): args = [model_name, field_name] if field_class: field_class_kwargs = field_class_kwargs or { 'srid': 4326, 'blank': blank } args.append(field_class(**field_class_kwargs)) operation = migration_class(*args) old_state = self.current_state.clone() operation.state_forwards('gis', self.current_state) with connection.schema_editor() as editor: operation.database_forwards('gis', editor, old_state, self.current_state)
def test_db_tablespace(self): with connection.schema_editor() as editor: # Index with db_tablespace attribute. for fields in [ # Field with db_tablespace specified on model. ['shortcut'], # Field without db_tablespace specified on model. ['author'], # Multi-column with db_tablespaces specified on model. ['shortcut', 'isbn'], # Multi-column without db_tablespace specified on model. ['title', 'author'], ]: with self.subTest(fields=fields): index = models.Index(fields=fields, db_tablespace='idx_tbls2') self.assertIn('"idx_tbls2"', str(index.create_sql(Book, editor)).lower()) # Indexes without db_tablespace attribute. for fields in [['author'], ['shortcut', 'isbn'], ['title', 'author']]: with self.subTest(fields=fields): index = models.Index(fields=fields) # The DEFAULT_INDEX_TABLESPACE setting can't be tested # because it's evaluated when the model class is defined. # As a consequence, @override_settings doesn't work. if settings.DEFAULT_INDEX_TABLESPACE: self.assertIn( '"%s"' % settings.DEFAULT_INDEX_TABLESPACE, str(index.create_sql(Book, editor)).lower()) else: self.assertNotIn('TABLESPACE', str(index.create_sql(Book, editor))) # Field with db_tablespace specified on the model and an index # without db_tablespace. index = models.Index(fields=['shortcut']) self.assertIn('"idx_tbls"', str(index.create_sql(Book, editor)).lower())
def _test_run_sql(self, app_label, should_run, hints=None): with override_settings(DATABASE_ROUTERS=[MigrateEverythingRouter()]): project_state = self.set_up_test_model(app_label) sql = """ INSERT INTO {0}_pony (pink, weight) VALUES (1, 3.55); INSERT INTO {0}_pony (pink, weight) VALUES (3, 5.0); """.format(app_label) operation = migrations.RunSQL(sql, hints=hints or {}) # Test the state alteration does nothing new_state = project_state.clone() operation.state_forwards(app_label, new_state) self.assertEqual(new_state, project_state) # Test the database alteration self.assertEqual(project_state.apps.get_model(app_label, "Pony").objects.count(), 0) with connection.schema_editor() as editor: operation.database_forwards(app_label, editor, project_state, new_state) Pony = project_state.apps.get_model(app_label, "Pony") if should_run: self.assertEqual(Pony.objects.count(), 2) else: self.assertEqual(Pony.objects.count(), 0)