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/nodes.py ADDED
@@ -0,0 +1,750 @@
1
+ """Immutable query expression nodes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, Union, runtime_checkable
8
+
9
+ if TYPE_CHECKING:
10
+ from pyoq.query.select_nodes import OrderNode, QueryNode
11
+
12
+
13
+ _ROW_VALUE_WIDTH = 2
14
+
15
+
16
+ @runtime_checkable
17
+ class NodeProvider(Protocol):
18
+ """Anything that can stand in an expression tree.
19
+
20
+ It lives here because both the expression layer and the raw templates
21
+ above it need the same idea, and one of them cannot import the other.
22
+ """
23
+
24
+ @property
25
+ def node(self) -> ExpressionNode: ...
26
+
27
+
28
+ class ScalarFamily(StrEnum):
29
+ BOOLEAN = "boolean"
30
+ NUMERIC = "numeric"
31
+ STRING = "string"
32
+ BINARY = "binary"
33
+ TEMPORAL = "temporal"
34
+ JSON = "json"
35
+ OTHER = "other"
36
+ NULL = "null"
37
+
38
+
39
+ class UnaryOperator(StrEnum):
40
+ NOT = "not"
41
+ NEGATE = "negate"
42
+ IS_NULL = "is-null"
43
+ IS_NOT_NULL = "is-not-null"
44
+ IS_TRUE = "is-true"
45
+ IS_FALSE = "is-false"
46
+
47
+
48
+ class BinaryOperator(StrEnum):
49
+ EQUAL = "equal"
50
+ NOT_EQUAL = "not-equal"
51
+ LESS_THAN = "less-than"
52
+ LESS_OR_EQUAL = "less-or-equal"
53
+ GREATER_THAN = "greater-than"
54
+ GREATER_OR_EQUAL = "greater-or-equal"
55
+ IS_DISTINCT_FROM = "is-distinct-from"
56
+ IS_NOT_DISTINCT_FROM = "is-not-distinct-from"
57
+ ADD = "add"
58
+ SUBTRACT = "subtract"
59
+ MULTIPLY = "multiply"
60
+ DIVIDE = "divide"
61
+ MODULO = "modulo"
62
+ CONCAT = "concat"
63
+ LIKE = "like"
64
+ NOT_LIKE = "not-like"
65
+
66
+
67
+ class VariadicOperator(StrEnum):
68
+ AND = "and"
69
+ OR = "or"
70
+ IN = "in"
71
+ NOT_IN = "not-in"
72
+ BETWEEN = "between"
73
+ NOT_BETWEEN = "not-between"
74
+
75
+
76
+ class FunctionName(StrEnum):
77
+ LOWER = "lower"
78
+ UPPER = "upper"
79
+ LENGTH = "length"
80
+ CONTAINS = "contains"
81
+ STARTS_WITH = "starts-with"
82
+ ENDS_WITH = "ends-with"
83
+ COALESCE = "coalesce"
84
+ NULLIF = "nullif"
85
+ GREATEST = "greatest"
86
+ LEAST = "least"
87
+
88
+
89
+ class AggregateName(StrEnum):
90
+ COUNT = "count"
91
+ SUM = "sum"
92
+ AVERAGE = "average"
93
+ MINIMUM = "minimum"
94
+ MAXIMUM = "maximum"
95
+ JSON_ARRAY_AGG = "json-array-agg"
96
+ JSON_OBJECT_AGG = "json-object-agg"
97
+
98
+
99
+ class DatePart(StrEnum):
100
+ YEAR = "year"
101
+ MONTH = "month"
102
+ DAY = "day"
103
+ HOUR = "hour"
104
+ MINUTE = "minute"
105
+ SECOND = "second"
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class BoundValueNode:
110
+ value: object
111
+ family: ScalarFamily
112
+ sensitive: bool = False
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class FieldNode:
117
+ name: str
118
+ table: str | None = None
119
+ schema: str | None = None
120
+ catalog: str | None = None
121
+ family: ScalarFamily = ScalarFamily.OTHER
122
+ # What the column was declared to hold, which a family cannot say: an
123
+ # integer, a float, and a decimal are all one family and three types.
124
+ value_type: type[object] | None = None
125
+ # Whether the column may hold no value, which decides whether anything
126
+ # read through it may answer with none.
127
+ nullable: bool = True
128
+
129
+ def __post_init__(self) -> None:
130
+ _require_identifier(self.name, "field")
131
+ _require_optional_identifier(self.table, "table")
132
+ _require_optional_identifier(self.schema, "schema")
133
+ _require_optional_identifier(self.catalog, "catalog")
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class UnaryNode:
138
+ operator: UnaryOperator
139
+ operand: ExpressionNode
140
+
141
+
142
+ @dataclass(frozen=True, slots=True)
143
+ class BinaryNode:
144
+ operator: BinaryOperator
145
+ left: ExpressionNode
146
+ right: ExpressionNode
147
+
148
+
149
+ @dataclass(frozen=True, slots=True)
150
+ class VariadicNode:
151
+ operator: VariadicOperator
152
+ operands: tuple[ExpressionNode, ...]
153
+
154
+ def __post_init__(self) -> None:
155
+ if not self.operands:
156
+ message = "variadic expression requires at least one operand"
157
+ raise ValueError(message)
158
+
159
+
160
+ @dataclass(frozen=True, slots=True)
161
+ class FunctionNode:
162
+ name: FunctionName
163
+ arguments: tuple[ExpressionNode, ...]
164
+
165
+ def __post_init__(self) -> None:
166
+ if not self.arguments:
167
+ message = "function expression requires at least one argument"
168
+ raise ValueError(message)
169
+
170
+
171
+ @dataclass(frozen=True, slots=True)
172
+ class ExtractNode:
173
+ part: DatePart
174
+ expression: ExpressionNode
175
+
176
+
177
+ _AGGREGATE_ARITY = {AggregateName.JSON_OBJECT_AGG: 2}
178
+ """Aggregates that read more than the one value the rest of them read."""
179
+
180
+
181
+ @dataclass(frozen=True, slots=True)
182
+ class AggregateNode:
183
+ name: AggregateName
184
+ arguments: tuple[ExpressionNode, ...]
185
+ distinct: bool = False
186
+
187
+ def __post_init__(self) -> None:
188
+ if self.name is AggregateName.COUNT:
189
+ if len(self.arguments) > 1:
190
+ message = "count aggregate accepts at most one argument"
191
+ raise ValueError(message)
192
+ elif len(self.arguments) != _AGGREGATE_ARITY.get(self.name, 1):
193
+ message = "aggregate requires exactly one argument"
194
+ raise ValueError(message)
195
+ if self.distinct and not self.arguments:
196
+ message = "distinct aggregate requires an argument"
197
+ raise ValueError(message)
198
+
199
+
200
+ RawPart: TypeAlias = Union[str, "ExpressionNode"]
201
+
202
+
203
+ @dataclass(frozen=True, slots=True)
204
+ class RawNode:
205
+ parts: tuple[RawPart, ...]
206
+ family: ScalarFamily = ScalarFamily.OTHER
207
+
208
+ def __post_init__(self) -> None:
209
+ if not self.parts or not any(
210
+ isinstance(part, str) and part for part in self.parts
211
+ ):
212
+ message = "raw expression requires structural SQL text"
213
+ raise ValueError(message)
214
+
215
+
216
+ @dataclass(frozen=True, slots=True)
217
+ class CollectionEntry:
218
+ """One named value each row of a collection carries."""
219
+
220
+ key: str
221
+ expression: ExpressionNode
222
+
223
+ def __post_init__(self) -> None:
224
+ if not self.key:
225
+ message = "a collection entry needs a key"
226
+ raise ValueError(message)
227
+
228
+
229
+ @dataclass(frozen=True, slots=True)
230
+ class CollectionOrderNode:
231
+ expression: ExpressionNode
232
+ descending: bool = False
233
+
234
+
235
+ def _require_bounded_collection(
236
+ limit: int | None,
237
+ orders: tuple[CollectionOrderNode, ...],
238
+ ) -> None:
239
+ """A limit takes the first rows, so something has to say which are first."""
240
+ if limit is None:
241
+ return
242
+ if limit < 1:
243
+ message = "a collection carries at least one row or is not limited"
244
+ raise ValueError(message)
245
+ if not orders:
246
+ message = "a limited collection is ordered, so the rows it keeps are known"
247
+ raise ValueError(message)
248
+
249
+
250
+ @dataclass(frozen=True, slots=True)
251
+ class CollectionJoinNode:
252
+ """A table a collection reads alongside the one the relation names.
253
+
254
+ A collection is still the rows of one relation. A join widens what each of
255
+ those rows carries and does not change which rows there are, so it is a
256
+ left join: an inner one would drop a row whose join matched nothing, which
257
+ is a row the collection was asked for going missing. A row that matched
258
+ nothing carries null for what the join would have added.
259
+ """
260
+
261
+ table: str
262
+ condition: ExpressionNode
263
+ schema: str | None = None
264
+ catalog: str | None = None
265
+ alias: str | None = None
266
+
267
+ def __post_init__(self) -> None:
268
+ if not self.table:
269
+ message = "a collection join needs a table to read"
270
+ raise ValueError(message)
271
+
272
+
273
+ @dataclass(frozen=True, slots=True)
274
+ class CollectionNode:
275
+ """A related table's rows, gathered into one value beside their parent.
276
+
277
+ The condition correlates the collection with the row it belongs to, so it
278
+ reads columns of both tables. The relation names one table, and a join
279
+ widens what each of its rows carries without changing which rows there
280
+ are, so a collection stays the rows of one relation.
281
+
282
+ ``limit`` bounds how many rows the collection carries. The rows are chosen
283
+ by the order, so a limit without one would take an arbitrary few, and it
284
+ is refused here rather than left to the dialect to decide. That is also
285
+ why a dialect that cannot order inside a collection cannot carry a limited
286
+ one: the order refusal already covers both.
287
+ """
288
+
289
+ table: str
290
+ entries: tuple[CollectionEntry, ...]
291
+ condition: ExpressionNode
292
+ orders: tuple[CollectionOrderNode, ...] = ()
293
+ schema: str | None = None
294
+ catalog: str | None = None
295
+ alias: str | None = None
296
+ joins: tuple[CollectionJoinNode, ...] = ()
297
+ limit: int | None = None
298
+
299
+ def __post_init__(self) -> None:
300
+ if not self.table:
301
+ message = "a collection needs a table to read"
302
+ raise ValueError(message)
303
+ if not self.entries:
304
+ message = "a collection needs at least one value per row"
305
+ raise ValueError(message)
306
+ _require_bounded_collection(self.limit, self.orders)
307
+ keys = tuple(entry.key for entry in self.entries)
308
+ if len(set(keys)) != len(keys):
309
+ message = "a collection cannot carry one key twice"
310
+ raise ValueError(message)
311
+
312
+
313
+ @dataclass(frozen=True, slots=True)
314
+ class VendorFunctionNode:
315
+ """A function this database has that PyOQ does not name itself.
316
+
317
+ The name is written into the SQL rather than bound, so it is checked to
318
+ be an identifier and nothing else. The arguments are expressions and
319
+ travel the way every other expression does.
320
+
321
+ ``dialects`` is the databases the function exists on. Empty means the
322
+ caller did not say, and it is written wherever it is asked for.
323
+ """
324
+
325
+ name: str
326
+ arguments: tuple[ExpressionNode, ...]
327
+ family: ScalarFamily
328
+ dialects: frozenset[str] = frozenset()
329
+ # Where the routine is kept, held apart so each part is quoted as itself
330
+ # rather than flattened into a name a dot could be part of.
331
+ schema: str | None = None
332
+ catalog: str | None = None
333
+ # What the caller declared it answers with, which a family cannot say and
334
+ # which is what reads the value back as the type it was promised to be.
335
+ value_type: type[object] | None = None
336
+
337
+
338
+ class ArrayOperation(StrEnum):
339
+ """What is being asked of an array and one other thing."""
340
+
341
+ HAS = "has"
342
+ LACKS = "lacks"
343
+ CONTAINS = "contains"
344
+ CONTAINED_BY = "contained-by"
345
+ OVERLAPS = "overlaps"
346
+ APPEND = "append"
347
+ PREPEND = "prepend"
348
+ CONCAT = "concat"
349
+ REMOVE = "remove"
350
+ REPLACE = "replace"
351
+
352
+
353
+ _ARRAY_ARITY = {ArrayOperation.REPLACE: 2}
354
+ """Operations that read more than the one value the rest of them read."""
355
+
356
+
357
+ @dataclass(frozen=True, slots=True)
358
+ class ArrayNode:
359
+ """An array asked about against something else."""
360
+
361
+ operation: ArrayOperation
362
+ expression: ExpressionNode
363
+ operands: tuple[ExpressionNode, ...]
364
+
365
+ def __post_init__(self) -> None:
366
+ if len(self.operands) != _ARRAY_ARITY.get(self.operation, 1):
367
+ message = "array operation was given the wrong number of values"
368
+ raise ValueError(message)
369
+
370
+
371
+ @dataclass(frozen=True, slots=True)
372
+ class ArrayElementNode:
373
+ """One element of an array, counted the way the value it reads back is.
374
+
375
+ A column of an array is read back as a tuple, and a tuple counts from
376
+ zero, so this does too. SQL counts arrays from one, which is a difference
377
+ written out here rather than left for a caller to remember.
378
+ """
379
+
380
+ expression: ExpressionNode
381
+ indexes: tuple[ExpressionNode, ...]
382
+
383
+ def __post_init__(self) -> None:
384
+ if not self.indexes:
385
+ message = "an element of an array is read at a position"
386
+ raise ValueError(message)
387
+
388
+
389
+ @dataclass(frozen=True, slots=True)
390
+ class ArrayLengthNode:
391
+ """How many elements an array holds, in one dimension or in all of them."""
392
+
393
+ expression: ExpressionNode
394
+ dimension: int | None = None
395
+
396
+
397
+ @dataclass(frozen=True, slots=True)
398
+ class ArrayConstructNode:
399
+ """An array built out of the values around it."""
400
+
401
+ elements: tuple[ExpressionNode, ...] = ()
402
+ # What one element holds, where every element holds the same thing.
403
+ element_type: type[object] | None = None
404
+
405
+
406
+ @dataclass(frozen=True, slots=True)
407
+ class ArrayDimensionsNode:
408
+ """How many dimensions an array has, which an empty array does not say."""
409
+
410
+ expression: ExpressionNode
411
+
412
+
413
+ class JsonOperation(StrEnum):
414
+ """What is being asked of a JSON value at a path."""
415
+
416
+ VALUE = "value"
417
+ TEXT = "text"
418
+ LENGTH = "length"
419
+ EXISTS = "exists"
420
+ REMOVE = "remove"
421
+
422
+
423
+ @dataclass(frozen=True, slots=True)
424
+ class JsonNode:
425
+ """Reading inside a JSON value, by a path each dialect spells its own way.
426
+
427
+ The path is steps rather than text, because the three dialects do not
428
+ agree on how a path is written and one of them reads another's spelling
429
+ as a key that is simply absent. Steps are a string for a member and an
430
+ integer for an element.
431
+ """
432
+
433
+ operation: JsonOperation
434
+ expression: ExpressionNode
435
+ path: tuple[str | int, ...] = ()
436
+
437
+
438
+ class JsonBuild(StrEnum):
439
+ """Whether a JSON value is being built as an object or as an array."""
440
+
441
+ OBJECT = "object"
442
+ ARRAY = "array"
443
+
444
+
445
+ @dataclass(frozen=True, slots=True)
446
+ class JsonBuildNode:
447
+ """A JSON value built out of the values around it.
448
+
449
+ An object reads its arguments as alternating members and values, so it
450
+ takes an even number of them and every other one names a member.
451
+ """
452
+
453
+ kind: JsonBuild
454
+ arguments: tuple[ExpressionNode, ...] = ()
455
+
456
+ def __post_init__(self) -> None:
457
+ if self.kind is JsonBuild.OBJECT and len(self.arguments) % 2:
458
+ message = "a JSON object is built from members and their values"
459
+ raise ValueError(message)
460
+
461
+
462
+ @dataclass(frozen=True, slots=True)
463
+ class JsonKeysNode:
464
+ """The members an object has, as a JSON array of their names.
465
+
466
+ The dialects do not agree on the order the names come back in, because
467
+ each one stores an object the way it stores an object.
468
+ """
469
+
470
+ expression: ExpressionNode
471
+
472
+
473
+ class JsonWrite(StrEnum):
474
+ """How a value is put into a JSON document."""
475
+
476
+ SET = "set"
477
+ INSERT = "insert"
478
+ REPLACE = "replace"
479
+ MERGE = "merge"
480
+ """Merge-patch: a null removes a member, and objects merge all the way down."""
481
+
482
+ CONCAT = "concat"
483
+ """One level only: a member is replaced whole, and a null is kept as one."""
484
+
485
+
486
+ @dataclass(frozen=True, slots=True)
487
+ class JsonWriteNode:
488
+ """A JSON document with something written into it.
489
+
490
+ The answer is a new document; nothing is changed in place, because an
491
+ expression cannot change anything. A merge has no path, because it is
492
+ written over the whole document.
493
+ """
494
+
495
+ operation: JsonWrite
496
+ expression: ExpressionNode
497
+ value: ExpressionNode
498
+ path: tuple[str | int, ...] = ()
499
+
500
+ def __post_init__(self) -> None:
501
+ if self.operation is JsonWrite.MERGE and self.path:
502
+ message = "a merge is written over the whole document, not at a path"
503
+ raise ValueError(message)
504
+
505
+
506
+ @dataclass(frozen=True, slots=True)
507
+ class JsonContainsNode:
508
+ """Whether one JSON value holds another, which is not a question of path."""
509
+
510
+ expression: ExpressionNode
511
+ value: ExpressionNode
512
+
513
+
514
+ @dataclass(frozen=True, slots=True)
515
+ class RowValueNode:
516
+ """Several values compared as one, the way a composite key is.
517
+
518
+ A row of one value is that value, and SQL reads it as one, so a row
519
+ value is only a row value from two columns up.
520
+ """
521
+
522
+ expressions: tuple[ExpressionNode, ...]
523
+
524
+ def __post_init__(self) -> None:
525
+ if len(self.expressions) < _ROW_VALUE_WIDTH:
526
+ message = "a row value needs at least two expressions"
527
+ raise ValueError(message)
528
+
529
+
530
+ class FrameKind(StrEnum):
531
+ """What a frame counts: rows, values, or peer groups."""
532
+
533
+ ROWS = "rows"
534
+ RANGE = "range"
535
+ GROUPS = "groups"
536
+
537
+
538
+ class FrameBound(StrEnum):
539
+ """Where a frame starts or ends, relative to the row being measured."""
540
+
541
+ UNBOUNDED_PRECEDING = "unbounded-preceding"
542
+ PRECEDING = "preceding"
543
+ CURRENT_ROW = "current-row"
544
+ FOLLOWING = "following"
545
+ UNBOUNDED_FOLLOWING = "unbounded-following"
546
+
547
+
548
+ class FrameExclusion(StrEnum):
549
+ """Which rows a frame leaves out even though it reaches them."""
550
+
551
+ CURRENT_ROW = "current-row"
552
+ GROUP = "group"
553
+ TIES = "ties"
554
+ NO_OTHERS = "no-others"
555
+
556
+
557
+ @dataclass(frozen=True, slots=True)
558
+ class FrameBoundNode:
559
+ """One edge of a frame, and how far from the row it sits."""
560
+
561
+ bound: FrameBound
562
+ offset: ExpressionNode | None = None
563
+
564
+ def __post_init__(self) -> None:
565
+ counted = self.bound in {FrameBound.PRECEDING, FrameBound.FOLLOWING}
566
+ if counted != (self.offset is not None):
567
+ message = "a counted frame edge needs a distance and no other does"
568
+ raise ValueError(message)
569
+
570
+
571
+ @dataclass(frozen=True, slots=True)
572
+ class FrameNode:
573
+ """The rows a window function reads, narrower than the whole partition."""
574
+
575
+ kind: FrameKind
576
+ start: FrameBoundNode
577
+ end: FrameBoundNode | None = None
578
+ exclusion: FrameExclusion | None = None
579
+
580
+
581
+ class WindowName(StrEnum):
582
+ ROW_NUMBER = "row-number"
583
+ RANK = "rank"
584
+ DENSE_RANK = "dense-rank"
585
+ PERCENT_RANK = "percent-rank"
586
+ CUME_DIST = "cume-dist"
587
+ NTILE = "ntile"
588
+ LAG = "lag"
589
+ LEAD = "lead"
590
+ FIRST_VALUE = "first-value"
591
+ LAST_VALUE = "last-value"
592
+ NTH_VALUE = "nth-value"
593
+
594
+
595
+ @dataclass(frozen=True, slots=True)
596
+ class WindowFunctionNode:
597
+ """A function that only means anything with a window behind it."""
598
+
599
+ name: WindowName
600
+ arguments: tuple[ExpressionNode, ...] = ()
601
+
602
+
603
+ @dataclass(frozen=True, slots=True)
604
+ class WindowSpecificationNode:
605
+ """What a row is measured against: its partition, in its order.
606
+
607
+ The rows a window covers are named the way a query names them, so the
608
+ order terms here are the same ones an ORDER BY is built from.
609
+ """
610
+
611
+ partition_by: tuple[ExpressionNode, ...] = ()
612
+ order_by: tuple[OrderNode, ...] = ()
613
+ frame: FrameNode | None = None
614
+
615
+
616
+ @dataclass(frozen=True, slots=True)
617
+ class WindowReferenceNode:
618
+ """A window the query declared once under a name."""
619
+
620
+ name: str
621
+
622
+
623
+ @dataclass(frozen=True, slots=True)
624
+ class NamedWindowNode:
625
+ """A window written once in a query and looked through by several rows."""
626
+
627
+ name: str
628
+ specification: WindowSpecificationNode
629
+
630
+
631
+ EVERY_ROW = WindowSpecificationNode()
632
+ """The window a function reads before anything narrows it."""
633
+
634
+
635
+ @dataclass(frozen=True, slots=True)
636
+ class WindowNode:
637
+ """A function and the window it looks through, spelled out or named."""
638
+
639
+ expression: ExpressionNode
640
+ window: WindowSpecificationNode | WindowReferenceNode = EVERY_ROW
641
+
642
+
643
+ @dataclass(frozen=True, slots=True)
644
+ class CastNode:
645
+ """A value asked for as another type, by the database rather than Python.
646
+
647
+ The Python type is the one fact stored. What SQL names it is a dialect
648
+ question, and what reads it back is a decoding question, and both are
649
+ answered from here rather than repeated alongside it.
650
+ """
651
+
652
+ expression: ExpressionNode
653
+ value_type: type[object]
654
+
655
+
656
+ @dataclass(frozen=True, slots=True)
657
+ class CaseNode:
658
+ """One value chosen from several, by the first condition that holds.
659
+
660
+ The branches are ordered, because SQL reads them in order and so does a
661
+ reader. A case with no branches chooses nothing and is not a value.
662
+ """
663
+
664
+ branches: tuple[tuple[ExpressionNode, ExpressionNode], ...]
665
+ otherwise: ExpressionNode | None = None
666
+
667
+ def __post_init__(self) -> None:
668
+ if not self.branches:
669
+ message = "case expression requires at least one branch"
670
+ raise ValueError(message)
671
+
672
+
673
+ @dataclass(frozen=True, slots=True)
674
+ class ExistsNode:
675
+ """Whether a query returns anything at all.
676
+
677
+ The query is held rather than a source, because what EXISTS asks about is
678
+ a whole statement. The import is type only, so an expression may contain a
679
+ query without the node layers depending on each other at runtime.
680
+ """
681
+
682
+ query: QueryNode
683
+ negated: bool = False
684
+
685
+
686
+ ExpressionNode = (
687
+ BoundValueNode
688
+ | FieldNode
689
+ | UnaryNode
690
+ | BinaryNode
691
+ | VariadicNode
692
+ | FunctionNode
693
+ | ExtractNode
694
+ | AggregateNode
695
+ | CaseNode
696
+ | CastNode
697
+ | VendorFunctionNode
698
+ | ArrayNode
699
+ | ArrayElementNode
700
+ | ArrayLengthNode
701
+ | ArrayConstructNode
702
+ | ArrayDimensionsNode
703
+ | JsonContainsNode
704
+ | JsonWriteNode
705
+ | JsonNode
706
+ | JsonBuildNode
707
+ | JsonKeysNode
708
+ | RowValueNode
709
+ | WindowFunctionNode
710
+ | WindowNode
711
+ | CollectionNode
712
+ | ExistsNode
713
+ | RawNode
714
+ )
715
+
716
+
717
+ def _require_identifier(value: str, label: str) -> None:
718
+ if not value or "\x00" in value:
719
+ message = f"{label} identifier cannot be empty or contain a null character"
720
+ raise ValueError(message)
721
+
722
+
723
+ def _require_optional_identifier(value: str | None, label: str) -> None:
724
+ if value is not None:
725
+ _require_identifier(value, label)
726
+
727
+
728
+ __all__ = (
729
+ "AggregateName",
730
+ "AggregateNode",
731
+ "BinaryNode",
732
+ "BinaryOperator",
733
+ "BoundValueNode",
734
+ "CollectionEntry",
735
+ "CollectionNode",
736
+ "CollectionOrderNode",
737
+ "DatePart",
738
+ "ExpressionNode",
739
+ "ExtractNode",
740
+ "FieldNode",
741
+ "FunctionName",
742
+ "FunctionNode",
743
+ "RawNode",
744
+ "RawPart",
745
+ "ScalarFamily",
746
+ "UnaryNode",
747
+ "UnaryOperator",
748
+ "VariadicNode",
749
+ "VariadicOperator",
750
+ )