django-fast-treenode 3.0.3__py3-none-any.whl → 3.0.4__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_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/METADATA +2 -2
- {django_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/RECORD +11 -11
- {django_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/WHEEL +1 -1
- treenode/admin/mixin.py +1 -1
- treenode/managers/queries.py +6 -18
- treenode/managers/tasks.py +80 -42
- treenode/utils/db/sqlcompat.py +33 -1
- treenode/utils/db/sqlquery.py +24 -0
- treenode/version.py +1 -1
- {django_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.4
|
4
4
|
Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -53,7 +53,7 @@ Classifier: Operating System :: OS Independent
|
|
53
53
|
Requires-Python: >=3.9
|
54
54
|
Description-Content-Type: text/markdown
|
55
55
|
License-File: LICENSE
|
56
|
-
Requires-Dist: Django>=
|
56
|
+
Requires-Dist: Django>=5.0
|
57
57
|
Requires-Dist: msgpack>=1.0.0
|
58
58
|
Requires-Dist: openpyxl>=3.0.0
|
59
59
|
Requires-Dist: pyyaml>=5.1
|
@@ -1,4 +1,4 @@
|
|
1
|
-
django_fast_treenode-3.0.
|
1
|
+
django_fast_treenode-3.0.4.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
2
|
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
4
|
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
@@ -7,18 +7,18 @@ treenode/settings.py,sha256=oSkcKXNVd28HXrlWZIH2VYinCMq-UdCDlX4KD0Qc_Xk,631
|
|
7
7
|
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
8
8
|
treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
|
9
9
|
treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
|
10
|
-
treenode/version.py,sha256=
|
10
|
+
treenode/version.py,sha256=agtMVvI22kmKiOiAyXlZeP14ej675T1Yd8F0FzHfXXk,220
|
11
11
|
treenode/widgets.py,sha256=61ed16bVqb1_R97jekDrbKS5pDVK4nzXSDwL3CDBYEk,4075
|
12
12
|
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
13
|
treenode/admin/admin.py,sha256=9_GC6JjWRiG_G84royRGAr10BSNIAj4N_33JXdzP8IY,7733
|
14
14
|
treenode/admin/changelist.py,sha256=KUYS9MaR8Ck_1xmMqupobxWKarrJEqmHuEG32CL01Bo,1662
|
15
15
|
treenode/admin/exporter.py,sha256=QE74V6W3tvwA5kCvBt1MmVlLOaWh-o8EU63cgmiwD5Q,5724
|
16
16
|
treenode/admin/importer.py,sha256=hK3D-1DZcoowGblRluGzng3n5Bf__hMsbNaIGXRpRdg,6263
|
17
|
-
treenode/admin/mixin.py,sha256=
|
17
|
+
treenode/admin/mixin.py,sha256=o83zTETOjvdHRjGR2pGthsY_RO7GVEimNUISgqqOBLs,10642
|
18
18
|
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
19
|
treenode/managers/managers.py,sha256=8OaFxtajyR1d7-UHyiUbifMBEF9cjfHTIEYPkYUWmt0,7166
|
20
|
-
treenode/managers/queries.py,sha256=
|
21
|
-
treenode/managers/tasks.py,sha256=
|
20
|
+
treenode/managers/queries.py,sha256=zZYeDVFXl-Hro9ubv0_zuN7XaVXQhyeeFx_GVZSMi30,10273
|
21
|
+
treenode/managers/tasks.py,sha256=b8deUAbCpD1Yov-PpjKNAx49rL4fejkGf64ih5JziF0,7138
|
22
22
|
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
23
|
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
24
24
|
treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
|
@@ -75,8 +75,8 @@ treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY
|
|
75
75
|
treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
|
76
76
|
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
77
77
|
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
78
|
-
treenode/utils/db/sqlcompat.py,sha256=
|
79
|
-
treenode/utils/db/sqlquery.py,sha256=
|
78
|
+
treenode/utils/db/sqlcompat.py,sha256=FTgEMu3kQ1c96nB1sUW-SFOmYEvdl8JlD9HvsRj_dO0,5704
|
79
|
+
treenode/utils/db/sqlquery.py,sha256=jHI0gLkdJCD6ulOenO4b0aSjyAxtJ4ZGvsQa0i3Aj8E,2958
|
80
80
|
treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
|
81
81
|
treenode/views/autoapi.py,sha256=o75e8IFsogbhZN_rbx3BKVnoruD96nWelnC5UzOqUDw,3628
|
82
82
|
treenode/views/autocomplete.py,sha256=Z7cBnC4Ihdyxm8zlbnG6CkZdVkM3TOTWRpw5mdhaIVA,1469
|
@@ -84,7 +84,7 @@ treenode/views/children.py,sha256=bygXaEBExxG3zIPL34_PYHLFFIqlQU2naqPIlyQ6e-s,11
|
|
84
84
|
treenode/views/common.py,sha256=mrmr40R91XVbMWcz5GZT-OjpnQ87F7XQZxu1W6rqpqI,617
|
85
85
|
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
86
86
|
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
87
|
-
django_fast_treenode-3.0.
|
88
|
-
django_fast_treenode-3.0.
|
89
|
-
django_fast_treenode-3.0.
|
90
|
-
django_fast_treenode-3.0.
|
87
|
+
django_fast_treenode-3.0.4.dist-info/METADATA,sha256=NbOy_OHnyPjubLhbssbpdM2jwGiModfoiE_CPNvQpM8,10249
|
88
|
+
django_fast_treenode-3.0.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
89
|
+
django_fast_treenode-3.0.4.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
90
|
+
django_fast_treenode-3.0.4.dist-info/RECORD,,
|
treenode/admin/mixin.py
CHANGED
@@ -84,7 +84,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
84
84
|
value = field(obj)
|
85
85
|
field_name = getattr(field, "__name__", "field")
|
86
86
|
else:
|
87
|
-
attr, value = lookup_field(field, obj, self)
|
87
|
+
r, attr, value = lookup_field(field, obj, self)
|
88
88
|
field_name = field
|
89
89
|
|
90
90
|
row_data.append(value)
|
treenode/managers/queries.py
CHANGED
@@ -5,13 +5,14 @@ Low-level SQL Query Manager.
|
|
5
5
|
Encapsulates all logic to retrieve related primary keys based on relationships
|
6
6
|
(e.g., ancestors, children, descendants, siblings, family, root) using raw SQL.
|
7
7
|
|
8
|
-
Version: 3.0.
|
8
|
+
Version: 3.0.4
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
12
12
|
|
13
13
|
|
14
14
|
from django.db import connection
|
15
|
+
from ..utils.db.sqlcompat import SQLCompat
|
15
16
|
|
16
17
|
|
17
18
|
class TreeQuery:
|
@@ -32,19 +33,6 @@ class TreeQuery:
|
|
32
33
|
cursor.execute(sql, params)
|
33
34
|
return cursor.fetchall()
|
34
35
|
|
35
|
-
def wrap_union_all(self, queries):
|
36
|
-
"""
|
37
|
-
Combine multiple SQL queries using UNION ALL.
|
38
|
-
|
39
|
-
Each query is a tuple: (sql, params).
|
40
|
-
Returns a tuple: (combined_sql, combined_params).
|
41
|
-
"""
|
42
|
-
union_query = " UNION ALL ".join(f"({q[0]})" for q in queries)
|
43
|
-
combined_params = []
|
44
|
-
for q in queries:
|
45
|
-
combined_params.extend(q[1])
|
46
|
-
return union_query, combined_params
|
47
|
-
|
48
36
|
def order_by(self, sql, order_by_clause):
|
49
37
|
"""Wrap the SQL in an outer query to enforce ordering."""
|
50
38
|
return f"SELECT * FROM ({sql}) AS combined ORDER BY {order_by_clause}"
|
@@ -76,7 +64,7 @@ class TreeQuery:
|
|
76
64
|
if include_self:
|
77
65
|
sql2 = f"SELECT id, priority FROM {self.db_table} WHERE id = %s"
|
78
66
|
params2 = [self.node.pk]
|
79
|
-
combined_sql, combined_params =
|
67
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(
|
80
68
|
[(sql1, params1), (sql2, params2)])
|
81
69
|
sql = self.order_by(combined_sql, "priority")
|
82
70
|
return sql, combined_params
|
@@ -115,7 +103,7 @@ class TreeQuery:
|
|
115
103
|
FROM {self.db_table}
|
116
104
|
WHERE id = %s
|
117
105
|
"""
|
118
|
-
union_sql, union_params =
|
106
|
+
union_sql, union_params = SQLCompat.wrap_union_all([
|
119
107
|
(base_sql, params),
|
120
108
|
(sql_self, [self.node.pk])
|
121
109
|
])
|
@@ -148,7 +136,7 @@ class TreeQuery:
|
|
148
136
|
|
149
137
|
if include_self:
|
150
138
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
151
|
-
union_sql, union_params =
|
139
|
+
union_sql, union_params = SQLCompat.wrap_union_all(
|
152
140
|
[(base_sql, params), (sql_self, [self.node.pk])])
|
153
141
|
else:
|
154
142
|
union_sql, union_params = base_sql, params
|
@@ -198,7 +186,7 @@ class TreeQuery:
|
|
198
186
|
if include_self:
|
199
187
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
200
188
|
queries.append((sql_self, [self.node.pk]))
|
201
|
-
combined_sql, combined_params =
|
189
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(queries)
|
202
190
|
combined_sql = self.order_by(combined_sql, "_depth, priority")
|
203
191
|
return combined_sql, combined_params
|
204
192
|
|
treenode/managers/tasks.py
CHANGED
@@ -2,24 +2,16 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode TaskQuery manager
|
4
4
|
|
5
|
-
Version: 3.0.
|
5
|
+
Version: 3.0.4
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
|
-
|
10
|
+
import atexit
|
11
|
+
from django.db import connection, transaction
|
11
12
|
|
12
13
|
from ..utils.db import TreePathCompiler
|
13
14
|
|
14
|
-
'''
|
15
|
-
try:
|
16
|
-
profile
|
17
|
-
except NameError:
|
18
|
-
def profile(func):
|
19
|
-
"""Profile."""
|
20
|
-
return func
|
21
|
-
'''
|
22
|
-
|
23
15
|
|
24
16
|
class TreeTaskQueue:
|
25
17
|
"""TreeTaskQueue Class."""
|
@@ -28,31 +20,93 @@ class TreeTaskQueue:
|
|
28
20
|
"""Init the task query."""
|
29
21
|
self.model = model
|
30
22
|
self.queue = []
|
23
|
+
self._running = False
|
24
|
+
|
25
|
+
# Register the execution queue when the interpreter exits
|
26
|
+
atexit.register(self._atexit_run)
|
27
|
+
|
28
|
+
def _atexit_run(self):
|
29
|
+
"""Run queue on interpreter exit if pending tasks exist."""
|
30
|
+
if self.queue and not self._running:
|
31
|
+
try:
|
32
|
+
self.run()
|
33
|
+
except Exception as e:
|
34
|
+
# Don't crash on completion, just log
|
35
|
+
print(f"[TreeTaskQueue] Error during atexit: {e}")
|
31
36
|
|
32
37
|
def add(self, mode, parent_id):
|
33
|
-
"""Add task to the
|
38
|
+
"""Add task to the queue.
|
39
|
+
|
40
|
+
Parameters:
|
41
|
+
mode (str): Task type (currently only "update").
|
42
|
+
parent_id (int|None): ID of parent node to update from (None = full tree).
|
43
|
+
"""
|
34
44
|
self.queue.append({"mode": mode, "parent_id": parent_id})
|
35
45
|
|
36
46
|
def run(self):
|
37
|
-
"""Run task queue.
|
47
|
+
"""Run task queue.
|
48
|
+
|
49
|
+
This method collects all queued tasks, optimizes them, and performs
|
50
|
+
a recursive rebuild of tree paths and depths using SQL. Locks the
|
51
|
+
required rows before running.
|
52
|
+
|
53
|
+
Uses Django's `transaction.atomic()` to ensure that any recursive CTE
|
54
|
+
execution or SAVEPOINT creation works properly under PostgreSQL.
|
55
|
+
"""
|
38
56
|
if len(self.queue) == 0:
|
39
57
|
return
|
58
|
+
|
59
|
+
self._running = True
|
40
60
|
try:
|
41
61
|
optimized = self._optimize()
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
62
|
+
if not optimized:
|
63
|
+
return
|
64
|
+
|
65
|
+
parent_ids = [t["parent_id"] for t in optimized if t["parent_id"] is not None]
|
66
|
+
|
67
|
+
with transaction.atomic():
|
68
|
+
if any(t["parent_id"] is None for t in optimized):
|
69
|
+
try:
|
70
|
+
with connection.cursor() as cursor:
|
71
|
+
cursor.execute(
|
72
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL FOR UPDATE NOWAIT"
|
73
|
+
)
|
74
|
+
except Exception as e:
|
75
|
+
print(f"[TreeTaskQueue] Skipped (root locked): {e}")
|
76
|
+
return
|
77
|
+
else:
|
78
|
+
try:
|
79
|
+
with connection.cursor() as cursor:
|
80
|
+
for parent_id in parent_ids:
|
81
|
+
cursor.execute(
|
82
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE id = %s FOR UPDATE NOWAIT",
|
83
|
+
[parent_id],
|
84
|
+
)
|
85
|
+
except Exception as e:
|
86
|
+
print(f"[TreeTaskQueue] Skipped (parent locked): {e}")
|
87
|
+
return
|
88
|
+
|
89
|
+
for task in optimized:
|
90
|
+
if task["mode"] == "update":
|
91
|
+
TreePathCompiler.update_path(
|
92
|
+
model=self.model,
|
93
|
+
parent_id=task["parent_id"]
|
94
|
+
)
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
print(f"[TreeTaskQueue] Error in run: {e}")
|
98
|
+
connection.rollback()
|
49
99
|
finally:
|
50
100
|
self.queue.clear()
|
51
101
|
self._running = False
|
52
102
|
|
53
|
-
# @profile
|
54
103
|
def _optimize(self):
|
55
|
-
"""Return optimized task queue (ID-only logic).
|
104
|
+
"""Return optimized task queue (ID-only logic).
|
105
|
+
|
106
|
+
Attempts to merge redundant or overlapping subtree updates into
|
107
|
+
the minimal set of unique parent IDs that need to be rebuilt.
|
108
|
+
If it finds a common root, it returns a single task for full rebuild.
|
109
|
+
"""
|
56
110
|
result_set = set()
|
57
111
|
id_set = set()
|
58
112
|
|
@@ -60,8 +114,6 @@ class TreeTaskQueue:
|
|
60
114
|
if task["mode"] == "update":
|
61
115
|
parent_id = task["parent_id"]
|
62
116
|
if parent_id is None:
|
63
|
-
# If we are already updating the entire tree, then
|
64
|
-
# the remaining tasks are meaningless # noqa: D501
|
65
117
|
return [{"mode": "update", "parent_id": None}]
|
66
118
|
else:
|
67
119
|
id_set.add(parent_id)
|
@@ -74,8 +126,6 @@ class TreeTaskQueue:
|
|
74
126
|
for other in id_list[:]:
|
75
127
|
ancestor = self._get_common_ancestor(current, other)
|
76
128
|
if ancestor is not None:
|
77
|
-
# If the common ancestor is the root, then we update
|
78
|
-
# the entire tree
|
79
129
|
if ancestor in self._get_root_ids():
|
80
130
|
return [{"mode": "update", "parent_id": None}]
|
81
131
|
if ancestor not in id_set:
|
@@ -87,36 +137,25 @@ class TreeTaskQueue:
|
|
87
137
|
if not merged:
|
88
138
|
result_set.add(current)
|
89
139
|
|
90
|
-
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
140
|
+
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
91
141
|
|
92
142
|
def _get_root_ids(self):
|
93
143
|
"""Return root node IDs."""
|
94
144
|
with connection.cursor() as cursor:
|
95
145
|
cursor.execute(
|
96
|
-
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
146
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
97
147
|
return [row[0] for row in cursor.fetchall()]
|
98
148
|
|
99
149
|
def _get_parent_id(self, node_id):
|
100
150
|
"""Return parent ID for a given node."""
|
101
151
|
with connection.cursor() as cursor:
|
102
152
|
cursor.execute(
|
103
|
-
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
153
|
+
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
104
154
|
row = cursor.fetchone()
|
105
155
|
return row[0] if row else None
|
106
156
|
|
107
|
-
'''
|
108
|
-
def _get_ancestor_path(self, node_id):
|
109
|
-
"""Return list of ancestor IDs including the node itself."""
|
110
|
-
path = []
|
111
|
-
while node_id is not None:
|
112
|
-
path.append(node_id)
|
113
|
-
node_id = self._get_parent_id(node_id)
|
114
|
-
return path[::-1] # root to leaf
|
115
|
-
'''
|
116
|
-
|
117
|
-
# @profile
|
118
157
|
def _get_ancestor_path(self, node_id):
|
119
|
-
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
158
|
+
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
120
159
|
table = self.model._meta.db_table
|
121
160
|
|
122
161
|
sql = f"""
|
@@ -140,7 +179,6 @@ class TreeTaskQueue:
|
|
140
179
|
|
141
180
|
return [row[0] for row in rows]
|
142
181
|
|
143
|
-
# @profile
|
144
182
|
def _get_common_ancestor(self, id1, id2):
|
145
183
|
"""Return common ancestor ID between two nodes."""
|
146
184
|
path1 = self._get_ancestor_path(id1)
|
treenode/utils/db/sqlcompat.py
CHANGED
@@ -125,12 +125,44 @@ class SQLCompat:
|
|
125
125
|
{set_clause};
|
126
126
|
"""
|
127
127
|
|
128
|
+
elif connection.vendor == "sqlite":
|
129
|
+
# SQLite workaround via temporary table
|
130
|
+
temp_table = "temp_tree_update"
|
131
|
+
cols = ["id"] + [cte_alias.get(f, f) for f in update_fields]
|
132
|
+
col_defs = ", ".join(f"{c} TEXT" for c in cols)
|
133
|
+
insert_cols = ", ".join(cols)
|
134
|
+
select_cols = ", ".join(cols)
|
135
|
+
|
136
|
+
set_clause = ", ".join(
|
137
|
+
f"{qf(f)} = (SELECT t.{cte_alias.get(f, f)} FROM {temp_table} t WHERE t.id = {qt}.id)" # noqa
|
138
|
+
for f in update_fields
|
139
|
+
)
|
140
|
+
|
141
|
+
return f"""
|
142
|
+
DROP TABLE IF EXISTS {temp_table};
|
143
|
+
CREATE TEMP TABLE {temp_table} ({col_defs});
|
144
|
+
|
145
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
146
|
+
{base_sql}
|
147
|
+
UNION ALL
|
148
|
+
{recursive_sql}
|
149
|
+
)
|
150
|
+
INSERT INTO {temp_table} ({insert_cols})
|
151
|
+
SELECT {select_cols} FROM tree_cte;
|
152
|
+
|
153
|
+
UPDATE {qt}
|
154
|
+
SET {set_clause}
|
155
|
+
WHERE id IN (SELECT id FROM {temp_table});
|
156
|
+
"""
|
157
|
+
|
128
158
|
else:
|
159
|
+
# Fallback: subqueries
|
160
|
+
# (still buggy in SQLite, hence above workaround)
|
129
161
|
set_clause = ", ".join(
|
130
162
|
f"{qf(f)} = (SELECT t.{f} FROM tree_cte t WHERE t.id = {qt}.id)"
|
131
163
|
for f in update_fields
|
132
164
|
)
|
133
|
-
where_clause =
|
165
|
+
where_clause = "id IN (SELECT id FROM tree_cte)"
|
134
166
|
return f"""
|
135
167
|
WITH RECURSIVE tree_cte {cte_header} AS (
|
136
168
|
{base_sql}
|
treenode/utils/db/sqlquery.py
CHANGED
@@ -66,5 +66,29 @@ class SQLQueue:
|
|
66
66
|
raise
|
67
67
|
self._items.clear()
|
68
68
|
|
69
|
+
@staticmethod
|
70
|
+
def wrap_union_all(queries):
|
71
|
+
"""
|
72
|
+
Combine multiple SQL queries using UNION ALL with vendor-specific handling.
|
73
|
+
Each query is a tuple: (sql, params).
|
74
|
+
Returns a tuple: (combined_sql, combined_params).
|
75
|
+
"""
|
76
|
+
if is_sqlite():
|
77
|
+
# SQLite требует одинаковое число и порядок столбцов. Добавим к каждому SELECT псевдонимы.
|
78
|
+
def alias_select(sql, alias_prefix, idx):
|
79
|
+
return f"SELECT * FROM ({sql}) AS {alias_prefix}_{idx}"
|
80
|
+
|
81
|
+
wrapped_queries = [
|
82
|
+
alias_select(q[0], "q", i) for i, q in enumerate(queries)
|
83
|
+
]
|
84
|
+
combined_sql = " UNION ALL ".join(wrapped_queries)
|
85
|
+
else:
|
86
|
+
combined_sql = " UNION ALL ".join(f"({q[0]})" for q in queries)
|
87
|
+
|
88
|
+
combined_params = []
|
89
|
+
for q in queries:
|
90
|
+
combined_params.extend(q[1])
|
91
|
+
|
92
|
+
return combined_sql, combined_params
|
69
93
|
|
70
94
|
# The End
|
treenode/version.py
CHANGED
{django_fast_treenode-3.0.3.dist-info → django_fast_treenode-3.0.4.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|