Skip to content

Conversation

charettes
Copy link
Member

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.

)["_order__max"]
)
fields = [
insert_fields = [
Copy link
Member Author

@charettes charettes Sep 16, 2025

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.

@charettes charettes force-pushed the ticket-27222-refresh-auto branch from f4c3faa to cb25194 Compare September 16, 2025 22:59
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):
Copy link
Member Author

@charettes charettes Sep 16, 2025

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.

Copy link
Member

@jacobtylerwalls jacobtylerwalls Sep 17, 2025

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.

Copy link
Member Author

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.

Copy link
Member Author

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)

@jacobtylerwalls jacobtylerwalls force-pushed the ticket-27222-refresh-auto branch from cb25194 to fca1dfc Compare September 17, 2025 02:20
@jacobtylerwalls jacobtylerwalls changed the title Refs #27222 -- Restored Model.save()'s refreshing of db_returning fields, even if a value is set. Refs #27222 -- Restored Model.save()'s refreshing of auto fields even if a value is set. Sep 17, 2025
@jacobtylerwalls jacobtylerwalls changed the title Refs #27222 -- Restored Model.save()'s refreshing of auto fields even if a value is set. Refs #27222 -- Restored Model.save()'s refreshing of db_returning fields even if a value is set. Sep 17, 2025
@jacobtylerwalls jacobtylerwalls force-pushed the ticket-27222-refresh-auto branch from fca1dfc to 1bece90 Compare September 17, 2025 02:26
Comment on lines +29 to +32
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)
Copy link
Member

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? 🤔

Copy link
Member

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 UUIDFields 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.

Copy link
Member Author

@charettes charettes Sep 17, 2025

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.

Copy link
Member

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/

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a 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.

@jacobtylerwalls
Copy link
Member

@charettes do you have advice on the failure of test_auto_field_with_value_refreshed on MySQL? I considered adding @skipUnlessDBFeature("can_return_columns_from_insert"), but it would cause Tim's mongo suite to skip.

@charettes
Copy link
Member Author

@jacobtylerwalls yes, you'd need to add back the and not (pk_set and field is meta.auto_field) criteria.

…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]>
@jacobtylerwalls jacobtylerwalls force-pushed the ticket-27222-refresh-auto branch from 1bece90 to b9533c8 Compare September 17, 2025 03:29
@jacobtylerwalls jacobtylerwalls merged commit 4fcc288 into django:main Sep 17, 2025
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants