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:
committed by
Mariusz Felisiak
parent
599f3e2cda
commit
7414704e88
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user