#36522 closed Bug (fixed)
Tuple lookups cannot handle right-hand sides mixing literal values and expressions
Reported by: | Jacob Walls | Owned by: | Simon Charette |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.2 |
Severity: | Release blocker | Keywords: | compositeprimarykey, F, composite primary key, rhs |
Cc: | Simon Charette | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
For this model in the test suite:
class Comment(models.Model): pk = models.CompositePrimaryKey("tenant", "id") tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name="comments", ) id = models.SmallIntegerField(unique=True, db_column="comment_id") ...
This fails:
-
tests/composite_pk/test_update.py
diff --git a/tests/composite_pk/test_update.py b/tests/composite_pk/test_update.py index 697383b007..e76210e75e 100644
a b class CompositePKUpdateTests(TestCase): 191 191 ) 192 192 with self.assertRaisesMessage(FieldError, msg): 193 193 Comment.objects.update(text=F("pk")) 194 195 def test_update_fields_expression(self): 196 self.comment_1.id = F("id") + 100 197 self.comment_1.save()
====================================================================== ERROR: test_update_fields_expression (composite_pk.test_update.CompositePKUpdateTests.test_update_fields_expression) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/jwalls/django/django/db/models/fields/__init__.py", line 2130, in get_prep_value return int(value) TypeError: int() argument must be a string, a bytes-like object or a real number, not 'CombinedExpression' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/jwalls/django/tests/composite_pk/test_update.py", line 197, in test_update_fields_expression self.comment_1.save() ~~~~~~~~~~~~~~~~~~~^^ File "/Users/jwalls/django/django/db/models/base.py", line 874, in save self.save_base( ~~~~~~~~~~~~~~^ using=using, ^^^^^^^^^^^^ ...<2 lines>... update_fields=update_fields, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/Users/jwalls/django/django/db/models/base.py", line 966, in save_base updated = self._save_table( raw, ...<4 lines>... update_fields, ) File "/Users/jwalls/django/django/db/models/base.py", line 1097, in _save_table updated = self._do_update( base_qs, using, pk_val, values, update_fields, forced_update ) File "/Users/jwalls/django/django/db/models/base.py", line 1165, in _do_update return filtered._update(values) > 0 ~~~~~~~~~~~~~~~~^^^^^^^^ File "/Users/jwalls/django/django/db/models/query.py", line 1319, in _update return query.get_compiler(self.db).execute_sql(ROW_COUNT) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 2103, in execute_sql row_count = super().execute_sql(result_type) File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 1611, in execute_sql sql, params = self.as_sql() ~~~~~~~~~~~^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 2089, in as_sql where, params = self.compile(self.query.where) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/where.py", line 151, in as_sql sql, params = compiler.compile(child) ~~~~~~~~~~~~~~~~^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/fields/tuple_lookups.py", line 130, in as_sql return super().as_sql(compiler, connection) ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/lookups.py", line 407, in as_sql return super().as_sql(compiler, connection) ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/lookups.py", line 239, in as_sql rhs_sql, rhs_params = self.process_rhs(compiler, connection) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/fields/tuple_lookups.py", line 109, in process_rhs return compiler.compile(Tuple(*args)) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 576, in compile sql, params = vendor_impl(self, self.connection) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/fields/tuple_lookups.py", line 46, in as_sqlite return self.as_sql(compiler, connection) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/expressions.py", line 1107, in as_sql arg_sql, arg_params = compiler.compile(arg) ~~~~~~~~~~~~~~~~^^^^^ File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 576, in compile sql, params = vendor_impl(self, self.connection) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/expressions.py", line 29, in as_sqlite sql, params = self.as_sql(compiler, connection, **extra_context) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/expressions.py", line 1175, in as_sql val = output_field.get_db_prep_value(val, connection=connection) File "/Users/jwalls/django/django/db/models/fields/__init__.py", line 2137, in get_db_prep_value value = super().get_db_prep_value(value, connection, prepared) File "/Users/jwalls/django/django/db/models/fields/__init__.py", line 1006, in get_db_prep_value value = self.get_prep_value(value) File "/Users/jwalls/django/django/db/models/fields/__init__.py", line 2132, in get_prep_value raise e.__class__( "Field '%s' expected a number but got %r." % (self.name, value), ) from e TypeError: Field 'id' expected a number but got <CombinedExpression: Col(composite_pk_comment, composite_pk.Comment.id) + Value(100)>.
The + 100
is not necessary to reproduce, just there for realism. Error without is the same: "expected a number but got Col(..."
If support is difficult, a FieldError
is perhaps better, see test_update_value_not_composite()
.
Change History (7)
comment:2 by , 6 weeks ago
Keywords: | composite primary key rhs added |
---|---|
Summary: | Assigning expressions to model fields does not work if the field participates in a composite primary key → Tuple lookups cannot handle right-hand sides mixing literal values and expressions |
Absolutely. Was really only expecting that this low-level failure might reveal something deeper amiss, so thanks for identifying it so quickly!
comment:3 by , 6 weeks ago
Owner: | set to |
---|---|
Status: | new → assigned |
Triage Stage: | Unreviewed → Accepted |
Accepting in this case, thanks for your continued iron out of the composite primary key feature.
comment:4 by , 6 weeks ago
Has patch: | set |
---|
comment:5 by , 6 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
Note:
See TracTickets
for help on using tickets.
You can't use primary key assignment (complete or partial) to model instances to perform an update as the value is used both for the desired value as well as the row lookup, that is even when composite primary keys are not involved.
Take the following model for example
Performing the following
will inevitably be a noop as the value assigned to
id
must be used for theSET
andWHERE
clause so it will result inIf you use
force_update=True
you'll get an appropriate error messagebut if you don't
save()
will attempt a subsequentINSERT
and crash asF
field reference cannot be usedIn other words, you can't use
Model.save
to update a primary key you must useQuerySet.update
for that, maybe be could add a mention toModel.save
docs for that but given we don't support database levelON UPDATE
anyway I'm not convinced it's worth documenting (it's uncommon for a primary key to be updated).Now the crash you're getting here surfaces another issue though which is that
TupleExact
lookup (and likely other tuple lookups) do not properly handle right-hand-sides mixing literal and expression values. You can get a similar crash to the one you ran into (caused byModel.save
doing the equivalent) by doingtests/composite_pk/test_filter.py
which can be solved by
django/db/models/fields/tuple_lookups.py
With this patch applied your provided test fails with the exact same signatures as the one when no composite primary keys are involved.
Should re-purpose this ticket to focus on the lack of support for
tuple: Expression
as right-hand-side for tuple lookups?