django-cte 1.3.3.dev20250530125340__py3-none-any.whl → 2.0.0.dev20250610173146__py3-none-any.whl
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.
- django_cte/__init__.py +3 -2
- django_cte/_deprecated.py +138 -0
- django_cte/cte.py +58 -52
- django_cte/jitmixin.py +28 -0
- django_cte/join.py +1 -1
- django_cte/meta.py +3 -3
- django_cte/query.py +133 -159
- django_cte/raw.py +4 -4
- {django_cte-1.3.3.dev20250530125340.dist-info → django_cte-2.0.0.dev20250610173146.dist-info}/METADATA +1 -1
- django_cte-2.0.0.dev20250610173146.dist-info/RECORD +12 -0
- django_cte/expressions.py +0 -45
- django_cte-1.3.3.dev20250530125340.dist-info/RECORD +0 -11
- {django_cte-1.3.3.dev20250530125340.dist-info → django_cte-2.0.0.dev20250610173146.dist-info}/WHEEL +0 -0
- {django_cte-1.3.3.dev20250530125340.dist-info → django_cte-2.0.0.dev20250610173146.dist-info}/licenses/LICENSE +0 -0
django_cte/__init__.py
CHANGED
|
@@ -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
|
+
)
|
django_cte/cte.py
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
|
-
from
|
|
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
|
-
|
|
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
|
|
14
|
-
"""Common Table Expression
|
|
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
|
|
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
|
|
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
|
|
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
|
|
100
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
django_cte/jitmixin.py
ADDED
|
@@ -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))
|
django_cte/join.py
CHANGED
django_cte/meta.py
CHANGED
|
@@ -3,7 +3,7 @@ import weakref
|
|
|
3
3
|
from django.db.models.expressions import Col, Expression
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class CTEColumns
|
|
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(
|
|
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(
|
|
98
|
+
return super().relabeled_clone(change_map)
|
|
99
99
|
|
|
100
100
|
clone = self.copy()
|
|
101
101
|
if self.cte_name in change_map:
|
django_cte/query.py
CHANGED
|
@@ -1,194 +1,168 @@
|
|
|
1
1
|
import django
|
|
2
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
3
|
from django.db.models.sql.constants import LOUTER
|
|
11
4
|
|
|
12
|
-
from .
|
|
5
|
+
from .jitmixin import JITMixin, jit_mixin
|
|
13
6
|
from .join import QJoin
|
|
14
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)`
|
|
15
12
|
|
|
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
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 = ()
|
|
60
18
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def generate_sql(cls, connection, query, as_sql):
|
|
65
|
-
if not query._with_ctes:
|
|
66
|
-
return as_sql()
|
|
19
|
+
@property
|
|
20
|
+
def combined_queries(self):
|
|
21
|
+
return self.__dict__.get("combined_queries", ())
|
|
67
22
|
|
|
23
|
+
@combined_queries.setter
|
|
24
|
+
def combined_queries(self, queries):
|
|
68
25
|
ctes = []
|
|
69
|
-
|
|
70
|
-
for
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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()
|
|
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
|
|
120
57
|
|
|
121
|
-
|
|
122
|
-
|
|
58
|
+
def get_compiler(self, *args, **kwargs):
|
|
59
|
+
return jit_mixin(super().get_compiler(*args, **kwargs), CTECompiler)
|
|
123
60
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
61
|
+
def chain(self, klass=None):
|
|
62
|
+
clone = jit_mixin(super().chain(klass), CTEQuery)
|
|
63
|
+
clone._with_ctes = self._with_ctes
|
|
64
|
+
return clone
|
|
127
65
|
|
|
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
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)
|
|
134
117
|
|
|
135
|
-
|
|
136
|
-
|
|
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()
|
|
137
123
|
|
|
124
|
+
if explain_query_or_info:
|
|
125
|
+
setattr(query, explain_attribute, explain_query_or_info)
|
|
138
126
|
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
sql.append(base_sql)
|
|
128
|
+
params.extend(base_params)
|
|
129
|
+
return " ".join(sql), tuple(params)
|
|
141
130
|
|
|
142
131
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
132
|
+
def get_cte_query_template(cte):
|
|
133
|
+
if cte.materialized:
|
|
134
|
+
return "{name} AS MATERIALIZED ({query})"
|
|
135
|
+
return "{name} AS ({query})"
|
|
148
136
|
|
|
149
137
|
|
|
150
138
|
def _ignore_with_col_aliases(cte_query):
|
|
151
139
|
if getattr(cte_query, "combined_queries", None):
|
|
152
|
-
|
|
153
|
-
|
|
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)
|
|
140
|
+
cte_query.combined_queries = tuple(
|
|
141
|
+
jit_mixin(q, NoAliasQuery) for q in cte_query.combined_queries
|
|
142
|
+
)
|
|
168
143
|
|
|
169
144
|
|
|
170
|
-
class
|
|
145
|
+
class CTECompiler(JITMixin):
|
|
146
|
+
"""Mixin for django.db.models.sql.compiler.SQLCompiler"""
|
|
147
|
+
_jit_mixin_prefix = "CTE"
|
|
171
148
|
|
|
172
149
|
def as_sql(self, *args, **kwargs):
|
|
173
150
|
def _as_sql():
|
|
174
|
-
return super(
|
|
175
|
-
return
|
|
151
|
+
return super(CTECompiler, self).as_sql(*args, **kwargs)
|
|
152
|
+
return generate_cte_sql(self.connection, self.query, _as_sql)
|
|
176
153
|
|
|
177
154
|
|
|
178
|
-
class
|
|
155
|
+
class NoAliasQuery(JITMixin):
|
|
156
|
+
"""Mixin for django.db.models.sql.compiler.Query"""
|
|
157
|
+
_jit_mixin_prefix = "NoAlias"
|
|
179
158
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# to call `self.query.chain(sql.DeleteQuery)` instead of
|
|
183
|
-
# `sql.DeleteQuery(self.model)`
|
|
159
|
+
def get_compiler(self, *args, **kwargs):
|
|
160
|
+
return jit_mixin(super().get_compiler(*args, **kwargs), NoAliasCompiler)
|
|
184
161
|
|
|
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
162
|
|
|
163
|
+
class NoAliasCompiler(JITMixin):
|
|
164
|
+
"""Mixin for django.db.models.sql.compiler.SQLCompiler"""
|
|
165
|
+
_jit_mixin_prefix = "NoAlias"
|
|
190
166
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
CTEDeleteQuery: CTEDeleteQueryCompiler,
|
|
194
|
-
}
|
|
167
|
+
def get_select(self, *, with_col_aliases=False, **kw):
|
|
168
|
+
return super().get_select(**kw)
|
django_cte/raw.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
29
|
-
class query
|
|
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)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
django_cte/__init__.py,sha256=4SGKhYSndhDMgsYFQAadko2b7WTByry4YYzqyOOKZc4,141
|
|
2
|
+
django_cte/_deprecated.py,sha256=OAEmWVqnoi-27m2nyrfTIwVwPE1J8VLrfspgRHW-EZI,5623
|
|
3
|
+
django_cte/cte.py,sha256=Rs_luCEFK39UEOm4rRaL2L5BztfvN9ST-Y7RVrjub_c,8129
|
|
4
|
+
django_cte/jitmixin.py,sha256=JRTRlbBhD8XD0GHwxg8wKdJSbcTv_zUrSyqtOSfgM5M,885
|
|
5
|
+
django_cte/join.py,sha256=2b1iBKFFiqBqYBmX4W9q2RfX17xXjZbbzClpUdbnUtA,3156
|
|
6
|
+
django_cte/meta.py,sha256=gNBtegkvg5CXcEPyt1nZMIF2UlsPMTUPStVJSsRni3I,3491
|
|
7
|
+
django_cte/query.py,sha256=djY7Bknl_NQSd1g6zy8N3fU3h2r8OfpyeJY_Zt9a7fg,5582
|
|
8
|
+
django_cte/raw.py,sha256=6_YDCKBsgXx1oQku4EcyK4qJmnmiyzEeZRxiYdkg8Y8,1045
|
|
9
|
+
django_cte-2.0.0.dev20250610173146.dist-info/licenses/LICENSE,sha256=mkLNw_QhpZ40jBEbuAosqH4ciA3KMrwb8aSYbTmy5gc,1508
|
|
10
|
+
django_cte-2.0.0.dev20250610173146.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
11
|
+
django_cte-2.0.0.dev20250610173146.dist-info/METADATA,sha256=2hqFagZWfP3zraJj8CyeLxrbdNrp5DESgsCqzSW1Tyc,2696
|
|
12
|
+
django_cte-2.0.0.dev20250610173146.dist-info/RECORD,,
|
django_cte/expressions.py
DELETED
|
@@ -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,11 +0,0 @@
|
|
|
1
|
-
django_cte/__init__.py,sha256=2ZCHug6-KqXs56srJW5rsMCPOLRB4nrWiHm0Jy3--k8,96
|
|
2
|
-
django_cte/cte.py,sha256=ihU73uLUcDk0twufGA9GjC4neiEYM-WKq_zVmiAFE4U,8243
|
|
3
|
-
django_cte/expressions.py,sha256=3MDu2ldum3LhRVX1U6jiyzR83avFG53H7P72IkgoD-w,1780
|
|
4
|
-
django_cte/join.py,sha256=HrO31WKx3j5RiEWf7dDOXbBfeeUPFEbNLo3gzzt3qbo,3164
|
|
5
|
-
django_cte/meta.py,sha256=PIu7iW_tu3gBCpiBRfVHvuJKSSND2IsaNhoT01WEEJM,3535
|
|
6
|
-
django_cte/query.py,sha256=zIDvWchXtLSc7o1PWp_t2UDDHknuFlfn3DPlDqfjWa0,6753
|
|
7
|
-
django_cte/raw.py,sha256=nROVPEVRc3_zKAFcED6KR8Rr6FTTn8jQDFQWfi-X3-A,1077
|
|
8
|
-
django_cte-1.3.3.dev20250530125340.dist-info/licenses/LICENSE,sha256=mkLNw_QhpZ40jBEbuAosqH4ciA3KMrwb8aSYbTmy5gc,1508
|
|
9
|
-
django_cte-1.3.3.dev20250530125340.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
10
|
-
django_cte-1.3.3.dev20250530125340.dist-info/METADATA,sha256=84e0zmdp5yOeAt6-O69Y1VHyZj-AqfTUGJip4l9l2Kw,2696
|
|
11
|
-
django_cte-1.3.3.dev20250530125340.dist-info/RECORD,,
|
{django_cte-1.3.3.dev20250530125340.dist-info → django_cte-2.0.0.dev20250610173146.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|