django-cte 1.3.3.dev20250530125340__tar.gz → 2.0.0.dev20250610173146__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cte
3
- Version: 1.3.3.dev20250530125340
3
+ Version: 2.0.0.dev20250610173146
4
4
  Summary: Common Table Expressions (CTE) for Django
5
5
  Author-email: Daniel Miller <millerdev@gmail.com>
6
6
  Requires-Python: >= 3.9
@@ -0,0 +1,4 @@
1
+ from .cte import CTE, with_cte, CTEManager, CTEQuerySet, With # noqa
2
+
3
+ __version__ = "2.0.0.dev20250610173146"
4
+ __all__ = ["CTE", "with_cte"]
@@ -0,0 +1,138 @@
1
+ try:
2
+ from warnings import deprecated
3
+ except ImportError:
4
+ from warnings import warn
5
+
6
+ # Copied from Python 3.13, lightly modified for Python 3.9 compatibility.
7
+ # Can be removed when the oldest supported Python version is 3.13.
8
+ class deprecated:
9
+ """Indicate that a class, function or overload is deprecated.
10
+
11
+ When this decorator is applied to an object, the type checker
12
+ will generate a diagnostic on usage of the deprecated object.
13
+
14
+ Usage:
15
+
16
+ @deprecated("Use B instead")
17
+ class A:
18
+ pass
19
+
20
+ @deprecated("Use g instead")
21
+ def f():
22
+ pass
23
+
24
+ @overload
25
+ @deprecated("int support is deprecated")
26
+ def g(x: int) -> int: ...
27
+ @overload
28
+ def g(x: str) -> int: ...
29
+
30
+ The warning specified by *category* will be emitted at runtime
31
+ on use of deprecated objects. For functions, that happens on calls;
32
+ for classes, on instantiation and on creation of subclasses.
33
+ If the *category* is ``None``, no warning is emitted at runtime.
34
+ The *stacklevel* determines where the
35
+ warning is emitted. If it is ``1`` (the default), the warning
36
+ is emitted at the direct caller of the deprecated object; if it
37
+ is higher, it is emitted further up the stack.
38
+ Static type checker behavior is not affected by the *category*
39
+ and *stacklevel* arguments.
40
+
41
+ The deprecation message passed to the decorator is saved in the
42
+ ``__deprecated__`` attribute on the decorated object.
43
+ If applied to an overload, the decorator
44
+ must be after the ``@overload`` decorator for the attribute to
45
+ exist on the overload as returned by ``get_overloads()``.
46
+
47
+ See PEP 702 for details.
48
+
49
+ """
50
+ def __init__(
51
+ self,
52
+ message: str,
53
+ /,
54
+ *,
55
+ category=DeprecationWarning,
56
+ stacklevel=1,
57
+ ):
58
+ if not isinstance(message, str):
59
+ raise TypeError(
60
+ f"Expected an object of type str for 'message', not {type(message).__name__!r}"
61
+ )
62
+ self.message = message
63
+ self.category = category
64
+ self.stacklevel = stacklevel
65
+
66
+ def __call__(self, arg, /):
67
+ # Make sure the inner functions created below don't
68
+ # retain a reference to self.
69
+ msg = self.message
70
+ category = self.category
71
+ stacklevel = self.stacklevel
72
+ if category is None:
73
+ arg.__deprecated__ = msg
74
+ return arg
75
+ elif isinstance(arg, type):
76
+ import functools
77
+ from types import MethodType
78
+
79
+ original_new = arg.__new__
80
+
81
+ @functools.wraps(original_new)
82
+ def __new__(cls, /, *args, **kwargs):
83
+ if cls is arg:
84
+ warn(msg, category=category, stacklevel=stacklevel + 1)
85
+ if original_new is not object.__new__:
86
+ return original_new(cls, *args, **kwargs)
87
+ # Mirrors a similar check in object.__new__.
88
+ elif cls.__init__ is object.__init__ and (args or kwargs):
89
+ raise TypeError(f"{cls.__name__}() takes no arguments")
90
+ else:
91
+ return original_new(cls)
92
+
93
+ arg.__new__ = staticmethod(__new__)
94
+
95
+ original_init_subclass = arg.__init_subclass__
96
+ # We need slightly different behavior if __init_subclass__
97
+ # is a bound method (likely if it was implemented in Python)
98
+ if isinstance(original_init_subclass, MethodType):
99
+ original_init_subclass = original_init_subclass.__func__
100
+
101
+ @functools.wraps(original_init_subclass)
102
+ def __init_subclass__(*args, **kwargs):
103
+ warn(msg, category=category, stacklevel=stacklevel + 1)
104
+ return original_init_subclass(*args, **kwargs)
105
+
106
+ arg.__init_subclass__ = classmethod(__init_subclass__)
107
+ # Or otherwise, which likely means it's a builtin such as
108
+ # object's implementation of __init_subclass__.
109
+ else:
110
+ @functools.wraps(original_init_subclass)
111
+ def __init_subclass__(*args, **kwargs):
112
+ warn(msg, category=category, stacklevel=stacklevel + 1)
113
+ return original_init_subclass(*args, **kwargs)
114
+
115
+ arg.__init_subclass__ = __init_subclass__
116
+
117
+ arg.__deprecated__ = __new__.__deprecated__ = msg
118
+ __init_subclass__.__deprecated__ = msg
119
+ return arg
120
+ elif callable(arg):
121
+ import functools
122
+ import inspect
123
+
124
+ @functools.wraps(arg)
125
+ def wrapper(*args, **kwargs):
126
+ warn(msg, category=category, stacklevel=stacklevel + 1)
127
+ return arg(*args, **kwargs)
128
+
129
+ if inspect.iscoroutinefunction(arg):
130
+ wrapper = inspect.markcoroutinefunction(wrapper)
131
+
132
+ arg.__deprecated__ = wrapper.__deprecated__ = msg
133
+ return wrapper
134
+ else:
135
+ raise TypeError(
136
+ "@deprecated decorator with non-None category must be applied to "
137
+ f"a class or callable, not {arg!r}"
138
+ )
@@ -1,17 +1,38 @@
1
- from django.db.models import Manager
1
+ from copy import copy
2
+
3
+ from django.db.models import Manager, sql
2
4
  from django.db.models.expressions import Ref
3
5
  from django.db.models.query import Q, QuerySet, ValuesIterable
4
6
  from django.db.models.sql.datastructures import BaseTable
5
7
 
8
+ from .jitmixin import jit_mixin
6
9
  from .join import QJoin, INNER
7
10
  from .meta import CTEColumnRef, CTEColumns
8
11
  from .query import CTEQuery
12
+ from ._deprecated import deprecated
13
+
14
+ __all__ = ["CTE", "with_cte"]
15
+
16
+
17
+ def with_cte(*ctes, select):
18
+ """Add Common Table Expression(s) (CTEs) to a model or queryset
9
19
 
10
- __all__ = ["With", "CTEManager", "CTEQuerySet"]
20
+ :param *ctes: One or more CTE objects.
21
+ :param select: A model class, queryset, or CTE to use as the base
22
+ query to which CTEs are attached.
23
+ :returns: A queryset with the given CTE added to it.
24
+ """
25
+ if isinstance(select, CTE):
26
+ select = select.queryset()
27
+ elif not isinstance(select, QuerySet):
28
+ select = select._default_manager.all()
29
+ jit_mixin(select.query, CTEQuery)
30
+ select.query._with_ctes += ctes
31
+ return select
11
32
 
12
33
 
13
- class With(object):
14
- """Common Table Expression query object: `WITH ...`
34
+ class CTE:
35
+ """Common Table Expression
15
36
 
16
37
  :param queryset: A queryset to use as the body of the CTE.
17
38
  :param name: Optional name parameter for the CTE (default: "cte").
@@ -41,7 +62,7 @@ class With(object):
41
62
 
42
63
  @classmethod
43
64
  def recursive(cls, make_cte_queryset, name="cte", materialized=False):
44
- """Recursive Common Table Expression: `WITH RECURSIVE ...`
65
+ """Recursive Common Table Expression
45
66
 
46
67
  :param make_cte_queryset: Function taking a single argument (a
47
68
  not-yet-fully-constructed cte object) and returning a `QuerySet`
@@ -58,10 +79,11 @@ class With(object):
58
79
  def join(self, model_or_queryset, *filter_q, **filter_kw):
59
80
  """Join this CTE to the given model or queryset
60
81
 
61
- This CTE will be refernced by the returned queryset, but the
82
+ This CTE will be referenced by the returned queryset, but the
83
+
62
84
  corresponding `WITH ...` statement will not be prepended to the
63
- queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
64
- achieve that outcome.
85
+ queryset's SQL output; use `with_cte(cte, select=cte.join(...))`
86
+ to achieve that outcome.
65
87
 
66
88
  :param model_or_queryset: Model class or queryset to which the
67
89
  CTE should be joined.
@@ -96,15 +118,15 @@ class With(object):
96
118
 
97
119
  This CTE will be referenced by the returned queryset, but the
98
120
  corresponding `WITH ...` statement will not be prepended to the
99
- queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
100
- achieve that outcome.
121
+ queryset's SQL output; use `with_cte(cte, select=cte)` to do
122
+ that.
101
123
 
102
124
  :returns: A queryset.
103
125
  """
104
126
  cte_query = self.query
105
127
  qs = cte_query.model._default_manager.get_queryset()
106
128
 
107
- query = CTEQuery(cte_query.model)
129
+ query = jit_mixin(sql.Query(cte_query.model), CTEQuery)
108
130
  query.join(BaseTable(self.name, None))
109
131
  query.default_cols = cte_query.default_cols
110
132
  query.deferred_loading = cte_query.deferred_loading
@@ -113,8 +135,7 @@ class With(object):
113
135
  qs._iterable_class = ValuesIterable
114
136
  for alias in getattr(cte_query, "selected", None) or ():
115
137
  if alias not in cte_query.annotations:
116
- field = cte_query.resolve_ref(alias).output_field
117
- col = CTEColumnRef(alias, self.name, field)
138
+ col = Ref(alias, cte_query.resolve_ref(alias))
118
139
  query.add_annotation(col, alias)
119
140
  if cte_query.annotations:
120
141
  for alias, value in cte_query.annotations.items():
@@ -128,29 +149,41 @@ class With(object):
128
149
  def _resolve_ref(self, name):
129
150
  selected = getattr(self.query, "selected", None)
130
151
  if selected and name in selected and name not in self.query.annotations:
131
- return Ref(name, self.query)
152
+ return Ref(name, self.query.resolve_ref(name))
132
153
  return self.query.resolve_ref(name)
133
154
 
155
+ def resolve_expression(self, *args, **kw):
156
+ if self.query is None:
157
+ raise ValueError("Cannot resolve recursive CTE without a query.")
158
+ clone = copy(self)
159
+ clone.query = clone.query.resolve_expression(*args, **kw)
160
+ return clone
161
+
162
+
163
+ @deprecated("Use `django_cte.CTE` instead.")
164
+ class With(CTE):
165
+
166
+ @staticmethod
167
+ @deprecated("Use `django_cte.CTE.recursive` instead.")
168
+ def recursive(*args, **kw):
169
+ return CTE.recursive(*args, **kw)
170
+
134
171
 
172
+ @deprecated("CTEQuerySet is deprecated. "
173
+ "CTEs can now be applied to any queryset using `with_cte()`")
135
174
  class CTEQuerySet(QuerySet):
136
175
  """QuerySet with support for Common Table Expressions"""
137
176
 
138
177
  def __init__(self, model=None, query=None, using=None, hints=None):
139
178
  # Only create an instance of a Query if this is the first invocation in
140
179
  # a query chain.
141
- if query is None:
142
- query = CTEQuery(model)
143
180
  super(CTEQuerySet, self).__init__(model, query, using, hints)
181
+ jit_mixin(self.query, CTEQuery)
144
182
 
183
+ @deprecated("Use `django_cte.with_cte(cte, select=...)` instead.")
145
184
  def with_cte(self, cte):
146
- """Add a Common Table Expression to this queryset
147
-
148
- The CTE `WITH ...` clause will be added to the queryset's SQL
149
- output (after other CTEs that have already been added) so it
150
- can be referenced in annotations, filters, etc.
151
- """
152
185
  qs = self._clone()
153
- qs.query._with_ctes.append(cte)
186
+ qs.query._with_ctes += cte,
154
187
  return qs
155
188
 
156
189
  def as_manager(cls):
@@ -162,36 +195,9 @@ class CTEQuerySet(QuerySet):
162
195
  as_manager.queryset_only = True
163
196
  as_manager = classmethod(as_manager)
164
197
 
165
- def _combinator_query(self, *args, **kw):
166
- clone = super()._combinator_query(*args, **kw)
167
- if clone.query.combinator:
168
- ctes = clone.query._with_ctes = []
169
- seen = {}
170
- for query in clone.query.combined_queries:
171
- for cte in getattr(query, "_with_ctes", []):
172
- if seen.get(cte.name) is cte:
173
- continue
174
- if cte.name in seen:
175
- raise ValueError(
176
- f"Found two or more CTEs named '{cte.name}'. "
177
- "Hint: assign a unique name to each CTE."
178
- )
179
- ctes.append(cte)
180
- seen[cte.name] = cte
181
- if ctes:
182
- def without_ctes(query):
183
- if getattr(query, "_with_ctes", None):
184
- query = query.clone()
185
- query._with_ctes = []
186
- return query
187
-
188
- clone.query.combined_queries = [
189
- without_ctes(query)
190
- for query in clone.query.combined_queries
191
- ]
192
- return clone
193
-
194
198
 
199
+ @deprecated("CTEMAnager is deprecated. "
200
+ "CTEs can now be applied to any queryset using `with_cte()`")
195
201
  class CTEManager(Manager.from_queryset(CTEQuerySet)):
196
202
  """Manager for models that perform CTE queries"""
197
203
 
@@ -0,0 +1,28 @@
1
+ def jit_mixin(obj, mixin):
2
+ """Apply mixin to object and return the object"""
3
+ if not isinstance(obj, mixin):
4
+ obj.__class__ = jit_mixin_type(obj.__class__, mixin)
5
+ return obj
6
+
7
+
8
+ def jit_mixin_type(base, *mixins):
9
+ assert not issubclass(base, mixins), (base, mixins)
10
+ mixed = _mixin_cache.get((base, mixins))
11
+ if mixed is None:
12
+ prefix = "".join(m._jit_mixin_prefix for m in mixins)
13
+ name = f"{prefix}{base.__name__}"
14
+ mixed = _mixin_cache[(base, mixins)] = type(name, (*mixins, base), {
15
+ "_jit_mixin_base": getattr(base, "_jit_mixin_base", base),
16
+ "_jit_mixins": mixins + getattr(base, "_jit_mixins", ()),
17
+ })
18
+ return mixed
19
+
20
+
21
+ _mixin_cache = {}
22
+
23
+
24
+ class JITMixin:
25
+
26
+ def __reduce__(self):
27
+ # make JITMixin classes pickleable
28
+ return (jit_mixin_type, (self._jit_mixin_base, *self._jit_mixins))
@@ -1,7 +1,7 @@
1
1
  from django.db.models.sql.constants import INNER
2
2
 
3
3
 
4
- class QJoin(object):
4
+ class QJoin:
5
5
  """Join clause with join condition from Q object clause
6
6
 
7
7
  :param parent_alias: Alias of parent table.
@@ -3,7 +3,7 @@ import weakref
3
3
  from django.db.models.expressions import Col, Expression
4
4
 
5
5
 
6
- class CTEColumns(object):
6
+ class CTEColumns:
7
7
 
8
8
  def __init__(self, cte):
9
9
  self._cte = weakref.ref(cte)
@@ -87,7 +87,7 @@ class CTEColumnRef(Expression):
87
87
  clone._alias = self._alias or query.table_map.get(
88
88
  self.cte_name, [self.cte_name])[0]
89
89
  return clone
90
- return super(CTEColumnRef, self).resolve_expression(
90
+ return super().resolve_expression(
91
91
  query, allow_joins, reuse, summarize, for_save)
92
92
 
93
93
  def relabeled_clone(self, change_map):
@@ -95,7 +95,7 @@ class CTEColumnRef(Expression):
95
95
  self.cte_name not in change_map
96
96
  and self._alias not in change_map
97
97
  ):
98
- return super(CTEColumnRef, self).relabeled_clone(change_map)
98
+ return super().relabeled_clone(change_map)
99
99
 
100
100
  clone = self.copy()
101
101
  if self.cte_name in change_map:
@@ -0,0 +1,168 @@
1
+ import django
2
+ from django.core.exceptions import EmptyResultSet
3
+ from django.db.models.sql.constants import LOUTER
4
+
5
+ from .jitmixin import JITMixin, jit_mixin
6
+ from .join import QJoin
7
+
8
+ # NOTE: it is currently not possible to execute delete queries that
9
+ # reference CTEs without patching `QuerySet.delete` (Django method)
10
+ # to call `self.query.chain(sql.DeleteQuery)` instead of
11
+ # `sql.DeleteQuery(self.model)`
12
+
13
+
14
+ class CTEQuery(JITMixin):
15
+ """A Query mixin that processes SQL compilation through a CTE compiler"""
16
+ _jit_mixin_prefix = "CTE"
17
+ _with_ctes = ()
18
+
19
+ @property
20
+ def combined_queries(self):
21
+ return self.__dict__.get("combined_queries", ())
22
+
23
+ @combined_queries.setter
24
+ def combined_queries(self, queries):
25
+ ctes = []
26
+ seen = {cte.name: cte for cte in self._with_ctes}
27
+ for query in queries:
28
+ for cte in getattr(query, "_with_ctes", ()):
29
+ if seen.get(cte.name) is cte:
30
+ continue
31
+ if cte.name in seen:
32
+ raise ValueError(
33
+ f"Found two or more CTEs named '{cte.name}'. "
34
+ "Hint: assign a unique name to each CTE."
35
+ )
36
+ ctes.append(cte)
37
+ seen[cte.name] = cte
38
+
39
+ if seen:
40
+ def without_ctes(query):
41
+ if getattr(query, "_with_ctes", None):
42
+ query = query.clone()
43
+ del query._with_ctes
44
+ return query
45
+
46
+ self._with_ctes += tuple(ctes)
47
+ queries = tuple(without_ctes(q) for q in queries)
48
+ self.__dict__["combined_queries"] = queries
49
+
50
+ def resolve_expression(self, *args, **kwargs):
51
+ clone = super().resolve_expression(*args, **kwargs)
52
+ clone._with_ctes = tuple(
53
+ cte.resolve_expression(*args, **kwargs)
54
+ for cte in clone._with_ctes
55
+ )
56
+ return clone
57
+
58
+ def get_compiler(self, *args, **kwargs):
59
+ return jit_mixin(super().get_compiler(*args, **kwargs), CTECompiler)
60
+
61
+ def chain(self, klass=None):
62
+ clone = jit_mixin(super().chain(klass), CTEQuery)
63
+ clone._with_ctes = self._with_ctes
64
+ return clone
65
+
66
+
67
+ def generate_cte_sql(connection, query, as_sql):
68
+ if not query._with_ctes:
69
+ return as_sql()
70
+
71
+ ctes = []
72
+ params = []
73
+ for cte in query._with_ctes:
74
+ if django.VERSION > (4, 2):
75
+ _ignore_with_col_aliases(cte.query)
76
+
77
+ alias = query.alias_map.get(cte.name)
78
+ should_elide_empty = (
79
+ not isinstance(alias, QJoin) or alias.join_type != LOUTER
80
+ )
81
+
82
+ compiler = cte.query.get_compiler(
83
+ connection=connection, elide_empty=should_elide_empty
84
+ )
85
+
86
+ qn = compiler.quote_name_unless_alias
87
+ try:
88
+ cte_sql, cte_params = compiler.as_sql()
89
+ except EmptyResultSet:
90
+ # If the CTE raises an EmptyResultSet the SqlCompiler still
91
+ # needs to know the information about this base compiler
92
+ # like, col_count and klass_info.
93
+ as_sql()
94
+ raise
95
+ template = get_cte_query_template(cte)
96
+ ctes.append(template.format(name=qn(cte.name), query=cte_sql))
97
+ params.extend(cte_params)
98
+
99
+ explain_attribute = "explain_info"
100
+ explain_info = getattr(query, explain_attribute, None)
101
+ explain_format = getattr(explain_info, "format", None)
102
+ explain_options = getattr(explain_info, "options", {})
103
+
104
+ explain_query_or_info = getattr(query, explain_attribute, None)
105
+ sql = []
106
+ if explain_query_or_info:
107
+ sql.append(
108
+ connection.ops.explain_query_prefix(
109
+ explain_format,
110
+ **explain_options
111
+ )
112
+ )
113
+ # this needs to get set to None so that the base as_sql() doesn't
114
+ # insert the EXPLAIN statement where it would end up between the
115
+ # WITH ... clause and the final SELECT
116
+ setattr(query, explain_attribute, None)
117
+
118
+ if ctes:
119
+ # Always use WITH RECURSIVE
120
+ # https://www.postgresql.org/message-id/13122.1339829536%40sss.pgh.pa.us
121
+ sql.extend(["WITH RECURSIVE", ", ".join(ctes)])
122
+ base_sql, base_params = as_sql()
123
+
124
+ if explain_query_or_info:
125
+ setattr(query, explain_attribute, explain_query_or_info)
126
+
127
+ sql.append(base_sql)
128
+ params.extend(base_params)
129
+ return " ".join(sql), tuple(params)
130
+
131
+
132
+ def get_cte_query_template(cte):
133
+ if cte.materialized:
134
+ return "{name} AS MATERIALIZED ({query})"
135
+ return "{name} AS ({query})"
136
+
137
+
138
+ def _ignore_with_col_aliases(cte_query):
139
+ if getattr(cte_query, "combined_queries", None):
140
+ cte_query.combined_queries = tuple(
141
+ jit_mixin(q, NoAliasQuery) for q in cte_query.combined_queries
142
+ )
143
+
144
+
145
+ class CTECompiler(JITMixin):
146
+ """Mixin for django.db.models.sql.compiler.SQLCompiler"""
147
+ _jit_mixin_prefix = "CTE"
148
+
149
+ def as_sql(self, *args, **kwargs):
150
+ def _as_sql():
151
+ return super(CTECompiler, self).as_sql(*args, **kwargs)
152
+ return generate_cte_sql(self.connection, self.query, _as_sql)
153
+
154
+
155
+ class NoAliasQuery(JITMixin):
156
+ """Mixin for django.db.models.sql.compiler.Query"""
157
+ _jit_mixin_prefix = "NoAlias"
158
+
159
+ def get_compiler(self, *args, **kwargs):
160
+ return jit_mixin(super().get_compiler(*args, **kwargs), NoAliasCompiler)
161
+
162
+
163
+ class NoAliasCompiler(JITMixin):
164
+ """Mixin for django.db.models.sql.compiler.SQLCompiler"""
165
+ _jit_mixin_prefix = "NoAlias"
166
+
167
+ def get_select(self, *, with_col_aliases=False, **kw):
168
+ return super().get_select(**kw)
@@ -7,14 +7,14 @@ def raw_cte_sql(sql, params, refs):
7
7
  :returns: Object that can be passed to `With`.
8
8
  """
9
9
 
10
- class raw_cte_ref(object):
10
+ class raw_cte_ref:
11
11
  def __init__(self, output_field):
12
12
  self.output_field = output_field
13
13
 
14
14
  def get_source_expressions(self):
15
15
  return []
16
16
 
17
- class raw_cte_compiler(object):
17
+ class raw_cte_compiler:
18
18
 
19
19
  def __init__(self, connection):
20
20
  self.connection = connection
@@ -25,8 +25,8 @@ def raw_cte_sql(sql, params, refs):
25
25
  def quote_name_unless_alias(self, name):
26
26
  return self.connection.ops.quote_name(name)
27
27
 
28
- class raw_cte_queryset(object):
29
- class query(object):
28
+ class raw_cte_queryset:
29
+ class query:
30
30
  @staticmethod
31
31
  def get_compiler(connection, *, elide_empty=None):
32
32
  return raw_cte_compiler(connection)
@@ -1,3 +0,0 @@
1
- from .cte import CTEManager, CTEQuerySet, With # noqa
2
-
3
- __version__ = "1.3.3.dev20250530125340"
@@ -1,45 +0,0 @@
1
- from django.db.models import Subquery
2
-
3
-
4
- class CTESubqueryResolver(object):
5
-
6
- def __init__(self, annotation):
7
- self.annotation = annotation
8
-
9
- def resolve_expression(self, *args, **kw):
10
- # source: django.db.models.expressions.Subquery.resolve_expression
11
- # --- begin copied code (lightly adapted) --- #
12
-
13
- # Need to recursively resolve these.
14
- def resolve_all(child):
15
- if hasattr(child, 'children'):
16
- [resolve_all(_child) for _child in child.children]
17
- if hasattr(child, 'rhs'):
18
- child.rhs = resolve(child.rhs)
19
-
20
- def resolve(child):
21
- if hasattr(child, 'resolve_expression'):
22
- resolved = child.resolve_expression(*args, **kw)
23
- # Add table alias to the parent query's aliases to prevent
24
- # quoting.
25
- if hasattr(resolved, 'alias') and \
26
- resolved.alias != resolved.target.model._meta.db_table:
27
- get_query(clone).external_aliases.add(resolved.alias)
28
- return resolved
29
- return child
30
-
31
- # --- end copied code --- #
32
-
33
- def get_query(clone):
34
- return clone.query
35
-
36
- # NOTE this uses the old (pre-Django 3) way of resolving.
37
- # Should a different technique should be used on Django 3+?
38
- clone = self.annotation.resolve_expression(*args, **kw)
39
- if isinstance(self.annotation, Subquery):
40
- for cte in getattr(get_query(clone), '_with_ctes', []):
41
- resolve_all(cte.query.where)
42
- for key, value in cte.query.annotations.items():
43
- if isinstance(value, Subquery):
44
- cte.query.annotations[key] = resolve(value)
45
- return clone
@@ -1,194 +0,0 @@
1
- import django
2
- from django.core.exceptions import EmptyResultSet
3
- from django.db import connections
4
- from django.db.models.sql import DeleteQuery, Query, UpdateQuery
5
- from django.db.models.sql.compiler import (
6
- SQLCompiler,
7
- SQLDeleteCompiler,
8
- SQLUpdateCompiler,
9
- )
10
- from django.db.models.sql.constants import LOUTER
11
-
12
- from .expressions import CTESubqueryResolver
13
- from .join import QJoin
14
-
15
-
16
- class CTEQuery(Query):
17
- """A Query which processes SQL compilation through the CTE compiler"""
18
-
19
- def __init__(self, *args, **kwargs):
20
- super(CTEQuery, self).__init__(*args, **kwargs)
21
- self._with_ctes = []
22
-
23
- def combine(self, other, connector):
24
- if other._with_ctes:
25
- if self._with_ctes:
26
- raise TypeError("cannot merge queries with CTEs on both sides")
27
- self._with_ctes = other._with_ctes[:]
28
- return super(CTEQuery, self).combine(other, connector)
29
-
30
- def get_compiler(self, using=None, connection=None, *args, **kwargs):
31
- """ Overrides the Query method get_compiler in order to return
32
- a CTECompiler.
33
- """
34
- # Copy the body of this method from Django except the final
35
- # return statement. We will ignore code coverage for this.
36
- if using is None and connection is None: # pragma: no cover
37
- raise ValueError("Need either using or connection")
38
- if using:
39
- connection = connections[using]
40
- # Check that the compiler will be able to execute the query
41
- for alias, aggregate in self.annotation_select.items():
42
- connection.ops.check_expression_support(aggregate)
43
- # Instantiate the custom compiler.
44
- klass = COMPILER_TYPES.get(self.__class__, CTEQueryCompiler)
45
- return klass(self, connection, using, *args, **kwargs)
46
-
47
- def add_annotation(self, annotation, *args, **kw):
48
- annotation = CTESubqueryResolver(annotation)
49
- super(CTEQuery, self).add_annotation(annotation, *args, **kw)
50
-
51
- def __chain(self, _name, klass=None, *args, **kwargs):
52
- klass = QUERY_TYPES.get(klass, self.__class__)
53
- clone = getattr(super(CTEQuery, self), _name)(klass, *args, **kwargs)
54
- clone._with_ctes = self._with_ctes[:]
55
- return clone
56
-
57
- def chain(self, klass=None):
58
- return self.__chain("chain", klass)
59
-
60
-
61
- class CTECompiler(object):
62
-
63
- @classmethod
64
- def generate_sql(cls, connection, query, as_sql):
65
- if not query._with_ctes:
66
- return as_sql()
67
-
68
- ctes = []
69
- params = []
70
- for cte in query._with_ctes:
71
- if django.VERSION > (4, 2):
72
- _ignore_with_col_aliases(cte.query)
73
-
74
- alias = query.alias_map.get(cte.name)
75
- should_elide_empty = (
76
- not isinstance(alias, QJoin) or alias.join_type != LOUTER
77
- )
78
-
79
- compiler = cte.query.get_compiler(
80
- connection=connection, elide_empty=should_elide_empty
81
- )
82
-
83
- qn = compiler.quote_name_unless_alias
84
- try:
85
- cte_sql, cte_params = compiler.as_sql()
86
- except EmptyResultSet:
87
- # If the CTE raises an EmptyResultSet the SqlCompiler still
88
- # needs to know the information about this base compiler
89
- # like, col_count and klass_info.
90
- as_sql()
91
- raise
92
- template = cls.get_cte_query_template(cte)
93
- ctes.append(template.format(name=qn(cte.name), query=cte_sql))
94
- params.extend(cte_params)
95
-
96
- explain_attribute = "explain_info"
97
- explain_info = getattr(query, explain_attribute, None)
98
- explain_format = getattr(explain_info, "format", None)
99
- explain_options = getattr(explain_info, "options", {})
100
-
101
- explain_query_or_info = getattr(query, explain_attribute, None)
102
- sql = []
103
- if explain_query_or_info:
104
- sql.append(
105
- connection.ops.explain_query_prefix(
106
- explain_format,
107
- **explain_options
108
- )
109
- )
110
- # this needs to get set to None so that the base as_sql() doesn't
111
- # insert the EXPLAIN statement where it would end up between the
112
- # WITH ... clause and the final SELECT
113
- setattr(query, explain_attribute, None)
114
-
115
- if ctes:
116
- # Always use WITH RECURSIVE
117
- # https://www.postgresql.org/message-id/13122.1339829536%40sss.pgh.pa.us
118
- sql.extend(["WITH RECURSIVE", ", ".join(ctes)])
119
- base_sql, base_params = as_sql()
120
-
121
- if explain_query_or_info:
122
- setattr(query, explain_attribute, explain_query_or_info)
123
-
124
- sql.append(base_sql)
125
- params.extend(base_params)
126
- return " ".join(sql), tuple(params)
127
-
128
- @classmethod
129
- def get_cte_query_template(cls, cte):
130
- if cte.materialized:
131
- return "{name} AS MATERIALIZED ({query})"
132
- return "{name} AS ({query})"
133
-
134
-
135
- class CTEUpdateQuery(UpdateQuery, CTEQuery):
136
- pass
137
-
138
-
139
- class CTEDeleteQuery(DeleteQuery, CTEQuery):
140
- pass
141
-
142
-
143
- QUERY_TYPES = {
144
- Query: CTEQuery,
145
- UpdateQuery: CTEUpdateQuery,
146
- DeleteQuery: CTEDeleteQuery,
147
- }
148
-
149
-
150
- def _ignore_with_col_aliases(cte_query):
151
- if getattr(cte_query, "combined_queries", None):
152
- for query in cte_query.combined_queries:
153
- query.ignore_with_col_aliases = True
154
-
155
-
156
- class CTEQueryCompiler(SQLCompiler):
157
-
158
- def as_sql(self, *args, **kwargs):
159
- def _as_sql():
160
- return super(CTEQueryCompiler, self).as_sql(*args, **kwargs)
161
- return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
162
-
163
- def get_select(self, **kw):
164
- if kw.get("with_col_aliases") \
165
- and getattr(self.query, "ignore_with_col_aliases", False):
166
- kw.pop("with_col_aliases")
167
- return super().get_select(**kw)
168
-
169
-
170
- class CTEUpdateQueryCompiler(SQLUpdateCompiler):
171
-
172
- def as_sql(self, *args, **kwargs):
173
- def _as_sql():
174
- return super(CTEUpdateQueryCompiler, self).as_sql(*args, **kwargs)
175
- return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
176
-
177
-
178
- class CTEDeleteQueryCompiler(SQLDeleteCompiler):
179
-
180
- # NOTE: it is currently not possible to execute delete queries that
181
- # reference CTEs without patching `QuerySet.delete` (Django method)
182
- # to call `self.query.chain(sql.DeleteQuery)` instead of
183
- # `sql.DeleteQuery(self.model)`
184
-
185
- def as_sql(self, *args, **kwargs):
186
- def _as_sql():
187
- return super(CTEDeleteQueryCompiler, self).as_sql(*args, **kwargs)
188
- return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
189
-
190
-
191
- COMPILER_TYPES = {
192
- CTEUpdateQuery: CTEUpdateQueryCompiler,
193
- CTEDeleteQuery: CTEDeleteQueryCompiler,
194
- }