pyoq-sql 1.0.2__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.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
pyoq/query/select.py ADDED
@@ -0,0 +1,793 @@
1
+ """Typed immutable SELECT query construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import replace
7
+ from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
8
+
9
+ from pyoq.errors import QueryStateError, QueryValidationError
10
+ from pyoq.query.expressions import Condition, Expression, require_boolean
11
+ from pyoq.query.nodes import (
12
+ BinaryNode,
13
+ BinaryOperator,
14
+ BoundValueNode,
15
+ ExistsNode,
16
+ ExpressionNode,
17
+ FieldNode,
18
+ ScalarFamily,
19
+ VariadicNode,
20
+ VariadicOperator,
21
+ )
22
+ from pyoq.query.projections import Projection, order_node, projection_node
23
+ from pyoq.query.raw import NodeProvider
24
+ from pyoq.query.select_nodes import (
25
+ JoinKind,
26
+ JoinNode,
27
+ LockNode,
28
+ LockStrength,
29
+ LockWait,
30
+ OrderNode,
31
+ ProjectionNode,
32
+ QueryNode,
33
+ SelectNode,
34
+ SetNode,
35
+ SetOperator,
36
+ SourceNode,
37
+ )
38
+ from pyoq.query.sources import (
39
+ ColumnedTable,
40
+ CommonTable,
41
+ SourceBase,
42
+ Subquery,
43
+ TableLike,
44
+ common_table,
45
+ resolve_source,
46
+ subquery_source,
47
+ table_source,
48
+ write_target,
49
+ )
50
+ from pyoq.query.windows import WindowSpecification
51
+
52
+ if TYPE_CHECKING:
53
+ from pyoq.descriptors import RelationshipDescriptor
54
+ from pyoq.query.writes import ColumnReference
55
+
56
+
57
+ ResultRow = TypeVar("ResultRow")
58
+ Value1 = TypeVar("Value1")
59
+ Value2 = TypeVar("Value2")
60
+ Value3 = TypeVar("Value3")
61
+ Value4 = TypeVar("Value4")
62
+ Value5 = TypeVar("Value5")
63
+ Value6 = TypeVar("Value6")
64
+ Value7 = TypeVar("Value7")
65
+ Value8 = TypeVar("Value8")
66
+
67
+
68
+ class Query(Generic[ResultRow]):
69
+ __slots__ = ("_node",)
70
+ _node: QueryNode
71
+
72
+ def __init__(self, node: QueryNode) -> None:
73
+ object.__setattr__(self, "_node", node)
74
+
75
+ def __setattr__(self, name: str, value: object) -> None:
76
+ message = "queries are immutable"
77
+ raise AttributeError(message)
78
+
79
+ @property
80
+ def node(self) -> QueryNode:
81
+ return self._node
82
+
83
+ def as_subquery(self, alias: str, /) -> Subquery[ResultRow]:
84
+ return cast(Subquery[ResultRow], subquery_source(self.node, alias))
85
+
86
+ def as_lateral(self, alias: str, /) -> Subquery[ResultRow]:
87
+ """A subquery that may read the rows to its left, row by row.
88
+
89
+ Not every dialect has one, so a dialect without it refuses the query
90
+ rather than evaluating the subquery once and quietly meaning
91
+ something else.
92
+ """
93
+ return cast(
94
+ Subquery[ResultRow],
95
+ subquery_source(self.node, alias, lateral=True),
96
+ )
97
+
98
+ def as_common_table(
99
+ self,
100
+ name: str,
101
+ /,
102
+ *,
103
+ columns: tuple[str, ...] = (),
104
+ recursive: bool = False,
105
+ ) -> CommonTable[ResultRow]:
106
+ if columns and len(columns) != _projection_count(self.node):
107
+ message = "common table column count must match its projection count"
108
+ raise QueryValidationError(message)
109
+ return cast(
110
+ CommonTable[ResultRow],
111
+ common_table(
112
+ name,
113
+ self.node,
114
+ columns=columns,
115
+ recursive=recursive,
116
+ ),
117
+ )
118
+
119
+ def order_by(self, *terms: object) -> Query[ResultRow]:
120
+ if not terms:
121
+ message = "ORDER BY requires at least one term"
122
+ raise QueryValidationError(message)
123
+ nodes = tuple(order_node(term) for term in terms)
124
+ return self._replace_order(nodes)
125
+
126
+ def limit(self, value: int, /) -> Query[ResultRow]:
127
+ return self._replace_limit(_page_value(value, "limit"))
128
+
129
+ def offset(self, value: int, /) -> Query[ResultRow]:
130
+ return self._replace_offset(_page_value(value, "offset"))
131
+
132
+ def union(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
133
+ return self._set(SetOperator.UNION, other)
134
+
135
+ def union_all(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
136
+ return self._set(SetOperator.UNION_ALL, other)
137
+
138
+ def intersect(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
139
+ return self._set(SetOperator.INTERSECT, other)
140
+
141
+ def except_(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
142
+ return self._set(SetOperator.EXCEPT, other)
143
+
144
+ def _set(
145
+ self,
146
+ operator: SetOperator,
147
+ other: Query[ResultRow],
148
+ ) -> SetQuery[ResultRow]:
149
+ if _projection_count(self.node) != _projection_count(other.node):
150
+ message = "set operation projection counts must match"
151
+ raise QueryValidationError(message)
152
+ return SetQuery(SetNode(self.node, operator, other.node))
153
+
154
+ def _replace_order(self, terms: tuple[OrderNode, ...]) -> Query[ResultRow]:
155
+ if isinstance(self.node, SelectNode):
156
+ return SelectQuery[ResultRow](replace(self.node, orders=terms))
157
+ return SetQuery[ResultRow](replace(self.node, orders=terms))
158
+
159
+ def _replace_limit(self, value: int) -> Query[ResultRow]:
160
+ if isinstance(self.node, SelectNode):
161
+ return SelectQuery[ResultRow](replace(self.node, limit=value))
162
+ return SetQuery[ResultRow](replace(self.node, limit=value))
163
+
164
+ def _replace_offset(self, value: int) -> Query[ResultRow]:
165
+ if isinstance(self.node, SelectNode):
166
+ return SelectQuery[ResultRow](replace(self.node, offset=value))
167
+ return SetQuery[ResultRow](replace(self.node, offset=value))
168
+
169
+
170
+ class SelectQuery(Query[ResultRow], Generic[ResultRow]):
171
+ __slots__ = ()
172
+
173
+ @property
174
+ def select_node(self) -> SelectNode:
175
+ node = self.node
176
+ if not isinstance(node, SelectNode):
177
+ message = "SELECT query contains an invalid node"
178
+ raise QueryStateError(message)
179
+ return node
180
+
181
+ def for_update(self) -> SelectQuery[ResultRow]:
182
+ """Hold the rows this reads, so nobody else changes them first."""
183
+ return self._locked(LockStrength.UPDATE)
184
+
185
+ def for_no_key_update(self) -> SelectQuery[ResultRow]:
186
+ """Hold the rows, but let their keys still be referenced."""
187
+ return self._locked(LockStrength.NO_KEY_UPDATE)
188
+
189
+ def for_share(self) -> SelectQuery[ResultRow]:
190
+ """Hold the rows against change, while letting others read them too."""
191
+ return self._locked(LockStrength.SHARE)
192
+
193
+ def for_key_share(self) -> SelectQuery[ResultRow]:
194
+ """Hold only the keys, against a change that would break a reference."""
195
+ return self._locked(LockStrength.KEY_SHARE)
196
+
197
+ def nowait(self) -> SelectQuery[ResultRow]:
198
+ """Fail rather than wait for a row somebody else already holds."""
199
+ return self._waiting(LockWait.NOWAIT)
200
+
201
+ def skip_locked(self) -> SelectQuery[ResultRow]:
202
+ """Pass over a row somebody else already holds, rather than wait."""
203
+ return self._waiting(LockWait.SKIP_LOCKED)
204
+
205
+ def of(self, *targets: TableLike | NodeProvider) -> SelectQuery[ResultRow]:
206
+ """Hold rows of these tables only, and not everything read.
207
+
208
+ A column names the table that holds it, because a lock is taken on
209
+ rows and a caller who knows the column should not have to say the
210
+ table again.
211
+ """
212
+ if not targets:
213
+ message = "a lock names at least one table or none at all"
214
+ raise QueryValidationError(message)
215
+ named = tuple(_locked_table(target) for target in targets)
216
+ return SelectQuery(
217
+ replace(self.select_node, lock=replace(self._lock, tables=named))
218
+ )
219
+
220
+ def _locked(self, strength: LockStrength) -> SelectQuery[ResultRow]:
221
+ node = self.select_node
222
+ current = node.lock
223
+ wait = LockWait.WAIT if current is None else current.wait
224
+ tables = () if current is None else current.tables
225
+ return SelectQuery(replace(node, lock=LockNode(strength, wait, tables)))
226
+
227
+ def _waiting(self, wait: LockWait) -> SelectQuery[ResultRow]:
228
+ return SelectQuery(
229
+ replace(self.select_node, lock=replace(self._lock, wait=wait))
230
+ )
231
+
232
+ @property
233
+ def _lock(self) -> LockNode:
234
+ lock = self.select_node.lock
235
+ if lock is None:
236
+ message = "how a lock waits is said after what it locks"
237
+ raise QueryStateError(message)
238
+ return lock
239
+
240
+ def distinct(self, enabled: bool = True, /) -> SelectQuery[ResultRow]:
241
+ return SelectQuery(
242
+ replace(self.select_node, distinct=_boolean_setting(enabled, "distinct"))
243
+ )
244
+
245
+ def from_(
246
+ self,
247
+ source: SourceBase | TableLike,
248
+ /,
249
+ ) -> SelectQuery[ResultRow]:
250
+ if self.select_node.source is not None:
251
+ message = "SELECT source is already defined"
252
+ raise QueryStateError(message)
253
+ return SelectQuery(replace(self.select_node, source=resolve_source(source)))
254
+
255
+ def join(self, source: SourceBase | TableLike, /) -> JoinStep[ResultRow]:
256
+ """An inner join, qualified by the clause that follows it."""
257
+ return self._joining(JoinKind.INNER, source)
258
+
259
+ def inner_join(
260
+ self,
261
+ source: SourceBase | TableLike,
262
+ /,
263
+ ) -> JoinStep[ResultRow]:
264
+ return self._joining(JoinKind.INNER, source)
265
+
266
+ def left_join(
267
+ self,
268
+ source: SourceBase | TableLike,
269
+ /,
270
+ ) -> JoinStep[ResultRow]:
271
+ return self._joining(JoinKind.LEFT, source)
272
+
273
+ def right_join(
274
+ self,
275
+ source: SourceBase | TableLike,
276
+ /,
277
+ ) -> JoinStep[ResultRow]:
278
+ return self._joining(JoinKind.RIGHT, source)
279
+
280
+ def full_join(
281
+ self,
282
+ source: SourceBase | TableLike,
283
+ /,
284
+ ) -> JoinStep[ResultRow]:
285
+ return self._joining(JoinKind.FULL, source)
286
+
287
+ def semi_join(
288
+ self,
289
+ source: SourceBase | TableLike,
290
+ /,
291
+ ) -> SemiJoinStep[ResultRow]:
292
+ """Rows that have a match, without bringing the match back.
293
+
294
+ No database writes it as a join, so neither does the SQL: it becomes
295
+ `EXISTS`, which is what a semi join means and what every dialect here
296
+ already understands.
297
+ """
298
+ return SemiJoinStep(self, resolve_source(source), negated=False)
299
+
300
+ def anti_join(
301
+ self,
302
+ source: SourceBase | TableLike,
303
+ /,
304
+ ) -> SemiJoinStep[ResultRow]:
305
+ """Rows that have no match, which is `NOT EXISTS`."""
306
+ return SemiJoinStep(self, resolve_source(source), negated=True)
307
+
308
+ def cross_join(
309
+ self,
310
+ source: SourceBase | TableLike,
311
+ /,
312
+ ) -> SelectQuery[ResultRow]:
313
+ """Every row against every row, which nothing qualifies."""
314
+ return self._joined(JoinNode(JoinKind.CROSS, self._source(source)))
315
+
316
+ def natural_join(
317
+ self,
318
+ source: SourceBase | TableLike,
319
+ /,
320
+ ) -> SelectQuery[ResultRow]:
321
+ """Joined on every column the two share, whatever those turn out to be."""
322
+ return self._natural(JoinKind.INNER, source)
323
+
324
+ def natural_left_join(
325
+ self,
326
+ source: SourceBase | TableLike,
327
+ /,
328
+ ) -> SelectQuery[ResultRow]:
329
+ return self._natural(JoinKind.LEFT, source)
330
+
331
+ def natural_right_join(
332
+ self,
333
+ source: SourceBase | TableLike,
334
+ /,
335
+ ) -> SelectQuery[ResultRow]:
336
+ return self._natural(JoinKind.RIGHT, source)
337
+
338
+ def natural_full_join(
339
+ self,
340
+ source: SourceBase | TableLike,
341
+ /,
342
+ ) -> SelectQuery[ResultRow]:
343
+ return self._natural(JoinKind.FULL, source)
344
+
345
+ def _joining(
346
+ self,
347
+ kind: JoinKind,
348
+ source: SourceBase | TableLike,
349
+ ) -> JoinStep[ResultRow]:
350
+ return JoinStep(self._joined, kind, self._source(source))
351
+
352
+ def _natural(
353
+ self,
354
+ kind: JoinKind,
355
+ source: SourceBase | TableLike,
356
+ ) -> SelectQuery[ResultRow]:
357
+ return self._joined(JoinNode(kind, self._source(source), natural=True))
358
+
359
+ def _source(self, source: SourceBase | TableLike) -> SourceNode:
360
+ _require_from(self.select_node)
361
+ return resolve_source(source)
362
+
363
+ def _joined(self, join: JoinNode) -> SelectQuery[ResultRow]:
364
+ return SelectQuery(
365
+ replace(self.select_node, joins=(*self.select_node.joins, join))
366
+ )
367
+
368
+ def where(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
369
+ require_boolean(condition, "WHERE")
370
+ if self.select_node.condition is not None:
371
+ message = "WHERE condition is already defined"
372
+ raise QueryStateError(message)
373
+ return SelectQuery(replace(self.select_node, condition=condition.node))
374
+
375
+ def group_by(self, *expressions: object) -> SelectQuery[ResultRow]:
376
+ if not expressions:
377
+ message = "GROUP BY requires at least one expression"
378
+ raise QueryValidationError(message)
379
+ nodes = tuple(_expression_node(value, "GROUP BY") for value in expressions)
380
+ return SelectQuery(replace(self.select_node, groups=nodes))
381
+
382
+ def having(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
383
+ require_boolean(condition, "HAVING")
384
+ if self.select_node.having is not None:
385
+ message = "HAVING condition is already defined"
386
+ raise QueryStateError(message)
387
+ return SelectQuery(replace(self.select_node, having=condition.node))
388
+
389
+ def window(
390
+ self,
391
+ *specifications: WindowSpecification,
392
+ ) -> SelectQuery[ResultRow]:
393
+ """Declare the windows this query's functions look through by name."""
394
+ if not specifications:
395
+ message = "WINDOW requires at least one named window"
396
+ raise QueryValidationError(message)
397
+ additions = tuple(item.declaration for item in specifications)
398
+ existing = {item.name for item in self.select_node.windows}
399
+ names = [item.name for item in additions]
400
+ if existing.intersection(names) or len(names) != len(set(names)):
401
+ message = "window names must be unique within a query"
402
+ raise QueryValidationError(message)
403
+ return SelectQuery(
404
+ replace(self.select_node, windows=self.select_node.windows + additions)
405
+ )
406
+
407
+ def with_(
408
+ self,
409
+ *common_tables: CommonTable[object],
410
+ ) -> SelectQuery[ResultRow]:
411
+ if not common_tables:
412
+ message = "WITH requires at least one common table"
413
+ raise QueryValidationError(message)
414
+ existing = {item.name for item in self.select_node.common_tables}
415
+ additions = tuple(item.definition for item in common_tables)
416
+ names = [item.name for item in additions]
417
+ if existing.intersection(names) or len(names) != len(set(names)):
418
+ message = "common table names must be unique within a query"
419
+ raise QueryValidationError(message)
420
+ return SelectQuery(
421
+ replace(
422
+ self.select_node,
423
+ common_tables=(*self.select_node.common_tables, *additions),
424
+ )
425
+ )
426
+
427
+ def order_by(self, *terms: object) -> SelectQuery[ResultRow]:
428
+ return _as_select(super().order_by(*terms))
429
+
430
+ def limit(self, value: int, /) -> SelectQuery[ResultRow]:
431
+ return _as_select(super().limit(value))
432
+
433
+ def offset(self, value: int, /) -> SelectQuery[ResultRow]:
434
+ return _as_select(super().offset(value))
435
+
436
+
437
+ class SetQuery(Query[ResultRow], Generic[ResultRow]):
438
+ __slots__ = ()
439
+
440
+ def order_by(self, *terms: object) -> SetQuery[ResultRow]:
441
+ return _as_set(super().order_by(*terms))
442
+
443
+ def limit(self, value: int, /) -> SetQuery[ResultRow]:
444
+ return _as_set(super().limit(value))
445
+
446
+ def offset(self, value: int, /) -> SetQuery[ResultRow]:
447
+ return _as_set(super().offset(value))
448
+
449
+
450
+ @overload
451
+ def select(
452
+ value1: Expression[Value1] | Projection[Value1],
453
+ /,
454
+ ) -> SelectQuery[tuple[Value1]]: ...
455
+
456
+
457
+ @overload
458
+ def select(
459
+ value1: Expression[Value1] | Projection[Value1],
460
+ value2: Expression[Value2] | Projection[Value2],
461
+ /,
462
+ ) -> SelectQuery[tuple[Value1, Value2]]: ...
463
+
464
+
465
+ @overload
466
+ def select(
467
+ value1: Expression[Value1] | Projection[Value1],
468
+ value2: Expression[Value2] | Projection[Value2],
469
+ value3: Expression[Value3] | Projection[Value3],
470
+ /,
471
+ ) -> SelectQuery[tuple[Value1, Value2, Value3]]: ...
472
+
473
+
474
+ @overload
475
+ def select(
476
+ value1: Expression[Value1] | Projection[Value1],
477
+ value2: Expression[Value2] | Projection[Value2],
478
+ value3: Expression[Value3] | Projection[Value3],
479
+ value4: Expression[Value4] | Projection[Value4],
480
+ /,
481
+ ) -> SelectQuery[tuple[Value1, Value2, Value3, Value4]]: ...
482
+
483
+
484
+ @overload
485
+ def select(
486
+ value1: Expression[Value1] | Projection[Value1],
487
+ value2: Expression[Value2] | Projection[Value2],
488
+ value3: Expression[Value3] | Projection[Value3],
489
+ value4: Expression[Value4] | Projection[Value4],
490
+ value5: Expression[Value5] | Projection[Value5],
491
+ /,
492
+ ) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
493
+
494
+
495
+ @overload
496
+ def select(
497
+ value1: Expression[Value1] | Projection[Value1],
498
+ value2: Expression[Value2] | Projection[Value2],
499
+ value3: Expression[Value3] | Projection[Value3],
500
+ value4: Expression[Value4] | Projection[Value4],
501
+ value5: Expression[Value5] | Projection[Value5],
502
+ value6: Expression[Value6] | Projection[Value6],
503
+ /,
504
+ ) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
505
+
506
+
507
+ @overload
508
+ def select(
509
+ value1: Expression[Value1] | Projection[Value1],
510
+ value2: Expression[Value2] | Projection[Value2],
511
+ value3: Expression[Value3] | Projection[Value3],
512
+ value4: Expression[Value4] | Projection[Value4],
513
+ value5: Expression[Value5] | Projection[Value5],
514
+ value6: Expression[Value6] | Projection[Value6],
515
+ value7: Expression[Value7] | Projection[Value7],
516
+ /,
517
+ ) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7]]: ...
518
+
519
+
520
+ @overload
521
+ def select(
522
+ value1: Expression[Value1] | Projection[Value1],
523
+ value2: Expression[Value2] | Projection[Value2],
524
+ value3: Expression[Value3] | Projection[Value3],
525
+ value4: Expression[Value4] | Projection[Value4],
526
+ value5: Expression[Value5] | Projection[Value5],
527
+ value6: Expression[Value6] | Projection[Value6],
528
+ value7: Expression[Value7] | Projection[Value7],
529
+ value8: Expression[Value8] | Projection[Value8],
530
+ /,
531
+ ) -> SelectQuery[
532
+ tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8]
533
+ ]: ...
534
+
535
+
536
+ @overload
537
+ def select(
538
+ value1: object,
539
+ value2: object,
540
+ value3: object,
541
+ value4: object,
542
+ value5: object,
543
+ value6: object,
544
+ value7: object,
545
+ value8: object,
546
+ /,
547
+ *values: object,
548
+ ) -> SelectQuery[tuple[object, ...]]:
549
+ """Wider than the overloads above name, where the element types stop.
550
+
551
+ A query is still a query past eight projections, and matching no overload
552
+ would take the whole chain down with it rather than just the tuple. Past
553
+ here a projection is checked when it is turned into one, and the row is
554
+ `object` per column, which is what is still known about it.
555
+ """
556
+
557
+
558
+ def select( # type: ignore[misc]
559
+ *values: object,
560
+ ) -> SelectQuery[object]:
561
+ if not values:
562
+ message = "SELECT requires at least one projection"
563
+ raise QueryValidationError(message)
564
+ return SelectQuery(SelectNode(tuple(projection_node(value) for value in values)))
565
+
566
+
567
+ def select_from(table: ColumnedTable, /) -> SelectQuery[tuple[object, ...]]:
568
+ """Every column the table declares, in the order it declares them.
569
+
570
+ The column list is written out rather than left as a star, because a star
571
+ changes meaning the moment a column is added and the caller reading the
572
+ row would not know it had. Values arrive as a tuple in that order, which
573
+ is the order the generated row type takes its arguments in.
574
+ """
575
+ columns = table.COLUMNS
576
+ if not columns:
577
+ message = (
578
+ "table names no columns to select; generated tables declare them, "
579
+ "and any other table has to be selected from by naming its columns"
580
+ )
581
+ raise QueryValidationError(message)
582
+ projections = tuple(projection_node(column) for column in columns)
583
+ node = SelectNode(projections, every_column=True)
584
+ query: SelectQuery[tuple[object, ...]] = SelectQuery(node)
585
+ return query.from_(table_source(table))
586
+
587
+
588
+ class SemiJoinStep(Generic[ResultRow]):
589
+ """A semi or anti join waiting for the predicate that matches the rows."""
590
+
591
+ __slots__ = ("_negated", "_query", "_source")
592
+ _query: SelectQuery[ResultRow]
593
+ _source: SourceNode
594
+ _negated: bool
595
+
596
+ def __init__(
597
+ self,
598
+ query: SelectQuery[ResultRow],
599
+ source: SourceNode,
600
+ *,
601
+ negated: bool,
602
+ ) -> None:
603
+ object.__setattr__(self, "_query", query)
604
+ object.__setattr__(self, "_source", source)
605
+ object.__setattr__(self, "_negated", negated)
606
+
607
+ def __setattr__(self, name: str, value: object) -> None:
608
+ message = "join steps are immutable"
609
+ raise AttributeError(message, name, value)
610
+
611
+ def on(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
612
+ require_boolean(condition, "JOIN")
613
+ inner = SelectNode(
614
+ (ProjectionNode(BoundValueNode(1, ScalarFamily.NUMERIC)),),
615
+ source=self._source,
616
+ condition=condition.node,
617
+ )
618
+ matched = ExistsNode(inner, negated=self._negated)
619
+ node = self._query.select_node
620
+ return SelectQuery(replace(node, matches=(*node.matches, matched)))
621
+
622
+
623
+ class JoinStep(Generic[ResultRow]):
624
+ """A join waiting to be told how the two tables line up.
625
+
626
+ SQL names the table first and qualifies it after, and so does this. The
627
+ step cannot be run or read: until it is qualified there is no query.
628
+ """
629
+
630
+ __slots__ = ("_join", "_kind", "_source")
631
+ _join: Callable[[JoinNode], SelectQuery[ResultRow]]
632
+ _kind: JoinKind
633
+ _source: SourceNode
634
+
635
+ def __init__(
636
+ self,
637
+ join: Callable[[JoinNode], SelectQuery[ResultRow]],
638
+ kind: JoinKind,
639
+ source: SourceNode,
640
+ ) -> None:
641
+ object.__setattr__(self, "_join", join)
642
+ object.__setattr__(self, "_kind", kind)
643
+ object.__setattr__(self, "_source", source)
644
+
645
+ def __setattr__(self, name: str, value: object) -> None:
646
+ message = "join steps are immutable"
647
+ raise AttributeError(message, name, value)
648
+
649
+ def on(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
650
+ """The predicate the rows are matched by."""
651
+ require_boolean(condition, "JOIN")
652
+ return self._join(JoinNode(self._kind, self._source, condition.node))
653
+
654
+ def using(self, *columns: str | ColumnReference) -> SelectQuery[ResultRow]:
655
+ """Matched on columns both tables name the same way."""
656
+ names = tuple(_using_name(column) for column in columns)
657
+ if not names:
658
+ message = "USING requires at least one column"
659
+ raise QueryValidationError(message)
660
+ return self._join(JoinNode(self._kind, self._source, using=names))
661
+
662
+ def on_key(
663
+ self, relation: RelationshipDescriptor[object, object], /
664
+ ) -> SelectQuery[ResultRow]:
665
+ """Matched on the foreign key the schema already declares.
666
+
667
+ Generation writes a descriptor for every key in both directions, so
668
+ the columns are a fact rather than something to restate here and get
669
+ wrong.
670
+ """
671
+ return self._join(JoinNode(self._kind, self._source, _key_condition(relation)))
672
+
673
+
674
+ def _using_name(column: object) -> str:
675
+ """The column's name in the database, which is what USING matches on.
676
+
677
+ A generated column and a hand written field both carry one, and both say
678
+ it the same way once the expression is a node.
679
+ """
680
+ if isinstance(column, str):
681
+ return column
682
+ node = getattr(column, "node", None)
683
+ if isinstance(node, FieldNode):
684
+ return node.name
685
+ message = "USING takes columns or their names"
686
+ raise QueryValidationError(message)
687
+
688
+
689
+ def _key_condition(
690
+ relation: RelationshipDescriptor[object, object],
691
+ ) -> ExpressionNode:
692
+ """One equality per key column, and the same shape for a composite key.
693
+
694
+ A descriptor validates that its two sides line up when it is built, so
695
+ the pairs here are known to match.
696
+ """
697
+ equalities = [
698
+ BinaryNode(
699
+ BinaryOperator.EQUAL,
700
+ FieldNode(source, relation.source_table),
701
+ FieldNode(target, relation.target_table),
702
+ )
703
+ for source, target in zip(
704
+ relation.source_columns, relation.target_columns, strict=True
705
+ )
706
+ ]
707
+ if len(equalities) == 1:
708
+ return equalities[0]
709
+ return VariadicNode(VariadicOperator.AND, tuple(equalities))
710
+
711
+
712
+ def exists(query: Query[ResultRow], /) -> Condition:
713
+ """Whether the query returns any row at all.
714
+
715
+ What is projected inside does not matter, so nothing is read out of it.
716
+ Databases understand that, and so do readers.
717
+ """
718
+ return Condition(ExistsNode(query.node))
719
+
720
+
721
+ def not_exists(query: Query[ResultRow], /) -> Condition:
722
+ """Whether the query returns nothing at all."""
723
+ return Condition(ExistsNode(query.node, negated=True))
724
+
725
+
726
+ def _expression_node(value: object, clause: str) -> ExpressionNode:
727
+ if isinstance(value, Expression):
728
+ return value.node
729
+ message = f"{clause} requires typed expressions"
730
+ raise TypeError(message)
731
+
732
+
733
+ def _require_from(node: SelectNode) -> None:
734
+ if node.source is None:
735
+ message = "JOIN requires a FROM source"
736
+ raise QueryStateError(message)
737
+
738
+
739
+ def _page_value(value: object, label: str) -> int:
740
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
741
+ message = f"{label} must be a non-negative integer"
742
+ raise QueryValidationError(message)
743
+ return value
744
+
745
+
746
+ def _locked_table(target: TableLike | NodeProvider) -> str:
747
+ if isinstance(target, TableLike):
748
+ return write_target(target).name
749
+ node = target.node
750
+ if not isinstance(node, FieldNode) or node.table is None:
751
+ message = "a lock names a table, or a column that names one"
752
+ raise QueryValidationError(message)
753
+ return node.table
754
+
755
+
756
+ def _boolean_setting(value: object, label: str) -> bool:
757
+ if not isinstance(value, bool):
758
+ message = f"{label} setting must be boolean"
759
+ raise QueryValidationError(message)
760
+ return value
761
+
762
+
763
+ def _projection_count(node: QueryNode) -> int:
764
+ if isinstance(node, SelectNode):
765
+ return len(node.projections)
766
+ return _projection_count(node.left)
767
+
768
+
769
+ def _as_select(query: Query[ResultRow]) -> SelectQuery[ResultRow]:
770
+ if isinstance(query, SelectQuery):
771
+ return query
772
+ message = "query operation did not preserve SELECT state"
773
+ raise QueryStateError(message)
774
+
775
+
776
+ def _as_set(query: Query[ResultRow]) -> SetQuery[ResultRow]:
777
+ if isinstance(query, SetQuery):
778
+ return query
779
+ message = "query operation did not preserve set state"
780
+ raise QueryStateError(message)
781
+
782
+
783
+ __all__ = (
784
+ "JoinStep",
785
+ "Query",
786
+ "SelectQuery",
787
+ "SemiJoinStep",
788
+ "SetQuery",
789
+ "exists",
790
+ "not_exists",
791
+ "select",
792
+ "select_from",
793
+ )