1
0
mirror of https://github.com/django/django.git synced 2025-10-24 06:06:09 +00:00

Fixed #470 -- Added support for database defaults on fields.

Special thanks to Hannes Ljungberg for finding multiple implementation
gaps.

Thanks also to Simon Charette, Adam Johnson, and Mariusz Felisiak for
reviews.
This commit is contained in:
Ian Foote
2020-11-22 22:27:57 +00:00
committed by Mariusz Felisiak
parent 599f3e2cda
commit 7414704e88
32 changed files with 1089 additions and 34 deletions

View File

@@ -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