|
@@ -1,14 +1,18 @@
|
|
|
+import math
|
|
|
+
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
|
|
from django.db import IntegrityError, connection, migrations, models, transaction
|
|
|
from django.db.migrations.migration import Migration
|
|
|
from django.db.migrations.operations.fields import FieldOperation
|
|
|
from django.db.migrations.state import ModelState, ProjectState
|
|
|
-from django.db.models.functions import Abs
|
|
|
+from django.db.models.expressions import Value
|
|
|
+from django.db.models.functions import Abs, Pi
|
|
|
from django.db.transaction import atomic
|
|
|
from django.test import (
|
|
|
SimpleTestCase,
|
|
|
ignore_warnings,
|
|
|
override_settings,
|
|
|
+ skipIfDBFeature,
|
|
|
skipUnlessDBFeature,
|
|
|
)
|
|
|
from django.test.utils import CaptureQueriesContext
|
|
@@ -1340,7 +1344,7 @@ class OperationTests(OperationTestBase):
|
|
|
self.assertEqual(operation.describe(), "Add field height to Pony")
|
|
|
self.assertEqual(operation.migration_name_fragment, "pony_height")
|
|
|
project_state, new_state = self.make_test_state("test_adfl", operation)
|
|
|
- self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 4)
|
|
|
+ self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 6)
|
|
|
field = new_state.models["test_adfl", "pony"].fields["height"]
|
|
|
self.assertEqual(field.default, 5)
|
|
|
# Test the database alteration
|
|
@@ -1528,7 +1532,7 @@ class OperationTests(OperationTestBase):
|
|
|
)
|
|
|
new_state = project_state.clone()
|
|
|
operation.state_forwards("test_adflpd", new_state)
|
|
|
- self.assertEqual(len(new_state.models["test_adflpd", "pony"].fields), 4)
|
|
|
+ self.assertEqual(len(new_state.models["test_adflpd", "pony"].fields), 6)
|
|
|
field = new_state.models["test_adflpd", "pony"].fields["height"]
|
|
|
self.assertEqual(field.default, models.NOT_PROVIDED)
|
|
|
# Test the database alteration
|
|
@@ -1547,6 +1551,169 @@ class OperationTests(OperationTestBase):
|
|
|
sorted(definition[2]), ["field", "model_name", "name", "preserve_default"]
|
|
|
)
|
|
|
|
|
|
+ def test_add_field_database_default(self):
|
|
|
+ """The AddField operation can set and unset a database default."""
|
|
|
+ app_label = "test_adfldd"
|
|
|
+ table_name = f"{app_label}_pony"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AddField(
|
|
|
+ "Pony", "height", models.FloatField(null=True, db_default=4)
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
|
|
+ field = new_state.models[app_label, "pony"].fields["height"]
|
|
|
+ self.assertEqual(field.default, models.NOT_PROVIDED)
|
|
|
+ self.assertEqual(field.db_default, Value(4))
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
|
|
|
+ self.assertColumnNotExists(table_name, "height")
|
|
|
+ # Add field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ self.assertColumnExists(table_name, "height")
|
|
|
+ new_model = new_state.apps.get_model(app_label, "pony")
|
|
|
+ old_pony = new_model.objects.get()
|
|
|
+ self.assertEqual(old_pony.height, 4)
|
|
|
+ new_pony = new_model.objects.create(weight=5)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ new_pony.refresh_from_db()
|
|
|
+ self.assertEqual(new_pony.height, 4)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ self.assertColumnNotExists(table_name, "height")
|
|
|
+ # Deconstruction.
|
|
|
+ definition = operation.deconstruct()
|
|
|
+ self.assertEqual(definition[0], "AddField")
|
|
|
+ self.assertEqual(definition[1], [])
|
|
|
+ self.assertEqual(
|
|
|
+ definition[2],
|
|
|
+ {
|
|
|
+ "field": field,
|
|
|
+ "model_name": "Pony",
|
|
|
+ "name": "height",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_add_field_database_default_special_char_escaping(self):
|
|
|
+ app_label = "test_adflddsce"
|
|
|
+ table_name = f"{app_label}_pony"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ old_pony_pk = (
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create(weight=4).pk
|
|
|
+ )
|
|
|
+ tests = ["%", "'", '"']
|
|
|
+ for db_default in tests:
|
|
|
+ with self.subTest(db_default=db_default):
|
|
|
+ operation = migrations.AddField(
|
|
|
+ "Pony",
|
|
|
+ "special_char",
|
|
|
+ models.CharField(max_length=1, db_default=db_default),
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
|
|
+ field = new_state.models[app_label, "pony"].fields["special_char"]
|
|
|
+ self.assertEqual(field.default, models.NOT_PROVIDED)
|
|
|
+ self.assertEqual(field.db_default, Value(db_default))
|
|
|
+ self.assertColumnNotExists(table_name, "special_char")
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(
|
|
|
+ app_label, editor, project_state, new_state
|
|
|
+ )
|
|
|
+ self.assertColumnExists(table_name, "special_char")
|
|
|
+ new_model = new_state.apps.get_model(app_label, "pony")
|
|
|
+ try:
|
|
|
+ new_pony = new_model.objects.create(weight=5)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ new_pony.refresh_from_db()
|
|
|
+ self.assertEqual(new_pony.special_char, db_default)
|
|
|
+
|
|
|
+ old_pony = new_model.objects.get(pk=old_pony_pk)
|
|
|
+ if connection.vendor != "oracle" or db_default != "'":
|
|
|
+ # The single quotation mark ' is properly quoted and is
|
|
|
+ # set for new rows on Oracle, however it is not set on
|
|
|
+ # existing rows. Skip the assertion as it's probably a
|
|
|
+ # bug in Oracle.
|
|
|
+ self.assertEqual(old_pony.special_char, db_default)
|
|
|
+ finally:
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(
|
|
|
+ app_label, editor, new_state, project_state
|
|
|
+ )
|
|
|
+
|
|
|
+ @skipUnlessDBFeature("supports_expression_defaults")
|
|
|
+ def test_add_field_database_default_function(self):
|
|
|
+ app_label = "test_adflddf"
|
|
|
+ table_name = f"{app_label}_pony"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AddField(
|
|
|
+ "Pony", "height", models.FloatField(db_default=Pi())
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
|
|
+ field = new_state.models[app_label, "pony"].fields["height"]
|
|
|
+ self.assertEqual(field.default, models.NOT_PROVIDED)
|
|
|
+ self.assertEqual(field.db_default, Pi())
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
|
|
|
+ self.assertColumnNotExists(table_name, "height")
|
|
|
+ # Add field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ self.assertColumnExists(table_name, "height")
|
|
|
+ new_model = new_state.apps.get_model(app_label, "pony")
|
|
|
+ old_pony = new_model.objects.get()
|
|
|
+ self.assertAlmostEqual(old_pony.height, math.pi)
|
|
|
+ new_pony = new_model.objects.create(weight=5)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ new_pony.refresh_from_db()
|
|
|
+ self.assertAlmostEqual(old_pony.height, math.pi)
|
|
|
+
|
|
|
+ def test_add_field_both_defaults(self):
|
|
|
+ """The AddField operation with both default and db_default."""
|
|
|
+ app_label = "test_adflbddd"
|
|
|
+ table_name = f"{app_label}_pony"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AddField(
|
|
|
+ "Pony", "height", models.FloatField(default=3, db_default=4)
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
|
|
+ field = new_state.models[app_label, "pony"].fields["height"]
|
|
|
+ self.assertEqual(field.default, 3)
|
|
|
+ self.assertEqual(field.db_default, Value(4))
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
|
|
|
+ self.assertColumnNotExists(table_name, "height")
|
|
|
+ # Add field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ self.assertColumnExists(table_name, "height")
|
|
|
+ new_model = new_state.apps.get_model(app_label, "pony")
|
|
|
+ old_pony = new_model.objects.get()
|
|
|
+ self.assertEqual(old_pony.height, 4)
|
|
|
+ new_pony = new_model.objects.create(weight=5)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ new_pony.refresh_from_db()
|
|
|
+ self.assertEqual(new_pony.height, 3)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ self.assertColumnNotExists(table_name, "height")
|
|
|
+ # Deconstruction.
|
|
|
+ definition = operation.deconstruct()
|
|
|
+ self.assertEqual(definition[0], "AddField")
|
|
|
+ self.assertEqual(definition[1], [])
|
|
|
+ self.assertEqual(
|
|
|
+ definition[2],
|
|
|
+ {
|
|
|
+ "field": field,
|
|
|
+ "model_name": "Pony",
|
|
|
+ "name": "height",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
def test_add_field_m2m(self):
|
|
|
"""
|
|
|
Tests the AddField operation with a ManyToManyField.
|
|
@@ -1558,7 +1725,7 @@ class OperationTests(OperationTestBase):
|
|
|
)
|
|
|
new_state = project_state.clone()
|
|
|
operation.state_forwards("test_adflmm", new_state)
|
|
|
- self.assertEqual(len(new_state.models["test_adflmm", "pony"].fields), 4)
|
|
|
+ self.assertEqual(len(new_state.models["test_adflmm", "pony"].fields), 6)
|
|
|
# Test the database alteration
|
|
|
self.assertTableNotExists("test_adflmm_pony_stables")
|
|
|
with connection.schema_editor() as editor:
|
|
@@ -1727,7 +1894,7 @@ class OperationTests(OperationTestBase):
|
|
|
self.assertEqual(operation.migration_name_fragment, "remove_pony_pink")
|
|
|
new_state = project_state.clone()
|
|
|
operation.state_forwards("test_rmfl", new_state)
|
|
|
- self.assertEqual(len(new_state.models["test_rmfl", "pony"].fields), 2)
|
|
|
+ self.assertEqual(len(new_state.models["test_rmfl", "pony"].fields), 4)
|
|
|
# Test the database alteration
|
|
|
self.assertColumnExists("test_rmfl_pony", "pink")
|
|
|
with connection.schema_editor() as editor:
|
|
@@ -1934,6 +2101,146 @@ class OperationTests(OperationTestBase):
|
|
|
self.assertEqual(definition[1], [])
|
|
|
self.assertEqual(sorted(definition[2]), ["field", "model_name", "name"])
|
|
|
|
|
|
+ def test_alter_field_add_database_default(self):
|
|
|
+ app_label = "test_alfladd"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AlterField(
|
|
|
+ "Pony", "weight", models.FloatField(db_default=4.5)
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ old_weight = project_state.models[app_label, "pony"].fields["weight"]
|
|
|
+ self.assertIs(old_weight.db_default, models.NOT_PROVIDED)
|
|
|
+ new_weight = new_state.models[app_label, "pony"].fields["weight"]
|
|
|
+ self.assertEqual(new_weight.db_default, Value(4.5))
|
|
|
+ with self.assertRaises(IntegrityError), transaction.atomic():
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create()
|
|
|
+ # Alter field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ pony = new_state.apps.get_model(app_label, "pony").objects.create()
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ pony.refresh_from_db()
|
|
|
+ self.assertEqual(pony.weight, 4.5)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ with self.assertRaises(IntegrityError), transaction.atomic():
|
|
|
+ project_state.apps.get_model(app_label, "pony").objects.create()
|
|
|
+ # Deconstruction.
|
|
|
+ definition = operation.deconstruct()
|
|
|
+ self.assertEqual(definition[0], "AlterField")
|
|
|
+ self.assertEqual(definition[1], [])
|
|
|
+ self.assertEqual(
|
|
|
+ definition[2],
|
|
|
+ {
|
|
|
+ "field": new_weight,
|
|
|
+ "model_name": "Pony",
|
|
|
+ "name": "weight",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_alter_field_change_default_to_database_default(self):
|
|
|
+ """The AlterField operation changing default to db_default."""
|
|
|
+ app_label = "test_alflcdtdd"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AlterField(
|
|
|
+ "Pony", "pink", models.IntegerField(db_default=4)
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ old_pink = project_state.models[app_label, "pony"].fields["pink"]
|
|
|
+ self.assertEqual(old_pink.default, 3)
|
|
|
+ self.assertIs(old_pink.db_default, models.NOT_PROVIDED)
|
|
|
+ new_pink = new_state.models[app_label, "pony"].fields["pink"]
|
|
|
+ self.assertIs(new_pink.default, models.NOT_PROVIDED)
|
|
|
+ self.assertEqual(new_pink.db_default, Value(4))
|
|
|
+ pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ self.assertEqual(pony.pink, 3)
|
|
|
+ # Alter field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ pony = new_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ pony.refresh_from_db()
|
|
|
+ self.assertEqual(pony.pink, 4)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ self.assertEqual(pony.pink, 3)
|
|
|
+
|
|
|
+ def test_alter_field_change_nullable_to_database_default_not_null(self):
|
|
|
+ """
|
|
|
+ The AlterField operation changing a null field to db_default.
|
|
|
+ """
|
|
|
+ app_label = "test_alflcntddnn"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ operation = migrations.AlterField(
|
|
|
+ "Pony", "green", models.IntegerField(db_default=4)
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ old_green = project_state.models[app_label, "pony"].fields["green"]
|
|
|
+ self.assertIs(old_green.db_default, models.NOT_PROVIDED)
|
|
|
+ new_green = new_state.models[app_label, "pony"].fields["green"]
|
|
|
+ self.assertEqual(new_green.db_default, Value(4))
|
|
|
+ old_pony = project_state.apps.get_model(app_label, "pony").objects.create(
|
|
|
+ weight=1
|
|
|
+ )
|
|
|
+ self.assertIsNone(old_pony.green)
|
|
|
+ # Alter field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ old_pony.refresh_from_db()
|
|
|
+ self.assertEqual(old_pony.green, 4)
|
|
|
+ pony = new_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ pony.refresh_from_db()
|
|
|
+ self.assertEqual(pony.green, 4)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ self.assertIsNone(pony.green)
|
|
|
+
|
|
|
+ @skipIfDBFeature("interprets_empty_strings_as_nulls")
|
|
|
+ def test_alter_field_change_blank_nullable_database_default_to_not_null(self):
|
|
|
+ app_label = "test_alflcbnddnn"
|
|
|
+ table_name = f"{app_label}_pony"
|
|
|
+ project_state = self.set_up_test_model(app_label)
|
|
|
+ default = "Yellow"
|
|
|
+ operation = migrations.AlterField(
|
|
|
+ "Pony",
|
|
|
+ "yellow",
|
|
|
+ models.CharField(blank=True, db_default=default, max_length=20),
|
|
|
+ )
|
|
|
+ new_state = project_state.clone()
|
|
|
+ operation.state_forwards(app_label, new_state)
|
|
|
+ self.assertColumnNull(table_name, "yellow")
|
|
|
+ pony = project_state.apps.get_model(app_label, "pony").objects.create(
|
|
|
+ weight=1, yellow=None
|
|
|
+ )
|
|
|
+ self.assertIsNone(pony.yellow)
|
|
|
+ # Alter field.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_forwards(app_label, editor, project_state, new_state)
|
|
|
+ self.assertColumnNotNull(table_name, "yellow")
|
|
|
+ pony.refresh_from_db()
|
|
|
+ self.assertEqual(pony.yellow, default)
|
|
|
+ pony = new_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
|
|
+ if not connection.features.can_return_columns_from_insert:
|
|
|
+ pony.refresh_from_db()
|
|
|
+ self.assertEqual(pony.yellow, default)
|
|
|
+ # Reversal.
|
|
|
+ with connection.schema_editor() as editor:
|
|
|
+ operation.database_backwards(app_label, editor, new_state, project_state)
|
|
|
+ self.assertColumnNull(table_name, "yellow")
|
|
|
+ pony = project_state.apps.get_model(app_label, "pony").objects.create(
|
|
|
+ weight=1, yellow=None
|
|
|
+ )
|
|
|
+ self.assertIsNone(pony.yellow)
|
|
|
+
|
|
|
def test_alter_field_add_db_column_noop(self):
|
|
|
"""
|
|
|
AlterField operation is a noop when adding only a db_column and the
|