sqlobjects 1.0.16__tar.gz → 1.2.0__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 (77) hide show
  1. sqlobjects-1.2.0/CHANGELOG.md +142 -0
  2. {sqlobjects-1.0.16/sqlobjects.egg-info → sqlobjects-1.2.0}/PKG-INFO +25 -1
  3. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/README.md +24 -0
  4. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/pyproject.toml +11 -11
  5. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/__init__.py +30 -0
  6. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/aggregate.py +4 -0
  7. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/base.py +21 -6
  8. sqlobjects-1.2.0/sqlobjects/expressions/cte.py +136 -0
  9. sqlobjects-1.2.0/sqlobjects/expressions/explain.py +38 -0
  10. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/function.py +123 -4
  11. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/mixins.py +4 -4
  12. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/scalar.py +8 -0
  13. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/terminal.py +62 -0
  14. sqlobjects-1.2.0/sqlobjects/expressions/window.py +256 -0
  15. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/core.py +11 -0
  16. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/types/__init__.py +2 -1
  17. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/types/registry.py +4 -0
  18. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/metadata.py +33 -12
  19. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/objects/core.py +21 -0
  20. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/queries/builder.py +27 -0
  21. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/queries/dialect.py +51 -0
  22. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/queries/executor.py +15 -54
  23. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/queryset.py +58 -14
  24. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/session.py +17 -3
  25. {sqlobjects-1.0.16 → sqlobjects-1.2.0/sqlobjects.egg-info}/PKG-INFO +25 -1
  26. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects.egg-info/SOURCES.txt +4 -0
  27. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/LICENSE +0 -0
  28. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/01-database-session-guide.md +0 -0
  29. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/02-model-definition-guide.md +0 -0
  30. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/03-query-operations-guide.md +0 -0
  31. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/04-crud-operations-guide.md +0 -0
  32. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/05-relationships-guide.md +0 -0
  33. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/06-validation-signals-guide.md +0 -0
  34. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/07-performance-guide.md +0 -0
  35. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/docs/rules/README.md +0 -0
  36. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/setup.cfg +0 -0
  37. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/__init__.py +0 -0
  38. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/_install_rules.py +0 -0
  39. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/cascade.py +0 -0
  40. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/database/__init__.py +0 -0
  41. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/database/config.py +0 -0
  42. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/database/manager.py +0 -0
  43. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/exceptions.py +0 -0
  44. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/expressions/subquery.py +0 -0
  45. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/__init__.py +0 -0
  46. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/functions.py +0 -0
  47. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/proxies.py +0 -0
  48. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/__init__.py +0 -0
  49. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/descriptors.py +0 -0
  50. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/managers.py +0 -0
  51. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/prefetch.py +0 -0
  52. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/strategies.py +0 -0
  53. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/relations/utils.py +0 -0
  54. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/shortcuts.py +0 -0
  55. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/types/base.py +0 -0
  56. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/types/comparators.py +0 -0
  57. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/fields/utils.py +0 -0
  58. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/internal/__init__.py +0 -0
  59. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/internal/operations.py +0 -0
  60. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/internal/results.py +0 -0
  61. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/mixins.py +0 -0
  62. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/model.py +0 -0
  63. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/objects/__init__.py +0 -0
  64. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/objects/bulk.py +0 -0
  65. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/objects/upsert.py +0 -0
  66. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/queries/__init__.py +0 -0
  67. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/signals.py +0 -0
  68. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/utils/__init__.py +0 -0
  69. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/utils/inspect.py +0 -0
  70. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/utils/naming.py +0 -0
  71. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/utils/pattern.py +0 -0
  72. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects/validators.py +0 -0
  73. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
  74. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects.egg-info/entry_points.txt +0 -0
  75. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects.egg-info/requires.txt +0 -0
  76. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/sqlobjects.egg-info/top_level.txt +0 -0
  77. {sqlobjects-1.0.16 → sqlobjects-1.2.0}/tests/test_config.py +0 -0
@@ -0,0 +1,142 @@
1
+ ## 1.2.0 (2026-02-25)
2
+
3
+ ### Feat
4
+
5
+ - **metadata**: improve constraint and index naming conventions
6
+
7
+ ## 1.1.0 (2026-02-14)
8
+
9
+ ### Feat
10
+
11
+ - update custom field type imports to use sqlobjects.fields.types
12
+ - implement CTE (Common Table Expressions) support
13
+ - add support for SQL window functions
14
+ - add EXPLAIN support with dialect-based architecture
15
+
16
+ ### Fix
17
+
18
+ - update test imports to use DeferredObject instead of DeferredFieldProxy
19
+
20
+ ## 1.0.16 (2025-11-18)
21
+
22
+ ### Fix
23
+
24
+ - correct ModelMixin inheritance
25
+
26
+ ## 1.0.15 (2025-11-18)
27
+
28
+ ### Feat
29
+
30
+ - preserve annotate fields and add serialization options
31
+
32
+ ## 1.0.14 (2025-11-18)
33
+
34
+ ### Feat
35
+
36
+ - add has_session() to check explicit session availability
37
+
38
+ ## 1.0.13 (2025-11-18)
39
+
40
+ ## 1.0.12 (2025-11-18)
41
+
42
+ ### Refactor
43
+
44
+ - move rules installer to independent scripts
45
+
46
+ ## 1.0.11 (2025-11-18)
47
+
48
+ ### Feat
49
+
50
+ - add AI assistant rules with auto-install support
51
+
52
+ ## 1.0.10 (2025-11-17)
53
+
54
+ ### Feat
55
+
56
+ - support Model class in join methods for cleaner API
57
+
58
+ ## 1.0.9 (2025-11-14)
59
+
60
+ ### Feat
61
+
62
+ - add optional tables parameter to create_tables/drop_tables
63
+
64
+ ## 1.0.8 (2025-11-11)
65
+
66
+ ### Feat
67
+
68
+ - add model-level relationship loading methods
69
+ - improve relation field type inference
70
+ - refactor relationship proxies
71
+
72
+ ## 1.0.7 (2025-10-14)
73
+
74
+ ### Feat
75
+
76
+ - add upsert support for PostgreSQL
77
+
78
+ ### Fix
79
+
80
+ - identity support for PostgreSQL
81
+
82
+ ### Refactor
83
+
84
+ - consolidate bulk and queryset logic
85
+
86
+ ### Perf
87
+
88
+ - improve bulk delete performance
89
+
90
+ ## 1.0.6 (2025-10-08)
91
+
92
+ ### Feat
93
+
94
+ - optimize field cache and state manager
95
+ - implement relationship prefetch support
96
+ - add kwargs parameter support to filter/exclude/get methods
97
+
98
+ ### Refactor
99
+
100
+ - unify cascade for model and queryset operations
101
+
102
+ ## 1.0.5 (2025-09-25)
103
+
104
+ ### Fix
105
+
106
+ - generate DDL using column definition order
107
+
108
+ ## 1.0.4 (2025-09-25)
109
+
110
+ ### Feat
111
+
112
+ - remove unnecessary exception catching
113
+ - implement insert or update using database upsert
114
+
115
+ ### Fix
116
+
117
+ - use pk column name instead of column instance for pgsql upsert
118
+
119
+ ### Refactor
120
+
121
+ - move field default value related methods to DataConversionMixin
122
+
123
+ ## 1.0.3 (2025-09-25)
124
+
125
+ ### Fix
126
+
127
+ - field default/default_factory not working
128
+
129
+ ## 1.0.2 (2025-09-24)
130
+
131
+ ### Feat
132
+
133
+ - add type support for StringColumn
134
+ - add support for cascade delete in relationships
135
+ - add cascade support to relationship fields
136
+ - add type checking for __registry__
137
+ - use base model to create database tables
138
+ - init public commit
139
+
140
+ ### Fix
141
+
142
+ - foreign key type inference issue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.0.16
3
+ Version: 1.2.0
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -246,6 +246,7 @@ sqlobjects-install-rules amazonq # or cursor, claude, kiro
246
246
  - [Relationships](docs/features/05-relationships.md) - Model relationships and loading strategies
247
247
  - [Validation & Signals](docs/features/06-validation-signals.md) - Data validation and lifecycle hooks
248
248
  - [Performance Optimization](docs/features/07-performance-optimization.md) - Performance tuning and best practices
249
+ - [Custom Field Types](docs/features/08-custom-field-types.md) - Extend with database-specific types
249
250
 
250
251
  ### Design Documentation
251
252
 
@@ -309,6 +310,29 @@ users = await User.objects.raw(
309
310
  )
310
311
  ```
311
312
 
313
+ ### Custom Field Types
314
+
315
+ ```python
316
+ # Extend with database-specific types
317
+ from sqlobjects.fields.types.registry import register_field_type
318
+ from sqlalchemy.dialects.postgresql import TSVECTOR
319
+
320
+ register_field_type(
321
+ TSVECTOR, "tsvector",
322
+ comparator=TSVectorComparator
323
+ )
324
+
325
+ class Document(ObjectModel):
326
+ content_vector: Column = column(type="tsvector") # PostgreSQL full-text search
327
+
328
+ # Query with custom types
329
+ docs = await Document.objects.filter(
330
+ Document.content_vector.match("python & programming")
331
+ ).all()
332
+ ```
333
+
334
+ See [Custom Field Types](docs/features/08-custom-field-types.md) for complete examples.
335
+
312
336
  ## 🧪 Testing
313
337
 
314
338
  SQLObjects includes comprehensive test coverage:
@@ -217,6 +217,7 @@ sqlobjects-install-rules amazonq # or cursor, claude, kiro
217
217
  - [Relationships](docs/features/05-relationships.md) - Model relationships and loading strategies
218
218
  - [Validation & Signals](docs/features/06-validation-signals.md) - Data validation and lifecycle hooks
219
219
  - [Performance Optimization](docs/features/07-performance-optimization.md) - Performance tuning and best practices
220
+ - [Custom Field Types](docs/features/08-custom-field-types.md) - Extend with database-specific types
220
221
 
221
222
  ### Design Documentation
222
223
 
@@ -280,6 +281,29 @@ users = await User.objects.raw(
280
281
  )
281
282
  ```
282
283
 
284
+ ### Custom Field Types
285
+
286
+ ```python
287
+ # Extend with database-specific types
288
+ from sqlobjects.fields.types.registry import register_field_type
289
+ from sqlalchemy.dialects.postgresql import TSVECTOR
290
+
291
+ register_field_type(
292
+ TSVECTOR, "tsvector",
293
+ comparator=TSVectorComparator
294
+ )
295
+
296
+ class Document(ObjectModel):
297
+ content_vector: Column = column(type="tsvector") # PostgreSQL full-text search
298
+
299
+ # Query with custom types
300
+ docs = await Document.objects.filter(
301
+ Document.content_vector.match("python & programming")
302
+ ).all()
303
+ ```
304
+
305
+ See [Custom Field Types](docs/features/08-custom-field-types.md) for complete examples.
306
+
283
307
  ## 🧪 Testing
284
308
 
285
309
  SQLObjects includes comprehensive test coverage:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.0.16"
3
+ version = "1.2.0"
4
4
  description = "Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -49,22 +49,22 @@ Changelog = "https://github.com/XtraVisionsAI/sqlobjects/blob/main/CHANGELOG.md"
49
49
 
50
50
  [dependency-groups]
51
51
  dev = [
52
- "pre-commit>=4.4.0",
53
- "pyright>=1.1.407",
54
- "ruff>=0.14.5",
55
- "setuptools>=80.9.0",
52
+ "pre-commit>=4.5.1",
53
+ "pyright>=1.1.408",
54
+ "ruff>=0.15.2",
55
+ "setuptools>=82.0.0",
56
56
  ]
57
57
  test = [
58
58
  "aiomysql>=0.3.2",
59
- "aiosqlite>=0.20.0",
60
- "asyncpg>=0.30.0",
61
- "pytest>=9.0.1",
59
+ "aiosqlite>=0.22.1",
60
+ "asyncpg>=0.31.0",
61
+ "pytest>=9.0.2",
62
62
  "pytest-asyncio>=1.3.0",
63
- "psutil>=7.1.3", # For memory monitoring in performance tests
63
+ "psutil>=7.2.2", # For memory monitoring in performance tests
64
64
  ]
65
65
 
66
66
  [build-system]
67
- requires = ["setuptools>=61.0", "wheel"]
67
+ requires = ["setuptools>=82.0.0", "wheel"]
68
68
  build-backend = "setuptools.build_meta"
69
69
 
70
70
  [tool.setuptools]
@@ -81,7 +81,7 @@ sqlobjects = ["py.typed"]
81
81
 
82
82
  [[tool.uv.index]]
83
83
  name = "tsinghua"
84
- url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
84
+ url = "https://mirrors.aliyun.com/pypi/simple"
85
85
  default = true
86
86
 
87
87
  [tool.commitizen]
@@ -1,5 +1,7 @@
1
1
  from .aggregate import AggregateExpression
2
2
  from .base import ComparisonExpression, QueryExpression
3
+ from .cte import CTEExpression
4
+ from .explain import ExplainResult
3
5
  from .function import FunctionExpression, func
4
6
  from .scalar import CountExpression, ExistsExpression, ScalarSubquery
5
7
  from .subquery import SubqueryExpression
@@ -15,14 +17,30 @@ from .terminal import (
15
17
  ValuesExpression,
16
18
  ValuesListExpression,
17
19
  )
20
+ from .window import (
21
+ DenseRankFunction,
22
+ FirstValueFunction,
23
+ LagFunction,
24
+ LastValueFunction,
25
+ LeadFunction,
26
+ NthValueFunction,
27
+ NtileFunction,
28
+ PercentRankFunction,
29
+ RankFunction,
30
+ RowNumberFunction,
31
+ WindowFunction,
32
+ WindowSpec,
33
+ )
18
34
 
19
35
 
20
36
  __all__ = [
21
37
  "func",
22
38
  "FunctionExpression",
23
39
  "SubqueryExpression",
40
+ "CTEExpression",
24
41
  "QueryExpression",
25
42
  "ComparisonExpression",
43
+ "ExplainResult",
26
44
  "AggregateExpression",
27
45
  "CountExpression",
28
46
  "ExistsExpression",
@@ -37,4 +55,16 @@ __all__ = [
37
55
  "DatesExpression",
38
56
  "DatetimesExpression",
39
57
  "GetItemExpression",
58
+ "WindowFunction",
59
+ "WindowSpec",
60
+ "RowNumberFunction",
61
+ "RankFunction",
62
+ "DenseRankFunction",
63
+ "PercentRankFunction",
64
+ "NtileFunction",
65
+ "LagFunction",
66
+ "LeadFunction",
67
+ "FirstValueFunction",
68
+ "LastValueFunction",
69
+ "NthValueFunction",
40
70
  ]
@@ -30,6 +30,10 @@ class AggregateExpression(QueryExpression[dict[str, Any]]):
30
30
  self._builder = builder
31
31
  self._aggregations = aggregations
32
32
 
33
+ def get_query(self):
34
+ """Return SQLAlchemy query object."""
35
+ return self._builder.build(self._builder.model_class.get_table())
36
+
33
37
  async def execute(self) -> dict[str, Any]:
34
38
  """Execute aggregation query and return results dictionary.
35
39
 
@@ -42,27 +42,42 @@ class QueryExpression(ABC, Generic[T_Result]):
42
42
  pass
43
43
 
44
44
  @abstractmethod
45
+ def get_query(self) -> Any:
46
+ """Get SQLAlchemy query object.
47
+
48
+ Returns:
49
+ SQLAlchemy Select/Update/Delete object
50
+ """
51
+ pass
52
+
45
53
  def get_sql(self) -> str:
46
- """Generate SQL string for this expression.
54
+ """Generate SQL string for debugging.
47
55
 
48
56
  Returns:
49
57
  SQL string representation
50
58
  """
51
- pass
59
+ query = self.get_query()
60
+ return str(query.compile(compile_kwargs={"literal_binds": True}))
52
61
 
53
- def explain(self, analyze: bool = False, verbose: bool = False) -> str:
54
- """Generate execution plan for this expression.
62
+ def explain(self, analyze: bool = False, verbose: bool = False):
63
+ """Return awaitable EXPLAIN result.
64
+
65
+ Usage:
66
+ plan = await expression.explain()
67
+ plan = await expression.explain(analyze=True, verbose=True)
55
68
 
56
69
  Args:
57
70
  analyze: Include actual execution statistics
58
71
  verbose: Include detailed execution information
59
72
 
60
73
  Returns:
61
- Query execution plan
74
+ ExplainResult that can be awaited
62
75
  """
76
+ from .explain import ExplainResult
77
+
63
78
  if not self._executor:
64
79
  raise RuntimeError("No executor available for explain operation")
65
- return self._executor.explain(self.get_sql(), analyze=analyze, verbose=verbose)
80
+ return ExplainResult(self, analyze, verbose)
66
81
 
67
82
  def __gt__(self, other) -> "ComparisonExpression":
68
83
  """Create greater-than comparison expression."""
@@ -0,0 +1,136 @@
1
+ """CTE (Common Table Expression) support for SQLObjects.
2
+
3
+ This module provides CTE functionality for building complex queries with
4
+ reusable subqueries and recursive queries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from sqlalchemy import CTE as SACTE
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from sqlobjects.queryset import QuerySet
16
+
17
+ __all__ = ["CTEExpression"]
18
+
19
+
20
+ class CTEExpression:
21
+ """CTE expression wrapper for query reuse.
22
+
23
+ A CTE (Common Table Expression) allows you to define a temporary named
24
+ result set that can be referenced within a SELECT, INSERT, UPDATE, or
25
+ DELETE statement.
26
+
27
+ Examples:
28
+ Basic CTE:
29
+ >>> adults = User.objects.filter(User.age >= 18).cte("adults")
30
+ >>> result = await User.objects.with_cte(adults).filter(adults.c.age < 30).all()
31
+
32
+ Recursive CTE:
33
+ >>> base = Employee.objects.filter(Employee.manager_id.is_(None)).cte("hierarchy", recursive=True)
34
+ >>> recursive = Employee.objects.join(base, Employee.manager_id == base.c.id)
35
+ >>> hierarchy = base.union_all(recursive)
36
+ >>> all_employees = await Employee.objects.with_cte(hierarchy).select_from(hierarchy).all()
37
+ """
38
+
39
+ def __init__(self, queryset: QuerySet, name: str, recursive: bool = False):
40
+ """Initialize CTE expression.
41
+
42
+ Args:
43
+ queryset: The QuerySet to convert to CTE
44
+ name: Name of the CTE
45
+ recursive: Whether this is a recursive CTE
46
+ """
47
+ self._queryset = queryset
48
+ self._name = name
49
+ self._recursive = recursive
50
+ self._cte_obj: SACTE | None = None
51
+ self._union_query: QuerySet | None = None
52
+
53
+ def _build_cte(self, session) -> SACTE:
54
+ """Build SQLAlchemy CTE object.
55
+
56
+ Args:
57
+ session: Database session for building the query
58
+
59
+ Returns:
60
+ SQLAlchemy CTE object
61
+ """
62
+ if self._cte_obj is not None:
63
+ return self._cte_obj
64
+
65
+ # Build base query
66
+ stmt = self._queryset._builder.build(session)
67
+
68
+ # Handle recursive CTE with UNION ALL
69
+ if self._recursive and self._union_query is not None:
70
+ # Build recursive part
71
+ recursive_stmt = self._union_query._builder.build(session)
72
+
73
+ # Combine with UNION ALL
74
+ combined = stmt.union_all(recursive_stmt)
75
+ cte_obj = combined.cte(self._name, recursive=True)
76
+ else:
77
+ # Non-recursive CTE
78
+ cte_obj = stmt.cte(self._name, recursive=self._recursive)
79
+
80
+ self._cte_obj = cte_obj
81
+ return cte_obj
82
+
83
+ @property
84
+ def c(self) -> Any:
85
+ """Access CTE columns (similar to Table.c).
86
+
87
+ Returns:
88
+ Column collection for the CTE
89
+
90
+ Note:
91
+ The CTE must be built before accessing columns.
92
+ This is typically done automatically when used in a query.
93
+ """
94
+ if self._cte_obj is None:
95
+ raise RuntimeError(f"CTE '{self._name}' has not been built yet. Use it in a query with .with_cte() first.")
96
+ return self._cte_obj.c
97
+
98
+ def union_all(self, recursive_queryset: QuerySet) -> CTEExpression:
99
+ """Add UNION ALL clause for recursive CTE.
100
+
101
+ This method is used to define the recursive part of a recursive CTE.
102
+
103
+ Args:
104
+ recursive_queryset: QuerySet for the recursive part
105
+
106
+ Returns:
107
+ Self for method chaining
108
+
109
+ Example:
110
+ >>> base = Employee.objects.filter(Employee.manager_id.is_(None)).cte("tree", recursive=True)
111
+ >>> recursive = Employee.objects.join(base, Employee.manager_id == base.c.id)
112
+ >>> tree = base.union_all(recursive)
113
+ """
114
+ if not self._recursive:
115
+ raise ValueError(
116
+ f"union_all() can only be used with recursive CTEs. "
117
+ f"Set recursive=True when creating CTE '{self._name}'."
118
+ )
119
+
120
+ self._union_query = recursive_queryset
121
+ return self
122
+
123
+ @property
124
+ def name(self) -> str:
125
+ """Get CTE name."""
126
+ return self._name
127
+
128
+ @property
129
+ def is_recursive(self) -> bool:
130
+ """Check if this is a recursive CTE."""
131
+ return self._recursive
132
+
133
+ def __repr__(self) -> str:
134
+ """String representation."""
135
+ recursive_str = ", recursive=True" if self._recursive else ""
136
+ return f"CTEExpression(name='{self._name}'{recursive_str})"
@@ -0,0 +1,38 @@
1
+ """EXPLAIN query analysis support."""
2
+
3
+
4
+ class ExplainResult:
5
+ """Awaitable result for EXPLAIN operations.
6
+
7
+ This class wraps EXPLAIN query execution and makes it awaitable,
8
+ providing a clean async interface for query plan analysis.
9
+
10
+ Examples:
11
+ plan = await User.objects.filter(User.age > 25).explain()
12
+ analysis = await User.objects.all().explain(analyze=True, verbose=True)
13
+ """
14
+
15
+ def __init__(self, expression, analyze: bool, verbose: bool):
16
+ """Initialize EXPLAIN result.
17
+
18
+ Args:
19
+ expression: QueryExpression to explain
20
+ analyze: Include actual execution statistics
21
+ verbose: Include detailed execution information
22
+ """
23
+ self.expression = expression
24
+ self.analyze = analyze
25
+ self.verbose = verbose
26
+
27
+ def __await__(self):
28
+ """Make this object awaitable."""
29
+ return self._execute().__await__()
30
+
31
+ async def _execute(self) -> str:
32
+ """Execute EXPLAIN and return plan.
33
+
34
+ Returns:
35
+ Query execution plan as string
36
+ """
37
+ query = self.expression.get_query()
38
+ return await self.expression._executor.explain(query, self.analyze, self.verbose)