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/writes.py ADDED
@@ -0,0 +1,628 @@
1
+ """Typed immutable write statement construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass, replace
7
+ from typing import (
8
+ Generic,
9
+ Protocol,
10
+ Self,
11
+ TypeVar,
12
+ TypeVarTuple,
13
+ overload,
14
+ runtime_checkable,
15
+ )
16
+
17
+ from pyoq.errors import QueryStateError, QueryValidationError
18
+ from pyoq.query.expressions import Expression, require_boolean
19
+ from pyoq.query.nodes import ExpressionNode, FieldNode, ScalarFamily
20
+ from pyoq.query.projections import Projection, projection_node
21
+ from pyoq.query.select_nodes import ProjectionNode
22
+ from pyoq.query.sources import TableLike, write_target
23
+ from pyoq.query.values import bind
24
+ from pyoq.query.write_nodes import (
25
+ AssignmentNode,
26
+ ConflictAction,
27
+ ConflictNode,
28
+ DeleteNode,
29
+ InsertNode,
30
+ UpdateNode,
31
+ WriteNode,
32
+ )
33
+ from pyoq.unset import UnsetType
34
+
35
+ Values = TypeVarTuple("Values")
36
+ NodeType = TypeVar("NodeType", bound=WriteNode, covariant=True)
37
+ ScopedNode = TypeVar("ScopedNode", bound="UpdateNode | DeleteNode")
38
+ AssignedValue = TypeVar("AssignedValue")
39
+ _EXCLUDED_ROW = "excluded"
40
+ ResultRow = TypeVar("ResultRow")
41
+ Value1 = TypeVar("Value1")
42
+ Value2 = TypeVar("Value2")
43
+ Value3 = TypeVar("Value3")
44
+ Value4 = TypeVar("Value4")
45
+ Value5 = TypeVar("Value5")
46
+ Value6 = TypeVar("Value6")
47
+ Value7 = TypeVar("Value7")
48
+ Value8 = TypeVar("Value8")
49
+
50
+
51
+ @runtime_checkable
52
+ class ColumnReference(Protocol):
53
+ @property
54
+ def node(self) -> ExpressionNode: ...
55
+
56
+ @property
57
+ def family(self) -> ScalarFamily: ...
58
+
59
+
60
+ @runtime_checkable
61
+ class WriteColumn(Protocol):
62
+ @property
63
+ def writable(self) -> bool: ...
64
+
65
+ @property
66
+ def generated(self) -> bool: ...
67
+
68
+ @property
69
+ def field_name(self) -> str | None: ...
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class ColumnBinding:
74
+ database_name: str
75
+ field_name: str | None = None
76
+
77
+
78
+ class WriteStatement(Generic[NodeType]):
79
+ __slots__ = ("_node",)
80
+ _node: NodeType
81
+
82
+ def __init__(self, node: NodeType) -> None:
83
+ object.__setattr__(self, "_node", node)
84
+
85
+ def __setattr__(self, name: str, value: object) -> None:
86
+ message = "write statements are immutable"
87
+ raise AttributeError(message)
88
+
89
+ @property
90
+ def node(self) -> NodeType:
91
+ return self._node
92
+
93
+
94
+ class ReturningStatement(WriteStatement[WriteNode], Generic[ResultRow]):
95
+ __slots__ = ()
96
+
97
+
98
+ class MutationStatement(WriteStatement[NodeType], Generic[NodeType]):
99
+ __slots__ = ()
100
+
101
+ def returning_all(self) -> ReturningStatement[tuple[object, ...]]:
102
+ """Every column the table declares, without listing them.
103
+
104
+ Generation writes the column list onto the table, so the statement
105
+ already knows what they are. A table written by hand declares none,
106
+ and is asked to name them.
107
+ """
108
+ target = self.node.target
109
+ if not target.columns:
110
+ message = (
111
+ "table names no columns to return; generated tables declare "
112
+ "them, and any other table has to name them"
113
+ )
114
+ raise QueryValidationError(message)
115
+ projections = tuple(
116
+ ProjectionNode(FieldNode(name, target.name, target.schema, target.catalog))
117
+ for name in target.columns
118
+ )
119
+ return ReturningStatement(replace(self.node, returning=projections))
120
+
121
+ @overload
122
+ def returning(
123
+ self,
124
+ value1: Expression[Value1] | Projection[Value1],
125
+ /,
126
+ ) -> ReturningStatement[tuple[Value1]]: ...
127
+
128
+ @overload
129
+ def returning(
130
+ self,
131
+ value1: Expression[Value1] | Projection[Value1],
132
+ value2: Expression[Value2] | Projection[Value2],
133
+ /,
134
+ ) -> ReturningStatement[tuple[Value1, Value2]]: ...
135
+
136
+ @overload
137
+ def returning(
138
+ self,
139
+ value1: Expression[Value1] | Projection[Value1],
140
+ value2: Expression[Value2] | Projection[Value2],
141
+ value3: Expression[Value3] | Projection[Value3],
142
+ /,
143
+ ) -> ReturningStatement[tuple[Value1, Value2, Value3]]: ...
144
+
145
+ @overload
146
+ def returning(
147
+ self,
148
+ value1: Expression[Value1] | Projection[Value1],
149
+ value2: Expression[Value2] | Projection[Value2],
150
+ value3: Expression[Value3] | Projection[Value3],
151
+ value4: Expression[Value4] | Projection[Value4],
152
+ /,
153
+ ) -> ReturningStatement[tuple[Value1, Value2, Value3, Value4]]: ...
154
+
155
+ @overload
156
+ def returning(
157
+ self,
158
+ value1: Expression[Value1] | Projection[Value1],
159
+ value2: Expression[Value2] | Projection[Value2],
160
+ value3: Expression[Value3] | Projection[Value3],
161
+ value4: Expression[Value4] | Projection[Value4],
162
+ value5: Expression[Value5] | Projection[Value5],
163
+ /,
164
+ ) -> ReturningStatement[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
165
+
166
+ @overload
167
+ def returning(
168
+ self,
169
+ value1: Expression[Value1] | Projection[Value1],
170
+ value2: Expression[Value2] | Projection[Value2],
171
+ value3: Expression[Value3] | Projection[Value3],
172
+ value4: Expression[Value4] | Projection[Value4],
173
+ value5: Expression[Value5] | Projection[Value5],
174
+ value6: Expression[Value6] | Projection[Value6],
175
+ /,
176
+ ) -> ReturningStatement[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
177
+
178
+ @overload
179
+ def returning(
180
+ self,
181
+ value1: Expression[Value1] | Projection[Value1],
182
+ value2: Expression[Value2] | Projection[Value2],
183
+ value3: Expression[Value3] | Projection[Value3],
184
+ value4: Expression[Value4] | Projection[Value4],
185
+ value5: Expression[Value5] | Projection[Value5],
186
+ value6: Expression[Value6] | Projection[Value6],
187
+ value7: Expression[Value7] | Projection[Value7],
188
+ /,
189
+ ) -> ReturningStatement[
190
+ tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7]
191
+ ]: ...
192
+
193
+ @overload
194
+ def returning(
195
+ self,
196
+ value1: Expression[Value1] | Projection[Value1],
197
+ value2: Expression[Value2] | Projection[Value2],
198
+ value3: Expression[Value3] | Projection[Value3],
199
+ value4: Expression[Value4] | Projection[Value4],
200
+ value5: Expression[Value5] | Projection[Value5],
201
+ value6: Expression[Value6] | Projection[Value6],
202
+ value7: Expression[Value7] | Projection[Value7],
203
+ value8: Expression[Value8] | Projection[Value8],
204
+ /,
205
+ ) -> ReturningStatement[
206
+ tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8]
207
+ ]: ...
208
+
209
+ def returning( # type: ignore[misc]
210
+ self,
211
+ *values: object,
212
+ ) -> ReturningStatement[object]:
213
+ if not values:
214
+ message = "RETURNING requires at least one projection"
215
+ raise QueryValidationError(message)
216
+ projections = tuple(projection_node(value) for value in values)
217
+ return ReturningStatement(replace(self.node, returning=projections))
218
+
219
+
220
+ class ScopedStatement(MutationStatement[ScopedNode], Generic[ScopedNode]):
221
+ __slots__ = ()
222
+
223
+ def where(self, condition: Expression[bool], /) -> Self:
224
+ require_boolean(condition, "WHERE")
225
+ node = self.node
226
+ if node.condition is not None:
227
+ message = "WHERE condition is already defined"
228
+ raise QueryStateError(message)
229
+ if node.unconditional:
230
+ message = "a full-table statement cannot also define a WHERE condition"
231
+ raise QueryStateError(message)
232
+ return type(self)(replace(node, condition=condition.node))
233
+
234
+ def all_rows(self) -> Self:
235
+ node = self.node
236
+ if node.condition is not None:
237
+ message = "a conditional statement cannot also target every row"
238
+ raise QueryStateError(message)
239
+ return type(self)(replace(node, unconditional=True))
240
+
241
+
242
+ class InsertStatement(MutationStatement[InsertNode], Generic[*Values]):
243
+ __slots__ = ("_columns",)
244
+ _columns: tuple[ColumnBinding, ...]
245
+
246
+ def __init__(
247
+ self,
248
+ node: InsertNode,
249
+ columns: tuple[ColumnBinding, ...],
250
+ ) -> None:
251
+ super().__init__(node)
252
+ object.__setattr__(self, "_columns", columns)
253
+
254
+ @property
255
+ def columns(self) -> tuple[ColumnBinding, ...]:
256
+ return self._columns
257
+
258
+ def values(self, *values: *Values) -> InsertStatement[*Values]:
259
+ row = _value_row(values, len(self._columns))
260
+ return self._with_rows((row,))
261
+
262
+ def values_many(self, rows: Iterable[object], /) -> InsertStatement[*Values]:
263
+ additions = tuple(_generated_row(row, self._columns) for row in rows)
264
+ if not additions:
265
+ message = "values_many requires at least one insert value"
266
+ raise QueryValidationError(message)
267
+ return self._with_rows(additions)
268
+
269
+ def on_conflict_do_nothing(
270
+ self,
271
+ *targets: ColumnReference,
272
+ ) -> InsertStatement[*Values]:
273
+ conflict = ConflictNode(
274
+ ConflictAction.NOTHING,
275
+ self._conflict_targets(targets),
276
+ )
277
+ return InsertStatement[*Values](self._with_conflict(conflict), self._columns)
278
+
279
+ def on_conflict_do_update(
280
+ self,
281
+ *targets: ColumnReference,
282
+ ) -> ConflictUpdate[*Values]:
283
+ conflict = ConflictNode(
284
+ ConflictAction.UPDATE,
285
+ self._conflict_targets(targets),
286
+ )
287
+ return ConflictUpdate[*Values](self._with_conflict(conflict), self._columns)
288
+
289
+ def _with_conflict(self, conflict: ConflictNode) -> InsertNode:
290
+ node = self.node
291
+ if node.conflict is not None:
292
+ message = "conflict resolution is already defined"
293
+ raise QueryStateError(message)
294
+ return replace(node, conflict=conflict)
295
+
296
+ def _conflict_targets(
297
+ self,
298
+ targets: tuple[ColumnReference, ...],
299
+ ) -> tuple[str, ...]:
300
+ selected = frozenset(binding.database_name for binding in self._columns)
301
+ names: list[str] = []
302
+ for target in targets:
303
+ name = _write_column(target, "ON CONFLICT").database_name
304
+ if name not in selected:
305
+ message = f"conflict target is not an inserted column: {name}"
306
+ raise QueryValidationError(message)
307
+ names.append(name)
308
+ return tuple(names)
309
+
310
+ def _with_rows(
311
+ self,
312
+ rows: tuple[tuple[ExpressionNode, ...], ...],
313
+ ) -> InsertStatement[*Values]:
314
+ node = self.node
315
+ return InsertStatement[*Values](
316
+ replace(node, rows=(*node.rows, *rows)),
317
+ self._columns,
318
+ )
319
+
320
+
321
+ class ConflictUpdate(InsertStatement[*Values], Generic[*Values]):
322
+ __slots__ = ()
323
+
324
+ def set(
325
+ self,
326
+ column: Expression[AssignedValue],
327
+ value: AssignedValue | Expression[AssignedValue],
328
+ /,
329
+ ) -> ConflictUpdate[*Values]:
330
+ conflict = self._conflict
331
+ return self._with_resolution(
332
+ replace(
333
+ conflict,
334
+ assignments=(*conflict.assignments, _assignment(column, value)),
335
+ )
336
+ )
337
+
338
+ def where(self, condition: Expression[bool], /) -> ConflictUpdate[*Values]:
339
+ require_boolean(condition, "ON CONFLICT")
340
+ conflict = self._conflict
341
+ if conflict.condition is not None:
342
+ message = "conflict update condition is already defined"
343
+ raise QueryStateError(message)
344
+ return self._with_resolution(replace(conflict, condition=condition.node))
345
+
346
+ @property
347
+ def _conflict(self) -> ConflictNode:
348
+ conflict = self.node.conflict
349
+ if conflict is None:
350
+ message = "conflict update statement has no conflict resolution"
351
+ raise QueryStateError(message)
352
+ return conflict
353
+
354
+ def _with_resolution(self, conflict: ConflictNode) -> ConflictUpdate[*Values]:
355
+ return ConflictUpdate[*Values](
356
+ replace(self.node, conflict=conflict),
357
+ self._columns,
358
+ )
359
+
360
+
361
+ class UpdateStatement(ScopedStatement[UpdateNode]):
362
+ __slots__ = ()
363
+
364
+ def set(
365
+ self,
366
+ column: Expression[AssignedValue],
367
+ value: AssignedValue | Expression[AssignedValue],
368
+ /,
369
+ ) -> UpdateStatement:
370
+ return self._with_assignments((_assignment(column, value),))
371
+
372
+ def set_values(
373
+ self,
374
+ values: object,
375
+ /,
376
+ *columns: ColumnReference,
377
+ ) -> UpdateStatement:
378
+ if not columns:
379
+ message = "set_values requires at least one column"
380
+ raise QueryValidationError(message)
381
+ assignments = _generated_assignments(values, columns)
382
+ if not assignments:
383
+ message = "update value leaves every selected column unset"
384
+ raise QueryValidationError(message)
385
+ return self._with_assignments(assignments)
386
+
387
+ def _with_assignments(
388
+ self,
389
+ assignments: tuple[AssignmentNode, ...],
390
+ ) -> UpdateStatement:
391
+ node = self.node
392
+ return UpdateStatement(
393
+ replace(node, assignments=(*node.assignments, *assignments))
394
+ )
395
+
396
+
397
+ class DeleteStatement(ScopedStatement[DeleteNode]):
398
+ __slots__ = ()
399
+
400
+
401
+ def update(table: TableLike, /) -> UpdateStatement:
402
+ return UpdateStatement(UpdateNode(write_target(table)))
403
+
404
+
405
+ def delete_from(table: TableLike, /) -> DeleteStatement:
406
+ return DeleteStatement(DeleteNode(write_target(table)))
407
+
408
+
409
+ def _assignment(
410
+ column: object,
411
+ value: object,
412
+ ) -> AssignmentNode:
413
+ binding = _write_column(column, "UPDATE")
414
+ return AssignmentNode(binding.database_name, _value_node(value))
415
+
416
+
417
+ def _generated_assignments(
418
+ values: object,
419
+ columns: tuple[ColumnReference, ...],
420
+ ) -> tuple[AssignmentNode, ...]:
421
+ assignments: list[AssignmentNode] = []
422
+ for column in columns:
423
+ binding = _write_column(column, "UPDATE")
424
+ attribute = _generated_attribute(values, binding)
425
+ if isinstance(attribute, UnsetType):
426
+ continue
427
+ assignments.append(
428
+ AssignmentNode(binding.database_name, _value_node(attribute))
429
+ )
430
+ return tuple(assignments)
431
+
432
+
433
+ @overload
434
+ def insert_into(
435
+ table: TableLike,
436
+ column1: Expression[Value1],
437
+ /,
438
+ ) -> InsertStatement[Value1]: ...
439
+
440
+
441
+ @overload
442
+ def insert_into(
443
+ table: TableLike,
444
+ column1: Expression[Value1],
445
+ column2: Expression[Value2],
446
+ /,
447
+ ) -> InsertStatement[Value1, Value2]: ...
448
+
449
+
450
+ @overload
451
+ def insert_into(
452
+ table: TableLike,
453
+ column1: Expression[Value1],
454
+ column2: Expression[Value2],
455
+ column3: Expression[Value3],
456
+ /,
457
+ ) -> InsertStatement[Value1, Value2, Value3]: ...
458
+
459
+
460
+ @overload
461
+ def insert_into(
462
+ table: TableLike,
463
+ column1: Expression[Value1],
464
+ column2: Expression[Value2],
465
+ column3: Expression[Value3],
466
+ column4: Expression[Value4],
467
+ /,
468
+ ) -> InsertStatement[Value1, Value2, Value3, Value4]: ...
469
+
470
+
471
+ @overload
472
+ def insert_into(
473
+ table: TableLike,
474
+ column1: Expression[Value1],
475
+ column2: Expression[Value2],
476
+ column3: Expression[Value3],
477
+ column4: Expression[Value4],
478
+ column5: Expression[Value5],
479
+ /,
480
+ ) -> InsertStatement[Value1, Value2, Value3, Value4, Value5]: ...
481
+
482
+
483
+ @overload
484
+ def insert_into(
485
+ table: TableLike,
486
+ column1: Expression[Value1],
487
+ column2: Expression[Value2],
488
+ column3: Expression[Value3],
489
+ column4: Expression[Value4],
490
+ column5: Expression[Value5],
491
+ column6: Expression[Value6],
492
+ /,
493
+ ) -> InsertStatement[Value1, Value2, Value3, Value4, Value5, Value6]: ...
494
+
495
+
496
+ @overload
497
+ def insert_into(
498
+ table: TableLike,
499
+ column1: Expression[Value1],
500
+ column2: Expression[Value2],
501
+ column3: Expression[Value3],
502
+ column4: Expression[Value4],
503
+ column5: Expression[Value5],
504
+ column6: Expression[Value6],
505
+ column7: Expression[Value7],
506
+ /,
507
+ ) -> InsertStatement[Value1, Value2, Value3, Value4, Value5, Value6, Value7]: ...
508
+
509
+
510
+ @overload
511
+ def insert_into(
512
+ table: TableLike,
513
+ column1: Expression[Value1],
514
+ column2: Expression[Value2],
515
+ column3: Expression[Value3],
516
+ column4: Expression[Value4],
517
+ column5: Expression[Value5],
518
+ column6: Expression[Value6],
519
+ column7: Expression[Value7],
520
+ column8: Expression[Value8],
521
+ /,
522
+ ) -> InsertStatement[
523
+ Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8
524
+ ]: ...
525
+
526
+
527
+ def insert_into( # type: ignore[misc]
528
+ table: TableLike,
529
+ *columns: object,
530
+ ) -> InsertStatement[object]:
531
+ target = write_target(table)
532
+ if not columns:
533
+ message = "INSERT requires at least one column"
534
+ raise QueryValidationError(message)
535
+ bindings = tuple(_write_column(column, "INSERT") for column in columns)
536
+ names = tuple(binding.database_name for binding in bindings)
537
+ return InsertStatement(InsertNode(target, names), bindings)
538
+
539
+
540
+ def excluded(column: Expression[AssignedValue], /) -> Expression[AssignedValue]:
541
+ binding = _write_column(column, "ON CONFLICT")
542
+ return Expression(
543
+ FieldNode(binding.database_name, _EXCLUDED_ROW, family=column.family),
544
+ column.family,
545
+ )
546
+
547
+
548
+ def _write_column(value: object, clause: str) -> ColumnBinding:
549
+ if not isinstance(value, Expression):
550
+ message = f"{clause} columns must be typed column expressions"
551
+ raise QueryValidationError(message)
552
+ node = value.node
553
+ if not isinstance(node, FieldNode):
554
+ message = f"{clause} columns must reference table columns"
555
+ raise QueryValidationError(message)
556
+ if not isinstance(value, WriteColumn):
557
+ return ColumnBinding(node.name)
558
+ _require_writable_column(value, node.name)
559
+ return ColumnBinding(node.name, value.field_name)
560
+
561
+
562
+ def _require_writable_column(column: WriteColumn, name: str) -> None:
563
+ if column.generated:
564
+ message = f"generated column cannot be written: {name}"
565
+ raise QueryValidationError(message)
566
+ if not column.writable:
567
+ message = f"column is not writable: {name}"
568
+ raise QueryValidationError(message)
569
+
570
+
571
+ def _value_row(
572
+ values: tuple[object, ...],
573
+ expected: int,
574
+ ) -> tuple[ExpressionNode, ...]:
575
+ if len(values) != expected:
576
+ message = "INSERT values must match the selected column count"
577
+ raise QueryValidationError(message)
578
+ return tuple(_value_node(value) for value in values)
579
+
580
+
581
+ def _generated_row(
582
+ value: object,
583
+ columns: tuple[ColumnBinding, ...],
584
+ ) -> tuple[ExpressionNode, ...]:
585
+ return tuple(_generated_value(value, column) for column in columns)
586
+
587
+
588
+ def _generated_attribute(value: object, column: ColumnBinding) -> object:
589
+ if column.field_name is None:
590
+ message = "generated write values require generated column descriptors"
591
+ raise QueryValidationError(message)
592
+ if not hasattr(value, column.field_name):
593
+ message = f"write value does not define column: {column.database_name}"
594
+ raise QueryValidationError(message)
595
+ return getattr(value, column.field_name)
596
+
597
+
598
+ def _generated_value(value: object, column: ColumnBinding) -> ExpressionNode:
599
+ attribute = _generated_attribute(value, column)
600
+ if isinstance(attribute, UnsetType):
601
+ message = f"insert value leaves column unset: {column.database_name}"
602
+ raise QueryValidationError(message)
603
+ return _value_node(attribute)
604
+
605
+
606
+ def _value_node(value: object) -> ExpressionNode:
607
+ if isinstance(value, Expression):
608
+ return value.node
609
+ return bind(value).node
610
+
611
+
612
+ __all__ = (
613
+ "ColumnBinding",
614
+ "ColumnReference",
615
+ "ConflictUpdate",
616
+ "DeleteStatement",
617
+ "InsertStatement",
618
+ "MutationStatement",
619
+ "ReturningStatement",
620
+ "ScopedStatement",
621
+ "UpdateStatement",
622
+ "WriteColumn",
623
+ "WriteStatement",
624
+ "delete_from",
625
+ "excluded",
626
+ "insert_into",
627
+ "update",
628
+ )
@@ -0,0 +1,66 @@
1
+ """Typed relation facts, navigation, fetch plans, and loading state."""
2
+
3
+ from pyoq.relations.batching import (
4
+ DEFAULT_KEY_LIMIT,
5
+ KeyBatch,
6
+ RelationBatch,
7
+ RelationBatchLoader,
8
+ plan_key_batches,
9
+ )
10
+ from pyoq.relations.derivation import derive_relations, table_relations
11
+ from pyoq.relations.fetching import (
12
+ MAXIMUM_FETCH_DEPTH,
13
+ FetchJoin,
14
+ FetchOrder,
15
+ FetchPlan,
16
+ FetchRequest,
17
+ FetchStrategy,
18
+ by_column,
19
+ by_expression,
20
+ validate_fetch_plan,
21
+ )
22
+ from pyoq.relations.graph import RelationGraph, build_relation_graph
23
+ from pyoq.relations.loading import RelationState, RelationValue
24
+ from pyoq.relations.model import (
25
+ RelationCardinality,
26
+ RelationDirection,
27
+ RelationEndpoint,
28
+ TypedRelation,
29
+ )
30
+ from pyoq.relations.planning import (
31
+ FetchCapabilities,
32
+ ResolvedFetch,
33
+ ResolvedFetchPlan,
34
+ resolve_fetch_plan,
35
+ )
36
+
37
+ __all__ = (
38
+ "DEFAULT_KEY_LIMIT",
39
+ "MAXIMUM_FETCH_DEPTH",
40
+ "FetchCapabilities",
41
+ "FetchJoin",
42
+ "FetchOrder",
43
+ "FetchPlan",
44
+ "FetchRequest",
45
+ "FetchStrategy",
46
+ "KeyBatch",
47
+ "RelationBatch",
48
+ "RelationBatchLoader",
49
+ "RelationCardinality",
50
+ "RelationDirection",
51
+ "RelationEndpoint",
52
+ "RelationGraph",
53
+ "RelationState",
54
+ "RelationValue",
55
+ "ResolvedFetch",
56
+ "ResolvedFetchPlan",
57
+ "TypedRelation",
58
+ "build_relation_graph",
59
+ "by_column",
60
+ "by_expression",
61
+ "derive_relations",
62
+ "plan_key_batches",
63
+ "resolve_fetch_plan",
64
+ "table_relations",
65
+ "validate_fetch_plan",
66
+ )