From 7414704e88d73dafbcfbb85f9bc54cb6111439d3 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Sun, 22 Nov 2020 22:27:57 +0000 Subject: 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. --- django/db/backends/base/features.py | 12 ++++ django/db/backends/base/schema.py | 88 +++++++++++++++++++++++++++--- django/db/backends/mysql/features.py | 7 +++ django/db/backends/mysql/schema.py | 29 ++++++++-- django/db/backends/oracle/features.py | 4 ++ django/db/backends/oracle/introspection.py | 2 +- django/db/backends/oracle/schema.py | 4 +- django/db/backends/postgresql/features.py | 1 + django/db/backends/sqlite3/features.py | 2 + django/db/backends/sqlite3/schema.py | 24 ++++++-- django/db/migrations/autodetector.py | 2 + django/db/models/base.py | 6 +- django/db/models/expressions.py | 37 +++++++++++++ django/db/models/fields/__init__.py | 61 ++++++++++++++++++++- django/db/models/functions/comparison.py | 1 + django/db/models/lookups.py | 4 ++ django/db/models/query.py | 9 +++ 17 files changed, 271 insertions(+), 22 deletions(-) (limited to 'django') 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( -- cgit v1.2.1