wemake-services / django-test-migrations

Test django schema and data migrations, including migrations' order and best practices.

Home Page:https://pypi.org/project/django-test-migrations/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

apply_initial_migration fails

Hafnernuss opened this issue · comments

Hi,

I've just discovered this package and wanted to test a new migration that I am writing. So far, the project consists of a few apps, and the app I want to write migration tests for has currently > 100 applied migrations.
For the sake of simplicity, I set migrate_from and migrate_to to an already applied migration (~101 -> 102, only a change in a field type) just to fiddle arround. Curiously, it failed.

Traceback:

`MySQLdb._exceptions.OperationalError: (3730, "Cannot drop table 'wagtailcore_collectionviewrestriction' referenced by a foreign key constraint 'wagtailcore_collecti_collectionviewrestri_47320efd_fk_wagtailco' on table 'wagtailcore_collectionviewrestriction_groups'.")

env/lib/python3.6/site-packages/MySQLdb/connections.py:239: OperationalError

The above exception was the direct cause of the following exception:
env/lib/python3.6/site-packages/django_test_migrations/contrib/unittest_case.py:36: in setUp
    self.migrate_from,
env/lib/python3.6/site-packages/django_test_migrations/migrator.py:46: in apply_initial_migration
    sql.drop_models_tables(self._database, style)
env/lib/python3.6/site-packages/django_test_migrations/sql.py:32: in drop_models_tables
    get_execute_sql_flush_for(connection)(database_name, sql_drop_tables)
env/lib/python3.6/site-packages/django/db/backends/base/operations.py:405: in execute_sql_flush
    cursor.execute(sql)
env/lib/python3.6/site-packages/django/db/backends/utils.py:68: in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
env/lib/python3.6/site-packages/django/db/backends/utils.py:77: in _execute_with_wrappers
    return executor(sql, params, many, context)
env/lib/python3.6/site-packages/django/db/backends/utils.py:86: in _execute
    return self.cursor.execute(sql, params)
env/lib/python3.6/site-packages/django/db/utils.py:90: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
env/lib/python3.6/site-packages/django/db/backends/utils.py:84: in _execute
    return self.cursor.execute(sql)
env/lib/python3.6/site-packages/django/db/backends/mysql/base.py:74: in execute
    return self.cursor.execute(query, args)
env/lib/python3.6/site-packages/MySQLdb/cursors.py:209: in execute
    res = self._query(query)
env/lib/python3.6/site-packages/MySQLdb/cursors.py:315: in _query
    db.query(q)

and a lot more other errors related to drop queries. As I understand it, during setup, django-test-migrations takes the current test_db and deletes all models in the database, and then reapplies all migrations up and including to migrate_from. In my case, this seems to fail. I cannot imagine what I could have done wrong in my testcase, as the error occurs during the setUp function of the MigratorTestCase.

I don't think that it has anything to do with wagtail, as the testcase seems to fail with different errors on each run:

self = <_mysql.connection closed at 0x35419e8>
query = b'DROP TABLE `auth_group` CASCADE'

    def query(self, query):
        # Since _mysql releases GIL while querying, we need immutable buffer.
        if isinstance(query, bytearray):
            query = bytes(query)
>       _mysql.connection.query(self, query)
E       django.db.utils.OperationalError: (3730, "Cannot drop table 'auth_group' referenced by a foreign key constraint 'auth_group_permissions_group_id_b120cbf9_fk_auth_group_id' on table 'auth_group_permissions'.")

Testcase:

class TestMigrations(MigratorTestCase):
    migrate_from = ('myapp', None)
    migrate_to = ('myapp', '001_initial')

    def prepare(self):
        pass

    def test_migration001(self):
        self.assertTrue(True)

Maybe I am missing something crucial and obvious, so here is what I did:

install django-test-migrations with pip
wrote a unittest testcase, as provided in the example (the actual tc is just an assertTrue(True)
ran manage.py test
I did not add anything to INSTALLED_APPS whatsoever.

Any ideas?

DB: MYSQL 8.0.19 mysqlclient (1.4.6)
Django: 3.0.2
django-test-migrations: 1.0.0

@skarzi my wild guess is that it can be mysql related failure. What do you think?

I did a quick test with the same project on another system that uses sqlite as backend. The error is different there and only occurs in the tearDown of the testcase:

self = <django.db.backends.utils.CursorWrapper object at 0x7f71205fd198>
sql = 'CREATE TABLE "wagtailsearch_editorspick" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "sort_order" integer NULL,...ription" text NOT NULL, "page_id" integer NOT NULL REFERENCES "wagtailcore_page" ("id") DEFERRABLE INITIALLY DEFERRED)'
params = None
ignored_wrapper_args = (False, {'connection': <django.db.backends.sqlite3.base.DatabaseWrapper object at 0x7f7122080780>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7f71205fd198>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
>               return self.cursor.execute(sql)

env/lib/python3.6/site-packages/django/db/backends/utils.py:84: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.sqlite3.base.SQLiteCursorWrapper object at 0x7f711fe83d38>
query = 'CREATE TABLE "wagtailsearch_editorspick" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "sort_order" integer NULL,...ription" text NOT NULL, "page_id" integer NOT NULL REFERENCES "wagtailcore_page" ("id") DEFERRABLE INITIALLY DEFERRED)'
params = None

    def execute(self, query, params=None):
        if params is None:
>           return Database.Cursor.execute(self, query)
E           sqlite3.OperationalError: table "wagtailsearch_editorspick" already exists

env/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py:394: OperationalError

The above exception was the direct cause of the following exception:
env/lib/python3.6/site-packages/django_test_migrations/contrib/unittest_case.py:50: in tearDown
    self._migrator.reset()
env/lib/python3.6/site-packages/django_test_migrations/migrator.py:69: in reset
    call_command('migrate', verbosity=0, database=self._database)
env/lib/python3.6/site-packages/django/core/management/__init__.py:168: in call_command
    return command.execute(*args, **defaults)
env/lib/python3.6/site-packages/django/core/management/base.py:369: in execute
    output = self.handle(*args, **options)
env/lib/python3.6/site-packages/django/core/management/base.py:83: in wrapped
    res = handle_func(*args, **kwargs)
env/lib/python3.6/site-packages/django/core/management/commands/migrate.py:233: in handle
    fake_initial=fake_initial,
env/lib/python3.6/site-packages/django/db/migrations/executor.py:117: in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
env/lib/python3.6/site-packages/django/db/migrations/executor.py:147: in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
env/lib/python3.6/site-packages/django/db/migrations/executor.py:245: in apply_migration
    state = migration.apply(state, schema_editor)
env/lib/python3.6/site-packages/django/db/migrations/migration.py:124: in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
env/lib/python3.6/site-packages/django/db/migrations/operations/models.py:92: in database_forwards
    schema_editor.create_model(model)
env/lib/python3.6/site-packages/django/db/backends/base/schema.py:324: in create_model
    self.execute(sql, params or None)
env/lib/python3.6/site-packages/django/db/backends/base/schema.py:142: in execute
    cursor.execute(sql, params)
env/lib/python3.6/site-packages/django/db/backends/utils.py:68: in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
env/lib/python3.6/site-packages/django/db/backends/utils.py:77: in _execute_with_wrappers
    return executor(sql, params, many, context)
env/lib/python3.6/site-packages/django/db/backends/utils.py:86: in _execute
    return self.cursor.execute(sql, params)
env/lib/python3.6/site-packages/django/db/utils.py:90: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
env/lib/python3.6/site-packages/django/db/backends/utils.py:84: in _execute
    return self.cursor.execute(sql)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.sqlite3.base.SQLiteCursorWrapper object at 0x7f711fe83d38>
query = 'CREATE TABLE "wagtailsearch_editorspick" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "sort_order" integer NULL,...ription" text NOT NULL, "page_id" integer NOT NULL REFERENCES "wagtailcore_page" ("id") DEFERRABLE INITIALLY DEFERRED)'
params = None

    def execute(self, query, params=None):
        if params is None:
>           return Database.Cursor.execute(self, query)
E           django.db.utils.OperationalError: table "wagtailsearch_editorspick" already exists

env/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py:394: OperationalError

but unlike with mysql, the error does not change on successive runs. implementing a dummy tearDown causes the test to pass.

@skarzi my wild guess is that it can be mysql related failure. What do you think?

The issue described by @Hafnernuss in the first comment is related to MySQL.
In django.db.backends.sql.operations.DatabaseOperations.sql_flush django disables FOREIGN_KEY_CHECKS before executing SQL FLUSH and the same behaviour should be used to trigger SQL DELETE queries.
Currently, we have django_test_migrations.db module, where we can move django_test_migrations.sql module and refactor it a bit to make adding such features per DB backend easier.
That's also a great time to focus on #107, so performing above mentioned refactoring will be easier.

@Hafnernuss could you please try to reproduce error described in your second comment without depending on wagtail?
It's quite big lib with a lot of migrations and it will make debugging much more complicated, so simpler and smaller example will be very welcome!

@skarzi sure, I'll try ;)

Sorry for the late reply. I had a little bit of time and played around.
What I did:

  • fresh mysql db
  • removed all wagtail references from the app
  • ran all migrations
  • created following testcase (same as above):
class TestMigrations(MigratorTestCase):
    migrate_from = ('myapp', None)
    migrate_to = ('myapp', '001_initial')

    def prepare(self):
        pass

    def test_migration001(self):
        self.assertTrue(True)

Here is the error I get:

    self.migrate_from,
env/lib/python3.6/site-packages/django_test_migrations/migrator.py:46: in apply_initial_migration
    sql.drop_models_tables(self._database, style)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

database_name = 'default'
style = <django.core.management.color.Style object at 0x7fcc0ed13518>

    def drop_models_tables(
        database_name: str,
        style: Optional[Style] = None,
    ) -> None:
        """Drop all installed Django's models tables."""
        style = style or no_style()
        connection = connections[database_name]
        tables = connection.introspection.django_table_names(
            only_existing=True,
            include_views=False,
        )
        sql_drop_tables = [
            connection.SchemaEditorClass.sql_delete_table % {
                'table': style.SQL_FIELD(connection.ops.quote_name(table)),
            }
            for table in tables
        ]
        if sql_drop_tables:
>           get_execute_sql_flush_for(connection)(database_name, sql_drop_tables)
E           TypeError: execute_sql_flush() takes 2 positional arguments but 3 were given

env/lib/python3.6/site-packages/django_test_migrations/sql.py:32: TypeError

I am completly clueless.

Edit: Unfortunately, I had to migrate to django 3.1.1 in the meantime.

Apparently, the error above is indeed caused by Django 3.1. I have created a MRE that runs on Django 3.0. The problem does not occur when usig SQLITE, however, it does when using MYSQL. The migration does not even have to be applied.

All you have to do is create a env (requirements provided) and change the database credentials in the settings.py file.

The error I recieve:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_migration001 (sample.tests.TestPopulatePlayerPositions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/mysql/base.py", line 74, in execute
    return self.cursor.execute(query, args)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 206, in execute
    res = self._query(query)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
MySQLdb._exceptions.OperationalError: (3730, "Cannot drop table 'django_content_type' referenced by a foreign key constraint 'auth_permission_content_type_id_2f476e4b_fk_django_co' on table 'auth_permission'.")

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

Traceback (most recent call last):
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django_test_migrations/contrib/unittest_case.py", line 35, in setUp
    self.old_state = self._migrator.apply_initial_migration(
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django_test_migrations/migrator.py", line 46, in apply_initial_migration
    sql.drop_models_tables(self._database, style)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django_test_migrations/sql.py", line 32, in drop_models_tables
    get_execute_sql_flush_for(connection)(database_name, sql_drop_tables)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/base/operations.py", line 405, in execute_sql_flush
    cursor.execute(sql)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/django/db/backends/mysql/base.py", line 74, in execute
    return self.cursor.execute(query, args)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 206, in execute
    res = self._query(query)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/home/tms/migrate_test/migrate_test/env/lib/python3.8/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (3730, "Cannot drop table 'django_content_type' referenced by a foreign key constraint 'auth_permission_content_type_id_2f476e4b_fk_django_co' on table 'auth_permission'.")

----------------------------------------------------------------------
Ran 1 test in 0.510s

FAILED (errors=1)

Tested on python 3.6 and 3.8.

migrate_test.zip

@Hafnernuss thank you for the great and detailed investigation!
I will try to fix issues related to MySQL in the following days.

Fix for django>=3.1 is already on master, but we need to fix a few other issues before releasing the new version.

Great to hear that! I suspected that the error was related to 3.1.x ;)
I think it is somehow related to foreign keys and the order in which models are deleted. I wonder especially how this can be solved if someone uses on_delete=PROTECT

Maybe this could also help?

SET FOREIGN_KEY_CHECKS=0; since the whole db is cleared, doesn't matter if the checks fail or not. Has to be reenabled though ;)

@skarzi anything I can help with to get the next release out? I'm getting hit by this also.

hi @tmm,

sure, thank you so much for your help! 👍

You can prepare PR with changes mentioned by me in #122 (comment).
Let's create some base class like BaseDatabaseConfiguration, but for database operations, so we can move there functions from django_test_migrations.sql and simply subclass and extend it for all supported vendors (currently only MySQL needs a different implementation of drop_models_table - we need to disable and then enable FOREIGN_KEY_CHECKS).
If something is not clear or you have some other ideas, please share it here