From a5c49174b96302f39682cce48f82da67b3b08351 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 8 Mar 2026 10:44:56 +0100 Subject: [PATCH 1/2] Fixed #12529 -- Fixed migrate --run-syncdb crash for existing model with truncated db_table names. --- django/core/management/commands/migrate.py | 4 +- .../unmigrated_app_syncdb/models.py | 5 ++ tests/migrations/test_commands.py | 53 +++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 268f669ba257..62ad29e43d8a 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand, CommandError, no_translations from django.core.management.sql import emit_post_migrate_signal, emit_pre_migrate_signal from django.db import DEFAULT_DB_ALIAS, connections, router +from django.db.backends.utils import truncate_name from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import AmbiguityError @@ -447,8 +448,9 @@ def sync_apps(self, connection, app_labels): def model_installed(model): opts = model._meta converter = connection.introspection.identifier_converter + max_name_length = connection.ops.max_name_length() return not ( - (converter(opts.db_table) in tables) + (converter(truncate_name(opts.db_table, max_name_length)) in tables) or ( opts.auto_created and converter(opts.auto_created._meta.db_table) in tables diff --git a/tests/migrations/migrations_test_apps/unmigrated_app_syncdb/models.py b/tests/migrations/migrations_test_apps/unmigrated_app_syncdb/models.py index 9f3179cd0d6d..c539b20d1184 100644 --- a/tests/migrations/migrations_test_apps/unmigrated_app_syncdb/models.py +++ b/tests/migrations/migrations_test_apps/unmigrated_app_syncdb/models.py @@ -7,3 +7,8 @@ class Classroom(models.Model): class Lesson(models.Model): classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE) + + +class VeryLongNameModel(models.Model): + class Meta: + db_table = "long_db_table_that_should_be_truncated_before_checking" diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 6a0d9bd6d28d..e9929c1eafa9 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -25,6 +25,7 @@ connections, models, ) +from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.utils import truncate_name from django.db.migrations.autodetector import MigrationAutodetector @@ -1222,10 +1223,10 @@ def test_migrate_syncdb_deferred_sql_executed_with_schemaeditor(self): create_table_count = len( [call for call in execute.mock_calls if "CREATE TABLE" in str(call)] ) - self.assertEqual(create_table_count, 2) + self.assertEqual(create_table_count, 3) # There's at least one deferred SQL for creating the foreign key # index. - self.assertGreater(len(execute.mock_calls), 2) + self.assertGreater(len(execute.mock_calls), 3) stdout = stdout.getvalue() self.assertIn("Synchronize unmigrated apps: unmigrated_app_syncdb", stdout) self.assertIn("Creating tables...", stdout) @@ -1259,8 +1260,54 @@ def test_migrate_syncdb_app_label(self): create_table_count = len( [call for call in execute.mock_calls if "CREATE TABLE" in str(call)] ) - self.assertEqual(create_table_count, 2) + self.assertEqual(create_table_count, 3) + self.assertGreater(len(execute.mock_calls), 3) + self.assertIn( + "Synchronize unmigrated app: unmigrated_app_syncdb", stdout.getvalue() + ) + + @override_settings( + INSTALLED_APPS=[ + "migrations.migrations_test_apps.unmigrated_app_syncdb", + "migrations.migrations_test_apps.unmigrated_app_simple", + ] + ) + def test_migrate_syncdb_installed_truncated_db_model(self): + """ + Running migrate --run-syncdb doesn't try to create models with long + truncated name if already exist. + """ + with connection.cursor() as cursor: + mock_existing_tables = connection.introspection.table_names(cursor) + # Add truncated name for the VeryLongNameModel to the list of + # existing table names. + table_name = truncate_name( + "long_db_table_that_should_be_truncated_before_checking", + connection.ops.max_name_length(), + ) + mock_existing_tables.append(table_name) + stdout = io.StringIO() + with ( + mock.patch.object(BaseDatabaseSchemaEditor, "execute") as execute, + mock.patch.object( + BaseDatabaseIntrospection, + "table_names", + return_value=mock_existing_tables, + ), + ): + call_command( + "migrate", "unmigrated_app_syncdb", run_syncdb=True, stdout=stdout + ) + create_table_calls = [ + str(call).upper() + for call in execute.mock_calls + if "CREATE TABLE" in str(call) + ] + self.assertEqual(len(create_table_calls), 2) self.assertGreater(len(execute.mock_calls), 2) + self.assertFalse( + any([table_name.upper() in call for call in create_table_calls]) + ) self.assertIn( "Synchronize unmigrated app: unmigrated_app_syncdb", stdout.getvalue() ) From 787166fe27b0e7c7f97505da5766cfa72e76ae25 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 8 Mar 2026 07:08:27 -0400 Subject: [PATCH 2/2] Added DatabaseFeatures.pattern_lookup_needs_param_pattern. It's useful on MongoDB. --- django/db/backends/base/features.py | 4 ++++ django/db/models/lookups.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index e8fa82aa21cc..22c05f28e9a2 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -424,6 +424,10 @@ class BaseDatabaseFeatures: # injection? prohibits_dollar_signs_in_column_aliases = False + # Should PatternLookup.process_rhs() use self.param_pattern? It's unneeded + # on databases that don't use LIKE for pattern matching. + pattern_lookup_needs_param_pattern = True + # A set of dotted paths to tests in Django's test suite that are expected # to fail on this database. django_test_expected_failures = set() diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 3489fe1e4015..eaf3d69a5967 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -598,10 +598,10 @@ def get_rhs_op(self, connection, rhs): def process_rhs(self, qn, connection): rhs, params = super().process_rhs(qn, connection) if self.rhs_is_direct_value() and params and not self.bilateral_transforms: - params = ( - self.param_pattern % connection.ops.prep_for_like_query(params[0]), - *params[1:], - ) + param = connection.ops.prep_for_like_query(params[0]) + if connection.features.pattern_lookup_needs_param_pattern: + param = self.param_pattern % param + params = (param, *params[1:]) return rhs, params