From 530dd193f2a1c791c703f3f961e2296cbb316bf9 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 14 Nov 2019 23:18:32 +0100 Subject: [PATCH] Fixed #29808 -- Fixed initial migration detection when identifiers are case-insensitive. Thanks Simon Charette for the review. --- django/db/migrations/executor.py | 20 +++++++++++-- tests/migrations/test_commands.py | 28 ++++++++++++++++++- .../fake_initial/0001_initial.py | 28 +++++++++++++++++++ .../fake_initial/__init__.py | 0 .../initial/0001_initial.py | 23 +++++++++++++++ .../initial/__init__.py | 0 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/migrations/test_fake_initial_case_insensitive/fake_initial/0001_initial.py create mode 100644 tests/migrations/test_fake_initial_case_insensitive/fake_initial/__init__.py create mode 100644 tests/migrations/test_fake_initial_case_insensitive/initial/0001_initial.py create mode 100644 tests/migrations/test_fake_initial_case_insensitive/initial/__init__.py diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index ee275cdaef3..1f65a7fe998 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -329,8 +329,11 @@ class MigrationExecutor: apps = after_state.apps found_create_model_migration = False found_add_field_migration = False + fold_identifier_case = self.connection.features.ignores_table_name_case with self.connection.cursor() as cursor: existing_table_names = set(self.connection.introspection.table_names(cursor)) + if fold_identifier_case: + existing_table_names = {name.casefold() for name in existing_table_names} # Make sure all create model and add field operations are done for operation in migration.operations: if isinstance(operation, migrations.CreateModel): @@ -341,7 +344,10 @@ class MigrationExecutor: model = global_apps.get_model(model._meta.swapped) if should_skip_detecting_model(migration, model): continue - if model._meta.db_table not in existing_table_names: + db_table = model._meta.db_table + if fold_identifier_case: + db_table = db_table.casefold() + if db_table not in existing_table_names: return False, project_state found_create_model_migration = True elif isinstance(operation, migrations.AddField): @@ -358,7 +364,10 @@ class MigrationExecutor: # Handle implicit many-to-many tables created by AddField. if field.many_to_many: - if field.remote_field.through._meta.db_table not in existing_table_names: + through_db_table = field.remote_field.through._meta.db_table + if fold_identifier_case: + through_db_table = through_db_table.casefold() + if through_db_table not in existing_table_names: return False, project_state else: found_add_field_migration = True @@ -368,7 +377,12 @@ class MigrationExecutor: table, ) for column in columns: - if column.name == field.column: + field_column = field.column + column_name = column.name + if fold_identifier_case: + column_name = column_name.casefold() + field_column = field_column.casefold() + if column_name == field_column: found_add_field_migration = True break else: diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 125be5de0ef..5f57dc7cadf 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -14,7 +14,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.utils import truncate_name from django.db.migrations.exceptions import InconsistentMigrationHistory from django.db.migrations.recorder import MigrationRecorder -from django.test import TestCase, override_settings +from django.test import TestCase, override_settings, skipUnlessDBFeature from .models import UnicodeModel, UnserializableModel from .routers import TestRouter @@ -197,6 +197,32 @@ class MigrateTests(MigrationTestBase): self.assertTableNotExists("migrations_tribble", using=db) self.assertTableNotExists("migrations_book", using=db) + @skipUnlessDBFeature('ignores_table_name_case') + def test_migrate_fake_initial_case_insensitive(self): + with override_settings(MIGRATION_MODULES={ + 'migrations': 'migrations.test_fake_initial_case_insensitive.initial', + }): + call_command('migrate', 'migrations', '0001', verbosity=0) + call_command('migrate', 'migrations', 'zero', fake=True, verbosity=0) + + with override_settings(MIGRATION_MODULES={ + 'migrations': 'migrations.test_fake_initial_case_insensitive.fake_initial', + }): + out = io.StringIO() + call_command( + 'migrate', + 'migrations', + '0001', + fake_initial=True, + stdout=out, + verbosity=1, + no_color=True, + ) + self.assertIn( + 'migrations.0001_initial... faked', + out.getvalue().lower(), + ) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_fake_split_initial"}) def test_migrate_fake_split_initial(self): """ diff --git a/tests/migrations/test_fake_initial_case_insensitive/fake_initial/0001_initial.py b/tests/migrations/test_fake_initial_case_insensitive/fake_initial/0001_initial.py new file mode 100644 index 00000000000..93a8bf392b9 --- /dev/null +++ b/tests/migrations/test_fake_initial_case_insensitive/fake_initial/0001_initial.py @@ -0,0 +1,28 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + operations = [ + migrations.CreateModel( + 'fakeinitialmodel', + [ + ('id', models.AutoField(primary_key=True)), + ('field', models.CharField(max_length=20)), + ], + options={ + 'db_table': 'migrations_mIxEd_cAsE_iNiTiAl_mOdEl', + }, + ), + migrations.AddField( + model_name='fakeinitialmodel', + name='field_mixed_case', + field=models.CharField(max_length=20, db_column='fIeLd_mIxEd_cAsE'), + ), + migrations.AddField( + model_name='fakeinitialmodel', + name='fake_initial_model', + field=models.ManyToManyField(to='migrations.fakeinitialmodel', db_table='m2m_mIxEd_cAsE'), + ), + ] diff --git a/tests/migrations/test_fake_initial_case_insensitive/fake_initial/__init__.py b/tests/migrations/test_fake_initial_case_insensitive/fake_initial/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/migrations/test_fake_initial_case_insensitive/initial/0001_initial.py b/tests/migrations/test_fake_initial_case_insensitive/initial/0001_initial.py new file mode 100644 index 00000000000..56a1b8a0518 --- /dev/null +++ b/tests/migrations/test_fake_initial_case_insensitive/initial/0001_initial.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + operations = [ + migrations.CreateModel( + name='fakeinitialmodel', + fields=[ + ('id', models.AutoField(primary_key=True)), + ('field', models.CharField(max_length=20)), + ('field_mixed_case', models.CharField(max_length=20, db_column='FiEld_MiXeD_CaSe')), + ( + 'fake_initial_mode', + models.ManyToManyField('migrations.FakeInitialModel', db_table='m2m_MiXeD_CaSe'), + ), + ], + options={ + 'db_table': 'migrations_MiXeD_CaSe_InItIaL_MoDel', + }, + ), + ] diff --git a/tests/migrations/test_fake_initial_case_insensitive/initial/__init__.py b/tests/migrations/test_fake_initial_case_insensitive/initial/__init__.py new file mode 100644 index 00000000000..e69de29bb2d