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/dsl/queries.py ADDED
@@ -0,0 +1,339 @@
1
+ """Queries that know which database they belong to.
2
+
3
+ PyOQ's builders never reach a database, and that stays true: everything here
4
+ wraps one of those builders together with the operations it will run against.
5
+ The builder is still the only place a query is described, and unwrapping one
6
+ with `query` gives back something that cannot execute.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from typing import TYPE_CHECKING, Generic, TypeVar
13
+
14
+ from pyoq.dsl.labels import labels_of
15
+ from pyoq.dsl.result import Result, Row
16
+ from pyoq.query.select import JoinStep, Query, SelectQuery, SemiJoinStep
17
+ from pyoq.query.select_nodes import SelectNode, SetNode
18
+
19
+ if TYPE_CHECKING:
20
+ from pyoq.descriptors import RelationshipDescriptor
21
+ from pyoq.query.execution import QueryOperations
22
+ from pyoq.query.expressions import Expression
23
+ from pyoq.query.sources import CommonTable, SourceBase, TableLike
24
+ from pyoq.query.writes import ColumnReference
25
+
26
+ ResultRow = TypeVar("ResultRow", bound=tuple[object, ...])
27
+ Mapped = TypeVar("Mapped")
28
+ ScalarValue = TypeVar("ScalarValue")
29
+
30
+
31
+ class BoundQuery(Generic[ResultRow]):
32
+ """A query and the database it will run against."""
33
+
34
+ __slots__ = ("_operations", "_query")
35
+ _operations: QueryOperations
36
+ _query: Query[ResultRow]
37
+
38
+ def __init__(
39
+ self,
40
+ operations: QueryOperations,
41
+ query: Query[ResultRow],
42
+ ) -> None:
43
+ object.__setattr__(self, "_operations", operations)
44
+ object.__setattr__(self, "_query", query)
45
+
46
+ def __setattr__(self, name: str, value: object) -> None:
47
+ message = f"{type(self).__name__} is immutable"
48
+ raise AttributeError(message, name, value)
49
+
50
+ @property
51
+ def query(self) -> Query[ResultRow]:
52
+ """The query on its own, which cannot reach a database."""
53
+ return self._query
54
+
55
+ def order_by(self, *terms: object) -> BoundQuery[ResultRow]:
56
+ return self._with(self._query.order_by(*terms))
57
+
58
+ def limit(self, value: int, /) -> BoundQuery[ResultRow]:
59
+ return self._with(self._query.limit(value))
60
+
61
+ def offset(self, value: int, /) -> BoundQuery[ResultRow]:
62
+ return self._with(self._query.offset(value))
63
+
64
+ def union(self, other: Query[ResultRow], /) -> BoundQuery[ResultRow]:
65
+ return self._with(self._query.union(other))
66
+
67
+ def union_all(self, other: Query[ResultRow], /) -> BoundQuery[ResultRow]:
68
+ return self._with(self._query.union_all(other))
69
+
70
+ def intersect(self, other: Query[ResultRow], /) -> BoundQuery[ResultRow]:
71
+ return self._with(self._query.intersect(other))
72
+
73
+ def except_(self, other: Query[ResultRow], /) -> BoundQuery[ResultRow]:
74
+ return self._with(self._query.except_(other))
75
+
76
+ def fetch_all(self) -> Result[ResultRow]:
77
+ """Every row, with the names they came back under."""
78
+ rows = tuple(self._operations.many(self._query))
79
+ return Result(rows, _labels(self._query))
80
+
81
+ def fetch_one(self) -> Row[ResultRow]:
82
+ """Exactly one row, because that is what the caller said to expect."""
83
+ return Row(self._operations.one(self._query), _labels(self._query))
84
+
85
+ def fetch_optional(self) -> Row[ResultRow] | None:
86
+ row = self._operations.one_or_none(self._query)
87
+ return None if row is None else Row(row, _labels(self._query))
88
+
89
+ def fetch_optional_into(
90
+ self,
91
+ factory: Callable[..., Mapped],
92
+ /,
93
+ ) -> Mapped | None:
94
+ """One row built by the caller's own type, or nothing.
95
+
96
+ The row and the many-row result both take a factory inline.
97
+ A row that may be missing had to be tested for first, which
98
+ is the one shape that could not be asked for in the chain.
99
+ """
100
+ row = self.fetch_optional()
101
+ return None if row is None else row.into(factory)
102
+
103
+ def fetch_value(
104
+ self: BoundQuery[tuple[ScalarValue]],
105
+ /,
106
+ ) -> ScalarValue:
107
+ """The single column of the single row.
108
+
109
+ Only a query that selected one column can answer this, which is why the
110
+ annotation says so rather than a check at run time.
111
+ """
112
+ return self._operations.scalar(self._query)
113
+
114
+ def _with(self, query: Query[ResultRow]) -> BoundQuery[ResultRow]:
115
+ return BoundQuery(self._operations, query)
116
+
117
+
118
+ class BoundSelect(BoundQuery[ResultRow], Generic[ResultRow]):
119
+ """A SELECT that knows its database, with the builder's own shape kept."""
120
+
121
+ __slots__ = ()
122
+
123
+ @property
124
+ def query(self) -> SelectQuery[ResultRow]:
125
+ return _as_select(self._query)
126
+
127
+ def distinct(self, enabled: bool = True, /) -> BoundSelect[ResultRow]:
128
+ return self._select(self.query.distinct(enabled))
129
+
130
+ def from_(
131
+ self,
132
+ source: SourceBase | TableLike,
133
+ /,
134
+ ) -> BoundSelect[ResultRow]:
135
+ return self._select(self.query.from_(source))
136
+
137
+ def join(self, source: SourceBase | TableLike, /) -> BoundJoinStep[ResultRow]:
138
+ """An inner join, qualified by the clause that follows it."""
139
+ return BoundJoinStep(self._select, self.query.join(source))
140
+
141
+ def inner_join(
142
+ self,
143
+ source: SourceBase | TableLike,
144
+ /,
145
+ ) -> BoundJoinStep[ResultRow]:
146
+ return BoundJoinStep(self._select, self.query.inner_join(source))
147
+
148
+ def left_join(
149
+ self,
150
+ source: SourceBase | TableLike,
151
+ /,
152
+ ) -> BoundJoinStep[ResultRow]:
153
+ return BoundJoinStep(self._select, self.query.left_join(source))
154
+
155
+ def right_join(
156
+ self,
157
+ source: SourceBase | TableLike,
158
+ /,
159
+ ) -> BoundJoinStep[ResultRow]:
160
+ return BoundJoinStep(self._select, self.query.right_join(source))
161
+
162
+ def full_join(
163
+ self,
164
+ source: SourceBase | TableLike,
165
+ /,
166
+ ) -> BoundJoinStep[ResultRow]:
167
+ return BoundJoinStep(self._select, self.query.full_join(source))
168
+
169
+ def semi_join(
170
+ self,
171
+ source: SourceBase | TableLike,
172
+ /,
173
+ ) -> BoundSemiJoinStep[ResultRow]:
174
+ """Rows that have a match, without bringing the match back."""
175
+ return BoundSemiJoinStep(self._select, self.query.semi_join(source))
176
+
177
+ def anti_join(
178
+ self,
179
+ source: SourceBase | TableLike,
180
+ /,
181
+ ) -> BoundSemiJoinStep[ResultRow]:
182
+ """Rows that have no match."""
183
+ return BoundSemiJoinStep(self._select, self.query.anti_join(source))
184
+
185
+ def cross_join(
186
+ self,
187
+ source: SourceBase | TableLike,
188
+ /,
189
+ ) -> BoundSelect[ResultRow]:
190
+ return self._select(self.query.cross_join(source))
191
+
192
+ def natural_join(
193
+ self,
194
+ source: SourceBase | TableLike,
195
+ /,
196
+ ) -> BoundSelect[ResultRow]:
197
+ return self._select(self.query.natural_join(source))
198
+
199
+ def natural_left_join(
200
+ self,
201
+ source: SourceBase | TableLike,
202
+ /,
203
+ ) -> BoundSelect[ResultRow]:
204
+ return self._select(self.query.natural_left_join(source))
205
+
206
+ def natural_right_join(
207
+ self,
208
+ source: SourceBase | TableLike,
209
+ /,
210
+ ) -> BoundSelect[ResultRow]:
211
+ return self._select(self.query.natural_right_join(source))
212
+
213
+ def natural_full_join(
214
+ self,
215
+ source: SourceBase | TableLike,
216
+ /,
217
+ ) -> BoundSelect[ResultRow]:
218
+ return self._select(self.query.natural_full_join(source))
219
+
220
+ def where(
221
+ self,
222
+ condition: Expression[bool],
223
+ /,
224
+ ) -> BoundSelect[ResultRow]:
225
+ return self._select(self.query.where(condition))
226
+
227
+ def group_by(self, *expressions: object) -> BoundSelect[ResultRow]:
228
+ return self._select(self.query.group_by(*expressions))
229
+
230
+ def having(
231
+ self,
232
+ condition: Expression[bool],
233
+ /,
234
+ ) -> BoundSelect[ResultRow]:
235
+ return self._select(self.query.having(condition))
236
+
237
+ def with_(
238
+ self,
239
+ *common_tables: CommonTable[object],
240
+ ) -> BoundSelect[ResultRow]:
241
+ return self._select(self.query.with_(*common_tables))
242
+
243
+ def order_by(self, *terms: object) -> BoundSelect[ResultRow]:
244
+ return self._select(self.query.order_by(*terms))
245
+
246
+ def limit(self, value: int, /) -> BoundSelect[ResultRow]:
247
+ return self._select(self.query.limit(value))
248
+
249
+ def offset(self, value: int, /) -> BoundSelect[ResultRow]:
250
+ return self._select(self.query.offset(value))
251
+
252
+ def _select(self, query: SelectQuery[ResultRow]) -> BoundSelect[ResultRow]:
253
+ return BoundSelect(self._operations, query)
254
+
255
+
256
+ def _as_select(query: Query[ResultRow]) -> SelectQuery[ResultRow]:
257
+ if not isinstance(query, SelectQuery):
258
+ message = "the query is no longer a SELECT"
259
+ raise TypeError(message)
260
+ return query
261
+
262
+
263
+ def _labels(query: Query[ResultRow]) -> tuple[str | None, ...]:
264
+ """The name each column came back under, where it has one.
265
+
266
+ A set operation takes its names from the first query in it, which is the
267
+ rule the databases themselves follow.
268
+ """
269
+ select = _first_select(query.node)
270
+ return () if select is None else labels_of(select.projections)
271
+
272
+
273
+ def _first_select(node: object) -> SelectNode | None:
274
+ while isinstance(node, SetNode):
275
+ node = node.left
276
+ return node if isinstance(node, SelectNode) else None
277
+
278
+
279
+ class BoundJoinStep(Generic[ResultRow]):
280
+ """A bound join waiting to be told how the two tables line up."""
281
+
282
+ __slots__ = ("_bind", "_step")
283
+ _bind: Callable[[SelectQuery[ResultRow]], BoundSelect[ResultRow]]
284
+ _step: JoinStep[ResultRow]
285
+
286
+ def __init__(
287
+ self,
288
+ bind: Callable[[SelectQuery[ResultRow]], BoundSelect[ResultRow]],
289
+ step: JoinStep[ResultRow],
290
+ ) -> None:
291
+ object.__setattr__(self, "_bind", bind)
292
+ object.__setattr__(self, "_step", step)
293
+
294
+ def __setattr__(self, name: str, value: object) -> None:
295
+ message = "join steps are immutable"
296
+ raise AttributeError(message, name, value)
297
+
298
+ def on(self, condition: Expression[bool], /) -> BoundSelect[ResultRow]:
299
+ """The predicate the rows are matched by."""
300
+ return self._bind(self._step.on(condition))
301
+
302
+ def using(self, *columns: str | ColumnReference) -> BoundSelect[ResultRow]:
303
+ """Matched on columns both tables name the same way."""
304
+ return self._bind(self._step.using(*columns))
305
+
306
+ def on_key(
307
+ self,
308
+ relation: RelationshipDescriptor[object, object],
309
+ /,
310
+ ) -> BoundSelect[ResultRow]:
311
+ """Matched on the foreign key the schema already declares."""
312
+ return self._bind(self._step.on_key(relation))
313
+
314
+
315
+ class BoundSemiJoinStep(Generic[ResultRow]):
316
+ """A bound semi or anti join waiting for its predicate."""
317
+
318
+ __slots__ = ("_bind", "_step")
319
+ _bind: Callable[[SelectQuery[ResultRow]], BoundSelect[ResultRow]]
320
+ _step: SemiJoinStep[ResultRow]
321
+
322
+ def __init__(
323
+ self,
324
+ bind: Callable[[SelectQuery[ResultRow]], BoundSelect[ResultRow]],
325
+ step: SemiJoinStep[ResultRow],
326
+ ) -> None:
327
+ object.__setattr__(self, "_bind", bind)
328
+ object.__setattr__(self, "_step", step)
329
+
330
+ def __setattr__(self, name: str, value: object) -> None:
331
+ message = "join steps are immutable"
332
+ raise AttributeError(message, name, value)
333
+
334
+ def on(self, condition: Expression[bool], /) -> BoundSelect[ResultRow]:
335
+ """The predicate a row is matched by."""
336
+ return self._bind(self._step.on(condition))
337
+
338
+
339
+ __all__ = ("BoundQuery", "BoundSelect")
pyoq/dsl/result.py ADDED
@@ -0,0 +1,164 @@
1
+ """What a fetched query hands back.
2
+
3
+ Rows arrive as typed tuples. This wraps them so the shape a caller wants is
4
+ asked for at the end of the chain rather than assembled around it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Generic, TypeVar, cast
10
+
11
+ from pyoq.errors import MultipleResultsError, NoResultError
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable, Iterator
15
+
16
+ Values = TypeVar("Values", bound=tuple[object, ...])
17
+ Mapped = TypeVar("Mapped")
18
+
19
+
20
+ class Row(tuple[object, ...], Generic[Values]):
21
+ """One row, and the names it came back under.
22
+
23
+ A tuple in every way a caller already relies on: it unpacks, indexes,
24
+ compares, and iterates. What it adds is the same question the many-row
25
+ result answers, which is the shape the caller wants it in.
26
+ """
27
+
28
+ _labels: tuple[str | None, ...]
29
+
30
+ def __new__(
31
+ cls,
32
+ values: Values,
33
+ labels: tuple[str | None, ...] = (),
34
+ ) -> Row[Values]:
35
+ row = super().__new__(cls, values)
36
+ row._labels = labels
37
+ return row
38
+
39
+ @property
40
+ def labels(self) -> tuple[str | None, ...]:
41
+ """One entry per column, None where the query gave it no name."""
42
+ return self._labels
43
+
44
+ def to_tuple(self) -> Values:
45
+ """The row as the tuple it was typed as."""
46
+ return cast("Values", tuple(self))
47
+
48
+ def to_list(self) -> list[object]:
49
+ return list(self)
50
+
51
+ def to_dict(self) -> dict[str, object]:
52
+ """Keyed by the names it came back under.
53
+
54
+ Every column has to have a name. An expression that was never given
55
+ one has nothing to key on, and inventing a name would invent a fact.
56
+ """
57
+ return dict(zip(_named(self._labels), self, strict=True))
58
+
59
+ def into(self, factory: Callable[..., Mapped], /) -> Mapped:
60
+ """Spread across a constructor, in the order it was selected."""
61
+ return factory(*self)
62
+
63
+
64
+ class Result(Generic[Values]):
65
+ """The rows a query returned, and the labels they came back under."""
66
+
67
+ __slots__ = ("_labels", "_rows")
68
+ _rows: tuple[Values, ...]
69
+ _labels: tuple[str | None, ...]
70
+
71
+ def __init__(
72
+ self,
73
+ rows: tuple[Values, ...],
74
+ labels: tuple[str | None, ...] = (),
75
+ ) -> None:
76
+ object.__setattr__(self, "_rows", rows)
77
+ object.__setattr__(self, "_labels", labels)
78
+
79
+ def __setattr__(self, name: str, value: object) -> None:
80
+ message = f"{type(self).__name__} is immutable"
81
+ raise AttributeError(message, name, value)
82
+
83
+ def __iter__(self) -> Iterator[Values]:
84
+ return iter(self._rows)
85
+
86
+ def __len__(self) -> int:
87
+ return len(self._rows)
88
+
89
+ def __bool__(self) -> bool:
90
+ return bool(self._rows)
91
+
92
+ def __repr__(self) -> str:
93
+ return f"Result({len(self._rows)} rows)"
94
+
95
+ @property
96
+ def labels(self) -> tuple[str | None, ...]:
97
+ """One entry per column, None where the query gave it no name."""
98
+ return self._labels
99
+
100
+ def to_list(self) -> list[Values]:
101
+ return list(self._rows)
102
+
103
+ def to_tuple(self) -> tuple[Values, ...]:
104
+ return self._rows
105
+
106
+ def map(self, mapper: Callable[[Values], Mapped], /) -> list[Mapped]:
107
+ """One value per row, built by whatever the caller passes."""
108
+ return [mapper(row) for row in self._rows]
109
+
110
+ def into(self, factory: Callable[..., Mapped], /) -> list[Mapped]:
111
+ """Each row spread across a constructor, in the order it was selected.
112
+
113
+ A dataclass, a named tuple, or any callable taking that many arguments
114
+ works, because the projection order is the argument order.
115
+ """
116
+ return [factory(*row) for row in self._rows]
117
+
118
+ def to_dicts(self) -> list[dict[str, object]]:
119
+ """Rows keyed by the names they came back under.
120
+
121
+ Every column has to have a name. An expression that was never given one
122
+ has nothing to key on, and inventing a name would be inventing a fact.
123
+ """
124
+ names = _named(self._labels)
125
+ return [dict(zip(names, row, strict=True)) for row in self._rows]
126
+
127
+ def to_dict(self) -> dict[str, object]:
128
+ """Exactly one row, keyed by the names it came back under.
129
+
130
+ The plural reads every row. This one says the caller expected a single
131
+ row, and reports it either way rather than quietly taking the first.
132
+ """
133
+ return self.one().to_dict()
134
+
135
+ def first(self) -> Row[Values] | None:
136
+ """The row at the front, keyed the way every row here is."""
137
+ if not self._rows:
138
+ return None
139
+ return Row(self._rows[0], self._labels)
140
+
141
+ def one(self) -> Row[Values]:
142
+ """Exactly one row, because that is what the caller said to expect."""
143
+ if len(self._rows) == 1:
144
+ return Row(self._rows[0], self._labels)
145
+ if not self._rows:
146
+ message = "query returned no rows"
147
+ raise NoResultError(message)
148
+ message = "query returned more than one row"
149
+ raise MultipleResultsError(message)
150
+
151
+
152
+ def _named(labels: tuple[str | None, ...]) -> tuple[str, ...]:
153
+ """Every label, or a refusal naming the columns that have none."""
154
+ unnamed = [position for position, label in enumerate(labels) if label is None]
155
+ if unnamed or not labels:
156
+ message = (
157
+ "every column needs a name to build a mapping; "
158
+ f"columns without one: {unnamed or 'all of them'}"
159
+ )
160
+ raise ValueError(message)
161
+ return tuple(label for label in labels if label is not None)
162
+
163
+
164
+ __all__ = ("Result",)