Code Monkey home page Code Monkey logo

django-pg-zero-downtime-migrations's Introduction

PyPI PyPI - Python Version PyPI - Django Version Postgres Version PyPI - License

PyPI - Downloads GitHub last commit Build Status

django-pg-zero-downtime-migrations

Django postgresql backend that apply migrations with respect to database locks.

Installation

pip install django-pg-zero-downtime-migrations

Usage

To enable zero downtime migrations for postgres just setup django backend provided by this package and add most safe settings:

DATABASES = {
    'default': {
        'ENGINE': 'django_zero_downtime_migrations.backends.postgres',
        #'ENGINE': 'django_zero_downtime_migrations.backends.postgis',
        ...
    }
}
ZERO_DOWNTIME_MIGRATIONS_LOCK_TIMEOUT = '2s'
ZERO_DOWNTIME_MIGRATIONS_STATEMENT_TIMEOUT = '2s'
ZERO_DOWNTIME_MIGRATIONS_FLEXIBLE_STATEMENT_TIMEOUT = True
ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE = True

NOTE: this backend brings zero downtime improvements only for migrations (schema and RunSQL operations, but not for RunPython operation), for other purpose it works the same as standard django backend.

NOTE: this package is in beta, please check your migrations SQL before applying on production and submit issue for any question.

Differences with standard django backend

This backend provides same result state, but different way and with additional guarantees for avoiding stuck table locks.

This backend doesn't use transactions for migrations (except RunPython operation), because not all SQL fixes can be run in transaction and it allows to avoid deadlocks for complex migration. So when your migration will down in middle of transaction you need fix it manually (instead potential downtime). For that reason good practice to make migration modules small as possible.

Deployment flow

There are requirements for zero downtime deployment:

  1. We have one database;
  2. We have several instances with application - application always should be available, even you restart one of instances;
  3. We have balancer before instances;
  4. Our application works fine before, on and after migration - old application works fine with old and new database schema version;
  5. Our application works fine before, on and after instance updating - old and new application versions work fine with new database schema version.

deployment timeline

Flow:

  1. apply migrations
  2. disconnect instance form balancer, restart it and back to balancer - repeat this operation one by one for all instances

If our deployment don't satisfy zero downtime deployment rules, then we split it to smaller deployments.

deployment flow

Settings

ZERO_DOWNTIME_MIGRATIONS_LOCK_TIMEOUT

Apply lock_timeout for SQL statements that require ACCESS EXCLUSIVE lock, default None:

ZERO_DOWNTIME_MIGRATIONS_LOCK_TIMEOUT = '2s'

Allowed values:

  • None - current postgres setting used
  • other - timeout will be applied, 0 and equivalents mean that timeout will be disabled

ZERO_DOWNTIME_MIGRATIONS_STATEMENT_TIMEOUT

Apply statement_timeout for SQL statements that require ACCESS EXCLUSIVE lock, default None:

ZERO_DOWNTIME_MIGRATIONS_STATEMENT_TIMEOUT = '2s'

Allowed values:

  • None - current postgres setting used
  • other - timeout will be applied, 0 and equivalents mean that timeout will be disabled

ZERO_DOWNTIME_MIGRATIONS_FLEXIBLE_STATEMENT_TIMEOUT

Set statement_timeout to 0ms for SQL statements that require SHARE UPDATE EXCLUSIVE lock that useful in case when statement_timeout enabled globally and you try run long-running operations like index creation or constraint validation, default False:

ZERO_DOWNTIME_MIGRATIONS_FLEXIBLE_STATEMENT_TIMEOUT = True

ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE

Enabled option doesn't allow run potential unsafe migration, default False:

ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE = True

ZERO_DOWNTIME_DEFERRED_SQL

Define way to apply deferred sql, default True:

ZERO_DOWNTIME_DEFERRED_SQL = True

Allowed values:

  • True - run deferred sql similar to default django way
  • False - run deferred sql as soon as possible

ZERO_DOWNTIME_MIGRATIONS_IDEMPOTENT_SQL

Define idempotent mode, default False:

ZERO_DOWNTIME_MIGRATIONS_IDEMPOTENT_SQL = False

Allowed values:

  • True - skip already applied sql migrations
  • False - standard non atomic django behaviour

As this backend doesn't use transactions for migrations any failed migration can be cause of stopped process in intermediate state. To avoid manual schema manipulation idempotent mode allows to rerun failed migration after fixed issue (eg. data issue or long running CRUD queries).

NOTE: idempotent mode checks rely only on name and index and constraint valid state, so it can ignore name collisions and recommended do not use it for CI checks.

PgBouncer and timeouts

In case you using PgBouncer and expect timeouts will work as expected you need make sure that run migrations using session pool_mode.

How it works

Postgres table level locks

Postgres has different locks on table level that can conflict with each other https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-TABLES:

ACCESS SHARE ROW SHARE ROW EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE SHARE ROW EXCLUSIVE EXCLUSIVE ACCESS EXCLUSIVE
ACCESS SHARE X
ROW SHARE X X
ROW EXCLUSIVE X X X X
SHARE UPDATE EXCLUSIVE X X X X X
SHARE X X X X X
SHARE ROW EXCLUSIVE X X X X X X
EXCLUSIVE X X X X X X X
ACCESS EXCLUSIVE X X X X X X X X

Migration and business logic locks

Lets split this lock to migration and business logic operations.

  • Migration operations work synchronously in one thread and cover schema migrations (data migrations conflict with business logic operations same as business logic conflict concurrently).
  • Business logic operations work concurrently.

Migration locks

lock operations
ACCESS EXCLUSIVE CREATE SEQUENCE, DROP SEQUENCE, CREATE TABLE, DROP TABLE *, ALTER TABLE **, DROP INDEX
SHARE CREATE INDEX
SHARE UPDATE EXCLUSIVE CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, ALTER TABLE VALIDATE CONSTRAINT ***

*: CREATE SEQUENCE, DROP SEQUENCE, CREATE TABLE, DROP TABLE shouldn't have conflicts, because your business logic shouldn't yet operate with created tables and shouldn't already operate with deleted tables.

**: Not all ALTER TABLE operations take ACCESS EXCLUSIVE lock, but all current django's migrations take it https://github.com/django/django/blob/master/django/db/backends/base/schema.py, https://github.com/django/django/blob/master/django/db/backends/postgresql/schema.py and https://www.postgresql.org/docs/current/static/sql-altertable.html.

***: Django doesn't have VALIDATE CONSTRAINT logic, but we will use it for some cases.

Business logic locks

lock operations conflict with lock conflict with operations
ACCESS SHARE SELECT ACCESS EXCLUSIVE ALTER TABLE, DROP INDEX
ROW SHARE SELECT FOR UPDATE ACCESS EXCLUSIVE, EXCLUSIVE ALTER TABLE, DROP INDEX
ROW EXCLUSIVE INSERT, UPDATE, DELETE ACCESS EXCLUSIVE, EXCLUSIVE, SHARE ROW EXCLUSIVE, SHARE ALTER TABLE, DROP INDEX, CREATE INDEX

So you can find that all django schema changes for exist table conflicts with business logic, but fortunately they are safe or has safe alternative in general.

Postgres row level locks

As business logic mostly works with table rows it's also important to understand lock conflicts on row level https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS:

lock FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE X
FOR SHARE X X
FOR NO KEY UPDATE X X X
FOR UPDATE X X X X

Main point there is if you have two transactions that update one row, then second transaction will wait until first will be completed. So for business logic and data migrations better to avoid updates for whole table and use batch operations instead.

NOTE: batch operations also can work faster because postgres can use more optimal execution plan with indexes for small data range.

Transactions FIFO waiting

postgres FIFO

Found same diagram in interesting article http://pankrat.github.io/2015/django-migrations-without-downtimes/.

In this diagram we can extract several metrics:

  1. operation time - time spent changing schema, in the case of long running operations on many rows tables like CREATE INDEX or ALTER TABLE ADD COLUMN SET DEFAULT, so you need a safe equivalent.
  2. waiting time - your migration will wait until all transactions complete, so there is issue for long running operations/transactions like analytic, so you need avoid it or disable during migration.
  3. queries per second + execution time and connections pool - if executing many queries, especially long running ones, they can consume all available database connections until the lock is released, so you need different optimizations there: run migrations when least busy, decrease query count and execution time, split data.
  4. too many operations in one transaction - you have issues in all previous points for one operation so if you have many operations in one transaction then you have more likelihood to get this issue, so you need avoid too many simultaneous operations in a single transaction (or even not run it in a transaction at all but being careful when an operation fails).

Dealing with timeouts

Postgres has two settings to dealing with waiting time and operation time presented in diagram: lock_timeout and statement_timeout.

SET lock_timeout TO '2s' allow you to avoid downtime when you have long running query/transaction before run migration (https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT).

SET statement_timeout TO '2s' allow you to avoid downtime when you have long running migration query (https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT).

Deadlocks

There no downtime issues for deadlocks, but too many operations in one transaction can take most conflicted lock and release it only after transaction commit or rollback. So it's a good idea to avoid ACCESS EXCLUSIVE lock operations and long time operations in one transaction. Deadlocks also can make you migration stuck on production deployment when different tables will be locked, for example, for FOREIGN KEY that take ACCESS EXCLUSIVE lock for two tables.

Rows and values storing

Postgres store values of different types different ways. If you try to convert one type to another and it stored different way then postgres will rewrite all values. Fortunately some types stored same way and postgres need to do nothing to change type, but in some cases postgres need to check that all values have same with new type limitations, for example string length.

Multiversion Concurrency Control

Regarding documentation https://www.postgresql.org/docs/current/static/mvcc-intro.html data consistency in postgres is maintained by using a multiversion model. This means that each SQL statement sees a snapshot of data. It has advantage for adding and deleting columns without any indexes, constrains and defaults do not change exist data, new version of data will be created on INSERT and UPDATE, delete just mark you record expired. All garbage will be collected later by VACUUM or AUTO VACUUM.

Django migrations hacks

Any schema changes can be processed with creation of new table and copy data to it, but it can take significant time.

# name safe safe alternative description
1 CREATE SEQUENCE X safe operation, because your business logic shouldn't operate with new sequence on migration time *
2 DROP SEQUENCE X safe operation, because your business logic shouldn't operate with this sequence on migration time *
3 CREATE TABLE X safe operation, because your business logic shouldn't operate with new table on migration time *
4 DROP TABLE X safe operation, because your business logic shouldn't operate with this table on migration time *
5 ALTER TABLE RENAME TO use updatable view unsafe operation, because it's too hard write business logic that operate with two tables simultaneously, so propose to use temporary updatable view and switch names in transaction *
6 ALTER TABLE SET TABLESPACE add new table and copy data unsafe operation, but probably you don't need it at all or often *
7 ALTER TABLE ADD COLUMN X safe operation if without SET NOT NULL, SET DEFAULT, PRIMARY KEY, UNIQUE *
8 ALTER TABLE ADD COLUMN SET DEFAULT add column and set default unsafe operation, because you spend time in migration to populate all values in table, so propose ALTER TABLE ADD COLUMN and then populate column and then SET DEFAULT *
9 ALTER TABLE ADD COLUMN SET NOT NULL +/- unsafe operation, because doesn't work without SET DEFAULT or after migration old code can insert rows without new column and raise exception, so propose ALTER TABLE ADD COLUMN and then populate column and then ALTER TABLE ALTER COLUMN SET NOT NULL * and **
10 ALTER TABLE ADD COLUMN PRIMARY KEY add index and add constraint unsafe operation, because you spend time in migration to CREATE INDEX, so propose ALTER TABLE ADD COLUMN and then CREATE INDEX CONCURRENTLY and then ALTER TABLE ADD CONSTRAINT PRIMARY KEY USING INDEX ***
11 ALTER TABLE ADD COLUMN UNIQUE add index and add constraint unsafe operation, because you spend time in migration to CREATE INDEX, so propose ALTER TABLE ADD COLUMN and then CREATE INDEX CONCURRENTLY and then ALTER TABLE ADD CONSTRAINT UNIQUE USING INDEX ***
12 ALTER TABLE ALTER COLUMN TYPE +/- unsafe operation, because you spend time in migration to check that all items in column valid or to change type, but some operations can be safe ****
13 ALTER TABLE ALTER COLUMN SET NOT NULL add check constraint before unsafe operation, because you spend time in migration to check that all items in column NOT NULL, so propose ALTER TABLE ADD CONSTRAINT CHECK and then ALTER TABLE VALIDATE CONSTRAINT and then ALTER TABLE ALTER COLUMN SET NOT NULL **
14 ALTER TABLE ALTER COLUMN DROP NOT NULL X safe operation
15 ALTER TABLE ALTER COLUMN SET DEFAULT X safe operation
16 ALTER TABLE ALTER COLUMN DROP DEFAULT X safe operation
17 ALTER TABLE DROP COLUMN X safe operation, because your business logic shouldn't operate with this column on migration time, however better ALTER TABLE ALTER COLUMN DROP NOT NULL, ALTER TABLE DROP CONSTRAINT and DROP INDEX before * and *****
18 ALTER TABLE RENAME COLUMN use updatable view unsafe operation, because it's too hard write business logic that operate with two columns simultaneously, so propose to use temporary updatable view and switch names in transaction *
19 ALTER TABLE ADD CONSTRAINT CHECK add as not valid and validate unsafe operation, because you spend time in migration to check constraint
20 ALTER TABLE DROP CONSTRAINT (CHECK) X safe operation
21 ALTER TABLE ADD CONSTRAINT FOREIGN KEY add as not valid and validate unsafe operation, because you spend time in migration to check constraint, lock two tables
22 ALTER TABLE DROP CONSTRAINT (FOREIGN KEY) X safe operation, lock two tables
23 ALTER TABLE ADD CONSTRAINT PRIMARY KEY add index and add constraint unsafe operation, because you spend time in migration to create index ***
24 ALTER TABLE DROP CONSTRAINT (PRIMARY KEY) X safe operation ***
25 ALTER TABLE ADD CONSTRAINT UNIQUE add index and add constraint unsafe operation, because you spend time in migration to create index ***
26 ALTER TABLE DROP CONSTRAINT (UNIQUE) X safe operation ***
27 ALTER TABLE ADD CONSTRAINT EXCLUDE add new table and copy data
28 ALTER TABLE DROP CONSTRAINT (EXCLUDE) X
29 CREATE INDEX CREATE INDEX CONCURRENTLY unsafe operation, because you spend time in migration to create index
30 DROP INDEX X DROP INDEX CONCURRENTLY safe operation ***
31 CREATE INDEX CONCURRENTLY X safe operation
32 DROP INDEX CONCURRENTLY X safe operation ***

*: main point with migration on production without downtime that your old and new code should correctly work before and after migration, lets look this point closely in Dealing with logic that should work before and after migration section.

**: postgres will check that all items in column NOT NULL that take time, lets look this point closely in Dealing with NOT NULL constraint section.

***: postgres will have same behaviour when you skip ALTER TABLE ADD CONSTRAINT UNIQUE USING INDEX and still unclear difference with CONCURRENTLY except difference in locks, lets look this point closely in Dealing with UNIQUE constraint.

****: lets look this point closely in Dealing with ALTER TABLE ALTER COLUMN TYPE section.

*****: if you check migration on CI with python manage.py makemigrations --check you can't drop column in code without migration creation, so in this case you can be useful back migration flow: apply code on all instances and then migrate database

Dealing with logic that should work before and after migration

Adding and removing models and columns

Migrations: CREATE SEQUENCE, DROP SEQUENCE, CREATE TABLE, DROP TABLE, ALTER TABLE ADD COLUMN, ALTER TABLE DROP COLUMN.

This migrations are pretty safe, because your logic doesn't work with this data before migration

Rename models

Migrations: ALTER TABLE RENAME TO.

Standard django's approach does not allow to operate simultaneously for old and new code with old and new table name, hopefully next workaround allows to rename table by splitting migration to few steps:

  1. provide code changes but replace standard migration with SeparateDatabaseAndState sql operation that in transaction rename table and create updatable view that has old table name
    • old code can work with updatable view by old name
    • new code can work with table by new name
  2. after new code deployment old code is not used anymore, so we can drop view
    • new code can work with renamed table
Rename columns

Migrations: ALTER TABLE RENAME COLUMN.

Standard django's approach does not allow to operate simultaneously for old and new code with old and new column name, hopefully next workaround allows to rename column by splitting migration to few steps:

  1. provide code changes but replace standard migration with SeparateDatabaseAndState sql operation that in transaction rename column, rename table to temporary and create updatable view that has old table name with both old and new columns
  2. after new code deployment old code is not used anymore, so in transaction we can drop view and rename table back
    • new code can work with renamed column
Changes for working logic

Migrations: ALTER TABLE SET TABLESPACE, ALTER TABLE ADD CONSTRAINT EXCLUDE.

For this migration too hard implement logic that will work correctly for all instances, so there are two ways to dealing with it:

  1. create new table, copy exist data, drop old table
  2. downtime
Create column with default

Migrations: ALTER TABLE ADD COLUMN SET DEFAULT.

Standard django's behaviour for creation column with default is populate all values with default. Django don't use database defaults permanently, so when you add new column with default django will create column with default and drop this default at once, eg. new default will come from django code. In this case you can have a gap when migration applied by not all instances has updated and at this moment new rows in table will be without default and probably you need update nullable values after that. So to avoid this case best way is avoid creation column with default and split column creation (with default for new rows) and data population to two migrations (with deployments).

Dealing with NOT NULL constraint

Postgres check that all column items NOT NULL when you applying NOT NULL constraint, for postgres 12 and newest it doesn't make this check if appropriate CHECK CONSTRAINT exists, but for older versions you can't defer this check as for NOT VALID. Fortunately we have some hacks and alternatives there for old postgres versions.

  1. Run migrations when load minimal to avoid negative affect of locking.
  2. SET statement_timeout and try to set NOT NULL constraint for small tables.
  3. Use CHECK (column IS NOT NULL) constraint instead that support NOT VALID option with next VALIDATE CONSTRAINT, see article for details https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c. There are additionally can be applied NOT NULL constraint via direct pg_catalog.pg_attribute attnotnull update, but it require superuser permissions.

Dealing with UNIQUE constraint

Postgres has two approaches for uniqueness: CREATE UNIQUE INDEX and ALTER TABLE ADD CONSTRAINT UNIQUE - both use unique index inside. Difference that we can find that we cannot apply DROP INDEX CONCURRENTLY for constraint. However it still unclear what difference for DROP INDEX and DROP INDEX CONCURRENTLY except difference in locks, but as we seen before both marked as safe - we don't spend time in DROP INDEX, just wait for lock. So as django use constraint for uniqueness we also have a hacks to use constraint safely.

Dealing with ALTER TABLE ALTER COLUMN TYPE

Next operations are safe:

  1. varchar(LESS) to varchar(MORE) where LESS < MORE
  2. varchar(ANY) to text
  3. numeric(LESS, SAME) to numeric(MORE, SAME) where LESS < MORE and SAME == SAME

For other operations propose to create new column and copy data to it. Eg. some types can be also safe, but you should check yourself.

django-pg-zero-downtime-migrations's People

Contributors

browniebroke avatar logston avatar morenoh149 avatar tbicr avatar tony avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-pg-zero-downtime-migrations's Issues

migrations.DeleteModel locks DB when it has ForeignKey field

Describe the bug
When dropping a table with a foreign key (pointing to another table) Postgres will lock the foreign table as well as the table being dropped. This causes all queries to be blocked by the operation.

Here is an article describing the problem:
http://db-oriented.com/2018/04/18/excessive-locking-when-dropping-a-table-in-11g/

3 Options for a fix here:

  1. Throw an error when a table contains foreign keys (forcing foreign keys to be dropped in a different operation)
  2. Rewrite the SQL operation in DeleteModel to drop foreign keys first
  3. Add lock_timeout to DROP TABLE operation

To Reproduce

  1. What model did you have?
class ModelA(models.Model):
  pass

class ModelB(models.Model):
  related_a = models.ForeignKey(ModelA)
  1. How did you change the model?
    Removed ModelB
  2. What migration were generated?
class Migration(migrations.Migration):	
    dependencies = [...]	

    operations = [	
        migrations.DeleteModel(name="ModelB"),
    ]
  1. What SQL was executed?
DROP TABLE model_b CASCADE;
  1. What issue did you get?
    Locked model_b table (expected) and locked model_a for 8 minutes

Expected behavior
Not lock model_a or throw a lock timeout after 2 seconds (ZERO_DOWNTIME_MIGRATIONS_LOCK_TIMEOUT)

Versions:

  • Postgres: 10.6
  • Python: 3.6
  • Django: 2.2.13
  • django-pg-zero-downtime-migrations library: 0.8

Flag for deeming CREATE INDEX operation unsafe

Currently when CREATE INDEX operation is detected, it is quietly replaced with CREATE INDEX CONCURRENTLY. It would be super useful to have a flag in settings, which would allow to choose whether the user wants backend to do that or just to consider this operation unsafe and possibly raise an exception (if an appropriate flag is set).

Unchanged `db_table` in `RenameModel` raises `Unsafe.ALTER_TABLE_RENAME`

Describe the bug

Renaming a model with a fixed table name (e.g. Book to OldBook) will lead to a migration that raises ALTER_TABLE_RENAME, when no schema changes occur.

To Reproduce

  1. What model did you have?

Django model with Meta.db_table = 'table_name' hard coded

from django.db import models
class Book(models.Model):
    # any fields
    name = models.CharField(max_length=128, blank=True)
    class Meta:
        db_table = 'books`
  1. How did you change the model?

Renaming the class from Book to OldBook will lead to a migration that raises ALTER_TABLE_RENAME, when in actuality no schema changes occur. (Correct me if I'm mistaken!)


โฏ poetry run ./manage.py makemigrations
Did you rename the book.Book model to OldBook? [y/N] y                                                                                                                                                           Migrations for 'book':
  src/project/book/migrations/0004_auto_20211230_2157.py                                                                                                                                                                             - Rename model Book to OldBook
  1. What migration were generated?
# Generated by Django 2.2.1 on 2021-12-30 22:18

from django.conf import settings
from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('book', '0002_auto_20201018_0102'),
    ]

    operations = [
        migrations.RenameModel(
            old_name='Book',
            new_name='OldBook',
        ),
    ]
  1. What SQL was executed?
poetry run ./manage.py sqlmigrate book 0004
BEGIN;
--
-- Rename model Book to OldBook
--
COMMIT;
  1. What issue did you get?
Traceback (most recent call last):
  File "./manage.py", line 21, in <module>
    main()
  File "./manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/commands/sqlmigrate.py", line 30, in execute
    return super().execute(*args, **options)
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "~/project/.venv/lib/python3.7/site-packages/django/core/management/commands/sqlmigrate.py", line 64, in handle
    sql_statements = executor.collect_sql(plan)
  File "~/project/.venv/lib/python3.7/site-packages/django/db/migrations/executor.py", line 225, in collect_sql
    state = migration.apply(state, schema_editor, collect_sql=True)
  File "~/project/.venv/lib/python3.7/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "~/project/.venv/lib/python3.7/site-packages/django/db/migrations/operations/models.py", line 353, in database_forwards
    new_model._meta.db_table,
  File "~/project/.venv/lib/python3.7/site-packages/django_zero_downtime_migrations/backends/postgres/schema.py", line 394, in alter_db_table
    raise UnsafeOperationException(Unsafe.ALTER_TABLE_RENAME)
django_zero_downtime_migrations.backends.postgres.schema.UnsafeOperationException: ALTER TABLE RENAME is unsafe operation
See details for save alternative https://github.com/tbicr/django-pg-zero-downtime-migrations#changes-for-working-logic

Expected behavior
When database tables are unaltered, don't raise

Versions:

  • Postgres: 12
  • Python: 3.7
  • Django: 2.2
  • django-pg-zero-downtime-migrations library: 0.10

DatabaseSchemaEditorMixin._alter_column_type_sql() takes 5 positional arguments but 7 were given

We try to applying this default django migration: auth.0002_alter_permission_name_max_length
and catched traseback:
File "/usr/local/lib/python3.10/site-packages/django/core/management/init.py", line 442, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.10/site-packages/django/core/management/init.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 412, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 458, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 106, in wrapper
res = handle_func(*args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/core/management/commands/migrate.py", line 356, in handle
post_migrate_state = executor.migrate(
File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 135, in migrate
state = self._migrate_all_forwards(
File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
state = self.apply_migration(
File "/usr/local/lib/python3.10/site-packages/django/db/migrations/executor.py", line 252, in apply_migration
state = migration.apply(state, schema_editor)
File "/usr/local/lib/python3.10/site-packages/django/db/migrations/migration.py", line 132, in apply
operation.database_forwards(
File "/usr/local/lib/python3.10/site-packages/django/db/migrations/operations/fields.py", line 235, in database_forwards
schema_editor.alter_field(from_model, from_field, to_field)
File "/usr/local/lib/python3.10/site-packages/django_zero_downtime_migrations/backends/postgres/schema.py", line 387, in alter_field
super().alter_field(model, old_field, new_field, strict)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/schema.py", line 830, in alter_field
self._alter_field(
File "/usr/local/lib/python3.10/site-packages/django/db/backends/postgresql/schema.py", line 287, in _alter_field
super()._alter_field(
File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/schema.py", line 1010, in _alter_field
fragment, other_actions = self._alter_column_type_sql(
TypeError: DatabaseSchemaEditorMixin._alter_column_type_sql() takes 5 positional arguments but 7 were given

Versions:

  • Postgres==13:
  • Python==3.10:
  • Django==4.2.2:
  • django-pg-zero-downtime-migrations==0.12:

Add column and set default may be a safe action

Under the "Django Migration Hacks" section the README on Row 8, it considers ALTER TABLE ADD COLUMN SET DEFAULT to be unsafe.

However, since Postgres 11, it could be considered safe (so long as the default value is not volatile) as suggested here: https://www.postgresql.org/docs/current/ddl-alter.html (under the "Tip" section).

Can this be investigated and updated, if needed? Alternatively, if it is still considered unsafe, could clarification be added?

Thank you in advance!

Postgis support

I wanted to give this library a try on our project, but it immediately broke our tests as we use PostGIS. I haven't tried anything yet, but we could probably solve this through inheritance in our project.

Would it be a welcome contribution here or would you rather keep this separate? Maybe we could split the main functionality through a Mixin, which would help reuse.

I'm not super familiar with DB backends internal, so please let me know if this doesn't sound feasible or if you see a better solution. Otherwise, I can try putting something together...

Django 2.2 and PostgreSQL 11

First thanks for this project!

It'd be great to see if the warnings/errors scheme on unsafe operation is something that could be integrated to Django's core.

I just wanted to give you a small heads up that the DatabaseSchemaEditor schema editor will change a bit in Django 2.2 to allow for check and unique constraint additions through the Meta.constraints API. Most of the details are in django/django#10406

Also PostgreSQL 11 allows non locking NOT NULL column addition so it'd be great to adjust the documentation in consequence and use pg_version detection to avoid raising an exception/warning when not necessary.

Using django-pg-zero-downtime-migrations our table is locked

Hi,

Thanks for your project!

We think we have same problem. We have a model (MyModel) with 1.000.000 of rows. And we add a field with default value we have a lock during 30-120 seconds. Something like this:

class MyModel(models.Model):
        ...
        # Adding a new field
        x = models.CharField(max_length=250, blank=True, default='XX')
        ...

And its migration:

class Migration(migrations.Migration):

    dependencies = [
        ('api', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='mymodel',
            name='x',
            field=models.CharField(blank=True, default='XX', max_length=250),
        ),
    ]

We have done a lot of tests, e.g.: we have created fields without default, but really default is "", and we get the same problem.

Currently, it is our workaround:

  1. We create every field with null=True and default=None. So PostgreSQL don't have to write in every row anything.
  2. We migrate via a django command every row of "MyModel".
  3. We update code, and remove null=False and adding default="XXX"
  4. We update the code in production enviroment
  5. We migrate via a django command every new row of "MyModel".
  6. We generate new migration to add "not null" constrain
  7. We run last migration (6 point)

When we find your solution we were very happy, our workaround is very tricky, but your solutions does not work for us. We get a little more time locked (35-125 seconds) instead of get zero downtime.

It is our settings:

DATABASES = {
    'default': {
        'ENGINE': 'django_zero_downtime_migrations_postgres_backend',
        #'ENGINE': 'django.contrib.gis.db.backends.postgis',
        ...
    }
}

ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE = True

ZERO_DOWNTIME_MIGRATIONS_USE_NOT_NULL = False

Please, could you tell us what is wrong in our case?
Please, could you upload a little project example?

Thanks another time,

AttributeError on django v3.2

Describe the bug
I'm getting the following error when running a migration with django v3.2:

AttributeError: 'DatabaseSchemaEditor' object has no attribute 'skip_default_on_alter'

Seems like in v0.11 there was this condition. I'm wondering if this should be checking django < 4 instead:

In more recent versions the condition is removed completely but there's still a reference to skip_default_on_alter

To Reproduce
Sorry don't have a reproducible example but I'm hoping the root cause will be clear.

Expected behavior
Migration succeeds

Versions:

  • Postgres: 13
  • Python: 3.10
  • Django: 3.2
  • django-pg-zero-downtime-migrations library: 0.11, 0.14

When migrating DB from scratch, early ran deferred SQL queries crash

Describe the bug

When we are creating and migrating a database from scratch, models that have foreign keys to other models use the deferred SQL statements.
This ensures that the target table was created before running the deferred SQL statements.

To Reproduce

Take basic installed apps:

INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
...

and run test with them:
It will crash with:

django.db.utils.ProgrammingError: relation "django_content_type" does not exist

So manage.py test creates the test database with a migrate --run-syncdb command.

This usually starts by creating all models, then add the FK constraints (as deferred statements.
But with this package, we create the auth_permission table, and then add the constraints to django_content_type => which fails ๐Ÿ˜ข
Related to this piece of code:

def _flush_deferred_sql(self):
"""As some alternative sql use deferred sql and deferred sql run after all operations in miration module
so good idea to run deferred sql as soon as possible to provide similar as possible state
between operations in migration module."""
for sql in self.deferred_sql:
self.execute(sql)
self.deferred_sql.clear()
def create_model(self, model):
super().create_model(model)
self._flush_deferred_sql()

For the example, a fix could be to swap the two Django apps - but the bug also occurs when both models are inside the same app.
I believe this could also happen with a migration adding two models, where one has a FK to another. So perhaps not only when migrating from scratch.

Expected behavior
To not crash :D

One idea, would somehow not to immediately run deferred SQL statements when migrating from scratch the databases
I'm also curious about other possible workaround.

Versions:

  • Postgres: 14.0
  • Python: 3.10
  • Django: 3.2
  • django-pg-zero-downtime-migrations library: 0.13

Django 4.1: `'DatabaseSchemaEditor'` has no attribute `'sql_create_sequence'`

Describe the bug

/.venv/lib/python3.10/site-packages/django_zero_downtime_migrations/backends/postgres/schema.py", line 153, in DatabaseSchemaEditorMixin
    sql_create_sequence = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_create_sequence, use_timeouts=False)
AttributeError: type object 'DatabaseSchemaEditor' has no attribute 'sql_create_sequence'

django_zero_downtime_migrations/backends/postgres/schema.py#L153

Removed in django 4.1 in django/django@2eea361

To Reproduce

  1. What model did you have?

https://github.com/django/django/blob/4.1.2/django/contrib/auth/base_user.py#L49

class AbstractBaseUser(models.Model):
    password = models.CharField(_("password"), max_length=128)
    last_login = models.DateTimeField(_("last login"), blank=True, null=True)

    is_active = True

    REQUIRED_FIELDS = []
  1. How did you change the model? Did not
  2. What migration were generated? None (from what I see)
  3. What SQL was executed? N/A
  4. What issue did you get?
File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/contrib/auth/models.py", line 3, in <module>
    from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/contrib/auth/base_user.py", line 49, in <module>
    class AbstractBaseUser(models.Model):
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/db/models/base.py", line 141, in __new__
    new_class.add_to_class("_meta", Options(meta, app_label))
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/db/models/base.py", line 369, in add_to_class
    value.contribute_to_class(cls, name)
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/db/models/options.py", line 231, in contribute_to_class
    self.db_table, connection.ops.max_name_length()
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/utils/connection.py", line 15, in __getattr__
    return getattr(self._connections[self._alias], item)
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/utils/connection.py", line 62, in __getitem__
    conn = self.create_connection(alias)
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/db/utils.py", line 193, in create_connection
    backend = load_backend(db["ENGINE"])
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django/db/utils.py", line 113, in load_backend
    return import_module("%s.base" % backend_name)
  File "/opt/hostedtoolcache/Python/3.10.1/x64/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django_zero_downtime_migrations/backends/postgres/base.py", line 5, in <module>
    from .schema import DatabaseSchemaEditor
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django_zero_downtime_migrations/backends/postgres/schema.py", line 144, in <module>
    class DatabaseSchemaEditorMixin:
  File "/home/runner/work/project/.venv/lib/python3.10/site-packages/django_zero_downtime_migrations/backends/postgres/schema.py", line 153, in DatabaseSchemaEditorMixin
    sql_create_sequence = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_create_sequence, use_timeouts=False)
AttributeError: type object 'DatabaseSchemaEditor' has no attribute 'sql_create_sequence'
Traceback (most recent call last):
  File "/home/runner/work/project/scan_migrations.py", line 120, in <module>
    exit_code = check_migrations()
  File "/home/runner/work/project/scan_migrations.py", line 96, in check_migrations
    if should_skip_migrations():
  File "/home/runner/work/project/scan_migrations.py", line 35, in should_skip_migrations
    migrations = subprocess.check_output(
  File "/opt/hostedtoolcache/Python/3.10.1/x64/lib/python3.10/subprocess.py", line 420, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/opt/hostedtoolcache/Python/3.10.1/x64/lib/python3.10/subprocess.py", line 524, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command 'poetry run ./manage.py migrate --plan' returned non-zero exit status 1.

Expected behavior

Shouldn't fail when checking migrations

Versions:

  • Postgres: 13
  • Python: 3.11
  • Django: 3.1.12
  • django-pg-zero-downtime-migrations library: 0.11

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.