-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
Refs #27222 -- Restored Model.save()'s refreshing of db_returning fields even if a value is set. #19871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refs #27222 -- Restored Model.save()'s refreshing of db_returning fields even if a value is set. #19871
Conversation
)["_order__max"] | ||
) | ||
fields = [ | ||
insert_fields = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the variable name as it made it much easier for me to reason about the origin of the f.generated and (pk_set or f is not meta.auto_field)
check.
f4c3faa
to
cb25194
Compare
django/db/models/base.py
Outdated
if field not in returning_fields: | ||
returning_fields.append(field) | ||
elif field.db_returning: | ||
elif field.db_returning and not (pk_set and field is meta.auto_field): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This preserves backward compatiblity for auto fields that have literal values overrides assigned to them transit through the database and be assigned back while avoiding to do so for other db_returning
fields.
The reason behind it is that otherwise on backends that don't support RETURNING
(MySQL) assigning an explicit literal value to a db_default
field still forces a refresh from the database which is not desired.
See the following failures on MySQL to get a better understanding of why this is important
field_defaults.tests.DefaultTests.test_null_db_default
file_storage.tests.FileFieldStorageTests.test_files
The logic added here maintains the symmetry with the insert_fields
filter criteria.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just so my understanding is clear, we intentionally treat only auto fields here, and not other custom db_returning fields?
Editing this test like so passes on 5.2.x but fails on both main and this patch:
diff --git a/tests/queries/test_db_returning.py b/tests/queries/test_db_returning.py
index 06efe023e1..9954fa836c 100644
--- a/tests/queries/test_db_returning.py
+++ b/tests/queries/test_db_returning.py
@@ -22,7 +22,7 @@ class ReturningValuesTests(TestCase):
)
def test_insert_returning_non_integer(self):
- obj = NonIntegerPKReturningModel.objects.create()
+ obj = NonIntegerPKReturningModel.objects.create(pk='2025-01-01')
self.assertTrue(obj.created)
self.assertIsInstance(obj.created, datetime.datetime)
If we're okay with this failure, then I won't add any further test edits and will just adjust docstring & commit to say "auto" fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just so my understanding is clear, we intentionally treat only auto fields here, and not other custom db_returning fields?
Correct, this is an artifact of how we've treated auto-field over the years; we've always used RETURNING
or (last_insert_id
on MySQL) and passed the value through AutoField.from_db_value
which triggers a type conversion.
Editing this test like so passes on 5.2.x but fails on both main and this patch:
I was about to say I would expect it to fail on MySQL but the test is skipped due to can_return_columns_from_insert
.
I guess we could check against against field in pk_fields
instead of is meta.auto_field
if we want to preserve this behavior but it's venturing in undefined behavior territory IMO.
Another alternative is to always do a database RETURNING
roundtrip if the backend supports it. I'll push a commit that does just that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jacobtylerwalls I ended up not pushing the change as to avoid stepping on your ongoing work here's what I had in mind
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 3aaca16bbe..aaf051d403 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -1148,6 +1148,9 @@ def _save_table(
if not f.generated and (pk_set or f is not meta.auto_field)
]
returning_fields = list(meta.db_returning_fields)
+ can_return_columns_from_insert = connections[
+ using
+ ].features.can_return_columns_from_insert
for field in insert_fields:
value = (
getattr(self, field.attname) if raw else field.pre_save(self, False)
@@ -1155,7 +1158,7 @@ def _save_table(
if hasattr(value, "resolve_expression"):
if field not in returning_fields:
returning_fields.append(field)
- elif field.db_returning and not (pk_set and field is meta.auto_field):
+ elif field.db_returning and not can_return_columns_from_insert:
returning_fields.remove(field)
results = self._do_insert(
cls._base_manager, using, insert_fields, returning_fields, raw
diff --git a/tests/queries/test_db_returning.py b/tests/queries/test_db_returning.py
index 06efe023e1..2a26e6384f 100644
--- a/tests/queries/test_db_returning.py
+++ b/tests/queries/test_db_returning.py
@@ -22,7 +22,7 @@ def test_insert_returning(self):
)
def test_insert_returning_non_integer(self):
- obj = NonIntegerPKReturningModel.objects.create()
+ obj = NonIntegerPKReturningModel.objects.create(pk="2025-01-01")
self.assertTrue(obj.created)
self.assertIsInstance(obj.created, datetime.datetime)
cb25194
to
fca1dfc
Compare
fca1dfc
to
1bece90
Compare
def test_insert_returning_non_integer_from_literal_value(self): | ||
obj = NonIntegerPKReturningModel.objects.create(pk="2025-01-01") | ||
self.assertTrue(obj.created) | ||
self.assertIsInstance(obj.created, datetime.datetime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I exploded this out into a test case so that if we yank it later for being "dubious" we can. Should I add a little comment that this usage is a little dubious? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was accustomed to seeing UUIDField
s used like this with string representations on queryset-level operations (although not in a db_returning context) so maybe this is fine as is without a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, it's always dubious to make ORM calls with the unexpected type (in the sense that it's not the type to_python
and full_clean
would provide if called) but since the lax typing enforcement has likely ossified in code bases it's more of an artifact of some database backends accepting the string representation of dates, uuid, and others as input that we can't pull the plug on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the pattern:
def view(request, book_id: str):
obj = get_object_or_404(Book, id=book_id)
…is super-common and relies on the database casting string to ID. I agree we can probably never remove this behaviour.
I blogged about one case where type conversion caused an outage, and some investigation across databases: https://adamj.eu/tech/2020/03/06/sql-implicit-type-conversion/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick work and helpful analysis.
@charettes do you have advice on the failure of |
@jacobtylerwalls yes, you'd need to add back the |
…lds even if a value is set. The logic could likely be adjusted to assign the pre_save value in most cases to avoid the database transit but it could break in subtle ways so it's not worth the complexity it would require. Regression in 9468043. Co-authored-by: Tim Graham <[email protected]>
1bece90
to
b9533c8
Compare
Regression in 9468043.
Discussed at https://github.com/django/django/pull/19285/files#r2076268448.
I can't push changes to #19868 to I created this other PR @timgraham, @jacobtylerwalls. Please adjust authorship to whatever feels best.