django-cte 1.3.3.dev20250526204410__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 ADDED
@@ -0,0 +1,3 @@
1
+ from .cte import CTEManager, CTEQuerySet, With # noqa
2
+
3
+ __version__ = "1.3.3.dev20250526204410"
django_cte/cte.py ADDED
@@ -0,0 +1,166 @@
1
+ from django.db.models import Manager
2
+ from django.db.models.query import Q, QuerySet, ValuesIterable
3
+ from django.db.models.sql.datastructures import BaseTable
4
+
5
+ from .join import QJoin, INNER
6
+ from .meta import CTEColumnRef, CTEColumns
7
+ from .query import CTEQuery
8
+
9
+ __all__ = ["With", "CTEManager", "CTEQuerySet"]
10
+
11
+
12
+ class With(object):
13
+ """Common Table Expression query object: `WITH ...`
14
+
15
+ :param queryset: A queryset to use as the body of the CTE.
16
+ :param name: Optional name parameter for the CTE (default: "cte").
17
+ This must be a unique name that does not conflict with other
18
+ entities (tables, views, functions, other CTE(s), etc.) referenced
19
+ in the given query as well any query to which this CTE will
20
+ eventually be added.
21
+ :param materialized: Optional parameter (default: False) which enforce
22
+ using of MATERIALIZED statement for supporting databases.
23
+ """
24
+
25
+ def __init__(self, queryset, name="cte", materialized=False):
26
+ self.query = None if queryset is None else queryset.query
27
+ self.name = name
28
+ self.col = CTEColumns(self)
29
+ self.materialized = materialized
30
+
31
+ def __getstate__(self):
32
+ return (self.query, self.name, self.materialized)
33
+
34
+ def __setstate__(self, state):
35
+ self.query, self.name, self.materialized = state
36
+ self.col = CTEColumns(self)
37
+
38
+ def __repr__(self):
39
+ return "<With {}>".format(self.name)
40
+
41
+ @classmethod
42
+ def recursive(cls, make_cte_queryset, name="cte", materialized=False):
43
+ """Recursive Common Table Expression: `WITH RECURSIVE ...`
44
+
45
+ :param make_cte_queryset: Function taking a single argument (a
46
+ not-yet-fully-constructed cte object) and returning a `QuerySet`
47
+ object. The returned `QuerySet` normally consists of an initial
48
+ statement unioned with a recursive statement.
49
+ :param name: See `name` parameter of `__init__`.
50
+ :param materialized: See `materialized` parameter of `__init__`.
51
+ :returns: The fully constructed recursive cte object.
52
+ """
53
+ cte = cls(None, name, materialized)
54
+ cte.query = make_cte_queryset(cte).query
55
+ return cte
56
+
57
+ def join(self, model_or_queryset, *filter_q, **filter_kw):
58
+ """Join this CTE to the given model or queryset
59
+
60
+ This CTE will be refernced by the returned queryset, but the
61
+ corresponding `WITH ...` statement will not be prepended to the
62
+ queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
63
+ achieve that outcome.
64
+
65
+ :param model_or_queryset: Model class or queryset to which the
66
+ CTE should be joined.
67
+ :param *filter_q: Join condition Q expressions (optional).
68
+ :param **filter_kw: Join conditions. All LHS fields (kwarg keys)
69
+ are assumed to reference `model_or_queryset` fields. Use
70
+ `cte.col.name` on the RHS to recursively reference CTE query
71
+ columns. For example: `cte.join(Book, id=cte.col.id)`
72
+ :returns: A queryset with the given model or queryset joined to
73
+ this CTE.
74
+ """
75
+ if isinstance(model_or_queryset, QuerySet):
76
+ queryset = model_or_queryset.all()
77
+ else:
78
+ queryset = model_or_queryset._default_manager.all()
79
+ join_type = filter_kw.pop("_join_type", INNER)
80
+ query = queryset.query
81
+
82
+ # based on Query.add_q: add necessary joins to query, but no filter
83
+ q_object = Q(*filter_q, **filter_kw)
84
+ map = query.alias_map
85
+ existing_inner = set(a for a in map if map[a].join_type == INNER)
86
+ on_clause, _ = query._add_q(q_object, query.used_aliases)
87
+ query.demote_joins(existing_inner)
88
+
89
+ parent = query.get_initial_alias()
90
+ query.join(QJoin(parent, self.name, self.name, on_clause, join_type))
91
+ return queryset
92
+
93
+ def queryset(self):
94
+ """Get a queryset selecting from this CTE
95
+
96
+ This CTE will be referenced by the returned queryset, but the
97
+ corresponding `WITH ...` statement will not be prepended to the
98
+ queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
99
+ achieve that outcome.
100
+
101
+ :returns: A queryset.
102
+ """
103
+ cte_query = self.query
104
+ qs = cte_query.model._default_manager.get_queryset()
105
+
106
+ query = CTEQuery(cte_query.model)
107
+ query.join(BaseTable(self.name, None))
108
+ query.default_cols = cte_query.default_cols
109
+ query.deferred_loading = cte_query.deferred_loading
110
+ if cte_query.annotations:
111
+ for alias, value in cte_query.annotations.items():
112
+ col = CTEColumnRef(alias, self.name, value.output_field)
113
+ query.add_annotation(col, alias)
114
+ if cte_query.values_select:
115
+ query.set_values(cte_query.values_select)
116
+ qs._iterable_class = ValuesIterable
117
+ query.annotation_select_mask = cte_query.annotation_select_mask
118
+
119
+ qs.query = query
120
+ return qs
121
+
122
+ def _resolve_ref(self, name):
123
+ return self.query.resolve_ref(name)
124
+
125
+
126
+ class CTEQuerySet(QuerySet):
127
+ """QuerySet with support for Common Table Expressions"""
128
+
129
+ def __init__(self, model=None, query=None, using=None, hints=None):
130
+ # Only create an instance of a Query if this is the first invocation in
131
+ # a query chain.
132
+ if query is None:
133
+ query = CTEQuery(model)
134
+ super(CTEQuerySet, self).__init__(model, query, using, hints)
135
+
136
+ def with_cte(self, cte):
137
+ """Add a Common Table Expression to this queryset
138
+
139
+ The CTE `WITH ...` clause will be added to the queryset's SQL
140
+ output (after other CTEs that have already been added) so it
141
+ can be referenced in annotations, filters, etc.
142
+ """
143
+ qs = self._clone()
144
+ qs.query._with_ctes.append(cte)
145
+ return qs
146
+
147
+ def as_manager(cls):
148
+ # Address the circular dependency between
149
+ # `CTEQuerySet` and `CTEManager`.
150
+ manager = CTEManager.from_queryset(cls)()
151
+ manager._built_with_as_manager = True
152
+ return manager
153
+ as_manager.queryset_only = True
154
+ as_manager = classmethod(as_manager)
155
+
156
+
157
+ class CTEManager(Manager.from_queryset(CTEQuerySet)):
158
+ """Manager for models that perform CTE queries"""
159
+
160
+ @classmethod
161
+ def from_queryset(cls, queryset_class, class_name=None):
162
+ if not issubclass(queryset_class, CTEQuerySet):
163
+ raise TypeError(
164
+ "models with CTE support need to use a CTEQuerySet")
165
+ return super(CTEManager, cls).from_queryset(
166
+ queryset_class, class_name=class_name)
@@ -0,0 +1,50 @@
1
+ import django
2
+ from django.db.models import Subquery
3
+
4
+
5
+ class CTESubqueryResolver(object):
6
+
7
+ def __init__(self, annotation):
8
+ self.annotation = annotation
9
+
10
+ def resolve_expression(self, *args, **kw):
11
+ # source: django.db.models.expressions.Subquery.resolve_expression
12
+ # --- begin copied code (lightly adapted) --- #
13
+
14
+ # Need to recursively resolve these.
15
+ def resolve_all(child):
16
+ if hasattr(child, 'children'):
17
+ [resolve_all(_child) for _child in child.children]
18
+ if hasattr(child, 'rhs'):
19
+ child.rhs = resolve(child.rhs)
20
+
21
+ def resolve(child):
22
+ if hasattr(child, 'resolve_expression'):
23
+ resolved = child.resolve_expression(*args, **kw)
24
+ # Add table alias to the parent query's aliases to prevent
25
+ # quoting.
26
+ if hasattr(resolved, 'alias') and \
27
+ resolved.alias != resolved.target.model._meta.db_table:
28
+ get_query(clone).external_aliases.add(resolved.alias)
29
+ return resolved
30
+ return child
31
+
32
+ # --- end copied code --- #
33
+
34
+ if django.VERSION < (3, 0):
35
+ def get_query(clone):
36
+ return clone.queryset.query
37
+ else:
38
+ def get_query(clone):
39
+ return clone.query
40
+
41
+ # NOTE this uses the old (pre-Django 3) way of resolving.
42
+ # Should a different technique should be used on Django 3+?
43
+ clone = self.annotation.resolve_expression(*args, **kw)
44
+ if isinstance(self.annotation, Subquery):
45
+ for cte in getattr(get_query(clone), '_with_ctes', []):
46
+ resolve_all(cte.query.where)
47
+ for key, value in cte.query.annotations.items():
48
+ if isinstance(value, Subquery):
49
+ cte.query.annotations[key] = resolve(value)
50
+ return clone
django_cte/join.py ADDED
@@ -0,0 +1,91 @@
1
+ from django.db.models.sql.constants import INNER
2
+
3
+
4
+ class QJoin(object):
5
+ """Join clause with join condition from Q object clause
6
+
7
+ :param parent_alias: Alias of parent table.
8
+ :param table_name: Name of joined table.
9
+ :param table_alias: Alias of joined table.
10
+ :param on_clause: Query `where_class` instance represenging the ON clause.
11
+ :param join_type: Join type (INNER or LOUTER).
12
+ """
13
+
14
+ filtered_relation = None
15
+
16
+ def __init__(self, parent_alias, table_name, table_alias,
17
+ on_clause, join_type=INNER, nullable=None):
18
+ self.parent_alias = parent_alias
19
+ self.table_name = table_name
20
+ self.table_alias = table_alias
21
+ self.on_clause = on_clause
22
+ self.join_type = join_type # LOUTER or INNER
23
+ self.nullable = join_type != INNER if nullable is None else nullable
24
+
25
+ @property
26
+ def identity(self):
27
+ return (
28
+ self.__class__,
29
+ self.table_name,
30
+ self.parent_alias,
31
+ self.join_type,
32
+ self.on_clause,
33
+ )
34
+
35
+ def __hash__(self):
36
+ return hash(self.identity)
37
+
38
+ def __eq__(self, other):
39
+ if not isinstance(other, QJoin):
40
+ return NotImplemented
41
+ return self.identity == other.identity
42
+
43
+ def equals(self, other):
44
+ return self.identity == other.identity
45
+
46
+ def as_sql(self, compiler, connection):
47
+ """Generate join clause SQL"""
48
+ on_clause_sql, params = self.on_clause.as_sql(compiler, connection)
49
+ if self.table_alias == self.table_name:
50
+ alias = ''
51
+ else:
52
+ alias = ' %s' % self.table_alias
53
+ qn = compiler.quote_name_unless_alias
54
+ sql = '%s %s%s ON %s' % (
55
+ self.join_type,
56
+ qn(self.table_name),
57
+ alias,
58
+ on_clause_sql
59
+ )
60
+ return sql, params
61
+
62
+ def relabeled_clone(self, change_map):
63
+ return self.__class__(
64
+ parent_alias=change_map.get(self.parent_alias, self.parent_alias),
65
+ table_name=self.table_name,
66
+ table_alias=change_map.get(self.table_alias, self.table_alias),
67
+ on_clause=self.on_clause.relabeled_clone(change_map),
68
+ join_type=self.join_type,
69
+ nullable=self.nullable,
70
+ )
71
+
72
+ class join_field:
73
+ # `Join.join_field` is used internally by `Join` as well as in
74
+ # `QuerySet.resolve_expression()`:
75
+ #
76
+ # isinstance(table, Join)
77
+ # and table.join_field.related_model._meta.db_table != alias
78
+ #
79
+ # Currently that does not apply here since `QJoin` is not an
80
+ # instance of `Join`, although maybe it should? Maybe this
81
+ # should have `related_model._meta.db_table` return
82
+ # `<QJoin>.table_name` or `<QJoin>.table_alias`?
83
+ #
84
+ # `PathInfo.join_field` is another similarly named attribute in
85
+ # Django that has a much more complicated interface, but luckily
86
+ # seems unrelated to `Join.join_field`.
87
+
88
+ class related_model:
89
+ class _meta:
90
+ # for QuerySet.set_group_by(allow_aliases=True)
91
+ local_concrete_fields = ()
django_cte/meta.py ADDED
@@ -0,0 +1,112 @@
1
+ import weakref
2
+
3
+ from django.db.models.expressions import Col, Expression
4
+
5
+
6
+ class CTEColumns(object):
7
+
8
+ def __init__(self, cte):
9
+ self._cte = weakref.ref(cte)
10
+
11
+ def __getattr__(self, name):
12
+ return CTEColumn(self._cte(), name)
13
+
14
+
15
+ class CTEColumn(Expression):
16
+
17
+ def __init__(self, cte, name, output_field=None):
18
+ self._cte = cte
19
+ self.table_alias = cte.name
20
+ self.name = self.alias = name
21
+ self._output_field = output_field
22
+
23
+ def __repr__(self):
24
+ return "<{} {}.{}>".format(
25
+ self.__class__.__name__,
26
+ self._cte.name,
27
+ self.name,
28
+ )
29
+
30
+ @property
31
+ def _ref(self):
32
+ if self._cte.query is None:
33
+ raise ValueError(
34
+ "cannot resolve '{cte}.{name}' in recursive CTE setup. "
35
+ "Hint: use ExpressionWrapper({cte}.col.{name}, "
36
+ "output_field=...)".format(cte=self._cte.name, name=self.name)
37
+ )
38
+ ref = self._cte._resolve_ref(self.name)
39
+ if ref is self or self in ref.get_source_expressions():
40
+ raise ValueError("Circular reference: {} = {}".format(self, ref))
41
+ return ref
42
+
43
+ @property
44
+ def target(self):
45
+ return self._ref.target
46
+
47
+ @property
48
+ def output_field(self):
49
+ # required to fix error caused by django commit
50
+ # 9d519d3dc4e5bd1d9ff3806b44624c3e487d61c1
51
+ if self._cte.query is None:
52
+ raise AttributeError
53
+
54
+ if self._output_field is not None:
55
+ return self._output_field
56
+ return self._ref.output_field
57
+
58
+ def as_sql(self, compiler, connection):
59
+ qn = compiler.quote_name_unless_alias
60
+ ref = self._ref
61
+ if isinstance(ref, Col) and self.name == "pk":
62
+ column = ref.target.column
63
+ else:
64
+ column = self.name
65
+ return "%s.%s" % (qn(self.table_alias), qn(column)), []
66
+
67
+ def relabeled_clone(self, relabels):
68
+ if self.table_alias is not None and self.table_alias in relabels:
69
+ clone = self.copy()
70
+ clone.table_alias = relabels[self.table_alias]
71
+ return clone
72
+ return self
73
+
74
+
75
+ class CTEColumnRef(Expression):
76
+
77
+ def __init__(self, name, cte_name, output_field):
78
+ self.name = name
79
+ self.cte_name = cte_name
80
+ self.output_field = output_field
81
+ self._alias = None
82
+
83
+ def resolve_expression(self, query=None, allow_joins=True, reuse=None,
84
+ summarize=False, for_save=False):
85
+ if query:
86
+ clone = self.copy()
87
+ clone._alias = self._alias or query.table_map.get(
88
+ self.cte_name, [self.cte_name])[0]
89
+ return clone
90
+ return super(CTEColumnRef, self).resolve_expression(
91
+ query, allow_joins, reuse, summarize, for_save)
92
+
93
+ def relabeled_clone(self, change_map):
94
+ if (
95
+ self.cte_name not in change_map
96
+ and self._alias not in change_map
97
+ ):
98
+ return super(CTEColumnRef, self).relabeled_clone(change_map)
99
+
100
+ clone = self.copy()
101
+ if self.cte_name in change_map:
102
+ clone._alias = change_map[self.cte_name]
103
+
104
+ if self._alias in change_map:
105
+ clone._alias = change_map[self._alias]
106
+ return clone
107
+
108
+ def as_sql(self, compiler, connection):
109
+ qn = compiler.quote_name_unless_alias
110
+ table = self._alias or compiler.query.table_map.get(
111
+ self.cte_name, [self.cte_name])[0]
112
+ return "%s.%s" % (qn(table), qn(self.name)), []
django_cte/query.py ADDED
@@ -0,0 +1,222 @@
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
+ from django.db.models.sql.where import ExtraWhere, WhereNode
12
+
13
+ from .expressions import CTESubqueryResolver
14
+ from .join import QJoin
15
+
16
+
17
+ class CTEQuery(Query):
18
+ """A Query which processes SQL compilation through the CTE compiler"""
19
+
20
+ def __init__(self, *args, **kwargs):
21
+ super(CTEQuery, self).__init__(*args, **kwargs)
22
+ self._with_ctes = []
23
+
24
+ def combine(self, other, connector):
25
+ if other._with_ctes:
26
+ if self._with_ctes:
27
+ raise TypeError("cannot merge queries with CTEs on both sides")
28
+ self._with_ctes = other._with_ctes[:]
29
+ return super(CTEQuery, self).combine(other, connector)
30
+
31
+ def get_compiler(self, using=None, connection=None, *args, **kwargs):
32
+ """ Overrides the Query method get_compiler in order to return
33
+ a CTECompiler.
34
+ """
35
+ # Copy the body of this method from Django except the final
36
+ # return statement. We will ignore code coverage for this.
37
+ if using is None and connection is None: # pragma: no cover
38
+ raise ValueError("Need either using or connection")
39
+ if using:
40
+ connection = connections[using]
41
+ # Check that the compiler will be able to execute the query
42
+ for alias, aggregate in self.annotation_select.items():
43
+ connection.ops.check_expression_support(aggregate)
44
+ # Instantiate the custom compiler.
45
+ klass = COMPILER_TYPES.get(self.__class__, CTEQueryCompiler)
46
+ return klass(self, connection, using, *args, **kwargs)
47
+
48
+ def add_annotation(self, annotation, *args, **kw):
49
+ annotation = CTESubqueryResolver(annotation)
50
+ super(CTEQuery, self).add_annotation(annotation, *args, **kw)
51
+
52
+ def __chain(self, _name, klass=None, *args, **kwargs):
53
+ klass = QUERY_TYPES.get(klass, self.__class__)
54
+ clone = getattr(super(CTEQuery, self), _name)(klass, *args, **kwargs)
55
+ clone._with_ctes = self._with_ctes[:]
56
+ return clone
57
+
58
+ if django.VERSION < (2, 0):
59
+ def clone(self, klass=None, *args, **kwargs):
60
+ return self.__chain("clone", klass, *args, **kwargs)
61
+
62
+ else:
63
+ def chain(self, klass=None):
64
+ return self.__chain("chain", klass)
65
+
66
+
67
+ class CTECompiler(object):
68
+
69
+ @classmethod
70
+ def generate_sql(cls, connection, query, as_sql):
71
+ if not query._with_ctes:
72
+ return as_sql()
73
+
74
+ ctes = []
75
+ params = []
76
+ for cte in query._with_ctes:
77
+ if django.VERSION > (4, 2):
78
+ _ignore_with_col_aliases(cte.query)
79
+
80
+ alias = query.alias_map.get(cte.name)
81
+ should_elide_empty = (
82
+ not isinstance(alias, QJoin) or alias.join_type != LOUTER
83
+ )
84
+
85
+ if django.VERSION >= (4, 0):
86
+ compiler = cte.query.get_compiler(
87
+ connection=connection, elide_empty=should_elide_empty
88
+ )
89
+ else:
90
+ compiler = cte.query.get_compiler(connection=connection)
91
+
92
+ qn = compiler.quote_name_unless_alias
93
+ try:
94
+ cte_sql, cte_params = compiler.as_sql()
95
+ except EmptyResultSet:
96
+ if django.VERSION < (4, 0) and not should_elide_empty:
97
+ # elide_empty is not available prior to Django 4.0. The
98
+ # below behavior emulates the logic of it, rebuilding
99
+ # the CTE query with a WHERE clause that is always false
100
+ # but that the SqlCompiler cannot optimize away. This is
101
+ # only required for left outer joins, as standard inner
102
+ # joins should be optimized and raise the EmptyResultSet
103
+ query = cte.query.copy()
104
+ query.where = WhereNode([ExtraWhere(["1 = 0"], [])])
105
+ compiler = query.get_compiler(connection=connection)
106
+ cte_sql, cte_params = compiler.as_sql()
107
+ else:
108
+ # If the CTE raises an EmptyResultSet the SqlCompiler still
109
+ # needs to know the information about this base compiler
110
+ # like, col_count and klass_info.
111
+ as_sql()
112
+ raise
113
+ template = cls.get_cte_query_template(cte)
114
+ ctes.append(template.format(name=qn(cte.name), query=cte_sql))
115
+ params.extend(cte_params)
116
+
117
+ # Required due to breaking change in django commit
118
+ # fc91ea1e50e5ef207f0f291b3f6c1942b10db7c7
119
+ if django.VERSION >= (4, 0):
120
+ explain_attribute = "explain_info"
121
+ explain_info = getattr(query, explain_attribute, None)
122
+ explain_format = getattr(explain_info, "format", None)
123
+ explain_options = getattr(explain_info, "options", {})
124
+ else:
125
+ explain_attribute = "explain_query"
126
+ explain_format = getattr(query, "explain_format", None)
127
+ explain_options = getattr(query, "explain_options", {})
128
+
129
+ explain_query_or_info = getattr(query, explain_attribute, None)
130
+ sql = []
131
+ if explain_query_or_info:
132
+ sql.append(
133
+ connection.ops.explain_query_prefix(
134
+ explain_format,
135
+ **explain_options
136
+ )
137
+ )
138
+ # this needs to get set to None so that the base as_sql() doesn't
139
+ # insert the EXPLAIN statement where it would end up between the
140
+ # WITH ... clause and the final SELECT
141
+ setattr(query, explain_attribute, None)
142
+
143
+ if ctes:
144
+ # Always use WITH RECURSIVE
145
+ # https://www.postgresql.org/message-id/13122.1339829536%40sss.pgh.pa.us
146
+ sql.extend(["WITH RECURSIVE", ", ".join(ctes)])
147
+ base_sql, base_params = as_sql()
148
+
149
+ if explain_query_or_info:
150
+ setattr(query, explain_attribute, explain_query_or_info)
151
+
152
+ sql.append(base_sql)
153
+ params.extend(base_params)
154
+ return " ".join(sql), tuple(params)
155
+
156
+ @classmethod
157
+ def get_cte_query_template(cls, cte):
158
+ if cte.materialized:
159
+ return "{name} AS MATERIALIZED ({query})"
160
+ return "{name} AS ({query})"
161
+
162
+
163
+ class CTEUpdateQuery(UpdateQuery, CTEQuery):
164
+ pass
165
+
166
+
167
+ class CTEDeleteQuery(DeleteQuery, CTEQuery):
168
+ pass
169
+
170
+
171
+ QUERY_TYPES = {
172
+ Query: CTEQuery,
173
+ UpdateQuery: CTEUpdateQuery,
174
+ DeleteQuery: CTEDeleteQuery,
175
+ }
176
+
177
+
178
+ def _ignore_with_col_aliases(cte_query):
179
+ if getattr(cte_query, "combined_queries", None):
180
+ for query in cte_query.combined_queries:
181
+ query.ignore_with_col_aliases = True
182
+
183
+
184
+ class CTEQueryCompiler(SQLCompiler):
185
+
186
+ def as_sql(self, *args, **kwargs):
187
+ def _as_sql():
188
+ return super(CTEQueryCompiler, self).as_sql(*args, **kwargs)
189
+ return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
190
+
191
+ def get_select(self, **kw):
192
+ if kw.get("with_col_aliases") \
193
+ and getattr(self.query, "ignore_with_col_aliases", False):
194
+ kw.pop("with_col_aliases")
195
+ return super().get_select(**kw)
196
+
197
+
198
+ class CTEUpdateQueryCompiler(SQLUpdateCompiler):
199
+
200
+ def as_sql(self, *args, **kwargs):
201
+ def _as_sql():
202
+ return super(CTEUpdateQueryCompiler, self).as_sql(*args, **kwargs)
203
+ return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
204
+
205
+
206
+ class CTEDeleteQueryCompiler(SQLDeleteCompiler):
207
+
208
+ # NOTE: it is currently not possible to execute delete queries that
209
+ # reference CTEs without patching `QuerySet.delete` (Django method)
210
+ # to call `self.query.chain(sql.DeleteQuery)` instead of
211
+ # `sql.DeleteQuery(self.model)`
212
+
213
+ def as_sql(self, *args, **kwargs):
214
+ def _as_sql():
215
+ return super(CTEDeleteQueryCompiler, self).as_sql(*args, **kwargs)
216
+ return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
217
+
218
+
219
+ COMPILER_TYPES = {
220
+ CTEUpdateQuery: CTEUpdateQueryCompiler,
221
+ CTEDeleteQuery: CTEDeleteQueryCompiler,
222
+ }
django_cte/raw.py ADDED
@@ -0,0 +1,38 @@
1
+ def raw_cte_sql(sql, params, refs):
2
+ """Raw CTE SQL
3
+
4
+ :param sql: SQL query (string).
5
+ :param params: List of bind parameters.
6
+ :param refs: Dict of output fields: `{"name": <Field instance>}`.
7
+ :returns: Object that can be passed to `With`.
8
+ """
9
+
10
+ class raw_cte_ref(object):
11
+ def __init__(self, output_field):
12
+ self.output_field = output_field
13
+
14
+ def get_source_expressions(self):
15
+ return []
16
+
17
+ class raw_cte_compiler(object):
18
+
19
+ def __init__(self, connection):
20
+ self.connection = connection
21
+
22
+ def as_sql(self):
23
+ return sql, params
24
+
25
+ def quote_name_unless_alias(self, name):
26
+ return self.connection.ops.quote_name(name)
27
+
28
+ class raw_cte_queryset(object):
29
+ class query(object):
30
+ @staticmethod
31
+ def get_compiler(connection, *, elide_empty=None):
32
+ return raw_cte_compiler(connection)
33
+
34
+ @staticmethod
35
+ def resolve_ref(name):
36
+ return raw_cte_ref(refs[name])
37
+
38
+ return raw_cte_queryset
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-cte
3
+ Version: 1.3.3.dev20250526204410
4
+ Summary: Common Table Expressions (CTE) for Django
5
+ Author-email: Daniel Miller <millerdev@gmail.com>
6
+ Requires-Python: >= 3.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Framework :: Django
22
+ Classifier: Framework :: Django :: 4
23
+ Classifier: Framework :: Django :: 4.2
24
+ Classifier: Framework :: Django :: 5
25
+ Classifier: Framework :: Django :: 5.0
26
+ Classifier: Framework :: Django :: 5.1
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ License-File: LICENSE
29
+ Requires-Dist: django
30
+ Project-URL: Home, https://github.com/dimagi/django-cte
31
+
32
+ # Common Table Expressions with Django
33
+
34
+ [![Build Status](https://github.com/dimagi/django-cte/actions/workflows/tests.yml/badge.svg)](https://github.com/dimagi/django-cte/actions/workflows/tests.yml)
35
+ [![PyPI version](https://badge.fury.io/py/django-cte.svg)](https://badge.fury.io/py/django-cte)
36
+
37
+ ## Installation
38
+ ```
39
+ pip install django-cte
40
+ ```
41
+
42
+
43
+ ## Documentation
44
+
45
+ The [django-cte documentation](https://dimagi.github.io/django-cte/) shows how
46
+ to use Common Table Expressions with the Django ORM.
47
+
48
+
49
+ ## Running tests
50
+
51
+ ```
52
+ cd django-cte
53
+ uv sync
54
+
55
+ pytest
56
+ ruff check
57
+
58
+ # To run tests against postgres
59
+ psql -U username -h localhost -p 5432 -c 'create database django_cte;'
60
+ export PG_DB_SETTINGS='{
61
+ "ENGINE":"django.db.backends.postgresql_psycopg2",
62
+ "NAME":"django_cte",
63
+ "USER":"username",
64
+ "PASSWORD":"password",
65
+ "HOST":"localhost",
66
+ "PORT":"5432"}'
67
+
68
+ # WARNING pytest will delete the test_django_cte database if it exists!
69
+ DB_SETTINGS="$PG_DB_SETTINGS" pytest
70
+ ```
71
+
72
+ All feature and bug contributions are expected to be covered by tests.
73
+
74
+
75
+ ## Publishing a new verison to PyPI
76
+
77
+ Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
78
+ in [`__init__.py`](django_cte/__init__.py).
79
+
80
+ A new version is published to https://test.pypi.org/p/django-cte on every
81
+ push to the *main* branch.
82
+
83
+ Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
84
+
@@ -0,0 +1,11 @@
1
+ django_cte/__init__.py,sha256=VFBvWKXfBtqdPTJcYi67U8mUDTxntsQxpHrjrGdCRhE,96
2
+ django_cte/cte.py,sha256=Xw2Ll-O3fujrQF-dsvXdQy4FzG8fQdsLYJk6GCXRI3M,6554
3
+ django_cte/expressions.py,sha256=taT2trCz4z0YmOGVXzR9XXquPR43OH6xghNWAplc3lM,1930
4
+ django_cte/join.py,sha256=HrO31WKx3j5RiEWf7dDOXbBfeeUPFEbNLo3gzzt3qbo,3164
5
+ django_cte/meta.py,sha256=PIu7iW_tu3gBCpiBRfVHvuJKSSND2IsaNhoT01WEEJM,3535
6
+ django_cte/query.py,sha256=A16buWFqWJZ2RA1WUby7tdC_uxmn9XNLuCewjJAg1mk,8314
7
+ django_cte/raw.py,sha256=nROVPEVRc3_zKAFcED6KR8Rr6FTTn8jQDFQWfi-X3-A,1077
8
+ django_cte-1.3.3.dev20250526204410.dist-info/licenses/LICENSE,sha256=mkLNw_QhpZ40jBEbuAosqH4ciA3KMrwb8aSYbTmy5gc,1508
9
+ django_cte-1.3.3.dev20250526204410.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
10
+ django_cte-1.3.3.dev20250526204410.dist-info/METADATA,sha256=BoO5aEF98J-66izORnA7QYxj_OB7cF80136D0NyoikM,2645
11
+ django_cte-1.3.3.dev20250526204410.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2018, Dimagi Inc., and individual contributors.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name Dimagi, nor the names of its contributors, may be used
12
+ to endorse or promote products derived from this software without
13
+ specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL DIMAGI INC. BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.