Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop support for Python 3.6-3.7, Django 3.2-4.1 #69

Merged
merged 9 commits into from
Feb 15, 2025
Merged
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ jobs:
- name: checkout code
uses: actions/checkout@v2
- name: run checks
run: docker-compose run --rm django-pg-zero-downtime-migrations-tests tox
run: docker compose run --rm django-pg-zero-downtime-migrations-tests tox
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# django-pg-zero-downtime-migrations changelog

## 0.17
- dropped support for Python 3.6 and 3.7
- dropped support for Django 3.2, 4.0 and 4.1

## 0.16
- changed `ADD COLUMN DEFAULT NULL` to safe operation for code default
- changed `ADD COLUMN DEFAULT NOT NULL` to safe operation for `db_default` in django 5.0+
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ RUN apt-get install -q -y --no-install-recommends software-properties-common git
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get update
RUN apt-get install -q -y --no-install-recommends \
python3.6 \
python3.7 python3.7-distutils \
python3.8 python3.8-distutils \
python3.9 python3.9-distutils \
python3.10 python3.10-distutils \
Expand Down
2 changes: 1 addition & 1 deletion django_zero_downtime_migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.16"
__version__ = "0.17"
188 changes: 33 additions & 155 deletions django_zero_downtime_migrations/backends/postgres/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from django.db.backends.postgresql.schema import (
DatabaseSchemaEditor as PostgresDatabaseSchemaEditor
)
from django.db.backends.utils import strip_quotes
from django.db.models import NOT_PROVIDED


Expand Down Expand Up @@ -254,17 +253,12 @@ class DatabaseSchemaEditorMixin:
"AND convalidated"
)

if django.VERSION[:2] >= (4, 1):
sql_alter_sequence_type = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_alter_sequence_type)
sql_add_identity = PGAccessExclusive(
PostgresDatabaseSchemaEditor.sql_add_identity,
idempotent_condition=Condition(_sql_identity_exists, False),
)
sql_drop_indentity = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_drop_indentity)
else:
sql_alter_sequence_type = PGAccessExclusive("ALTER SEQUENCE IF EXISTS %(sequence)s AS %(type)s")
sql_create_sequence = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_create_sequence)
sql_set_sequence_owner = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_set_sequence_owner)
sql_alter_sequence_type = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_alter_sequence_type)
sql_add_identity = PGAccessExclusive(
PostgresDatabaseSchemaEditor.sql_add_identity,
idempotent_condition=Condition(_sql_identity_exists, False),
)
sql_drop_indentity = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_drop_indentity)
sql_delete_sequence = PGAccessExclusive(PostgresDatabaseSchemaEditor.sql_delete_sequence)
sql_create_table = PGAccessExclusive(
PostgresDatabaseSchemaEditor.sql_create_table,
Expand Down Expand Up @@ -447,9 +441,8 @@ class DatabaseSchemaEditorMixin:
PostgresDatabaseSchemaEditor.sql_delete_index_concurrently
)

if django.VERSION[:2] >= (4, 2):
sql_alter_table_comment = PGShareUpdateExclusive(PostgresDatabaseSchemaEditor.sql_alter_table_comment)
sql_alter_column_comment = PGShareUpdateExclusive(PostgresDatabaseSchemaEditor.sql_alter_column_comment)
sql_alter_table_comment = PGShareUpdateExclusive(PostgresDatabaseSchemaEditor.sql_alter_table_comment)
sql_alter_column_comment = PGShareUpdateExclusive(PostgresDatabaseSchemaEditor.sql_alter_column_comment)

_sql_column_not_null = MultiStatementSQL(
PGAccessExclusive(
Expand Down Expand Up @@ -845,56 +838,16 @@ def _add_column_primary_key(self, model, field):
return ""

def _add_column_unique(self, model, field):
if django.VERSION[:2] >= (4, 0):
self.deferred_sql.append(self._create_unique_sql(model, [field]))
else:
self.deferred_sql.append(self._create_unique_sql(model, [field.column]))
self.deferred_sql.append(self._create_unique_sql(model, [field]))
return ""

if django.VERSION[:2] <= (3, 2):
def skip_default_on_alter(self, field):
"""
Some backends don't accept default values for certain columns types
(i.e. MySQL longtext and longblob) in the ALTER COLUMN statement.
"""
return False

def column_sql(self, model, field, include_default=False):
"""
Return the column definition for a field. The field must already have
had set_attributes_from_name() called.
"""
if not include_default:
return super().column_sql(model, field, include_default)

# Get the column's type and use that as the basis of the SQL.
field_db_params = field.db_parameters(connection=self.connection)
column_db_type = field_db_params["type"]
# Check for fields that aren't actually columns (e.g. M2M).
if column_db_type is None:
return None, None
params = []
return (
" ".join(
# This appends to the params being returned.
self._iter_column_sql(
column_db_type,
params,
model,
field,
include_default,
)
),
params,
)

def _patched_iter_column_sql(
self, column_db_type, params, model, field, field_db_params, include_default
):
yield column_db_type
if field_db_params.get("collation"):
yield self._collate_sql(field_db_params.get("collation"))
if django.VERSION >= (4, 2) and self.connection.features.supports_comments_inline and field.db_comment:
if self.connection.features.supports_comments_inline and field.db_comment:
yield self._comment_sql(field.db_comment)
# Work out nullability.
null = field.null
Expand Down Expand Up @@ -955,49 +908,27 @@ def _patched_iter_column_sql(
):
yield self.connection.ops.tablespace_sql(tablespace, inline=True)

if django.VERSION >= (4, 1):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was automatable with django-upgrade, unlike the other branches where the version is specified as django.VERSION[:2]

probably one to keep in mind for the future where adding these kind of branches: https://github.com/adamchainz/django-upgrade?tab=readme-ov-file#versioned-blocks

def _iter_column_sql(
self, column_db_type, params, model, field, field_db_params, include_default
):
if not include_default:
yield from super()._iter_column_sql(
column_db_type,
params,
model,
field,
field_db_params,
include_default,
)
else:
yield from self._patched_iter_column_sql(
column_db_type,
params,
model,
field,
field_db_params,
include_default,
)
else:
def _iter_column_sql(
self, column_db_type, params, model, field, include_default
):
if not include_default:
yield from super()._iter_column_sql(
column_db_type,
params,
model,
field,
include_default,
)
else:
yield from self._patched_iter_column_sql(
column_db_type,
params,
model,
field,
{},
include_default,
)
def _iter_column_sql(
self, column_db_type, params, model, field, field_db_params, include_default
):
if not include_default:
yield from super()._iter_column_sql(
column_db_type,
params,
model,
field,
field_db_params,
include_default,
)
else:
yield from self._patched_iter_column_sql(
column_db_type,
params,
model,
field,
field_db_params,
include_default,
)

def _alter_column_set_not_null(self, model, new_field):
self.deferred_sql.append(self._sql_column_not_null % {
Expand Down Expand Up @@ -1056,68 +987,15 @@ def _immediate_type_cast(self, old_type, new_type):
return new_type_precision >= old_type_precision and new_type_scale == old_type_scale
return False

if django.VERSION[:2] < (4, 1):
def _get_sequence_name(self, table, column):
with self.connection.cursor() as cursor:
for sequence in self.connection.introspection.get_sequences(cursor, table):
if sequence["column"] == column:
return sequence["name"]
return None

# TODO: after django 4.1 support drop replace *args, **kwargs with original signature
def _alter_column_type_sql(self, model, old_field, new_field, new_type, *args, **kwargs):
def _alter_column_type_sql(self, model, old_field, new_field, new_type, old_collation, new_collation):
old_db_params = old_field.db_parameters(connection=self.connection)
old_type = old_db_params["type"]
if not self._immediate_type_cast(old_type, new_type):
if self.RAISE_FOR_UNSAFE:
raise UnsafeOperationException(Unsafe.ALTER_COLUMN_TYPE)
else:
warnings.warn(UnsafeOperationWarning(Unsafe.ALTER_COLUMN_TYPE))
if django.VERSION[:2] < (4, 1):
# old django versions runs in transaction next queries for autofield type changes:
# - alter column type
# - drop sequence with old type
# - create sequence with new type
# - alter column set default
# - set sequence current value
# - set sequence to field
# if we run this queries without transaction
# then concurrent insertions between drop sequence and end of migration can fail
# so simplify migration to two safe steps: alter colum type and alter sequence type
serial_fields_map = {
"bigserial": "bigint",
"serial": "integer",
"smallserial": "smallint",
}
if new_type.lower() in serial_fields_map:
column = strip_quotes(new_field.column)
table = strip_quotes(model._meta.db_table)
sequence_name = self._get_sequence_name(table, column)
if sequence_name is not None:
using_sql = ""
if self._field_data_type(old_field) != self._field_data_type(new_field):
using_sql = " USING %(column)s::%(type)s"
return (
(
(self.sql_alter_column_type + using_sql)
% {
"column": self.quote_name(column),
"type": serial_fields_map[new_type.lower()],
},
[],
),
[
(
self.sql_alter_sequence_type
% {
"sequence": self.quote_name(sequence_name),
"type": serial_fields_map[new_type.lower()],
},
[],
),
],
)
return super()._alter_column_type_sql(model, old_field, new_field, new_type, *args, **kwargs)
return super()._alter_column_type_sql(model, old_field, new_field, new_type, old_collation, new_collation)


class DatabaseSchemaEditor(DatabaseSchemaEditorMixin, PostgresDatabaseSchemaEditor):
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.7"
services:
pg16:
image: postgres:16-alpine
Expand Down
9 changes: 2 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,19 @@ def _get_long_description():
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Framework :: Django',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
],
keywords='django postgres postgresql migrations',
packages=find_packages(exclude=['manage*', 'tests*']),
python_requires='>=3.6',
python_requires='>=3.8',
install_requires=[
'django>=3.2',
'django>=4.2',
]
)
44 changes: 5 additions & 39 deletions tests/integration/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,6 @@ def test_good_flow_drop_table_with_constraints():


@skip_for_default_django_backend
@pytest.mark.skipif(
django.VERSION[:2] < (4, 0),
reason="django before 4.0 case",
)
@pytest.mark.django_db(transaction=True)
@modify_settings(INSTALLED_APPS={"append": "tests.apps.good_flow_drop_column_with_constraints"})
@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True)
Expand Down Expand Up @@ -499,14 +495,6 @@ def test_idempotency_create_table():
"test_model_id" integer NULL
);
""")
if django.VERSION[:2] < (4, 1):
_create_table_sql = one_line_sql("""
CREATE TABLE "idempotency_create_table_app_relatedtesttable" (
"id" serial NOT NULL PRIMARY KEY,
"test_field_int" integer NULL,
"test_model_id" integer NULL
);
""")
_create_unique_index_sql = one_line_sql("""
CREATE UNIQUE INDEX CONCURRENTLY "idempotency_create_table_app_relatedtesttable_uniq"
ON "idempotency_create_table_app_relatedtesttable" ("test_model_id", "test_field_int");
Expand Down Expand Up @@ -1805,15 +1793,6 @@ def test_idempotency_add_primary_key():
ALTER TABLE "idempotency_add_primary_key_app_relatedtesttable"
DROP COLUMN "id" CASCADE;
""")
_create_unique_index_sql_before_django41 = one_line_sql("""
CREATE UNIQUE INDEX CONCURRENTLY "idempotency_add_primary__test_field_int_e9cebf24_uniq"
ON "idempotency_add_primary_key_app_relatedtesttable" ("test_field_int");
""")
_create_unique_constraint_sql_before_django41 = one_line_sql("""
ALTER TABLE "idempotency_add_primary_key_app_relatedtesttable"
ADD CONSTRAINT "idempotency_add_primary__test_field_int_e9cebf24_uniq"
UNIQUE USING INDEX "idempotency_add_primary__test_field_int_e9cebf24_uniq";
""")
_create_unique_index_sql = one_line_sql("""
CREATE UNIQUE INDEX CONCURRENTLY "idempotency_add_primary_k_test_field_int_e9cebf24_pk"
ON "idempotency_add_primary_key_app_relatedtesttable" ("test_field_int");
Expand Down Expand Up @@ -1865,20 +1844,11 @@ def test_idempotency_add_primary_key():
call_command("migrate", "idempotency_add_primary_key_app", "0001")
with override_settings(ZERO_DOWNTIME_MIGRATIONS_IDEMPOTENT_SQL=False):
migration_sql = call_command("sqlmigrate", "idempotency_add_primary_key_app", "0002")
if django.VERSION[:2] < (4, 1):
assert split_sql_queries(migration_sql) == [
_drop_column_sql,
_create_unique_index_sql_before_django41,
_create_unique_constraint_sql_before_django41,
_create_unique_index_sql,
_create_primary_key_sql,
]
else:
assert split_sql_queries(migration_sql) == [
_drop_column_sql,
_create_unique_index_sql,
_create_primary_key_sql,
]
assert split_sql_queries(migration_sql) == [
_drop_column_sql,
_create_unique_index_sql,
_create_primary_key_sql,
]

# migrate case 1
with override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=False):
Expand Down Expand Up @@ -2022,10 +1992,6 @@ def old_schema_compatible(dump: str) -> str:


@skip_for_default_django_backend
@pytest.mark.skipif(
django.VERSION[:2] < (4, 1),
reason="django after 4.1 case",
)
@pytest.mark.django_db(transaction=True)
@modify_settings(INSTALLED_APPS={"append": "tests.apps.idempotency_add_auto_field_app"})
@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True)
Expand Down
Loading