Skip to content

Commit

Permalink
Handle left outer joins in versions before 4.0
Browse files Browse the repository at this point in the history
Before Django 4.0, the `elide_empty` argument on the compiler did not
exist. To ensure that left outer joins in these versions of Django are
not optimized away by the SqlCompile, essentially the same behavior is
added to the error handling in those versions of Django. This creates
a copy of the CTE query, forces the where to have a always-false
condition that the SqlCompiler cannot optimize away, and then builds
the SQL for that query instead.
  • Loading branch information
camuthig committed May 22, 2024
1 parent 5d6ef62 commit a5eeaa3
Showing 1 changed file with 30 additions and 9 deletions.
39 changes: 30 additions & 9 deletions django_cte/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SQLUpdateCompiler,
)
from django.db.models.sql.constants import LOUTER
from django.db.models.sql.where import ExtraWhere, AND, WhereNode

from .expressions import CTESubqueryResolver
from .join import QJoin
Expand Down Expand Up @@ -79,21 +80,41 @@ def generate_sql(cls, connection, query, as_sql):
if django.VERSION > (4, 2):
_ignore_with_col_aliases(cte.query)

elide_empty = True
alias = query.alias_map.get(cte.name)
if isinstance(alias, QJoin) and alias.join_type == LOUTER:
elide_empty = False
is_left_outer = (
isinstance(alias, QJoin) and alias.join_type == LOUTER
)

if django.VERSION >= (4, 0):
compiler = cte.query.get_compiler(
connection=connection, elide_empty=not is_left_outer
)
else:
compiler = cte.query.get_compiler(connection=connection)

compiler = cte.query.get_compiler(connection=connection, elide_empty=elide_empty)
qn = compiler.quote_name_unless_alias
try:
cte_sql, cte_params = compiler.as_sql()
except EmptyResultSet:
# If the CTE raises an EmptyResultSet the SqlCompiler still
# needs to know the information about this base compiler like,
# col_count and klass_info.
as_sql()
raise
if django.VERSION < (4, 0) and is_left_outer:
# elide_empty is not available prior to Django 4.0. The
# below behavior emulates the logic of it, rebuilding
# the CTE query with a WHERE clause that is always false
# but that the SqlCompiler cannot optimize away. This is
# only required for left outer joins, as standard inner
# joins should be optimized and raise the EmptyResultSet
query = cte.query.copy()
query.where = WhereNode(
[ExtraWhere(["1 = 0"], [])], AND
)
compiler = query.get_compiler(connection=connection)
cte_sql, cte_params = compiler.as_sql()
else:
# If the CTE raises an EmptyResultSet the SqlCompiler still
# needs to know the information about this base compiler
# like, col_count and klass_info.
as_sql()
raise
template = cls.get_cte_query_template(cte)
ctes.append(template.format(name=qn(cte.name), query=cte_sql))
params.extend(cte_params)
Expand Down

0 comments on commit a5eeaa3

Please sign in to comment.