From 156e2d59cf92eb252c2f6ef9bb0b65f26c707de2 Mon Sep 17 00:00:00 2001 From: Akshesh Date: Sat, 25 Jun 2016 22:02:56 +0530 Subject: Fixed #26709 -- Added class-based indexes. Added the AddIndex and RemoveIndex operations to use them in migrations. Thanks markush, mjtamlyn, timgraham, and charettes for review and advice. --- tests/migrations/test_autodetector.py | 10 ++++-- tests/migrations/test_operations.py | 61 +++++++++++++++++++++++++++++++++- tests/migrations/test_state.py | 8 ++--- tests/model_indexes/__init__.py | 0 tests/model_indexes/models.py | 7 ++++ tests/model_indexes/tests.py | 62 +++++++++++++++++++++++++++++++++++ tests/schema/tests.py | 21 ++++++++++++ 7 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 tests/model_indexes/__init__.py create mode 100644 tests/model_indexes/models.py create mode 100644 tests/model_indexes/tests.py (limited to 'tests') diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index cb20927e07..1a1f6a1250 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -1263,7 +1263,9 @@ class AutodetectorTests(TestCase): # Right number/type of migrations? self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"]) - self.assertOperationAttributes(changes, "testapp", 0, 0, name="AuthorProxy", options={"proxy": True}) + self.assertOperationAttributes( + changes, "testapp", 0, 0, name="AuthorProxy", options={"proxy": True, "indexes": []} + ) # Now, we test turning a proxy model into a non-proxy model # It should delete the proxy then make the real one changes = self.get_changes( @@ -1273,7 +1275,7 @@ class AutodetectorTests(TestCase): self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes(changes, "testapp", 0, ["DeleteModel", "CreateModel"]) self.assertOperationAttributes(changes, "testapp", 0, 0, name="AuthorProxy") - self.assertOperationAttributes(changes, "testapp", 0, 1, name="AuthorProxy", options={}) + self.assertOperationAttributes(changes, "testapp", 0, 1, name="AuthorProxy", options={"indexes": []}) def test_proxy_custom_pk(self): """ @@ -1296,7 +1298,9 @@ class AutodetectorTests(TestCase): # Right number/type of migrations? self.assertNumberMigrations(changes, 'testapp', 1) self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"]) - self.assertOperationAttributes(changes, 'testapp', 0, 0, name="AuthorUnmanaged", options={"managed": False}) + self.assertOperationAttributes( + changes, 'testapp', 0, 0, name="AuthorUnmanaged", options={"managed": False, "indexes": []} + ) def test_unmanaged_to_managed(self): # Now, we test turning an unmanaged model into a managed model diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 3707caf106..01a9c8d886 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -51,7 +51,7 @@ class OperationTestBase(MigrationTestBase): return project_state, new_state def set_up_test_model( - self, app_label, second_model=False, third_model=False, + self, app_label, second_model=False, third_model=False, multicol_index=False, related_model=False, mti_model=False, proxy_model=False, manager_model=False, unique_together=False, options=False, db_table=None, index_together=False): """ @@ -96,6 +96,11 @@ class OperationTestBase(MigrationTestBase): ], options=model_options, )] + if multicol_index: + operations.append(migrations.AddIndex( + "Pony", + models.Index(fields=["pink", "weight"], name="pony_test_idx") + )) if second_model: operations.append(migrations.CreateModel( "Stable", @@ -1375,6 +1380,60 @@ class OperationTests(OperationTestBase): operation = migrations.AlterUniqueTogether("Pony", None) self.assertEqual(operation.describe(), "Alter unique_together for Pony (0 constraint(s))") + def test_add_index(self): + """ + Test the AddIndex operation. + """ + project_state = self.set_up_test_model("test_adin") + index = models.Index(fields=["pink"]) + operation = migrations.AddIndex("Pony", index) + self.assertEqual(operation.describe(), "Create index on field(s) pink of model Pony") + new_state = project_state.clone() + operation.state_forwards("test_adin", new_state) + # Test the database alteration + self.assertEqual(len(new_state.models["test_adin", "pony"].options['indexes']), 1) + self.assertIndexNotExists("test_adin_pony", ["pink"]) + with connection.schema_editor() as editor: + operation.database_forwards("test_adin", editor, project_state, new_state) + self.assertIndexExists("test_adin_pony", ["pink"]) + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_adin", editor, new_state, project_state) + self.assertIndexNotExists("test_adin_pony", ["pink"]) + # And deconstruction + definition = operation.deconstruct() + self.assertEqual(definition[0], "AddIndex") + self.assertEqual(definition[1], []) + self.assertEqual(definition[2], {'model_name': "Pony", 'index': index}) + + def test_remove_index(self): + """ + Test the RemoveIndex operation. + """ + project_state = self.set_up_test_model("test_rmin", multicol_index=True) + self.assertTableExists("test_rmin_pony") + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + operation = migrations.RemoveIndex("Pony", "pony_test_idx") + self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony") + new_state = project_state.clone() + operation.state_forwards("test_rmin", new_state) + # Test the state alteration + self.assertEqual(len(new_state.models["test_rmin", "pony"].options['indexes']), 0) + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + # Test the database alteration + with connection.schema_editor() as editor: + operation.database_forwards("test_rmin", editor, project_state, new_state) + self.assertIndexNotExists("test_rmin_pony", ["pink", "weight"]) + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_rmin", editor, new_state, project_state) + self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + # And deconstruction + definition = operation.deconstruct() + self.assertEqual(definition[0], "RemoveIndex") + self.assertEqual(definition[1], []) + self.assertEqual(definition[2], {'model_name': "Pony", 'name': "pony_test_idx"}) + def test_alter_index_together(self): """ Tests the AlterIndexTogether operation. diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index d165221aa6..dae4fabb67 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -125,7 +125,7 @@ class StateTests(SimpleTestCase): self.assertIs(author_state.fields[3][1].null, True) self.assertEqual( author_state.options, - {"unique_together": {("name", "bio")}, "index_together": {("bio", "age")}} + {"unique_together": {("name", "bio")}, "index_together": {("bio", "age")}, "indexes": []} ) self.assertEqual(author_state.bases, (models.Model, )) @@ -135,13 +135,13 @@ class StateTests(SimpleTestCase): self.assertEqual(book_state.fields[1][1].max_length, 1000) self.assertIs(book_state.fields[2][1].null, False) self.assertEqual(book_state.fields[3][1].__class__.__name__, "ManyToManyField") - self.assertEqual(book_state.options, {"verbose_name": "tome", "db_table": "test_tome"}) + self.assertEqual(book_state.options, {"verbose_name": "tome", "db_table": "test_tome", "indexes": []}) self.assertEqual(book_state.bases, (models.Model, )) self.assertEqual(author_proxy_state.app_label, "migrations") self.assertEqual(author_proxy_state.name, "AuthorProxy") self.assertEqual(author_proxy_state.fields, []) - self.assertEqual(author_proxy_state.options, {"proxy": True, "ordering": ["name"]}) + self.assertEqual(author_proxy_state.options, {"proxy": True, "ordering": ["name"], "indexes": []}) self.assertEqual(author_proxy_state.bases, ("migrations.author", )) self.assertEqual(sub_author_state.app_label, "migrations") @@ -960,7 +960,7 @@ class ModelStateTests(SimpleTestCase): self.assertEqual(author_state.fields[1][1].max_length, 255) self.assertIs(author_state.fields[2][1].null, False) self.assertIs(author_state.fields[3][1].null, True) - self.assertEqual(author_state.options, {'swappable': 'TEST_SWAPPABLE_MODEL'}) + self.assertEqual(author_state.options, {'swappable': 'TEST_SWAPPABLE_MODEL', 'indexes': []}) self.assertEqual(author_state.bases, (models.Model, )) self.assertEqual(author_state.managers, []) diff --git a/tests/model_indexes/__init__.py b/tests/model_indexes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_indexes/models.py b/tests/model_indexes/models.py new file mode 100644 index 0000000000..598a4cb808 --- /dev/null +++ b/tests/model_indexes/models.py @@ -0,0 +1,7 @@ +from django.db import models + + +class Book(models.Model): + title = models.CharField(max_length=50) + author = models.CharField(max_length=50) + pages = models.IntegerField(db_column='page_count') diff --git a/tests/model_indexes/tests.py b/tests/model_indexes/tests.py new file mode 100644 index 0000000000..71bd8c91c0 --- /dev/null +++ b/tests/model_indexes/tests.py @@ -0,0 +1,62 @@ +from django.db import models +from django.test import TestCase + +from .models import Book + + +class IndexesTests(TestCase): + + def test_repr(self): + index = models.Index(fields=['title']) + multi_col_index = models.Index(fields=['title', 'author']) + self.assertEqual(repr(index), "") + self.assertEqual(repr(multi_col_index), "") + + def test_eq(self): + index = models.Index(fields=['title']) + same_index = models.Index(fields=['title']) + another_index = models.Index(fields=['title', 'author']) + self.assertEqual(index, same_index) + self.assertNotEqual(index, another_index) + + def test_raises_error_without_field(self): + msg = 'At least one field is required to define an index.' + with self.assertRaisesMessage(ValueError, msg): + models.Index() + + def test_max_name_length(self): + msg = 'Index names cannot be longer than 30 characters.' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='looooooooooooong_index_name_idx') + + def test_name_constraints(self): + msg = 'Index names cannot start with an underscore (_).' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='_name_starting_with_underscore') + + msg = 'Index names cannot start with a number (0-9).' + with self.assertRaisesMessage(ValueError, msg): + models.Index(fields=['title'], name='5name_starting_with_number') + + def test_name_auto_generation(self): + index = models.Index(fields=['author']) + index.model = Book + self.assertEqual(index.name, 'model_index_author_0f5565_idx') + + # fields may be truncated in the name. db_column is used for naming. + long_field_index = models.Index(fields=['pages']) + long_field_index.model = Book + self.assertEqual(long_field_index.name, 'model_index_page_co_69235a_idx') + + # suffix can't be longer than 3 characters. + long_field_index.suffix = 'suff' + msg = 'Index too long for multiple database support. Is self.suffix longer than 3 characters?' + with self.assertRaisesMessage(AssertionError, msg): + long_field_index.get_name() + + def test_deconstruction(self): + index = models.Index(fields=['title']) + path, args, kwargs = index.deconstruct() + self.assertEqual(path, 'django.db.models.Index') + self.assertEqual(args, ()) + self.assertEqual(kwargs, {'fields': ['title']}) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 8b35b77c7b..a3e4f8227a 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -16,6 +16,7 @@ from django.db.models.fields import ( from django.db.models.fields.related import ( ForeignKey, ForeignObject, ManyToManyField, OneToOneField, ) +from django.db.models.indexes import Index from django.db.transaction import atomic from django.test import ( TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature, @@ -1443,6 +1444,26 @@ class SchemaTests(TransactionTestCase): columns = self.column_classes(Author) self.assertEqual(columns['name'][0], "CharField") + def test_add_remove_index(self): + """ + Tests index addition and removal + """ + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Author) + # Ensure the table is there and has no index + self.assertNotIn('title', self.get_indexes(Author._meta.db_table)) + # Add the index + index = Index(fields=['name'], name='author_title_idx') + index.model = Author + with connection.schema_editor() as editor: + editor.add_index(index) + self.assertIn('name', self.get_indexes(Author._meta.db_table)) + # Drop the index + with connection.schema_editor() as editor: + editor.remove_index(index) + self.assertNotIn('name', self.get_indexes(Author._meta.db_table)) + def test_indexes(self): """ Tests creation/altering of indexes -- cgit v1.3