Fix raw_cte_queryset in Subqueries#132
Conversation
When a CTE is created with raw_cte_sql the query object, raw_cte_queryset.query, does not implement resolve_expression, causing an AttributeError. Specifically, this occurs when we use it within a Subquery. This patch adds resolve_expression implementation to raw_cte_queryset.query that simply returns cls. This satisfies django ORM because of duck typing and since the query is raw SQL, no actual expression resolution is needed.
|
|
||
| @classmethod | ||
| def resolve_expression(cls, *args, **kwargs): | ||
| return cls |
There was a problem hiding this comment.
Is this pattern used in Django? I'm trying to understand if this is a valid return value for this method.
Does the return value matter? Would it work just as well to raise NotImplementedError or return None?
There was a problem hiding this comment.
Yea, Django expects resolve_expression() to return itself, with any nested expressions resolved. There's no further resolving to do here though so I think returning the "mock" class is correct thing to do. Here's the Django doc ref: Expression API - resolve_expression().
would it work just as well to raise NotImplementedError
No, we then fail if we ever have nested raw cte's
_______________________ TestRawCTE.test_raw_cte_subquery _______________________
self = <tests.test_raw.TestRawCTE testMethod=test_raw_cte_subquery>
def test_raw_cte_subquery(self):
cte = CTE(raw_cte_sql(
"SELECT name as region_name FROM region WHERE name = %s",
["earth"],
{"region_name": text_field}
))
cte_qs = with_cte(
cte,
select=cte.join(Region, name=cte.col.region_name)
)
> regions = Region.objects.filter(name__in=cte_qs)
tests/test_raw.py:66:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.13/site-packages/django/db/models/manager.py:87: in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:1436: in filter
return self._filter_or_exclude(False, args, kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:1454: in _filter_or_exclude
clone._filter_or_exclude_inplace(negate, args, kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:1461: in _filter_or_exclude_inplace
self._query.add_q(Q(*args, **kwargs))
.venv/lib/python3.13/site-packages/django/db/models/sql/query.py:1546: in add_q
clause, _ = self._add_q(q_object, self.used_aliases)
.venv/lib/python3.13/site-packages/django/db/models/sql/query.py:1577: in _add_q
child_clause, needed_inner = self.build_filter(
.venv/lib/python3.13/site-packages/django/db/models/sql/query.py:1436: in build_filter
value = self.resolve_lookup_value(value, can_reuse, allow_joins, summarize)
.venv/lib/python3.13/site-packages/django/db/models/sql/query.py:1204: in resolve_lookup_value
value = value.resolve_expression(
.venv/lib/python3.13/site-packages/django/db/models/query.py:1923: in resolve_expression
query = self.query.resolve_expression(*args, **kwargs)
django_cte/query.py:52: in resolve_expression
clone._with_ctes = tuple(
django_cte/query.py:53: in <genexpr>
cte.resolve_expression(*args, **kwargs)
django_cte/cte.py:180: in resolve_expression
clone.query = clone.query.resolve_expression(*args, **kw)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'django_cte.raw.raw_cte_sql.<locals>.raw_cte_queryset.query'>
args = (<django.db.models.sql.query.Query object at 0x7fbbf1cbe440>,)
kwargs = {'allow_joins': True, 'reuse': set(), 'summarize': False}
@classmethod
def resolve_expression(cls, *args, **kwargs):
> raise NotImplementedError
E NotImplementedError
django_cte/raw.py:40: NotImplementedError
would it work just as well to return None
No, this needs to return a query class or else this fails later on.
_______________________________________________________ TestRawCTE.test_raw_cte_subquery _______________________________________________________
self = <tests.test_raw.TestRawCTE testMethod=test_raw_cte_subquery>
def test_raw_cte_subquery(self):
cte = CTE(raw_cte_sql(
"SELECT name as region_name FROM region WHERE name = %s",
["earth"],
{"region_name": text_field}
))
cte_qs = with_cte(
cte,
select=cte.join(Region, name=cte.col.region_name)
)
regions = Region.objects.filter(name__in=cte_qs)
> self.assertEqual(list(regions.values_list('name', flat=True)), ['earth'])
tests/test_raw.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.13/site-packages/django/db/models/query.py:398: in __iter__
self._fetch_all()
.venv/lib/python3.13/site-packages/django/db/models/query.py:1881: in _fetch_all
self._result_cache = list(self._iterable_class(self))
.venv/lib/python3.13/site-packages/django/db/models/query.py:285: in __iter__
for row in compiler.results_iter(
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1513: in results_iter
results = self.execute_sql(
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1549: in execute_sql
sql, params = self.as_sql()
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:764: in as_sql
self.compile(self.where) if self.where is not None else ("", [])
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:546: in compile
sql, params = node.as_sql(self, self.connection)
.venv/lib/python3.13/site-packages/django/db/models/sql/where.py:145: in as_sql
sql, params = compiler.compile(child)
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:546: in compile
sql, params = node.as_sql(self, self.connection)
.venv/lib/python3.13/site-packages/django/db/models/lookups.py:465: in as_sql
return super().as_sql(compiler, connection)
.venv/lib/python3.13/site-packages/django/db/models/lookups.py:225: in as_sql
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
.venv/lib/python3.13/site-packages/django/db/models/lookups.py:452: in process_rhs
return super().process_rhs(compiler, connection)
.venv/lib/python3.13/site-packages/django/db/models/lookups.py:286: in process_rhs
return super().process_rhs(compiler, connection)
.venv/lib/python3.13/site-packages/django/db/models/lookups.py:118: in process_rhs
sql, params = compiler.compile(value)
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:546: in compile
sql, params = node.as_sql(self, self.connection)
.venv/lib/python3.13/site-packages/django/db/models/sql/query.py:1197: in as_sql
sql, params = self.get_compiler(connection=connection).as_sql()
django_cte/query.py:152: in as_sql
return generate_cte_sql(self.connection, self.query, _as_sql)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
connection = <DatabaseWrapper vendor='sqlite' alias='default'>, query = <django_cte.jitmixin.CTEQuery object at 0x7f5fdd2a2530>
as_sql = <function CTECompiler.as_sql.<locals>._as_sql at 0x7f5fdd267d80>
def generate_cte_sql(connection, query, as_sql):
if not query._with_ctes:
return as_sql()
ctes = []
params = []
for cte in query._with_ctes:
if django.VERSION > (4, 2):
_ignore_with_col_aliases(cte.query)
alias = query.alias_map.get(cte.name)
should_elide_empty = (
not isinstance(alias, QJoin) or alias.join_type != LOUTER
)
> compiler = cte.query.get_compiler(
connection=connection, elide_empty=should_elide_empty
)
E AttributeError: 'NoneType' object has no attribute 'get_compiler'
django_cte/query.py:82: AttributeError
When a CTE is created with raw_cte_sql the query object, raw_cte_queryset.query, does not implement resolve_expression, causing an AttributeError. Specifically, this occurs when we use it within a Subquery. This patch adds resolve_expression implementation to raw_cte_queryset.query that simply returns cls. This satisfies django ORM because of duck typing and since the query is raw SQL, no actual expression resolution is needed.