diff --git a/CHANGELOG.md b/CHANGELOG.md index 986ddd56..767ea2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ List of the most important changes for each release. +## 0.6.10 +- Fixes Django migration issue introduced in 0.6.7 allowing nullable fields with PostgreSQL backends + ## 0.6.9 - Fixes un-ordered selection of buffers during sync which can allow duplicates to be synced with PostgreSQL backends - Moves updating of database counters to occur in the same DB transaction as updates to the Store diff --git a/morango/__init__.py b/morango/__init__.py index 67fbfc90..096c6aae 100644 --- a/morango/__init__.py +++ b/morango/__init__.py @@ -3,4 +3,4 @@ from __future__ import unicode_literals default_app_config = "morango.apps.MorangoConfig" -__version__ = "0.6.9" +__version__ = "0.6.10" diff --git a/morango/migrations/0020_postgres_fix_nullable.py b/morango/migrations/0020_postgres_fix_nullable.py new file mode 100644 index 00000000..bcacf178 --- /dev/null +++ b/morango/migrations/0020_postgres_fix_nullable.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2022-01-13 18:07 +from __future__ import unicode_literals + +from django.db import migrations + + +def apply(apps, schema_editor): + # sqlite does not allow ALTER COLUMN, but also isn't affected by this issue + if "postgresql" in schema_editor.connection.vendor: + schema_editor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage DROP NOT NULL") + schema_editor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage_status DROP NOT NULL") + + +def revert(apps, schema_editor): + # sqlite does not allow ALTER COLUMN, but also isn't affected by this issue + if "postgresql" in schema_editor.connection.vendor: + schema_editor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage SET NOT NULL") + schema_editor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage_status SET NOT NULL") + + +class Migration(migrations.Migration): + """ + Applies nullable change made to 0018_auto_20210714_2216.py after it was released + """ + + dependencies = [ + ("morango", "0019_auto_20220113_1807"), + ] + + operations = [ + migrations.RunPython(apply, reverse_code=revert), + ] diff --git a/tests/testapp/tests/helpers.py b/tests/testapp/tests/helpers.py index 46603e54..e74f7afe 100644 --- a/tests/testapp/tests/helpers.py +++ b/tests/testapp/tests/helpers.py @@ -6,6 +6,9 @@ import factory import mock +from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.test import TestCase from django.test.testcases import LiveServerTestCase from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone @@ -442,3 +445,38 @@ def stage_status(self): def update_state(self, stage=None, stage_status=None): self._stage = stage or self._stage self._stage_status = stage_status or self._stage_status + + +class TestMigrations(TestCase): + # Modified from https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ + + migrate_from = None + migrate_to = None + app = None + + def setUp(self): + assert ( + self.migrate_from and self.migrate_to + ), "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ + ) + + migrate_from = [(self.app, self.migrate_from)] + migrate_to = [(self.app, self.migrate_to)] + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(migrate_from).apps + + # Reverse to the original migration + executor.migrate(migrate_from) + + self.setUpBeforeMigration(old_apps) + + # Run the migration to test + executor = MigrationExecutor(connection) + executor.loader.build_graph() # reload. + executor.migrate(migrate_to) + + self.apps = executor.loader.project_state(migrate_to).apps + + def setUpBeforeMigration(self, apps): + pass diff --git a/tests/testapp/tests/test_migrations.py b/tests/testapp/tests/test_migrations.py new file mode 100644 index 00000000..d6673452 --- /dev/null +++ b/tests/testapp/tests/test_migrations.py @@ -0,0 +1,54 @@ +import uuid + +import pytest + +from django.conf import settings +from django.db import connection +from django.db.utils import IntegrityError +from django.utils import timezone + +from .helpers import TestMigrations + + +@pytest.mark.skipif(not settings.MORANGO_TEST_POSTGRESQL, reason="Only postgres") +class MorangoNullableMigrationTest(TestMigrations): + """ + Test migration that applies nullable status to `transfer_stage` and `transfer_stage_status` + """ + + app = "morango" + migrate_from = "0018_auto_20210714_2216" + migrate_to = "0020_postgres_fix_nullable" + + def setUpBeforeMigration(self, apps): + # simulate as if 0018_auto_20210714_2216 hadn't applied Nullablity to the columns, + # a change which we added after the migration might have run on other + SyncSession = apps.get_model("morango", "SyncSession") + + with connection.cursor() as cursor: + cursor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage SET NOT NULL") + cursor.execute("ALTER TABLE morango_transfersession ALTER COLUMN transfer_stage_status SET NOT NULL") + + self.sync_session = SyncSession.objects.create( + id=uuid.uuid4().hex, + profile="facilitydata", + last_activity_timestamp=timezone.now(), + ) + + def test_nullable(self): + TransferSession = self.apps.get_model("morango", "TransferSession") + + try: + transfer_session = TransferSession.objects.create( + id=uuid.uuid4().hex, + sync_session_id=self.sync_session.id, + push=True, + last_activity_timestamp=timezone.now(), + transfer_stage=None, + transfer_stage_status=None, + ) + except IntegrityError: + self.fail("Couldn't create TransferSession with nullable fields") + + self.assertIsNone(transfer_session.transfer_stage) + self.assertIsNone(transfer_session.transfer_stage_status)