From f8fce8d4dce7cf6d272a29351faddd5fa76f1aa0 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 27 Jan 2025 15:11:19 -0500 Subject: [PATCH] [5.2.x] Refs #36148 -- Relied on a feature switch to define tuple lookups support. This should allow backends more easily opt-in or out of native support and rely on the fallback if unavailable. Backport of a0a765ddeb5056c85e084773d3f6432e2a426638 from main. --- django/db/backends/base/features.py | 3 ++ django/db/backends/oracle/features.py | 1 + django/db/backends/sqlite3/features.py | 1 + django/db/models/fields/tuple_lookups.py | 49 ++++++++++++++---------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 53b48673de..0f8e6f41a6 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -368,6 +368,9 @@ class BaseDatabaseFeatures: # Does the backend support unlimited character columns? supports_unlimited_charfield = False + # Does the backend support native tuple lookups (=, >, <, IN)? + supports_tuple_lookups = True + # Collation names for use by the Django test suite. test_collations = { "ci": None, # Case-insensitive. diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index ad9ab8da55..7ebe69c6dd 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -81,6 +81,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): allows_multiple_constraints_on_same_fields = False supports_json_field_contains = False supports_collation_on_textfield = False + supports_tuple_lookups = False test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'" django_test_expected_failures = { # A bug in Django/oracledb with respect to string handling (#23843). diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 60893561df..6ab6308ece 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -61,6 +61,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)' supports_default_keyword_in_insert = False supports_unlimited_charfield = True + supports_tuple_lookups = False @cached_property def django_test_skips(self): diff --git a/django/db/models/fields/tuple_lookups.py b/django/db/models/fields/tuple_lookups.py index 57d5c0a13f..1e77f095c8 100644 --- a/django/db/models/fields/tuple_lookups.py +++ b/django/db/models/fields/tuple_lookups.py @@ -96,9 +96,20 @@ class TupleLookupMixin: ) return "(%s)" % sql, params + def get_fallback_sql(self, compiler, connection): + raise NotImplementedError( + f"{self.__class__.__name__}.get_fallback_sql() must be implemented " + f"for backends that don't have the supports_tuple_lookups feature enabled." + ) + + def as_sql(self, compiler, connection): + if not connection.features.supports_tuple_lookups: + return self.get_fallback_sql(compiler, connection) + return super().as_sql(compiler, connection) + class TupleExact(TupleLookupMixin, Exact): - def as_oracle(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): # Process right-hand-side to trigger sanitization. self.process_rhs(compiler, connection) # e.g.: (a, b, c) == (x, y, z) as SQL: @@ -132,7 +143,7 @@ class TupleIsNull(TupleLookupMixin, IsNull): class TupleGreaterThan(TupleLookupMixin, GreaterThan): - def as_oracle(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): # Process right-hand-side to trigger sanitization. self.process_rhs(compiler, connection) # e.g.: (a, b, c) > (x, y, z) as SQL: @@ -160,7 +171,7 @@ class TupleGreaterThan(TupleLookupMixin, GreaterThan): class TupleGreaterThanOrEqual(TupleLookupMixin, GreaterThanOrEqual): - def as_oracle(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): # Process right-hand-side to trigger sanitization. self.process_rhs(compiler, connection) # e.g.: (a, b, c) >= (x, y, z) as SQL: @@ -188,7 +199,7 @@ class TupleGreaterThanOrEqual(TupleLookupMixin, GreaterThanOrEqual): class TupleLessThan(TupleLookupMixin, LessThan): - def as_oracle(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): # Process right-hand-side to trigger sanitization. self.process_rhs(compiler, connection) # e.g.: (a, b, c) < (x, y, z) as SQL: @@ -216,7 +227,7 @@ class TupleLessThan(TupleLookupMixin, LessThan): class TupleLessThanOrEqual(TupleLookupMixin, LessThanOrEqual): - def as_oracle(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): # Process right-hand-side to trigger sanitization. self.process_rhs(compiler, connection) # e.g.: (a, b, c) <= (x, y, z) as SQL: @@ -315,17 +326,19 @@ class TupleIn(TupleLookupMixin, In): return compiler.compile(Tuple(*result)) - def as_sql(self, compiler, connection): - if not self.rhs_is_direct_value(): - return self.as_subquery(compiler, connection) - return super().as_sql(compiler, connection) + def as_subquery_sql(self, compiler, connection): + lhs = self.lhs + rhs = self.rhs + if isinstance(lhs, ColPairs): + rhs = rhs.clone() + rhs.set_values([source.name for source in lhs.sources]) + lhs = Tuple(lhs) + return compiler.compile(In(lhs, rhs)) - def as_sqlite(self, compiler, connection): + def get_fallback_sql(self, compiler, connection): rhs = self.rhs if not rhs: raise EmptyResultSet - if not self.rhs_is_direct_value(): - return self.as_subquery(compiler, connection) # e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL: # WHERE (a = x1 AND b = y1 AND c = z1) OR (a = x2 AND b = y2 AND c = z2) @@ -338,14 +351,10 @@ class TupleIn(TupleLookupMixin, In): return root.as_sql(compiler, connection) - def as_subquery(self, compiler, connection): - lhs = self.lhs - rhs = self.rhs - if isinstance(lhs, ColPairs): - rhs = rhs.clone() - rhs.set_values([source.name for source in lhs.sources]) - lhs = Tuple(lhs) - return compiler.compile(In(lhs, rhs)) + def as_sql(self, compiler, connection): + if not self.rhs_is_direct_value(): + return self.as_subquery_sql(compiler, connection) + return super().as_sql(compiler, connection) tuple_lookups = {