pyoq-sql 1.0.0__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 (264) hide show
  1. pyoq/__init__.py +7 -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 +19 -0
  9. pyoq/config/loader.py +204 -0
  10. pyoq/config/models.py +243 -0
  11. pyoq/descriptors.py +165 -0
  12. pyoq/diagnostics/__init__.py +68 -0
  13. pyoq/diagnostics/budget.py +136 -0
  14. pyoq/diagnostics/events.py +137 -0
  15. pyoq/diagnostics/fingerprint.py +267 -0
  16. pyoq/diagnostics/instrumented.py +237 -0
  17. pyoq/diagnostics/metrics.py +61 -0
  18. pyoq/diagnostics/observation.py +227 -0
  19. pyoq/diagnostics/scoped.py +103 -0
  20. pyoq/django/__init__.py +15 -0
  21. pyoq/django/apps.py +17 -0
  22. pyoq/django/execution.py +317 -0
  23. pyoq/django/generation.py +59 -0
  24. pyoq/django/management/__init__.py +0 -0
  25. pyoq/django/management/commands/__init__.py +0 -0
  26. pyoq/django/management/commands/makemigrations.py +53 -0
  27. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  28. pyoq/django/parameters.py +101 -0
  29. pyoq/django/schema.py +379 -0
  30. pyoq/django/settings.py +87 -0
  31. pyoq/django/timeouts.py +105 -0
  32. pyoq/dsl/__init__.py +64 -0
  33. pyoq/dsl/aio/__init__.py +31 -0
  34. pyoq/dsl/aio/context.py +295 -0
  35. pyoq/dsl/aio/queries.py +335 -0
  36. pyoq/dsl/aio/writes.py +368 -0
  37. pyoq/dsl/context.py +326 -0
  38. pyoq/dsl/entry.py +37 -0
  39. pyoq/dsl/labels.py +36 -0
  40. pyoq/dsl/queries.py +339 -0
  41. pyoq/dsl/result.py +164 -0
  42. pyoq/dsl/writes.py +360 -0
  43. pyoq/errors.py +317 -0
  44. pyoq/fastapi/__init__.py +32 -0
  45. pyoq/fastapi/dependencies.py +167 -0
  46. pyoq/fastapi/lifespan.py +119 -0
  47. pyoq/fetching/__init__.py +55 -0
  48. pyoq/fetching/collections.py +136 -0
  49. pyoq/fetching/execution.py +587 -0
  50. pyoq/fetching/joined.py +79 -0
  51. pyoq/fetching/nesting.py +183 -0
  52. pyoq/fetching/plans.py +541 -0
  53. pyoq/fetching/select_in.py +149 -0
  54. pyoq/fetching/tables.py +110 -0
  55. pyoq/generation/__init__.py +54 -0
  56. pyoq/generation/cleanup.py +44 -0
  57. pyoq/generation/contracts.py +248 -0
  58. pyoq/generation/drift.py +169 -0
  59. pyoq/generation/lock.py +33 -0
  60. pyoq/generation/manifest.py +114 -0
  61. pyoq/generation/model.py +1001 -0
  62. pyoq/generation/pipeline.py +119 -0
  63. pyoq/generation/rendering/__init__.py +5 -0
  64. pyoq/generation/rendering/domains.py +51 -0
  65. pyoq/generation/rendering/enums.py +29 -0
  66. pyoq/generation/rendering/exports.py +70 -0
  67. pyoq/generation/rendering/imports.py +63 -0
  68. pyoq/generation/rendering/package.py +56 -0
  69. pyoq/generation/rendering/relations.py +133 -0
  70. pyoq/generation/rendering/routines.py +396 -0
  71. pyoq/generation/rendering/rows.py +79 -0
  72. pyoq/generation/rendering/source.py +121 -0
  73. pyoq/generation/rendering/tables.py +300 -0
  74. pyoq/generation/rendering/writes.py +514 -0
  75. pyoq/generation/validation.py +27 -0
  76. pyoq/generation/writer.py +184 -0
  77. pyoq/hydration/__init__.py +24 -0
  78. pyoq/hydration/engine.py +155 -0
  79. pyoq/hydration/identity.py +194 -0
  80. pyoq/hydration/plan.py +116 -0
  81. pyoq/migrations/__init__.py +9 -0
  82. pyoq/migrations/alembic.py +106 -0
  83. pyoq/migrations/hooks.py +75 -0
  84. pyoq/naming.py +261 -0
  85. pyoq/policies/__init__.py +47 -0
  86. pyoq/policies/bypass.py +122 -0
  87. pyoq/policies/governed.py +430 -0
  88. pyoq/policies/model.py +242 -0
  89. pyoq/policies/rewriting.py +263 -0
  90. pyoq/py.typed +1 -0
  91. pyoq/query/__init__.py +312 -0
  92. pyoq/query/aggregates.py +172 -0
  93. pyoq/query/arrays.py +65 -0
  94. pyoq/query/binding.py +52 -0
  95. pyoq/query/capabilities.py +317 -0
  96. pyoq/query/casts.py +73 -0
  97. pyoq/query/choices.py +185 -0
  98. pyoq/query/decoding.py +360 -0
  99. pyoq/query/documents.py +56 -0
  100. pyoq/query/execution/__init__.py +63 -0
  101. pyoq/query/execution/aio/__init__.py +31 -0
  102. pyoq/query/execution/aio/operations.py +228 -0
  103. pyoq/query/execution/aio/pooling.py +233 -0
  104. pyoq/query/execution/aio/streaming.py +161 -0
  105. pyoq/query/execution/aio/transactions.py +105 -0
  106. pyoq/query/execution/batch.py +96 -0
  107. pyoq/query/execution/binding_style.py +30 -0
  108. pyoq/query/execution/compilation.py +48 -0
  109. pyoq/query/execution/context.py +61 -0
  110. pyoq/query/execution/control.py +50 -0
  111. pyoq/query/execution/operations.py +224 -0
  112. pyoq/query/execution/planning.py +107 -0
  113. pyoq/query/execution/pooling.py +279 -0
  114. pyoq/query/execution/results.py +36 -0
  115. pyoq/query/execution/streaming.py +178 -0
  116. pyoq/query/execution/transactions.py +95 -0
  117. pyoq/query/expressions.py +1200 -0
  118. pyoq/query/fields.py +60 -0
  119. pyoq/query/mysql/__init__.py +59 -0
  120. pyoq/query/mysql/aio/__init__.py +38 -0
  121. pyoq/query/mysql/aio/commands.py +389 -0
  122. pyoq/query/mysql/aio/driver.py +196 -0
  123. pyoq/query/mysql/aio/executor.py +123 -0
  124. pyoq/query/mysql/aio/factory.py +26 -0
  125. pyoq/query/mysql/aio/operations.py +38 -0
  126. pyoq/query/mysql/aio/pool.py +53 -0
  127. pyoq/query/mysql/aio/transactions.py +313 -0
  128. pyoq/query/mysql/commands.py +354 -0
  129. pyoq/query/mysql/compiler.py +134 -0
  130. pyoq/query/mysql/context.py +20 -0
  131. pyoq/query/mysql/executor.py +126 -0
  132. pyoq/query/mysql/expressions.py +244 -0
  133. pyoq/query/mysql/factory.py +46 -0
  134. pyoq/query/mysql/health.py +66 -0
  135. pyoq/query/mysql/identifiers.py +9 -0
  136. pyoq/query/mysql/model.py +79 -0
  137. pyoq/query/mysql/operations.py +43 -0
  138. pyoq/query/mysql/parameters.py +69 -0
  139. pyoq/query/mysql/planning.py +20 -0
  140. pyoq/query/mysql/pool.py +67 -0
  141. pyoq/query/mysql/transactions.py +331 -0
  142. pyoq/query/mysql/writes.py +73 -0
  143. pyoq/query/nodes.py +750 -0
  144. pyoq/query/postgres/__init__.py +48 -0
  145. pyoq/query/postgres/aio/__init__.py +25 -0
  146. pyoq/query/postgres/aio/bulk.py +56 -0
  147. pyoq/query/postgres/aio/commands.py +264 -0
  148. pyoq/query/postgres/aio/executor.py +152 -0
  149. pyoq/query/postgres/aio/factory.py +26 -0
  150. pyoq/query/postgres/aio/operations.py +26 -0
  151. pyoq/query/postgres/aio/pool.py +40 -0
  152. pyoq/query/postgres/aio/transactions.py +295 -0
  153. pyoq/query/postgres/bulk.py +62 -0
  154. pyoq/query/postgres/commands.py +238 -0
  155. pyoq/query/postgres/compiler.py +114 -0
  156. pyoq/query/postgres/context.py +20 -0
  157. pyoq/query/postgres/executor.py +147 -0
  158. pyoq/query/postgres/expressions.py +311 -0
  159. pyoq/query/postgres/factory.py +24 -0
  160. pyoq/query/postgres/health.py +24 -0
  161. pyoq/query/postgres/identifiers.py +9 -0
  162. pyoq/query/postgres/model.py +81 -0
  163. pyoq/query/postgres/operations.py +25 -0
  164. pyoq/query/postgres/parameters.py +71 -0
  165. pyoq/query/postgres/planning.py +20 -0
  166. pyoq/query/postgres/pool.py +52 -0
  167. pyoq/query/postgres/transactions.py +295 -0
  168. pyoq/query/postgres/writes.py +37 -0
  169. pyoq/query/projections.py +105 -0
  170. pyoq/query/raw.py +90 -0
  171. pyoq/query/recursion.py +265 -0
  172. pyoq/query/rendering/__init__.py +1 -0
  173. pyoq/query/rendering/expressions.py +913 -0
  174. pyoq/query/rendering/identifiers.py +40 -0
  175. pyoq/query/rendering/projections.py +63 -0
  176. pyoq/query/rendering/queries.py +334 -0
  177. pyoq/query/rendering/sources.py +66 -0
  178. pyoq/query/rendering/writes.py +176 -0
  179. pyoq/query/results.py +459 -0
  180. pyoq/query/routines.py +196 -0
  181. pyoq/query/rows.py +156 -0
  182. pyoq/query/select.py +793 -0
  183. pyoq/query/select_nodes.py +277 -0
  184. pyoq/query/sources.py +236 -0
  185. pyoq/query/sqlite/__init__.py +43 -0
  186. pyoq/query/sqlite/commands.py +201 -0
  187. pyoq/query/sqlite/compiler.py +139 -0
  188. pyoq/query/sqlite/context.py +20 -0
  189. pyoq/query/sqlite/executor.py +119 -0
  190. pyoq/query/sqlite/expressions.py +224 -0
  191. pyoq/query/sqlite/factory.py +32 -0
  192. pyoq/query/sqlite/health.py +28 -0
  193. pyoq/query/sqlite/identifiers.py +9 -0
  194. pyoq/query/sqlite/model.py +73 -0
  195. pyoq/query/sqlite/operations.py +36 -0
  196. pyoq/query/sqlite/parameters.py +50 -0
  197. pyoq/query/sqlite/planning.py +20 -0
  198. pyoq/query/sqlite/pool.py +50 -0
  199. pyoq/query/sqlite/streaming.py +13 -0
  200. pyoq/query/sqlite/transactions.py +274 -0
  201. pyoq/query/sqlite/writes.py +35 -0
  202. pyoq/query/statements.py +27 -0
  203. pyoq/query/values.py +23 -0
  204. pyoq/query/vendor.py +162 -0
  205. pyoq/query/windows.py +424 -0
  206. pyoq/query/write_nodes.py +174 -0
  207. pyoq/query/writes.py +628 -0
  208. pyoq/relations/__init__.py +66 -0
  209. pyoq/relations/batching.py +219 -0
  210. pyoq/relations/derivation.py +111 -0
  211. pyoq/relations/fetching.py +355 -0
  212. pyoq/relations/graph.py +245 -0
  213. pyoq/relations/loading.py +74 -0
  214. pyoq/relations/model.py +75 -0
  215. pyoq/relations/planning.py +206 -0
  216. pyoq/runtime/__init__.py +9 -0
  217. pyoq/runtime/kernels.py +25 -0
  218. pyoq/runtime/python.py +43 -0
  219. pyoq/runtime/selection.py +73 -0
  220. pyoq/sanic/__init__.py +32 -0
  221. pyoq/sanic/scope.py +197 -0
  222. pyoq/sanic/workers.py +129 -0
  223. pyoq/schema/__init__.py +108 -0
  224. pyoq/schema/codec.py +711 -0
  225. pyoq/schema/models.py +604 -0
  226. pyoq/schema/mysql/__init__.py +16 -0
  227. pyoq/schema/mysql/connection.py +72 -0
  228. pyoq/schema/mysql/dsn.py +72 -0
  229. pyoq/schema/mysql/records.py +354 -0
  230. pyoq/schema/mysql/reflection.py +309 -0
  231. pyoq/schema/mysql/source.py +30 -0
  232. pyoq/schema/mysql/sql.py +128 -0
  233. pyoq/schema/mysql/types.py +105 -0
  234. pyoq/schema/postgres/__init__.py +13 -0
  235. pyoq/schema/postgres/connection.py +62 -0
  236. pyoq/schema/postgres/records.py +384 -0
  237. pyoq/schema/postgres/reflection.py +466 -0
  238. pyoq/schema/postgres/source.py +30 -0
  239. pyoq/schema/postgres/sql.py +246 -0
  240. pyoq/schema/postgres/types.py +98 -0
  241. pyoq/schema/registry.py +45 -0
  242. pyoq/schema/source.py +15 -0
  243. pyoq/schema/sqlite/__init__.py +6 -0
  244. pyoq/schema/sqlite/connection.py +54 -0
  245. pyoq/schema/sqlite/records.py +167 -0
  246. pyoq/schema/sqlite/reflection.py +393 -0
  247. pyoq/schema/sqlite/source.py +30 -0
  248. pyoq/schema/sqlite/sql.py +254 -0
  249. pyoq/schema/sqlite/types.py +74 -0
  250. pyoq/serving/__init__.py +19 -0
  251. pyoq/serving/databases.py +107 -0
  252. pyoq/snapshots/__init__.py +20 -0
  253. pyoq/snapshots/drift.py +312 -0
  254. pyoq/snapshots/files.py +96 -0
  255. pyoq/snapshots/routing.py +40 -0
  256. pyoq/snapshots/source.py +33 -0
  257. pyoq/tracing/__init__.py +5 -0
  258. pyoq/tracing/spans.py +89 -0
  259. pyoq/unset.py +14 -0
  260. pyoq_sql-1.0.0.dist-info/METADATA +3034 -0
  261. pyoq_sql-1.0.0.dist-info/RECORD +264 -0
  262. pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
  263. pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
  264. pyoq_sql-1.0.0.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,295 @@
1
+ """Owned asynchronous PostgreSQL transactions and nested savepoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import TypeVar, cast
7
+
8
+ import psycopg
9
+
10
+ from pyoq.errors import ExecutionError, QueryValidationError, TransactionStateError
11
+ from pyoq.query.execution import (
12
+ BulkPlan,
13
+ CompiledQuery,
14
+ ExecuteResult,
15
+ ExecutionControl,
16
+ StreamPolicy,
17
+ )
18
+ from pyoq.query.execution.aio import (
19
+ AsyncRowStream,
20
+ AsyncStreamResource,
21
+ AsyncTransactionState,
22
+ )
23
+ from pyoq.query.postgres.aio.bulk import run_pipeline
24
+ from pyoq.query.postgres.aio.commands import open_cursor, run_sql
25
+ from pyoq.query.postgres.aio.operations import PostgresAsyncQueryOperations
26
+ from pyoq.query.postgres.aio.pool import PostgresAsyncPool
27
+ from pyoq.query.postgres.bulk import pipeline_supported
28
+ from pyoq.query.postgres.compiler import PostgresCompiler
29
+ from pyoq.query.postgres.parameters import adapt_parameters
30
+ from pyoq.query.postgres.transactions import TransactionMode
31
+ from pyoq.query.select import Query
32
+
33
+ OperationResult = TypeVar("OperationResult")
34
+ ResultRow = TypeVar("ResultRow")
35
+
36
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
37
+ AsyncCursor = psycopg.AsyncCursor[tuple[object, ...]]
38
+ _AsyncState = AsyncTransactionState[AsyncConnection, "PostgresAsyncTransaction"]
39
+
40
+ _STREAM_CURSOR_PREFIX = "pyoq_stream_"
41
+
42
+
43
+ class PostgresAsyncTransaction(PostgresAsyncQueryOperations):
44
+ __slots__ = (
45
+ "_control",
46
+ "_entered",
47
+ "_finished",
48
+ "_mode",
49
+ "_parent",
50
+ "_pool",
51
+ "_savepoint_name",
52
+ "_state",
53
+ "_stream_sequence",
54
+ )
55
+
56
+ def __init__(
57
+ self,
58
+ pool: PostgresAsyncPool,
59
+ compiler: PostgresCompiler,
60
+ mode: TransactionMode = TransactionMode.READ_COMMITTED,
61
+ control: ExecutionControl | None = None,
62
+ *,
63
+ parent: PostgresAsyncTransaction | None = None,
64
+ state: _AsyncState | None = None,
65
+ ) -> None:
66
+ super().__init__(compiler)
67
+ if not _is_transaction_mode(mode):
68
+ raise QueryValidationError("PostgreSQL transaction mode is invalid")
69
+ self._pool = pool
70
+ self._mode = mode
71
+ self._control = control
72
+ self._parent = parent
73
+ self._state = state or AsyncTransactionState()
74
+ self._savepoint_name: str | None = None
75
+ self._stream_sequence = 0
76
+ self._entered = False
77
+ self._finished = False
78
+
79
+ async def __aenter__(self) -> PostgresAsyncTransaction:
80
+ if self._entered or self._finished:
81
+ raise TransactionStateError("transaction cannot be entered twice")
82
+ if self._parent is None:
83
+ await self._enter_outer()
84
+ else:
85
+ await self._enter_savepoint()
86
+ self._entered = True
87
+ return self
88
+
89
+ async def __aexit__(
90
+ self,
91
+ exception_type: type[BaseException] | None,
92
+ exception: BaseException | None,
93
+ traceback: object,
94
+ ) -> None:
95
+ if not self._entered or self._finished:
96
+ return
97
+ if self._parent is None:
98
+ await self._exit_outer(exception_type)
99
+ else:
100
+ await self._exit_savepoint(exception_type)
101
+
102
+ def savepoint(
103
+ self,
104
+ *,
105
+ control: ExecutionControl | None = None,
106
+ ) -> PostgresAsyncTransaction:
107
+ self._state.require_current(self)
108
+ return PostgresAsyncTransaction(
109
+ self._pool,
110
+ self.compiler,
111
+ self._mode,
112
+ control or self._control,
113
+ parent=self,
114
+ state=self._state,
115
+ )
116
+
117
+ async def execute_chunks(
118
+ self,
119
+ plan: BulkPlan,
120
+ control: ExecutionControl | None,
121
+ ) -> tuple[ExecuteResult, ...]:
122
+ if not pipeline_supported(self.compiler.capabilities.pipeline):
123
+ return await super().execute_chunks(plan, control)
124
+ connection = self._state.require_current(self)
125
+ return await run_pipeline(
126
+ connection,
127
+ plan.chunks,
128
+ control or self._control,
129
+ self._state.invalidate,
130
+ )
131
+
132
+ def stream(
133
+ self,
134
+ query: Query[ResultRow],
135
+ /,
136
+ *,
137
+ policy: StreamPolicy | None = None,
138
+ ) -> AsyncRowStream[ResultRow]:
139
+ self._state.require_current(self)
140
+ selected = policy or StreamPolicy(control=self._control)
141
+ if selected.control is None and self._control is not None:
142
+ selected = StreamPolicy(selected.batch_size, self._control)
143
+ statement = self.compiler.compile(query)
144
+ stream: AsyncRowStream[ResultRow]
145
+ stream = AsyncRowStream(
146
+ lambda: self._open_stream(stream, statement, selected),
147
+ selected,
148
+ statement.row_types,
149
+ )
150
+ return stream
151
+
152
+ async def _run(
153
+ self,
154
+ statement: CompiledQuery,
155
+ operation: Callable[[AsyncCursor], Awaitable[OperationResult]],
156
+ control: ExecutionControl | None,
157
+ ) -> OperationResult:
158
+ parameters = adapt_parameters(statement.parameters)
159
+ connection = self._state.require_current(self)
160
+ return await run_sql(
161
+ connection,
162
+ statement.sql,
163
+ parameters,
164
+ operation,
165
+ control or self._control,
166
+ self._state.invalidate,
167
+ )
168
+
169
+ async def _enter_outer(self) -> None:
170
+ await self._state.adopt(self._pool.checkout(), self)
171
+ try:
172
+ await self._run_transaction_sql(f"BEGIN {self._mode.value}")
173
+ except BaseException:
174
+ await self._release_outer()
175
+ raise
176
+
177
+ async def _enter_savepoint(self) -> None:
178
+ self._state.require_current(cast("PostgresAsyncTransaction", self._parent))
179
+ name = self._state.next_savepoint()
180
+ await self._run_transaction_sql(f'SAVEPOINT "{name}"', scope=self._parent)
181
+ self._savepoint_name = name
182
+ self._state.current_scope = self
183
+
184
+ async def _exit_outer(self, exception_type: type[BaseException] | None) -> None:
185
+ try:
186
+ self._state.require_scope(self)
187
+ except TransactionStateError:
188
+ await self._abort_misnested_outer(exception_type)
189
+ return
190
+ await self._state.close_stream()
191
+ command = "ROLLBACK" if exception_type is not None else "COMMIT"
192
+ try:
193
+ if not self._state.broken:
194
+ await self._run_transaction_sql(command)
195
+ except ExecutionError:
196
+ self._state.invalidate()
197
+ if exception_type is None:
198
+ raise
199
+ finally:
200
+ await self._release_outer()
201
+
202
+ async def _exit_savepoint(
203
+ self,
204
+ exception_type: type[BaseException] | None,
205
+ ) -> None:
206
+ self._state.require_scope(self)
207
+ await self._state.close_stream()
208
+ name = cast("str", self._savepoint_name)
209
+ try:
210
+ await self._finish_savepoint(name, exception_type)
211
+ except ExecutionError:
212
+ self._state.invalidate()
213
+ if exception_type is None:
214
+ raise
215
+ finally:
216
+ self._state.current_scope = self._parent
217
+ self._finished = True
218
+
219
+ async def _finish_savepoint(
220
+ self,
221
+ name: str,
222
+ exception_type: type[BaseException] | None,
223
+ ) -> None:
224
+ if self._state.broken:
225
+ return
226
+ if exception_type is not None:
227
+ await self._run_transaction_sql(f'ROLLBACK TO SAVEPOINT "{name}"')
228
+ await self._run_transaction_sql(f'RELEASE SAVEPOINT "{name}"')
229
+
230
+ async def _run_transaction_sql(
231
+ self,
232
+ sql: str,
233
+ *,
234
+ scope: PostgresAsyncTransaction | None = None,
235
+ ) -> None:
236
+ connection = self._state.require_current(scope or self, allow_stream=True)
237
+ await run_sql(
238
+ connection,
239
+ sql,
240
+ (),
241
+ _discard_cursor,
242
+ self._control,
243
+ self._state.invalidate,
244
+ )
245
+
246
+ async def _open_stream(
247
+ self,
248
+ stream: AsyncRowStream[ResultRow],
249
+ statement: CompiledQuery,
250
+ policy: StreamPolicy,
251
+ ) -> AsyncStreamResource:
252
+ parameters = adapt_parameters(statement.parameters)
253
+ connection = self._state.require_current(self)
254
+ self._stream_sequence += 1
255
+ session = await open_cursor(
256
+ connection,
257
+ statement.sql,
258
+ parameters,
259
+ policy.control,
260
+ self._state.invalidate,
261
+ server_side_name=f"{_STREAM_CURSOR_PREFIX}{self._stream_sequence}",
262
+ )
263
+ self._state.active_stream = cast("AsyncRowStream[object]", stream)
264
+ return AsyncStreamResource(session, self._state.release_stream)
265
+
266
+ async def _release_outer(self) -> None:
267
+ self._finished = True
268
+ await self._state.release()
269
+
270
+ async def _abort_misnested_outer(
271
+ self,
272
+ exception_type: type[BaseException] | None,
273
+ ) -> None:
274
+ connection = self._state.require_owner()
275
+ await self._state.close_stream()
276
+ try:
277
+ await connection.rollback()
278
+ except psycopg.Error:
279
+ self._state.invalidate()
280
+ finally:
281
+ await self._release_outer()
282
+ if exception_type is None:
283
+ message = "transaction exited with an active nested savepoint"
284
+ raise TransactionStateError(message)
285
+
286
+
287
+ async def _discard_cursor(cursor: AsyncCursor) -> None:
288
+ del cursor
289
+
290
+
291
+ def _is_transaction_mode(value: object) -> bool:
292
+ return isinstance(value, TransactionMode)
293
+
294
+
295
+ __all__ = ("PostgresAsyncTransaction",)
@@ -0,0 +1,62 @@
1
+ """Pipelined PostgreSQL bulk chunk execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+
7
+ import psycopg
8
+
9
+ from pyoq.query.execution import BulkChunk, ExecuteResult, ExecutionControl
10
+ from pyoq.query.postgres.commands import PostgresExecutionMonitor
11
+ from pyoq.query.postgres.parameters import adapt_parameters
12
+
13
+ Connection = psycopg.Connection[tuple[object, ...]]
14
+ Invalidator = Callable[[], None]
15
+
16
+
17
+ def pipeline_supported(enabled: bool) -> bool:
18
+ return enabled and psycopg.capabilities.has_pipeline()
19
+
20
+
21
+ def run_pipeline(
22
+ connection: Connection,
23
+ chunks: tuple[BulkChunk, ...],
24
+ control: ExecutionControl | None,
25
+ invalidate: Invalidator,
26
+ ) -> tuple[ExecuteResult, ...]:
27
+ monitor = PostgresExecutionMonitor(control)
28
+ cursors: list[psycopg.Cursor[tuple[object, ...]]] = []
29
+ try:
30
+ monitor.install(connection, invalidate)
31
+ with connection.pipeline():
32
+ for chunk in chunks:
33
+ monitor.checkpoint()
34
+ cursors.append(_send(connection, chunk))
35
+ monitor.checkpoint()
36
+ return tuple(ExecuteResult(max(cursor.rowcount, 0)) for cursor in cursors)
37
+ except psycopg.Error as error:
38
+ monitor.translate_driver_error(error, invalidate)
39
+ finally:
40
+ for cursor in cursors:
41
+ _close_quietly(cursor)
42
+ monitor.close(connection, invalidate)
43
+
44
+
45
+ def _send(
46
+ connection: Connection,
47
+ chunk: BulkChunk,
48
+ ) -> psycopg.Cursor[tuple[object, ...]]:
49
+ cursor = connection.cursor()
50
+ parameters = adapt_parameters(chunk.statement.parameters)
51
+ cursor.execute(chunk.statement.sql.encode(), parameters or None)
52
+ return cursor
53
+
54
+
55
+ def _close_quietly(cursor: psycopg.Cursor[tuple[object, ...]]) -> None:
56
+ try:
57
+ cursor.close()
58
+ except psycopg.Error:
59
+ return
60
+
61
+
62
+ __all__ = ("pipeline_supported", "run_pipeline")
@@ -0,0 +1,238 @@
1
+ """PostgreSQL cursor execution, timeouts, and cancellation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from threading import Event, Thread
7
+ from time import monotonic
8
+ from typing import NoReturn, TypeVar, cast
9
+
10
+ import psycopg
11
+
12
+ from pyoq.errors import QueryCancelledError, QueryExecutionError, QueryTimeoutError
13
+ from pyoq.query.execution import ExecutionControl
14
+ from pyoq.query.postgres.health import is_cancellation, is_connection_failure
15
+ from pyoq.query.postgres.parameters import PostgresParameter
16
+
17
+ OperationResult = TypeVar("OperationResult")
18
+ Invalidator = Callable[[], None]
19
+ Connection = psycopg.Connection[tuple[object, ...]]
20
+
21
+ _WATCHDOG_INTERVAL = 0.025
22
+ _MILLISECONDS = 1000
23
+
24
+
25
+ class PostgresExecutionMonitor:
26
+ """Server-side statement timeout plus a cancellation watchdog."""
27
+
28
+ __slots__ = ("_control", "_started", "_stop", "_timeout_applied", "_watchdog")
29
+
30
+ def __init__(self, control: ExecutionControl | None) -> None:
31
+ self._control = control or ExecutionControl()
32
+ self._started = monotonic() if self._control.timeout is not None else 0.0
33
+ self._stop = Event()
34
+ self._watchdog: Thread | None = None
35
+ self._timeout_applied = False
36
+
37
+ def install(self, connection: Connection, invalidate: Invalidator) -> None:
38
+ self.checkpoint()
39
+ self._apply_timeout(connection, invalidate)
40
+ token = self._control.cancellation_token
41
+ if token is None:
42
+ return
43
+ watchdog = Thread(
44
+ target=self._watch,
45
+ args=(connection,),
46
+ daemon=True,
47
+ name="pyoq-postgres-cancel",
48
+ )
49
+ self._watchdog = watchdog
50
+ watchdog.start()
51
+
52
+ def checkpoint(self) -> None:
53
+ token = self._control.cancellation_token
54
+ if token is not None and token.cancelled:
55
+ raise QueryCancelledError("PostgreSQL query execution was cancelled")
56
+ if self._expired():
57
+ raise QueryTimeoutError("PostgreSQL query execution exceeded its timeout")
58
+
59
+ def translate_driver_error(
60
+ self,
61
+ error: psycopg.Error,
62
+ invalidate: Invalidator,
63
+ ) -> NoReturn:
64
+ if is_connection_failure(error):
65
+ invalidate()
66
+ self.checkpoint()
67
+ if is_cancellation(error):
68
+ raise QueryCancelledError(
69
+ "PostgreSQL query execution was cancelled"
70
+ ) from error
71
+ _raise_query_execution_error(error)
72
+
73
+ def close(self, connection: Connection, invalidate: Invalidator) -> None:
74
+ self._stop.set()
75
+ watchdog = self._watchdog
76
+ self._watchdog = None
77
+ if watchdog is not None:
78
+ watchdog.join()
79
+ self._clear_timeout(connection, invalidate)
80
+
81
+ def _watch(self, connection: Connection) -> None:
82
+ token = cast("object", self._control.cancellation_token)
83
+ while not self._stop.wait(_WATCHDOG_INTERVAL):
84
+ if getattr(token, "cancelled", False):
85
+ _cancel_quietly(connection)
86
+ return
87
+
88
+ def _apply_timeout(self, connection: Connection, invalidate: Invalidator) -> None:
89
+ timeout = self._control.timeout
90
+ if timeout is None:
91
+ return
92
+ milliseconds = max(int(timeout * _MILLISECONDS), 1)
93
+ try:
94
+ connection.execute(f"SET statement_timeout = {milliseconds}".encode())
95
+ except psycopg.Error as error:
96
+ self.translate_driver_error(error, invalidate)
97
+ self._timeout_applied = True
98
+
99
+ def _clear_timeout(self, connection: Connection, invalidate: Invalidator) -> None:
100
+ if not self._timeout_applied:
101
+ return
102
+ self._timeout_applied = False
103
+ try:
104
+ connection.execute(b"SET statement_timeout = 0")
105
+ except psycopg.Error:
106
+ invalidate()
107
+
108
+ def _expired(self) -> bool:
109
+ timeout = self._control.timeout
110
+ return timeout is not None and monotonic() - self._started >= timeout
111
+
112
+
113
+ class PostgresCursorSession:
114
+ __slots__ = ("_closed", "_connection", "_cursor", "_invalidate", "_monitor")
115
+
116
+ def __init__(
117
+ self,
118
+ connection: Connection,
119
+ cursor: psycopg.Cursor[tuple[object, ...]],
120
+ monitor: PostgresExecutionMonitor,
121
+ invalidate: Invalidator,
122
+ ) -> None:
123
+ self._connection = connection
124
+ self._cursor = cursor
125
+ self._monitor = monitor
126
+ self._invalidate = invalidate
127
+ self._closed = False
128
+
129
+ @property
130
+ def cursor(self) -> psycopg.Cursor[tuple[object, ...]]:
131
+ return self._cursor
132
+
133
+ def checkpoint(self) -> None:
134
+ self._monitor.checkpoint()
135
+
136
+ def translate_driver_error(self, error: psycopg.Error) -> NoReturn:
137
+ self._monitor.translate_driver_error(error, self._invalidate)
138
+
139
+ def fetch_batch(self, size: int) -> list[tuple[object, ...]]:
140
+ try:
141
+ self._monitor.checkpoint()
142
+ rows = self._cursor.fetchmany(size)
143
+ self._monitor.checkpoint()
144
+ except psycopg.Error as error:
145
+ self._monitor.translate_driver_error(error, self._invalidate)
146
+ return rows
147
+
148
+ def close(self) -> None:
149
+ if self._closed:
150
+ return
151
+ self._closed = True
152
+ _close_cursor(self._cursor, self._invalidate)
153
+ self._monitor.close(self._connection, self._invalidate)
154
+
155
+
156
+ def run_sql(
157
+ connection: Connection,
158
+ sql: str,
159
+ parameters: tuple[PostgresParameter, ...],
160
+ operation: Callable[[psycopg.Cursor[tuple[object, ...]]], OperationResult],
161
+ control: ExecutionControl | None,
162
+ invalidate: Invalidator,
163
+ ) -> OperationResult:
164
+ session = open_cursor(connection, sql, parameters, control, invalidate)
165
+ try:
166
+ result = operation(session.cursor)
167
+ session.checkpoint()
168
+ return result
169
+ except psycopg.Error as error:
170
+ session.translate_driver_error(error)
171
+ finally:
172
+ session.close()
173
+
174
+
175
+ def open_cursor(
176
+ connection: Connection,
177
+ sql: str,
178
+ parameters: tuple[PostgresParameter, ...],
179
+ control: ExecutionControl | None,
180
+ invalidate: Invalidator,
181
+ *,
182
+ server_side_name: str | None = None,
183
+ ) -> PostgresCursorSession:
184
+ monitor = PostgresExecutionMonitor(control)
185
+ cursor: psycopg.Cursor[tuple[object, ...]] | None = None
186
+ try:
187
+ monitor.install(connection, invalidate)
188
+ cursor = connection.cursor(name=server_side_name or "")
189
+ cursor.execute(sql.encode(), parameters or None)
190
+ monitor.checkpoint()
191
+ return PostgresCursorSession(connection, cursor, monitor, invalidate)
192
+ except psycopg.Error as error:
193
+ _release(cursor, monitor, connection, invalidate)
194
+ monitor.translate_driver_error(error, invalidate)
195
+ except BaseException:
196
+ _release(cursor, monitor, connection, invalidate)
197
+ raise
198
+
199
+
200
+ def _release(
201
+ cursor: psycopg.Cursor[tuple[object, ...]] | None,
202
+ monitor: PostgresExecutionMonitor,
203
+ connection: Connection,
204
+ invalidate: Invalidator,
205
+ ) -> None:
206
+ if cursor is not None:
207
+ _close_cursor(cursor, invalidate)
208
+ monitor.close(connection, invalidate)
209
+
210
+
211
+ def _close_cursor(
212
+ cursor: psycopg.Cursor[tuple[object, ...]],
213
+ invalidate: Invalidator,
214
+ ) -> None:
215
+ try:
216
+ cursor.close()
217
+ except psycopg.Error:
218
+ invalidate()
219
+
220
+
221
+ def _cancel_quietly(connection: Connection) -> None:
222
+ try:
223
+ connection.cancel()
224
+ except psycopg.Error:
225
+ return
226
+
227
+
228
+ def _raise_query_execution_error(error: psycopg.Error) -> NoReturn:
229
+ message = f"PostgreSQL query execution failed: {error}"
230
+ raise QueryExecutionError(message) from error
231
+
232
+
233
+ __all__ = (
234
+ "PostgresCursorSession",
235
+ "PostgresExecutionMonitor",
236
+ "open_cursor",
237
+ "run_sql",
238
+ )
@@ -0,0 +1,114 @@
1
+ """PostgreSQL query and statement compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.errors import UnsupportedQueryError
6
+ from pyoq.query.execution import CompiledQuery, StatementKind
7
+ from pyoq.query.postgres.context import PostgresCompilationContext
8
+ from pyoq.query.postgres.expressions import PostgresExpressionCompiler
9
+ from pyoq.query.postgres.identifiers import QUOTING
10
+ from pyoq.query.postgres.model import PostgresCapabilities
11
+ from pyoq.query.postgres.writes import PostgresWriteCompilation
12
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
13
+ from pyoq.query.rendering.queries import QueryRenderer
14
+ from pyoq.query.select_nodes import JoinKind, NullOrder, SelectNode, SetNode
15
+ from pyoq.query.statements import StatementNode, StatementProvider
16
+ from pyoq.query.write_nodes import CallNode, DeleteNode, InsertNode, UpdateNode
17
+
18
+ _STATEMENT_NODES = (
19
+ SelectNode,
20
+ SetNode,
21
+ InsertNode,
22
+ UpdateNode,
23
+ DeleteNode,
24
+ CallNode,
25
+ )
26
+
27
+
28
+ class PostgresCompiler:
29
+ __slots__ = ("_capabilities",)
30
+
31
+ def __init__(
32
+ self,
33
+ capabilities: PostgresCapabilities | None = None,
34
+ ) -> None:
35
+ self._capabilities = capabilities or PostgresCapabilities()
36
+
37
+ @property
38
+ def capabilities(self) -> PostgresCapabilities:
39
+ return self._capabilities
40
+
41
+ def compile(
42
+ self,
43
+ statement: StatementProvider | StatementNode,
44
+ /,
45
+ ) -> CompiledQuery:
46
+ node = statement if isinstance(statement, _STATEMENT_NODES) else statement.node
47
+ context = PostgresCompilationContext(self.capabilities)
48
+ if isinstance(node, CallNode):
49
+ sql = PostgresWriteCompilation(context).compile_call(node)
50
+ return context.result(sql, StatementKind.CALL, node)
51
+ if isinstance(node, (InsertNode, UpdateNode, DeleteNode)):
52
+ sql, statement_kind = PostgresWriteCompilation(context).compile(node)
53
+ return context.result(sql, statement_kind, node)
54
+ sql = _PostgresQueryCompilation(context).compile(node)
55
+ return context.result(sql, StatementKind.SELECT, node)
56
+
57
+
58
+ class _PostgresQueryCompilation(QueryRenderer):
59
+ __slots__ = ("_context",)
60
+
61
+ def __init__(self, context: PostgresCompilationContext) -> None:
62
+ self._context = context
63
+ super().__init__(PostgresExpressionCompiler(context))
64
+
65
+ @property
66
+ def dialect(self) -> str:
67
+ return "PostgreSQL"
68
+
69
+ @property
70
+ def quoting(self) -> IdentifierQuoting:
71
+ return QUOTING
72
+
73
+ def require_join_capability(self, kind: JoinKind) -> None:
74
+ if kind is JoinKind.RIGHT and not self._context.capabilities.right_join:
75
+ message = "PostgreSQL RIGHT JOIN is disabled by compiler capabilities"
76
+ raise UnsupportedQueryError(message)
77
+ if kind is JoinKind.FULL and not self._context.capabilities.full_join:
78
+ message = "PostgreSQL FULL JOIN is disabled by compiler capabilities"
79
+ raise UnsupportedQueryError(message)
80
+
81
+ def null_ordered(
82
+ self,
83
+ expression: str,
84
+ direction: str,
85
+ nulls: NullOrder,
86
+ ) -> str:
87
+ if not self._context.capabilities.null_ordering:
88
+ message = "PostgreSQL null ordering is disabled by compiler capabilities"
89
+ raise UnsupportedQueryError(message)
90
+ return super().null_ordered(expression, direction, nulls)
91
+
92
+ def require_recursive_common_tables(self) -> None:
93
+ if not self._context.capabilities.recursive_common_tables:
94
+ message = "PostgreSQL recursive common tables are disabled"
95
+ raise UnsupportedQueryError(message)
96
+
97
+ def require_lateral_sources(self) -> None:
98
+ if not self._context.capabilities.lateral_sources:
99
+ message = "PostgreSQL has no lateral sources"
100
+ raise UnsupportedQueryError(message)
101
+
102
+ def append_pagination(
103
+ self,
104
+ fragments: list[str],
105
+ limit: int | None,
106
+ offset: int | None,
107
+ ) -> None:
108
+ if limit is not None:
109
+ fragments.append(f"LIMIT {self._context.bind_page_value(limit)}")
110
+ if offset is not None:
111
+ fragments.append(f"OFFSET {self._context.bind_page_value(offset)}")
112
+
113
+
114
+ __all__ = ("PostgresCompiler",)