summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorIan Foote <python@ian.feete.org>2020-11-22 22:27:57 +0000
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-05-12 19:11:40 +0200
commit7414704e88d73dafbcfbb85f9bc54cb6111439d3 (patch)
treef51136b16e457d7f46e01ff3cc06308faf0923db /django
parent599f3e2cda50ab084915ffd08edb5ad6cad61415 (diff)
downloaddjango-7414704e88d73dafbcfbb85f9bc54cb6111439d3.tar.gz
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.
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/base/features.py12
-rw-r--r--django/db/backends/base/schema.py88
-rw-r--r--django/db/backends/mysql/features.py7
-rw-r--r--django/db/backends/mysql/schema.py29
-rw-r--r--django/db/backends/oracle/features.py4
-rw-r--r--django/db/backends/oracle/introspection.py2
-rw-r--r--django/db/backends/oracle/schema.py4
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/backends/sqlite3/features.py2
-rw-r--r--django/db/backends/sqlite3/schema.py24
-rw-r--r--django/db/migrations/autodetector.py2
-rw-r--r--django/db/models/base.py6
-rw-r--r--django/db/models/expressions.py37
-rw-r--r--django/db/models/fields/__init__.py61
-rw-r--r--django/db/models/functions/comparison.py1
-rw-r--r--django/db/models/lookups.py4
-rw-r--r--django/db/models/query.py9
17 files changed, 271 insertions, 22 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 11fa807c1b..11dd079110 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -201,6 +201,15 @@ class BaseDatabaseFeatures:
# Does the backend require literal defaults, rather than parameterized ones?
requires_literal_defaults = False
+ # Does the backend support functions in defaults?
+ supports_expression_defaults = True
+
+ # Does the backend support the DEFAULT keyword in insert queries?
+ supports_default_keyword_in_insert = True
+
+ # Does the backend support the DEFAULT keyword in bulk insert queries?
+ supports_default_keyword_in_bulk_insert = True
+
# Does the backend require a connection reset after each material schema change?
connection_persists_old_columns = False
@@ -361,6 +370,9 @@ class BaseDatabaseFeatures:
# SQL template override for tests.aggregation.tests.NowUTC
test_now_utc_template = None
+ # SQL to create a model instance using the database defaults.
+ insert_test_table_with_defaults = None
+
# A set of dotted paths to tests in Django's test suite that are expected
# to fail on this database.
django_test_expected_failures = set()
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 6b03450e2f..01b56151be 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -12,7 +12,7 @@ from django.db.backends.ddl_references import (
Table,
)
from django.db.backends.utils import names_digest, split_identifier, truncate_name
-from django.db.models import Deferrable, Index
+from django.db.models import NOT_PROVIDED, Deferrable, Index
from django.db.models.sql import Query
from django.db.transaction import TransactionManagementError, atomic
from django.utils import timezone
@@ -296,6 +296,12 @@ class BaseDatabaseSchemaEditor:
yield self._comment_sql(field.db_comment)
# Work out nullability.
null = field.null
+ # Add database default.
+ if field.db_default is not NOT_PROVIDED:
+ default_sql, default_params = self.db_default_sql(field)
+ yield f"DEFAULT {default_sql}"
+ params.extend(default_params)
+ include_default = False
# Include a default value, if requested.
include_default = (
include_default
@@ -400,6 +406,22 @@ class BaseDatabaseSchemaEditor:
"""
return "%s"
+ def db_default_sql(self, field):
+ """Return the sql and params for the field's database default."""
+ from django.db.models.expressions import Value
+
+ sql = "%s" if isinstance(field.db_default, Value) else "(%s)"
+ query = Query(model=field.model)
+ compiler = query.get_compiler(connection=self.connection)
+ default_sql, params = compiler.compile(field.db_default)
+ if self.connection.features.requires_literal_defaults:
+ # Some databases doesn't support parameterized defaults (Oracle,
+ # SQLite). If this is the case, the individual schema backend
+ # should implement prepare_default().
+ default_sql %= tuple(self.prepare_default(p) for p in params)
+ params = []
+ return sql % default_sql, params
+
@staticmethod
def _effective_default(field):
# This method allows testing its logic without a connection.
@@ -1025,6 +1047,21 @@ class BaseDatabaseSchemaEditor:
)
actions.append(fragment)
post_actions.extend(other_actions)
+
+ if new_field.db_default is not NOT_PROVIDED:
+ if (
+ old_field.db_default is NOT_PROVIDED
+ or new_field.db_default != old_field.db_default
+ ):
+ actions.append(
+ self._alter_column_database_default_sql(model, old_field, new_field)
+ )
+ elif old_field.db_default is not NOT_PROVIDED:
+ actions.append(
+ self._alter_column_database_default_sql(
+ model, old_field, new_field, drop=True
+ )
+ )
# When changing a column NULL constraint to NOT NULL with a given
# default value, we need to perform 4 steps:
# 1. Add a default for new incoming writes
@@ -1033,7 +1070,11 @@ class BaseDatabaseSchemaEditor:
# 4. Drop the default again.
# Default change?
needs_database_default = False
- if old_field.null and not new_field.null:
+ if (
+ old_field.null
+ and not new_field.null
+ and new_field.db_default is NOT_PROVIDED
+ ):
old_default = self.effective_default(old_field)
new_default = self.effective_default(new_field)
if (
@@ -1051,9 +1092,9 @@ class BaseDatabaseSchemaEditor:
if fragment:
null_actions.append(fragment)
# Only if we have a default and there is a change from NULL to NOT NULL
- four_way_default_alteration = new_field.has_default() and (
- old_field.null and not new_field.null
- )
+ four_way_default_alteration = (
+ new_field.has_default() or new_field.db_default is not NOT_PROVIDED
+ ) and (old_field.null and not new_field.null)
if actions or null_actions:
if not four_way_default_alteration:
# If we don't have to do a 4-way default alteration we can
@@ -1074,15 +1115,20 @@ class BaseDatabaseSchemaEditor:
params,
)
if four_way_default_alteration:
+ if new_field.db_default is NOT_PROVIDED:
+ default_sql = "%s"
+ params = [new_default]
+ else:
+ default_sql, params = self.db_default_sql(new_field)
# Update existing rows with default value
self.execute(
self.sql_update_with_default
% {
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(new_field.column),
- "default": "%s",
+ "default": default_sql,
},
- [new_default],
+ params,
)
# Since we didn't run a NOT NULL change before we need to do it
# now
@@ -1264,6 +1310,34 @@ class BaseDatabaseSchemaEditor:
params,
)
+ def _alter_column_database_default_sql(
+ self, model, old_field, new_field, drop=False
+ ):
+ """
+ Hook to specialize column database default alteration.
+
+ Return a (sql, params) fragment to add or drop (depending on the drop
+ argument) a default to new_field's column.
+ """
+ if drop:
+ sql = self.sql_alter_column_no_default
+ default_sql = ""
+ params = []
+ else:
+ sql = self.sql_alter_column_default
+ default_sql, params = self.db_default_sql(new_field)
+
+ new_db_params = new_field.db_parameters(connection=self.connection)
+ return (
+ sql
+ % {
+ "column": self.quote_name(new_field.column),
+ "type": new_db_params["type"],
+ "default": default_sql,
+ },
+ params,
+ )
+
def _alter_column_type_sql(
self, model, old_field, new_field, new_type, old_collation, new_collation
):
diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index 9e17d33e93..0bb0f91f55 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -51,6 +51,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
# indexed expression.
collate_as_index_expression = True
+ insert_test_table_with_defaults = "INSERT INTO {} () VALUES ()"
supports_order_by_nulls_modifier = False
order_by_nulls_first = True
@@ -342,3 +343,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
if self.connection.mysql_is_mariadb:
return self.connection.mysql_version >= (10, 5, 2)
return True
+
+ @cached_property
+ def supports_expression_defaults(self):
+ if self.connection.mysql_is_mariadb:
+ return True
+ return self.connection.mysql_version >= (8, 0, 13)
diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py
index 31829506c1..bfe5a2e805 100644
--- a/django/db/backends/mysql/schema.py
+++ b/django/db/backends/mysql/schema.py
@@ -209,11 +209,15 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
self._create_missing_fk_index(model, fields=fields)
return super()._delete_composed_index(model, fields, *args)
- def _set_field_new_type_null_status(self, field, new_type):
+ def _set_field_new_type(self, field, new_type):
"""
- Keep the null property of the old field. If it has changed, it will be
- handled separately.
+ Keep the NULL and DEFAULT properties of the old field. If it has
+ changed, it will be handled separately.
"""
+ if field.db_default is not NOT_PROVIDED:
+ default_sql, params = self.db_default_sql(field)
+ default_sql %= tuple(self.quote_value(p) for p in params)
+ new_type += f" DEFAULT {default_sql}"
if field.null:
new_type += " NULL"
else:
@@ -223,7 +227,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def _alter_column_type_sql(
self, model, old_field, new_field, new_type, old_collation, new_collation
):
- new_type = self._set_field_new_type_null_status(old_field, new_type)
+ new_type = self._set_field_new_type(old_field, new_type)
return super()._alter_column_type_sql(
model, old_field, new_field, new_type, old_collation, new_collation
)
@@ -242,7 +246,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
return field_db_params["check"]
def _rename_field_sql(self, table, old_field, new_field, new_type):
- new_type = self._set_field_new_type_null_status(old_field, new_type)
+ new_type = self._set_field_new_type(old_field, new_type)
return super()._rename_field_sql(table, old_field, new_field, new_type)
def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment):
@@ -252,3 +256,18 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def _comment_sql(self, comment):
comment_sql = super()._comment_sql(comment)
return f" COMMENT {comment_sql}"
+
+ def _alter_column_null_sql(self, model, old_field, new_field):
+ if new_field.db_default is NOT_PROVIDED:
+ return super()._alter_column_null_sql(model, old_field, new_field)
+
+ new_db_params = new_field.db_parameters(connection=self.connection)
+ type_sql = self._set_field_new_type(new_field, new_db_params["type"])
+ return (
+ "MODIFY %(column)s %(type)s"
+ % {
+ "column": self.quote_name(new_field.column),
+ "type": type_sql,
+ },
+ [],
+ )
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index 05dc552a98..2ef9e4300c 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -32,6 +32,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
atomic_transactions = False
nulls_order_largest = True
requires_literal_defaults = True
+ supports_default_keyword_in_bulk_insert = False
closed_cursor_error_class = InterfaceError
bare_select_suffix = " FROM DUAL"
# Select for update with limit can be achieved on Oracle, but not with the
@@ -130,6 +131,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"annotations.tests.NonAggregateAnnotationTestCase."
"test_custom_functions_can_ref_other_functions",
}
+ insert_test_table_with_defaults = (
+ "INSERT INTO {} VALUES (DEFAULT, DEFAULT, DEFAULT)"
+ )
@cached_property
def introspected_field_types(self):
diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py
index 5d1e3e6761..c4a734f7ec 100644
--- a/django/db/backends/oracle/introspection.py
+++ b/django/db/backends/oracle/introspection.py
@@ -156,7 +156,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
field_map = {
column: (
display_size,
- default if default != "NULL" else None,
+ default.rstrip() if default and default != "NULL" else None,
collation,
is_autofield,
is_json,
diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py
index 0d70522a2a..c8dd64650f 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -198,7 +198,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
return self.normalize_name(for_name + "_" + suffix)
def prepare_default(self, value):
- return self.quote_value(value)
+ # Replace % with %% as %-formatting is applied in
+ # FormatStylePlaceholderCursor._fix_for_params().
+ return self.quote_value(value).replace("%", "%%")
def _field_should_be_indexed(self, model, field):
create_index = super()._field_should_be_indexed(model, field)
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 732b30b0a4..29b6a4f6c5 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -76,6 +76,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"swedish_ci": "sv-x-icu",
}
test_now_utc_template = "STATEMENT_TIMESTAMP() AT TIME ZONE 'UTC'"
+ insert_test_table_with_defaults = "INSERT INTO {} DEFAULT VALUES"
django_test_skips = {
"opclasses are PostgreSQL only.": {
diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py
index 7dd1c39702..f471b72cb2 100644
--- a/django/db/backends/sqlite3/features.py
+++ b/django/db/backends/sqlite3/features.py
@@ -59,6 +59,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
PRIMARY KEY(column_1, column_2)
)
"""
+ insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)'
+ supports_default_keyword_in_insert = False
@cached_property
def django_test_skips(self):
diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py
index 2ca9a01855..46ba07092d 100644
--- a/django/db/backends/sqlite3/schema.py
+++ b/django/db/backends/sqlite3/schema.py
@@ -6,7 +6,7 @@ from django.db import NotSupportedError
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.ddl_references import Statement
from django.db.backends.utils import strip_quotes
-from django.db.models import UniqueConstraint
+from django.db.models import NOT_PROVIDED, UniqueConstraint
from django.db.transaction import atomic
@@ -233,9 +233,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if create_field:
body[create_field.name] = create_field
# Choose a default and insert it into the copy map
- if not create_field.many_to_many and create_field.concrete:
+ if (
+ create_field.db_default is NOT_PROVIDED
+ and not create_field.many_to_many
+ and create_field.concrete
+ ):
mapping[create_field.column] = self.prepare_default(
- self.effective_default(create_field),
+ self.effective_default(create_field)
)
# Add in any altered fields
for alter_field in alter_fields:
@@ -244,9 +248,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
mapping.pop(old_field.column, None)
body[new_field.name] = new_field
if old_field.null and not new_field.null:
+ if new_field.db_default is NOT_PROVIDED:
+ default = self.prepare_default(self.effective_default(new_field))
+ else:
+ default, _ = self.db_default_sql(new_field)
case_sql = "coalesce(%(col)s, %(default)s)" % {
"col": self.quote_name(old_field.column),
- "default": self.prepare_default(self.effective_default(new_field)),
+ "default": default,
}
mapping[new_field.column] = case_sql
else:
@@ -381,6 +389,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def add_field(self, model, field):
"""Create a field on a model."""
+ from django.db.models.expressions import Value
+
# Special-case implicit M2M tables.
if field.many_to_many and field.remote_field.through._meta.auto_created:
self.create_model(field.remote_field.through)
@@ -394,6 +404,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
# COLUMN statement because DROP DEFAULT is not supported in
# ALTER TABLE.
or self.effective_default(field) is not None
+ # Fields with non-constant defaults cannot by handled by ALTER
+ # TABLE ADD COLUMN statement.
+ or (
+ field.db_default is not NOT_PROVIDED
+ and not isinstance(field.db_default, Value)
+ )
):
self._remake_table(model, create_field=field)
else:
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index 23c97e5474..154ac44419 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -1040,6 +1040,7 @@ class MigrationAutodetector:
preserve_default = (
field.null
or field.has_default()
+ or field.db_default is not models.NOT_PROVIDED
or field.many_to_many
or (field.blank and field.empty_strings_allowed)
or (isinstance(field, time_fields) and field.auto_now)
@@ -1187,6 +1188,7 @@ class MigrationAutodetector:
old_field.null
and not new_field.null
and not new_field.has_default()
+ and new_field.db_default is models.NOT_PROVIDED
and not new_field.many_to_many
):
field = new_field.clone()
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 344508e0e2..7aabe0b667 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -971,8 +971,10 @@ class Model(AltersData, metaclass=ModelBase):
not raw
and not force_insert
and self._state.adding
- and meta.pk.default
- and meta.pk.default is not NOT_PROVIDED
+ and (
+ (meta.pk.default and meta.pk.default is not NOT_PROVIDED)
+ or (meta.pk.db_default and meta.pk.db_default is not NOT_PROVIDED)
+ )
):
force_insert = True
# If possible, try an UPDATE. If that doesn't update anything, do an INSERT.
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index d412e7657e..e1861759c4 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -176,6 +176,8 @@ class BaseExpression:
filterable = True
# Can the expression can be used as a source expression in Window?
window_compatible = False
+ # Can the expression be used as a database default value?
+ allowed_default = False
def __init__(self, output_field=None):
if output_field is not None:
@@ -733,6 +735,10 @@ class CombinedExpression(SQLiteNumericMixin, Expression):
c.rhs = rhs
return c
+ @cached_property
+ def allowed_default(self):
+ return self.lhs.allowed_default and self.rhs.allowed_default
+
class DurationExpression(CombinedExpression):
def compile(self, side, compiler, connection):
@@ -804,6 +810,8 @@ class TemporalSubtraction(CombinedExpression):
class F(Combinable):
"""An object capable of resolving references to existing query objects."""
+ allowed_default = False
+
def __init__(self, name):
"""
Arguments:
@@ -987,6 +995,10 @@ class Func(SQLiteNumericMixin, Expression):
copy.extra = self.extra.copy()
return copy
+ @cached_property
+ def allowed_default(self):
+ return all(expression.allowed_default for expression in self.source_expressions)
+
@deconstructible(path="django.db.models.Value")
class Value(SQLiteNumericMixin, Expression):
@@ -995,6 +1007,7 @@ class Value(SQLiteNumericMixin, Expression):
# Provide a default value for `for_save` in order to allow unresolved
# instances to be compiled until a decision is taken in #25425.
for_save = False
+ allowed_default = True
def __init__(self, value, output_field=None):
"""
@@ -1069,6 +1082,8 @@ class Value(SQLiteNumericMixin, Expression):
class RawSQL(Expression):
+ allowed_default = True
+
def __init__(self, sql, params, output_field=None):
if output_field is None:
output_field = fields.Field()
@@ -1110,6 +1125,13 @@ class Star(Expression):
return "*", []
+class DatabaseDefault(Expression):
+ """Placeholder expression for the database default in an insert query."""
+
+ def as_sql(self, compiler, connection):
+ return "DEFAULT", []
+
+
class Col(Expression):
contains_column_references = True
possibly_multivalued = False
@@ -1213,6 +1235,7 @@ class ExpressionList(Func):
class OrderByList(Func):
+ allowed_default = False
template = "ORDER BY %(expressions)s"
def __init__(self, *expressions, **extra):
@@ -1270,6 +1293,10 @@ class ExpressionWrapper(SQLiteNumericMixin, Expression):
def __repr__(self):
return "{}({})".format(self.__class__.__name__, self.expression)
+ @property
+ def allowed_default(self):
+ return self.expression.allowed_default
+
class NegatedExpression(ExpressionWrapper):
"""The logical negation of a conditional expression."""
@@ -1397,6 +1424,10 @@ class When(Expression):
cols.extend(source.get_group_by_cols())
return cols
+ @cached_property
+ def allowed_default(self):
+ return self.condition.allowed_default and self.result.allowed_default
+
@deconstructible(path="django.db.models.Case")
class Case(SQLiteNumericMixin, Expression):
@@ -1494,6 +1525,12 @@ class Case(SQLiteNumericMixin, Expression):
return self.default.get_group_by_cols()
return super().get_group_by_cols()
+ @cached_property
+ def allowed_default(self):
+ return self.default.allowed_default and all(
+ case_.allowed_default for case_ in self.cases
+ )
+
class Subquery(BaseExpression, Combinable):
"""
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 4416898d80..18b48c0e72 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -202,6 +202,7 @@ class Field(RegisterLookupMixin):
validators=(),
error_messages=None,
db_comment=None,
+ db_default=NOT_PROVIDED,
):
self.name = name
self.verbose_name = verbose_name # May be set by set_attributes_from_name
@@ -212,6 +213,13 @@ class Field(RegisterLookupMixin):
self.remote_field = rel
self.is_relation = self.remote_field is not None
self.default = default
+ if db_default is not NOT_PROVIDED and not hasattr(
+ db_default, "resolve_expression"
+ ):
+ from django.db.models.expressions import Value
+
+ db_default = Value(db_default)
+ self.db_default = db_default
self.editable = editable
self.serialize = serialize
self.unique_for_date = unique_for_date
@@ -263,6 +271,7 @@ class Field(RegisterLookupMixin):
return [
*self._check_field_name(),
*self._check_choices(),
+ *self._check_db_default(**kwargs),
*self._check_db_index(),
*self._check_db_comment(**kwargs),
*self._check_null_allowed_for_primary_keys(),
@@ -379,6 +388,39 @@ class Field(RegisterLookupMixin):
)
]
+ def _check_db_default(self, databases=None, **kwargs):
+ from django.db.models.expressions import Value
+
+ if (
+ self.db_default is NOT_PROVIDED
+ or isinstance(self.db_default, Value)
+ or databases is None
+ ):
+ return []
+ errors = []
+ for db in databases:
+ if not router.allow_migrate_model(db, self.model):
+ continue
+ connection = connections[db]
+
+ if not getattr(self.db_default, "allowed_default", False) and (
+ connection.features.supports_expression_defaults
+ ):
+ msg = f"{self.db_default} cannot be used in db_default."
+ errors.append(checks.Error(msg, obj=self, id="fields.E012"))
+
+ if not (
+ connection.features.supports_expression_defaults
+ or "supports_expression_defaults"
+ in self.model._meta.required_db_features
+ ):
+ msg = (
+ f"{connection.display_name} does not support default database "
+ "values with expressions (db_default)."
+ )
+ errors.append(checks.Error(msg, obj=self, id="fields.E011"))
+ return errors
+
def _check_db_index(self):
if self.db_index not in (None, True, False):
return [
@@ -558,6 +600,7 @@ class Field(RegisterLookupMixin):
"null": False,
"db_index": False,
"default": NOT_PROVIDED,
+ "db_default": NOT_PROVIDED,
"editable": True,
"serialize": True,
"unique_for_date": None,
@@ -876,7 +919,10 @@ class Field(RegisterLookupMixin):
@property
def db_returning(self):
"""Private API intended only to be used by Django itself."""
- return False
+ return (
+ self.db_default is not NOT_PROVIDED
+ and connection.features.can_return_columns_from_insert
+ )
def set_attributes_from_name(self, name):
self.name = self.name or name
@@ -929,7 +975,13 @@ class Field(RegisterLookupMixin):
def pre_save(self, model_instance, add):
"""Return field's value just before saving."""
- return getattr(model_instance, self.attname)
+ value = getattr(model_instance, self.attname)
+ if not connection.features.supports_default_keyword_in_insert:
+ from django.db.models.expressions import DatabaseDefault
+
+ if isinstance(value, DatabaseDefault):
+ return self.db_default
+ return value
def get_prep_value(self, value):
"""Perform preliminary non-db specific value checks and conversions."""
@@ -968,6 +1020,11 @@ class Field(RegisterLookupMixin):
return self.default
return lambda: self.default
+ if self.db_default is not NOT_PROVIDED:
+ from django.db.models.expressions import DatabaseDefault
+
+ return DatabaseDefault
+
if (
not self.empty_strings_allowed
or self.null
diff --git a/django/db/models/functions/comparison.py b/django/db/models/functions/comparison.py
index de7eef4cdc..108d904712 100644
--- a/django/db/models/functions/comparison.py
+++ b/django/db/models/functions/comparison.py
@@ -105,6 +105,7 @@ class Coalesce(Func):
class Collate(Func):
function = "COLLATE"
template = "%(expressions)s %(function)s %(collation)s"
+ allowed_default = False
# Inspired from
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
collation_re = _lazy_re_compile(r"^[\w\-]+$")
diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py
index 46ebe3f3a2..91342a864a 100644
--- a/django/db/models/lookups.py
+++ b/django/db/models/lookups.py
@@ -185,6 +185,10 @@ class Lookup(Expression):
sql = f"CASE WHEN {sql} THEN 1 ELSE 0 END"
return sql, params
+ @cached_property
+ def allowed_default(self):
+ return self.lhs.allowed_default and self.rhs.allowed_default
+
class Transform(RegisterLookupMixin, Func):
"""
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 56ad4d5c20..a5b0f464a9 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -654,10 +654,19 @@ class QuerySet(AltersData):
return await sync_to_async(self.create)(**kwargs)
def _prepare_for_bulk_create(self, objs):
+ from django.db.models.expressions import DatabaseDefault
+
+ connection = connections[self.db]
for obj in objs:
if obj.pk is None:
# Populate new PK values.
obj.pk = obj._meta.pk.get_pk_value_on_save(obj)
+ if not connection.features.supports_default_keyword_in_bulk_insert:
+ for field in obj._meta.fields:
+ value = getattr(obj, field.attname)
+ if isinstance(value, DatabaseDefault):
+ setattr(obj, field.attname, field.db_default)
+
obj._prepare_related_fields_for_save(operation_name="bulk_create")
def _check_bulk_create_options(