From fc281d228bece059835de784765760a4730142c9 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sat, 29 Jun 2024 17:29:58 +0100 Subject: [PATCH] Handle null stream fields in stream field migrations --- .../blocks/migrations/migrate_operation.py | 2 + .../streamfield_migrations/test_migrations.py | 53 +++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/wagtail/blocks/migrations/migrate_operation.py b/wagtail/blocks/migrations/migrate_operation.py index 1f2a2b3326..c95c7ed0e4 100644 --- a/wagtail/blocks/migrations/migrate_operation.py +++ b/wagtail/blocks/migrations/migrate_operation.py @@ -110,6 +110,8 @@ class MigrateStreamData(RunPython): updated_model_instances_buffer = [] for instance in model_queryset.iterator(chunk_size=self.chunk_size): + if instance.raw_content is None: + continue revision_query_maker.append_instance_data_for_revision_query(instance) diff --git a/wagtail/tests/streamfield_migrations/test_migrations.py b/wagtail/tests/streamfield_migrations/test_migrations.py index d5ab20c548..37952b46a1 100644 --- a/wagtail/tests/streamfield_migrations/test_migrations.py +++ b/wagtail/tests/streamfield_migrations/test_migrations.py @@ -1,7 +1,8 @@ import datetime import json -from django.db.models import F, JSONField +from django.db import connection +from django.db.models import F, JSONField, TextField from django.db.models.functions import Cast from django.test import TestCase from django.utils import timezone @@ -24,8 +25,8 @@ class BaseMigrationTest(TestCase, MigrationTestMixin): ] app_name = None - def setUp(self): - instances = [ + def _get_test_instances(self): + return [ self.factory( content__0__char1="Test char 1", content__1__char1="Test char 2", @@ -44,6 +45,9 @@ class BaseMigrationTest(TestCase, MigrationTestMixin): ), ] + def setUp(self): + instances = self._get_test_instances() + self.original_raw_data = {} self.original_revisions = {} @@ -209,3 +213,46 @@ class TestPage(BaseMigrationTest): def test_migrate_revisions_from_date(self): self._test_migrate_revisions_from_date() + + +class TestNullStreamField(BaseMigrationTest): + """ + Migrations are processed if the underlying JSON is null. + + This might occur if we're operating on a StreamField that was added to a model that + had existing records. + """ + + model = models.SamplePage + factory = factories.SamplePageFactory + has_revisions = True + app_name = "streamfield_migration_tests" + + def _get_test_instances(self): + return self.factory.create_batch(1, content=None) + + def setUp(self): + super().setUp() + + # Bypass StreamField/StreamBlock processing that cast a None stream field value + # to the empty StreamValue, and set the underlying JSON to null. + with connection.cursor() as cursor: + cursor.execute(f"UPDATE {self.model._meta.db_table} SET content = 'null'") + + def assert_null_content(self): + """ + The raw JSON of all instances for this test is null. + """ + + instances = self.model.objects.all().annotate( + raw_content=Cast(F("content"), TextField()) + ) + + for instance in instances: + with self.subTest(instance=instance): + self.assertEqual(instance.raw_content, "null") + + def test_migrate_stream_data(self): + self.assert_null_content() + self.apply_migration() + self.assert_null_content()