summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
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(