Django test running migrations against wrong database with multi DB setup

Hello,

We’re trying to run tests in a multi-db project, however we’re having issues where the test migrations runner is using the original DB name, e.g. mydb instead of the test DB name test_mydb.

The actual database creation works without issue, and this does not seem to affect the default database connection.

The Django version is 4.1.10

The rough DATABASE config we’re using is:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'djangodb',
        'USER': "...",
        'PASSWORD': "...",
        'HOST': "...",
        "PORT": "...",
        'CONN_MAX_AGE': 60,
        'OPTIONS': {
            'init_command': 'SET default_storage_engine=INNODB; SET sql_mode="STRICT_TRANS_TABLES"',
            'read_default_file': 'my.cnf',
        }
    },
    'db_1': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_1',
        'USER': "...",
        'PASSWORD': "...",
        'HOST': "...",
        "PORT": "...",
        'CONN_MAX_AGE': 60,
        'OPTIONS': {
            'init_command': 'SET default_storage_engine=INNODB; SET sql_mode="STRICT_TRANS_TABLES"',
            'read_default_file': 'my.cnf',
        }
    },
    'slave_db_2': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_2',
        'USER': "...",
        'PASSWORD': "...",
        'HOST': "...",
        "PORT": "...",
        'TEST': {
            'MIRROR': 'db_2"',
        },
        'CONN_MAX_AGE': 60,
        'OPTIONS': {
            'init_command': 'SET default_storage_engine=INNODB; SET sql_mode="STRICT_TRANS_TABLES"',
            'read_default_file': 'my.cnf',
        }
    },
    'db_3': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_3',
        'USER': "...",
        'PASSWORD': "...",
        'HOST': "...",
        "PORT": "...",
        'CONN_MAX_AGE': 60,
        'OPTIONS': {
            'init_command': 'SET default_storage_engine=INNODB; SET sql_mode="STRICT_TRANS_TABLES"',
            'read_default_file': 'my.cnf',
        }
    },
}

We’re also using a custom DB router, incase that has implications:

import threading
import importlib
from django.conf import settings

DATABASE_SLAVES = []

for dbname, params in settings.DATABASES.items():
    if not dbname.startswith('slave_'):
        continue
    DATABASE_SLAVES.append(dbname)

_locals = threading.local()

def slave_switch(value):
    _locals.use_slave = value

def using_slave():
    return getattr(_locals, 'use_slave', False)

class DBRouter(object):
    """A router to control all database operations"""

    def db_for_read(self, model, **hints):
        connection = model.connection_name\
                if hasattr(model,'connection_name') else 'default'
        connection = 'slave_%s' % (connection)\
                if using_slave()\
                and 'slave_%s' % (connection) in DATABASE_SLAVES\
                else connection
        return connection

    def db_for_write(self, model, **hints):
        if hasattr(model,'connection_name'):
            return model.connection_name
        return None

    def allow_relation(self, model1, model2, **hints):
        if hasattr(model1,'connection_name')\
                and hasattr(model2,'connection_name'):
            return model1.connection_name == model2.connection_name
        return None

    def allow_syncdb(self, db, model):
        if hasattr(model,'connection_name'):
            return model.connection_name == db
        return db == 'default'

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if 'model' in hints:
            model = hints['model']
            if model.__module__ == '__fake__':
                try:
                    module = importlib.import_module('%s.models' % app_label)
                    model = getattr(module, model._meta.label.split('.')[1])
                except:
                    return db == 'default'
            if hasattr(model, 'connection_name'):
                return model.connection_name == db
            else:
                return db == 'default'
        return None

We get the following error when running tests when the runner comes to apply migrations, as it’s running against the non-empty development database:

Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
Running migrations:
  Applying mymodels.0002_initial...2024-07-17 11:11:27,646 (p:645964,t:140164725249856) :-: DEBUG - django.db.backends.schema - execute - L:186 - CREATE TABLE `articles` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `headline` varchar(255) NOT NULL, `feedContent` longtext NULL, `pageContent` longtext NULL, `author` varchar(255) NULL, `url` varchar(255) NOT NULL, `pubDate` datetime(6) NULL, `foundDate` datetime(6) NULL, `language` varchar(150) NULL, `duplicateGroupId` bigint NULL); (params None)
Traceback (most recent call last):
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 87, in _execute
    return self.cursor.execute(sql)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/mysql/base.py", line 75, in execute
    return self.cursor.execute(query, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/cursors.py", line 179, in execute
    res = self._query(mogrified_query)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/cursors.py", line 330, in _query
    db.query(q)
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
MySQLdb.OperationalError: (1050, "Table 'articles' already exists")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "project/manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "project/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 446, in execute_from_command_line
    utility.execute()
  File "project/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 440, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "project/.venv/lib/python3.11/site-packages/django/core/management/commands/test.py", line 24, in run_from_argv
    super().run_from_argv(argv)
  File "project/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 402, in run_from_argv
    self.execute(*args, **cmd_options)
  File "project/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 448, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/core/management/commands/test.py", line 68, in handle
    failures = test_runner.run_tests(test_labels)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/test/runner.py", line 1050, in run_tests
    old_config = self.setup_databases(
                 ^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/test/runner.py", line 946, in setup_databases
    return _setup_databases(
           ^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/test/utils.py", line 220, in setup_databases
    connection.creation.create_test_db(
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/base/creation.py", line 78, in create_test_db
    call_command(
  File "project/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 198, in call_command
    return command.execute(*args, **defaults)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 448, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 96, in wrapped
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 349, in handle
    post_migrate_state = executor.migrate(
                         ^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 135, in migrate
    state = self._migrate_all_forwards(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
    state = self.apply_migration(
            ^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 252, in apply_migration
    state = migration.apply(state, schema_editor)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/migrations/migration.py", line 130, in apply
    operation.database_forwards(
  File "project/.venv/lib/python3.11/site-packages/django/db/migrations/operations/models.py", line 96, in database_forwards
    schema_editor.create_model(model)
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/base/schema.py", line 447, in create_model
    self.execute(sql, params or None)
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/base/schema.py", line 199, in execute
    cursor.execute(sql, params)
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "project/.venv/lib/python3.11/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 87, in _execute
    return self.cursor.execute(sql)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/django/db/backends/mysql/base.py", line 75, in execute
    return self.cursor.execute(query, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/cursors.py", line 179, in execute
    res = self._query(mogrified_query)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/cursors.py", line 330, in _query
    db.query(q)
  File "project/.venv/lib/python3.11/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (1050, "Table 'articles' already exists")

Any help would be much appreciated!

Apparently this was due to using django-db-connection-pool · PyPI and was fixed once switching back to django.db.backends.mysql