summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2025-01-27 15:11:19 -0500
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-02-11 09:15:58 +0100
commitf8fce8d4dce7cf6d272a29351faddd5fa76f1aa0 (patch)
treecd039d4f02d8bcdec6d78d6831239eb48a67a406
parent16c7dc543c3a0e8a4c7554e36dd6166d5c12c872 (diff)
[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.
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/oracle/features.py1
-rw-r--r--django/db/backends/sqlite3/features.py1
-rw-r--r--django/db/models/fields/tuple_lookups.py49
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 = {