django-fast-treenode 3.0.3__tar.gz → 3.0.4__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.
Files changed (117) hide show
  1. {django_fast_treenode-3.0.3/django_fast_treenode.egg-info → django_fast_treenode-3.0.4}/PKG-INFO +2 -2
  2. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4/django_fast_treenode.egg-info}/PKG-INFO +2 -2
  3. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/requires.txt +1 -1
  4. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/roadmap.md +17 -14
  5. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/pyproject.toml +2 -2
  6. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/setup.py +2 -2
  7. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/mixin.py +1 -1
  8. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/queries.py +6 -18
  9. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/tasks.py +80 -42
  10. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/sqlcompat.py +33 -1
  11. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/sqlquery.py +24 -0
  12. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/version.py +1 -1
  13. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/LICENSE +0 -0
  14. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/MANIFEST.in +0 -0
  15. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/README.md +0 -0
  16. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/SOURCES.txt +0 -0
  17. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
  18. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/top_level.txt +0 -0
  19. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/.gitignore +0 -0
  20. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/.nojekyll +0 -0
  21. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/about.md +0 -0
  22. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/admin.md +0 -0
  23. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/api.md +0 -0
  24. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/apifirst.md +0 -0
  25. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/cache.md +0 -0
  26. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/customization.md +0 -0
  27. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/dnd.md +0 -0
  28. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/import_export.md +0 -0
  29. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/index.md +0 -0
  30. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/insert-after.jpg +0 -0
  31. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/insert-as-child.jpg +0 -0
  32. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/installation.md +0 -0
  33. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/migration.md +0 -0
  34. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/models.md +0 -0
  35. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/requirements.txt +0 -0
  36. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/using.md +0 -0
  37. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/setup.cfg +0 -0
  38. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/tests/test_suite.py +0 -0
  39. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/__init__.py +0 -0
  40. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/__init__.py +0 -0
  41. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/admin.py +0 -0
  42. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/changelist.py +0 -0
  43. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/exporter.py +0 -0
  44. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/importer.py +0 -0
  45. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/apps.py +0 -0
  46. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/cache.py +0 -0
  47. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/forms.py +0 -0
  48. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/__init__.py +0 -0
  49. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/managers.py +0 -0
  50. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/__init__.py +0 -0
  51. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/decorators.py +0 -0
  52. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/factory.py +0 -0
  53. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/__init__.py +0 -0
  54. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/ancestors.py +0 -0
  55. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/children.py +0 -0
  56. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/descendants.py +0 -0
  57. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/family.py +0 -0
  58. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/logical.py +0 -0
  59. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/node.py +0 -0
  60. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/properties.py +0 -0
  61. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/roots.py +0 -0
  62. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/siblings.py +0 -0
  63. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/tree.py +0 -0
  64. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/update.py +0 -0
  65. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/models.py +0 -0
  66. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/settings.py +0 -0
  67. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/signals.py +0 -0
  68. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/.gitkeep +0 -0
  69. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/.gitkeep +0 -0
  70. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/tree_widget.css +0 -0
  71. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_admin.css +0 -0
  72. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_tabs.css +0 -0
  73. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/.gitkeep +0 -0
  74. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/lz-string.min.js +0 -0
  75. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/tree_widget.js +0 -0
  76. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/treenode_admin.js +0 -0
  77. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/AUTHORS.txt +0 -0
  78. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/LICENSE.txt +0 -0
  79. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/external/jquery/jquery.js +0 -0
  80. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  81. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  82. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  83. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  84. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  85. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  86. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/index.html +0 -0
  87. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.css +0 -0
  88. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.js +0 -0
  89. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.min.css +0 -0
  90. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.min.js +0 -0
  91. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.structure.css +0 -0
  92. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +0 -0
  93. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.theme.css +0 -0
  94. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +0 -0
  95. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/package.json +0 -0
  96. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/.gitkeep +0 -0
  97. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/.gitkeep +0 -0
  98. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
  99. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_changelist.html +0 -0
  100. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_import_export.html +0 -0
  101. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_rows.html +0 -0
  102. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/widgets/tree_widget.html +0 -0
  103. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/tests.py +0 -0
  104. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/urls.py +0 -0
  105. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/__init__.py +0 -0
  106. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/__init__.py +0 -0
  107. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/compiler.py +0 -0
  108. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/db_vendor.py +0 -0
  109. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/service.py +0 -0
  110. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/__init__.py +0 -0
  111. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/autoapi.py +0 -0
  112. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/autocomplete.py +0 -0
  113. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/children.py +0 -0
  114. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/common.py +0 -0
  115. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/crud.py +0 -0
  116. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/search.py +0 -0
  117. {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.0.3
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>=4.0
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.0.3
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>=4.0
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>=4.0
1
+ Django>=5.0
2
2
  msgpack>=1.0.0
3
3
  openpyxl>=3.0.0
4
4
  pyyaml>=5.1
@@ -1,6 +1,6 @@
1
1
  ## Roadmap
2
2
 
3
- The **`django-fast-treenode`** package will continue to evolve from its original concept—combining the benefits of the hybrid models into a high-performance solution for managing and visualizing hierarchical data in Django projects.
3
+ The **`django-fast-treenode`** package will continue to evolve from its original concept—combining the benefits of hybrid models into a high-performance solution for managing and visualizing hierarchical data in Django projects.
4
4
 
5
5
  The focus is on **speed, usability, and flexibility**.
6
6
 
@@ -8,32 +8,36 @@ The focus is on **speed, usability, and flexibility**.
8
8
 
9
9
  The 3.x release series will focus on strengthening TreeNode Framework in terms of security, usability, and performance scalability, while maintaining backward compatibility and architectural cleanliness.
10
10
 
11
+ * **Version 3.1 — Background Task Worker (Production Mode)**
11
12
 
12
- * **Version 3.1 JWT Authentication for API**
13
+ - Introduce a centralized queue manager with a multiprocessing or Redis-based backend.
14
+ - Add a built-in worker process for safe and efficient task execution in production environments.
15
+ - Provide a fallback auto-run mode for DEBUG environments (using `atexit` or thread-based handler).
16
+ - Ensure task queue consistency across multiple WSGI workers or scripts.
17
+
18
+ * **Version 3.2 — JWT Authentication for API**
13
19
 
14
20
  - Introduce optional JWT-based token authentication for the auto-generated API.
15
21
  - Allow easy activation through a single setting (`TREENODE_API_USE_JWT = True`).
16
22
  - Preserve backward compatibility: API remains open unless explicitly protected.
17
23
  - Foundation for future security features (e.g., user-specific trees, audit trails).
18
24
 
19
- * **Version 3.2 — Admin Usability Improvements**
25
+ * **Version 3.3 — Admin Usability Improvements**
20
26
 
21
27
  Focus: enhance operational safety and optimize workflows for large-scale trees.
22
28
 
23
29
  - **Safe Import Preview**: Implement a staging layer for imports, allowing users to review and confirm imported data before committing changes.
24
-
25
30
  - **Incremental Export**: Support selective export of nodes modified after a specified date or revision marker.
26
31
 
27
-
28
- * **Version 3.3 — Soft Deletion Support**
32
+ * **Version 3.4 — Soft Deletion Support**
29
33
 
30
34
  Focus: improve real-world resilience without sacrificing performance.
31
35
 
32
36
  - Add optional support for "soft delete" behavior (`is_deleted` field).
33
37
  - Modify core queries and cache invalidation logic to respect soft-deleted nodes.
34
38
  - Add a new task type to the internal task queue system for bulk logical deletions.
35
-
36
- * **Version 3.4 — Cache System Enhancements**
39
+
40
+ * **Version 3.5 — Cache System Enhancements**
37
41
 
38
42
  Focus: lay the foundation for scaling Treenode Framework to extreme node counts (>100,000 nodes).
39
43
 
@@ -45,13 +49,12 @@ Each step in the 3.x roadmap is intended to strengthen the framework's key princ
45
49
 
46
50
  #### Long-Term Vision
47
51
 
52
+ * **Version 4.0 – Improved Architecture**
48
53
 
49
- * **Version 4.0 Improved architecture**
50
-
51
- The main debut idea of ​​version 4.0 is to further develop the hybrid approach. This version will implement a new new architectural solution that is designed to increase the speed of selecting descendants, and therefore moving subtrees, and remove the existing restrictions on the maximum nesting depth of the tree (currently the recommended value when using up to 1000 levels).
54
+ The main debut idea of version 4.0 is to further develop the hybrid approach. This version will implement a new architectural solution that is designed to increase the speed of selecting descendants, and therefore moving subtrees, and remove the existing restrictions on the maximum nesting depth of the tree (currently the recommended value when using up to 1000 levels).
52
55
 
53
- - Speed ​​up the operation of extracting descendants.
54
- - Speeding up operations for moving large subtrees.
56
+ - Speed up the operation of extracting descendants.
57
+ - Speed up operations for moving large subtrees.
55
58
  - Performance optimization when working with trees that have extreme depth (more than 2000 levels).
56
59
 
57
60
  * **Version 5.0 – Beyond Django ORM**
@@ -67,4 +70,4 @@ So, each milestone is designed to improve performance, scalability, and flexibil
67
70
 
68
71
  Stay tuned for updates!
69
72
 
70
- Your wishes, objections, and comments are welcome.
73
+ Your wishes, objections, and comments are welcome.
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-fast-treenode"
7
- version = "3.0.3"
7
+ version = "3.0.4"
8
8
  description = "Treenode Framework for supporting tree (hierarchical) data structure in Django projects"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Timur Kady", email = "timurkady@yandex.com" }]
11
11
  license = { file = "LICENSE" }
12
12
  requires-python = ">=3.9"
13
13
  dependencies = [
14
- 'Django>=4.0',
14
+ 'Django>=5.0',
15
15
  'msgpack>=1.0.0',
16
16
  'openpyxl>=3.0.0',
17
17
  'pyyaml>=5.1',
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='django-fast-treenode',
5
- version='3.0.3',
5
+ version='3.0.4',
6
6
  description='Treenode Framework for supporting tree (hierarchical) data structure in Django projects',
7
7
  long_description=open('README.md', encoding='utf-8').read(),
8
8
  long_description_content_type='text/markdown',
@@ -14,7 +14,7 @@ setup(
14
14
  license='MIT',
15
15
  license_files=['LICENSE'],
16
16
  install_requires=[
17
- 'Django>=4.0',
17
+ 'Django>=5.0',
18
18
  'msgpack>=1.0.0',
19
19
  'openpyxl>=3.0.0',
20
20
  'pyyaml>=5.1',
@@ -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)
@@ -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.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 = self.wrap_union_all(
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 = self.wrap_union_all([
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 = self.wrap_union_all(
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 = self.wrap_union_all(queries)
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
 
@@ -2,24 +2,16 @@
2
2
  """
3
3
  TreeNode TaskQuery manager
4
4
 
5
- Version: 3.0.0
5
+ Version: 3.0.4
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
9
9
 
10
- from django.db import connection
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 query."""
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
- for task in optimized:
43
- if task["mode"] == "update":
44
- parent_id = task["parent_id"]
45
- TreePathCompiler.update_path(
46
- model=self.model,
47
- parent_id=parent_id
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)] # noqa: D501
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") # noqa: D501
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]) # noqa: D501
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.""" # noqa: D501
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)
@@ -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 = f"id IN (SELECT id FROM tree_cte)"
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}
@@ -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
@@ -9,4 +9,4 @@ Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.0.3'
12
+ __version__ = '3.0.4'