Skip to content

Fix raw_cte_queryset in Subqueries#132

Merged
millerdev merged 1 commit into
dimagi:mainfrom
wawrzdev:raw_cte_queryset
Jan 13, 2026
Merged

Fix raw_cte_queryset in Subqueries#132
millerdev merged 1 commit into
dimagi:mainfrom
wawrzdev:raw_cte_queryset

Conversation

@wawrzdev
Copy link
Copy Markdown
Contributor

@wawrzdev wawrzdev commented Jan 6, 2026

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.

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.
@millerdev millerdev enabled auto-merge January 8, 2026 17:32
Comment thread django_cte/raw.py

@classmethod
def resolve_expression(cls, *args, **kwargs):
return cls
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

@wawrzdev wawrzdev Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@millerdev millerdev merged commit 40645ac into dimagi:main Jan 13, 2026
17 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.

2 participants