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
@@ -0,0 +1,277 @@
1
+ """Immutable structural nodes for SELECT queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+ from typing import TypeAlias
8
+
9
+ from pyoq.query.nodes import ExpressionNode, NamedWindowNode
10
+
11
+
12
+ class JoinKind(StrEnum):
13
+ INNER = "inner"
14
+ LEFT = "left"
15
+ RIGHT = "right"
16
+ FULL = "full"
17
+ CROSS = "cross"
18
+
19
+
20
+ class OrderDirection(StrEnum):
21
+ ASCENDING = "ascending"
22
+ DESCENDING = "descending"
23
+
24
+
25
+ class NullOrder(StrEnum):
26
+ FIRST = "first"
27
+ LAST = "last"
28
+
29
+
30
+ class SetOperator(StrEnum):
31
+ UNION = "union"
32
+ UNION_ALL = "union-all"
33
+ INTERSECT = "intersect"
34
+ EXCEPT = "except"
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class TableSourceNode:
39
+ name: str
40
+ schema: str | None = None
41
+ catalog: str | None = None
42
+ alias: str | None = None
43
+
44
+ def __post_init__(self) -> None:
45
+ _require_identifier(self.name, "table")
46
+ _require_optional_identifier(self.schema, "schema")
47
+ _require_optional_identifier(self.catalog, "catalog")
48
+ _require_optional_identifier(self.alias, "table alias")
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ProjectionNode:
53
+ expression: ExpressionNode
54
+ alias: str | None = None
55
+
56
+ def __post_init__(self) -> None:
57
+ _require_optional_identifier(self.alias, "projection alias")
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class OrderNode:
62
+ expression: ExpressionNode
63
+ direction: OrderDirection
64
+ nulls: NullOrder | None = None
65
+
66
+
67
+ class LockStrength(StrEnum):
68
+ """How much of a row a lock holds, from the whole row down to its key."""
69
+
70
+ UPDATE = "update"
71
+ NO_KEY_UPDATE = "no-key-update"
72
+ SHARE = "share"
73
+ KEY_SHARE = "key-share"
74
+
75
+
76
+ class LockWait(StrEnum):
77
+ """What a statement does about rows somebody else has already locked."""
78
+
79
+ WAIT = "wait"
80
+ NOWAIT = "nowait"
81
+ SKIP_LOCKED = "skip-locked"
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class LockNode:
86
+ """Rows held for this transaction, so nobody else changes them first.
87
+
88
+ ``tables`` narrows the lock to some of what the query reads, which is
89
+ what keeps a join from locking rows it only looked at.
90
+ """
91
+
92
+ strength: LockStrength
93
+ wait: LockWait = LockWait.WAIT
94
+ tables: tuple[str, ...] = ()
95
+
96
+
97
+ @dataclass(frozen=True, slots=True)
98
+ class SelectNode:
99
+ projections: tuple[ProjectionNode, ...]
100
+ source: SourceNode | None = None
101
+ joins: tuple[JoinNode, ...] = ()
102
+ condition: ExpressionNode | None = None
103
+ matches: tuple[ExpressionNode, ...] = ()
104
+ """What a semi or anti join asks of each row.
105
+
106
+ Kept apart from the condition so that `where` stays a clause a caller
107
+ defines once, and a semi join neither collides with it nor depends on
108
+ having been written after it.
109
+ """
110
+
111
+ groups: tuple[ExpressionNode, ...] = ()
112
+ having: ExpressionNode | None = None
113
+ orders: tuple[OrderNode, ...] = ()
114
+ limit: int | None = None
115
+ offset: int | None = None
116
+ common_tables: tuple[CommonTableNode, ...] = ()
117
+ windows: tuple[NamedWindowNode, ...] = ()
118
+ """Windows written once here and looked through by name."""
119
+
120
+ distinct: bool = False
121
+ lock: LockNode | None = None
122
+ every_column: bool = False
123
+ """Whether the projection is written as a star.
124
+
125
+ The columns are still carried, because they are what names a row's fields
126
+ and says what type each one reads back as. Only the SQL is shorter.
127
+ """
128
+
129
+ def __post_init__(self) -> None:
130
+ if not self.projections:
131
+ message = "SELECT requires at least one projection"
132
+ raise ValueError(message)
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class SetNode:
137
+ left: QueryNode
138
+ operator: SetOperator
139
+ right: QueryNode
140
+ orders: tuple[OrderNode, ...] = ()
141
+ limit: int | None = None
142
+ offset: int | None = None
143
+
144
+
145
+ @dataclass(frozen=True, slots=True)
146
+ class SubquerySourceNode:
147
+ query: QueryNode
148
+ alias: str
149
+ lateral: bool = False
150
+ """Whether the subquery may read the rows to its left.
151
+
152
+ An ordinary subquery in a FROM clause is evaluated on its own. A lateral
153
+ one is evaluated per row of what came before it, which is the whole point
154
+ and also why not every dialect has it.
155
+ """
156
+
157
+ def __post_init__(self) -> None:
158
+ _require_identifier(self.alias, "subquery alias")
159
+
160
+
161
+ @dataclass(frozen=True, slots=True)
162
+ class CommonTableSourceNode:
163
+ name: str
164
+
165
+ def __post_init__(self) -> None:
166
+ _require_identifier(self.name, "common table")
167
+
168
+
169
+ @dataclass(frozen=True, slots=True)
170
+ class CommonTableNode:
171
+ name: str
172
+ query: QueryNode
173
+ columns: tuple[str, ...] = ()
174
+ recursive: bool = False
175
+
176
+ def __post_init__(self) -> None:
177
+ _require_identifier(self.name, "common table")
178
+ for column in self.columns:
179
+ _require_identifier(column, "common table column")
180
+
181
+
182
+ @dataclass(frozen=True, slots=True)
183
+ class JoinNode:
184
+ kind: JoinKind
185
+ source: SourceNode
186
+ condition: ExpressionNode | None = None
187
+ using: tuple[str, ...] = ()
188
+ natural: bool = False
189
+
190
+ def __post_init__(self) -> None:
191
+ _validate_join(self)
192
+
193
+
194
+ def _validate_join(node: JoinNode) -> None:
195
+ """A join is qualified once, by the one clause SQL allows it.
196
+
197
+ `ON`, `USING`, and `NATURAL` each say how two tables line up, and a join
198
+ that named two of them would be asking the database to decide which.
199
+ """
200
+ for column in node.using:
201
+ _require_identifier(column, "join column")
202
+ qualifiers = (
203
+ node.condition is not None,
204
+ bool(node.using),
205
+ node.natural,
206
+ )
207
+ named = sum(qualifiers)
208
+ if node.kind is JoinKind.CROSS:
209
+ if named:
210
+ message = "cross join cannot be qualified"
211
+ raise ValueError(message)
212
+ return
213
+ if named == 0:
214
+ message = "qualified join requires ON, USING, or NATURAL"
215
+ raise ValueError(message)
216
+ if named > 1:
217
+ message = "qualified join takes one of ON, USING, or NATURAL"
218
+ raise ValueError(message)
219
+
220
+
221
+ QueryNode: TypeAlias = SelectNode | SetNode
222
+
223
+
224
+ @dataclass(frozen=True, slots=True)
225
+ class JsonElementsSourceNode:
226
+ """The elements of a JSON array, read one row at a time.
227
+
228
+ Every dialect can turn an array into rows, and each spells it so
229
+ differently that only the alias and the one column are shared.
230
+ """
231
+
232
+ expression: ExpressionNode
233
+ alias: str
234
+
235
+
236
+ ELEMENT_COLUMN = "value"
237
+ """What the one column of an expanded JSON array is called on every dialect."""
238
+
239
+
240
+ SourceNode: TypeAlias = (
241
+ TableSourceNode
242
+ | SubquerySourceNode
243
+ | CommonTableSourceNode
244
+ | JsonElementsSourceNode
245
+ )
246
+
247
+
248
+ def _require_identifier(value: str, label: str) -> None:
249
+ if not value or "\x00" in value:
250
+ message = f"{label} identifier cannot be empty or contain a null character"
251
+ raise ValueError(message)
252
+
253
+
254
+ def _require_optional_identifier(value: str | None, label: str) -> None:
255
+ if value is not None:
256
+ _require_identifier(value, label)
257
+
258
+
259
+ __all__ = (
260
+ "ELEMENT_COLUMN",
261
+ "CommonTableNode",
262
+ "CommonTableSourceNode",
263
+ "JoinKind",
264
+ "JoinNode",
265
+ "JsonElementsSourceNode",
266
+ "NullOrder",
267
+ "OrderDirection",
268
+ "OrderNode",
269
+ "ProjectionNode",
270
+ "QueryNode",
271
+ "SelectNode",
272
+ "SetNode",
273
+ "SetOperator",
274
+ "SourceNode",
275
+ "SubquerySourceNode",
276
+ "TableSourceNode",
277
+ )
pyoq/query/sources.py ADDED
@@ -0,0 +1,236 @@
1
+ """Typed query sources and write targets for generated table descriptors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar, Generic, Protocol, TypeVar, cast, runtime_checkable
6
+
7
+ from pyoq.errors import QueryValidationError
8
+ from pyoq.query.fields import Field, field
9
+ from pyoq.query.nodes import FieldNode
10
+ from pyoq.query.raw import NodeProvider
11
+ from pyoq.query.select_nodes import (
12
+ ELEMENT_COLUMN,
13
+ CommonTableNode,
14
+ CommonTableSourceNode,
15
+ JsonElementsSourceNode,
16
+ QueryNode,
17
+ SourceNode,
18
+ SubquerySourceNode,
19
+ TableSourceNode,
20
+ )
21
+ from pyoq.query.write_nodes import WriteTargetNode
22
+
23
+ SourceRow = TypeVar("SourceRow", covariant=True)
24
+ FieldValue = TypeVar("FieldValue")
25
+
26
+
27
+ @runtime_checkable
28
+ class TableLike(Protocol):
29
+ @property
30
+ def database_name(self) -> str: ...
31
+
32
+ @property
33
+ def schema_name(self) -> str | None: ...
34
+
35
+ @property
36
+ def catalog_name(self) -> str | None: ...
37
+
38
+
39
+ @runtime_checkable
40
+ class ColumnedTable(TableLike, Protocol):
41
+ """A table that names its own columns, which a generated table does."""
42
+
43
+ COLUMNS: ClassVar[tuple[object, ...]]
44
+
45
+
46
+ class SourceBase:
47
+ __slots__ = ("_node", "_qualifier")
48
+ _node: SourceNode
49
+ _qualifier: str
50
+
51
+ def __init__(self, node: SourceNode, qualifier: str) -> None:
52
+ object.__setattr__(self, "_node", node)
53
+ object.__setattr__(self, "_qualifier", qualifier)
54
+
55
+ def __setattr__(self, name: str, value: object) -> None:
56
+ message = "query sources are immutable"
57
+ raise AttributeError(message)
58
+
59
+ @property
60
+ def node(self) -> SourceNode:
61
+ return self._node
62
+
63
+
64
+ class Source(SourceBase, Generic[SourceRow]):
65
+ __slots__ = ()
66
+
67
+ def field(
68
+ self,
69
+ value_type: type[FieldValue],
70
+ database_name: str,
71
+ /,
72
+ ) -> Field[FieldValue]:
73
+ schema_name, catalog_name = _source_qualification(self.node)
74
+ return field(
75
+ value_type,
76
+ database_name,
77
+ table_name=self._qualifier,
78
+ schema_name=schema_name,
79
+ catalog_name=catalog_name,
80
+ )
81
+
82
+
83
+ class Subquery(Source[SourceRow], Generic[SourceRow]):
84
+ __slots__ = ()
85
+
86
+
87
+ class CommonTable(Source[SourceRow], Generic[SourceRow]):
88
+ __slots__ = ("_definition",)
89
+ _definition: CommonTableNode
90
+
91
+ def __init__(self, definition: CommonTableNode) -> None:
92
+ super().__init__(CommonTableSourceNode(definition.name), definition.name)
93
+ object.__setattr__(self, "_definition", definition)
94
+
95
+ @property
96
+ def definition(self) -> CommonTableNode:
97
+ return self._definition
98
+
99
+
100
+ def table_source(table: TableLike, /, *, alias: str | None = None) -> Source[object]:
101
+ _require_table_instance(table)
102
+ require_identifier(table.database_name, "table")
103
+ _require_optional_identifier(table.schema_name, "schema")
104
+ _require_optional_identifier(table.catalog_name, "catalog")
105
+ _require_optional_identifier(alias, "table alias")
106
+ qualifier = alias or table.database_name
107
+ return Source(
108
+ TableSourceNode(
109
+ table.database_name,
110
+ table.schema_name,
111
+ table.catalog_name,
112
+ alias,
113
+ ),
114
+ qualifier,
115
+ )
116
+
117
+
118
+ def json_elements(
119
+ document: NodeProvider,
120
+ /,
121
+ *,
122
+ alias: str,
123
+ ) -> Source[object]:
124
+ """The elements of a JSON array, read one row at a time.
125
+
126
+ The rows have one column, named the same on every dialect, so a document
127
+ is joined to and filtered like any other source.
128
+ """
129
+ require_identifier(alias, "json elements alias")
130
+ return Source(JsonElementsSourceNode(document.node, alias), alias)
131
+
132
+
133
+ def write_target(table: TableLike, /) -> WriteTargetNode:
134
+ _require_table_instance(table)
135
+ require_identifier(table.database_name, "table")
136
+ _require_optional_identifier(table.schema_name, "schema")
137
+ _require_optional_identifier(table.catalog_name, "catalog")
138
+ return WriteTargetNode(
139
+ table.database_name,
140
+ table.schema_name,
141
+ table.catalog_name,
142
+ _declared_columns(table),
143
+ )
144
+
145
+
146
+ def _declared_columns(table: TableLike) -> tuple[str, ...]:
147
+ """What a generated table names, and nothing for one written by hand."""
148
+ declared: object = getattr(table, "COLUMNS", ())
149
+ if not isinstance(declared, tuple):
150
+ return ()
151
+ columns = cast("tuple[object, ...]", declared)
152
+ names: list[str] = []
153
+ for column in columns:
154
+ node = getattr(column, "node", None)
155
+ if isinstance(node, FieldNode):
156
+ names.append(node.name)
157
+ return tuple(names)
158
+
159
+
160
+ def subquery_source(
161
+ query: QueryNode,
162
+ alias: str,
163
+ /,
164
+ *,
165
+ lateral: bool = False,
166
+ ) -> Subquery[object]:
167
+ require_identifier(alias, "subquery alias")
168
+ return Subquery(SubquerySourceNode(query, alias, lateral=lateral), alias)
169
+
170
+
171
+ def common_table(
172
+ name: str,
173
+ query: QueryNode,
174
+ /,
175
+ *,
176
+ columns: tuple[str, ...] = (),
177
+ recursive: bool = False,
178
+ ) -> CommonTable[object]:
179
+ require_identifier(name, "common table")
180
+ for column in columns:
181
+ require_identifier(column, "common table column")
182
+ return CommonTable(CommonTableNode(name, query, columns, recursive))
183
+
184
+
185
+ def resolve_source(value: object) -> SourceNode:
186
+ if isinstance(value, SourceBase):
187
+ return value.node
188
+ if isinstance(value, TableLike):
189
+ return table_source(value).node
190
+ message = "query source must be a table, subquery, or common table"
191
+ raise TypeError(message)
192
+
193
+
194
+ def _require_table_instance(table: TableLike) -> None:
195
+ if isinstance(table, type):
196
+ message = (
197
+ "query source must be a table descriptor instance; a generated table "
198
+ "class is the typed column and builder namespace, and the generated "
199
+ "table constant beside it is the query source"
200
+ )
201
+ raise QueryValidationError(message)
202
+
203
+
204
+ def _source_qualification(node: SourceNode) -> tuple[str | None, str | None]:
205
+ if isinstance(node, TableSourceNode) and node.alias is None:
206
+ return node.schema, node.catalog
207
+ return None, None
208
+
209
+
210
+ def require_identifier(value: str, label: str) -> None:
211
+ """A name written into the SQL, held to being one a database can hold."""
212
+ if not value or "\x00" in value:
213
+ message = f"{label} identifier cannot be empty or contain a null character"
214
+ raise ValueError(message)
215
+
216
+
217
+ def _require_optional_identifier(value: str | None, label: str) -> None:
218
+ if value is not None:
219
+ require_identifier(value, label)
220
+
221
+
222
+ __all__ = (
223
+ "ELEMENT_COLUMN",
224
+ "ColumnedTable",
225
+ "CommonTable",
226
+ "Source",
227
+ "SourceBase",
228
+ "Subquery",
229
+ "TableLike",
230
+ "common_table",
231
+ "json_elements",
232
+ "resolve_source",
233
+ "subquery_source",
234
+ "table_source",
235
+ "write_target",
236
+ )
@@ -0,0 +1,43 @@
1
+ """SQLite query compilation."""
2
+
3
+ from pyoq.query.sqlite.compiler import QueryProvider, SQLiteCompiler
4
+ from pyoq.query.sqlite.executor import SQLiteExecutor
5
+ from pyoq.query.sqlite.factory import SQLiteConnectionFactory
6
+ from pyoq.query.sqlite.model import CompiledQuery, SQLiteCapabilities
7
+ from pyoq.query.sqlite.parameters import (
8
+ SQLiteParameter,
9
+ adapt_parameter,
10
+ adapt_parameters,
11
+ )
12
+ from pyoq.query.sqlite.planning import SQLiteBulkPlanner
13
+ from pyoq.query.sqlite.pool import (
14
+ SQLiteConnectionFactoryProtocol,
15
+ SQLiteConnectionLease,
16
+ SQLitePool,
17
+ SQLitePoolPolicy,
18
+ SQLitePoolStats,
19
+ )
20
+ from pyoq.query.sqlite.streaming import SQLiteRowStream, SQLiteStreamPolicy
21
+ from pyoq.query.sqlite.transactions import SQLiteTransaction, TransactionMode
22
+
23
+ __all__ = (
24
+ "CompiledQuery",
25
+ "QueryProvider",
26
+ "SQLiteBulkPlanner",
27
+ "SQLiteCapabilities",
28
+ "SQLiteCompiler",
29
+ "SQLiteConnectionFactory",
30
+ "SQLiteConnectionFactoryProtocol",
31
+ "SQLiteConnectionLease",
32
+ "SQLiteExecutor",
33
+ "SQLiteParameter",
34
+ "SQLitePool",
35
+ "SQLitePoolPolicy",
36
+ "SQLitePoolStats",
37
+ "SQLiteRowStream",
38
+ "SQLiteStreamPolicy",
39
+ "SQLiteTransaction",
40
+ "TransactionMode",
41
+ "adapt_parameter",
42
+ "adapt_parameters",
43
+ )