pyoq-sql 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,48 @@
1
+ """PostgreSQL query compilation and execution."""
2
+
3
+ from pyoq.query.postgres.commands import PostgresCursorSession
4
+ from pyoq.query.postgres.compiler import PostgresCompiler
5
+ from pyoq.query.postgres.context import PostgresCompilationContext
6
+ from pyoq.query.postgres.executor import PostgresExecutor
7
+ from pyoq.query.postgres.expressions import PostgresExpressionCompiler
8
+ from pyoq.query.postgres.factory import PostgresConnectionFactory
9
+ from pyoq.query.postgres.model import PostgresCapabilities
10
+ from pyoq.query.postgres.operations import PostgresQueryOperations
11
+ from pyoq.query.postgres.parameters import (
12
+ PostgresParameter,
13
+ adapt_parameter,
14
+ adapt_parameters,
15
+ )
16
+ from pyoq.query.postgres.planning import PostgresBulkPlanner
17
+ from pyoq.query.postgres.pool import (
18
+ PostgresConnectionFactoryProtocol,
19
+ PostgresConnectionLease,
20
+ PostgresPool,
21
+ PostgresPoolPolicy,
22
+ PostgresPoolStats,
23
+ )
24
+ from pyoq.query.postgres.transactions import PostgresTransaction, TransactionMode
25
+ from pyoq.query.postgres.writes import PostgresWriteCompilation
26
+
27
+ __all__ = (
28
+ "PostgresBulkPlanner",
29
+ "PostgresCapabilities",
30
+ "PostgresCompilationContext",
31
+ "PostgresCompiler",
32
+ "PostgresConnectionFactory",
33
+ "PostgresConnectionFactoryProtocol",
34
+ "PostgresConnectionLease",
35
+ "PostgresCursorSession",
36
+ "PostgresExecutor",
37
+ "PostgresExpressionCompiler",
38
+ "PostgresParameter",
39
+ "PostgresPool",
40
+ "PostgresPoolPolicy",
41
+ "PostgresPoolStats",
42
+ "PostgresQueryOperations",
43
+ "PostgresTransaction",
44
+ "PostgresWriteCompilation",
45
+ "TransactionMode",
46
+ "adapt_parameter",
47
+ "adapt_parameters",
48
+ )
@@ -0,0 +1,25 @@
1
+ """Asynchronous PostgreSQL execution."""
2
+
3
+ from pyoq.query.postgres.aio.commands import (
4
+ PostgresAsyncCursorSession,
5
+ PostgresAsyncExecutionMonitor,
6
+ )
7
+ from pyoq.query.postgres.aio.executor import PostgresAsyncExecutor
8
+ from pyoq.query.postgres.aio.factory import PostgresAsyncConnectionFactory
9
+ from pyoq.query.postgres.aio.operations import PostgresAsyncQueryOperations
10
+ from pyoq.query.postgres.aio.pool import (
11
+ PostgresAsyncConnectionLease,
12
+ PostgresAsyncPool,
13
+ )
14
+ from pyoq.query.postgres.aio.transactions import PostgresAsyncTransaction
15
+
16
+ __all__ = (
17
+ "PostgresAsyncConnectionFactory",
18
+ "PostgresAsyncConnectionLease",
19
+ "PostgresAsyncCursorSession",
20
+ "PostgresAsyncExecutionMonitor",
21
+ "PostgresAsyncExecutor",
22
+ "PostgresAsyncPool",
23
+ "PostgresAsyncQueryOperations",
24
+ "PostgresAsyncTransaction",
25
+ )
@@ -0,0 +1,56 @@
1
+ """Pipelined asynchronous 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.aio.commands import PostgresAsyncExecutionMonitor
11
+ from pyoq.query.postgres.parameters import adapt_parameters
12
+
13
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
14
+ AsyncCursor = psycopg.AsyncCursor[tuple[object, ...]]
15
+ Invalidator = Callable[[], None]
16
+
17
+
18
+ async def run_pipeline(
19
+ connection: AsyncConnection,
20
+ chunks: tuple[BulkChunk, ...],
21
+ control: ExecutionControl | None,
22
+ invalidate: Invalidator,
23
+ ) -> tuple[ExecuteResult, ...]:
24
+ monitor = PostgresAsyncExecutionMonitor(control)
25
+ cursors: list[AsyncCursor] = []
26
+ try:
27
+ await monitor.install(connection, invalidate)
28
+ async with connection.pipeline():
29
+ for chunk in chunks:
30
+ monitor.checkpoint()
31
+ cursors.append(await _send(connection, chunk))
32
+ monitor.checkpoint()
33
+ return tuple(ExecuteResult(max(cursor.rowcount, 0)) for cursor in cursors)
34
+ except psycopg.Error as error:
35
+ monitor.translate_driver_error(error, invalidate)
36
+ finally:
37
+ for cursor in cursors:
38
+ await _close_quietly(cursor)
39
+ await monitor.close(connection, invalidate)
40
+
41
+
42
+ async def _send(connection: AsyncConnection, chunk: BulkChunk) -> AsyncCursor:
43
+ cursor = connection.cursor()
44
+ parameters = adapt_parameters(chunk.statement.parameters)
45
+ await cursor.execute(chunk.statement.sql.encode(), parameters or None)
46
+ return cursor
47
+
48
+
49
+ async def _close_quietly(cursor: AsyncCursor) -> None:
50
+ try:
51
+ await cursor.close()
52
+ except psycopg.Error:
53
+ return
54
+
55
+
56
+ __all__ = ("run_pipeline",)
@@ -0,0 +1,264 @@
1
+ """Asynchronous PostgreSQL cursor execution, timeouts, and cancellation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from asyncio import CancelledError, Event, Task, create_task, shield, wait_for
6
+ from collections.abc import Awaitable, Callable
7
+ from time import monotonic
8
+ from typing import NoReturn, TypeVar
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
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
20
+ AsyncCursor = psycopg.AsyncCursor[tuple[object, ...]]
21
+
22
+ _WATCHDOG_INTERVAL = 0.025
23
+ _MILLISECONDS = 1000
24
+
25
+
26
+ async def await_watchdog(watchdog: Task[None]) -> None:
27
+ """Wait for a watchdog this monitor owns, without inheriting its fate.
28
+
29
+ Cleanup runs while an error is already in flight, so a watchdog that some
30
+ outer shutdown cancelled must not turn a release into a cancellation. Being
31
+ cancelled ourselves is a different matter and still propagates, which is
32
+ what the shield distinguishes: awaiting a task directly would cancel it
33
+ along with us and leave the two cases indistinguishable.
34
+ """
35
+ try:
36
+ await shield(watchdog)
37
+ except CancelledError:
38
+ if not watchdog.cancelled():
39
+ raise
40
+
41
+
42
+ class PostgresAsyncExecutionMonitor:
43
+ __slots__ = ("_control", "_started", "_stop", "_timeout_applied", "_watchdog")
44
+
45
+ def __init__(self, control: ExecutionControl | None) -> None:
46
+ self._control = control or ExecutionControl()
47
+ self._started = monotonic() if self._control.timeout is not None else 0.0
48
+ self._stop = Event()
49
+ self._watchdog: Task[None] | None = None
50
+ self._timeout_applied = False
51
+
52
+ async def install(
53
+ self,
54
+ connection: AsyncConnection,
55
+ invalidate: Invalidator,
56
+ ) -> None:
57
+ self.checkpoint()
58
+ await self._apply_timeout(connection, invalidate)
59
+ if self._control.cancellation_token is None:
60
+ return
61
+ self._watchdog = create_task(self._watch(connection))
62
+
63
+ def checkpoint(self) -> None:
64
+ token = self._control.cancellation_token
65
+ if token is not None and token.cancelled:
66
+ raise QueryCancelledError("PostgreSQL query execution was cancelled")
67
+ if self._expired():
68
+ raise QueryTimeoutError("PostgreSQL query execution exceeded its timeout")
69
+
70
+ def translate_driver_error(
71
+ self,
72
+ error: psycopg.Error,
73
+ invalidate: Invalidator,
74
+ ) -> NoReturn:
75
+ if is_connection_failure(error):
76
+ invalidate()
77
+ self.checkpoint()
78
+ if is_cancellation(error):
79
+ raise QueryCancelledError(
80
+ "PostgreSQL query execution was cancelled"
81
+ ) from error
82
+ _raise_query_execution_error(error)
83
+
84
+ async def close(
85
+ self,
86
+ connection: AsyncConnection,
87
+ invalidate: Invalidator,
88
+ ) -> None:
89
+ self._stop.set()
90
+ watchdog = self._watchdog
91
+ self._watchdog = None
92
+ if watchdog is not None:
93
+ await await_watchdog(watchdog)
94
+ await self._clear_timeout(connection, invalidate)
95
+
96
+ async def _watch(self, connection: AsyncConnection) -> None:
97
+ token = self._control.cancellation_token
98
+ while True:
99
+ try:
100
+ await wait_for(self._stop.wait(), _WATCHDOG_INTERVAL)
101
+ except (TimeoutError, CancelledError):
102
+ if token is not None and token.cancelled:
103
+ await _cancel_quietly(connection)
104
+ return
105
+ continue
106
+ return
107
+
108
+ async def _apply_timeout(
109
+ self,
110
+ connection: AsyncConnection,
111
+ invalidate: Invalidator,
112
+ ) -> None:
113
+ timeout = self._control.timeout
114
+ if timeout is None:
115
+ return
116
+ milliseconds = max(int(timeout * _MILLISECONDS), 1)
117
+ try:
118
+ await connection.execute(f"SET statement_timeout = {milliseconds}".encode())
119
+ except psycopg.Error as error:
120
+ self.translate_driver_error(error, invalidate)
121
+ self._timeout_applied = True
122
+
123
+ async def _clear_timeout(
124
+ self,
125
+ connection: AsyncConnection,
126
+ invalidate: Invalidator,
127
+ ) -> None:
128
+ if not self._timeout_applied:
129
+ return
130
+ self._timeout_applied = False
131
+ try:
132
+ await connection.execute(b"SET statement_timeout = 0")
133
+ except psycopg.Error:
134
+ invalidate()
135
+
136
+ def _expired(self) -> bool:
137
+ timeout = self._control.timeout
138
+ return timeout is not None and monotonic() - self._started >= timeout
139
+
140
+
141
+ class PostgresAsyncCursorSession:
142
+ __slots__ = ("_closed", "_connection", "_cursor", "_invalidate", "_monitor")
143
+
144
+ def __init__(
145
+ self,
146
+ connection: AsyncConnection,
147
+ cursor: AsyncCursor,
148
+ monitor: PostgresAsyncExecutionMonitor,
149
+ invalidate: Invalidator,
150
+ ) -> None:
151
+ self._connection = connection
152
+ self._cursor = cursor
153
+ self._monitor = monitor
154
+ self._invalidate = invalidate
155
+ self._closed = False
156
+
157
+ @property
158
+ def cursor(self) -> AsyncCursor:
159
+ return self._cursor
160
+
161
+ def checkpoint(self) -> None:
162
+ self._monitor.checkpoint()
163
+
164
+ def translate_driver_error(self, error: psycopg.Error) -> NoReturn:
165
+ self._monitor.translate_driver_error(error, self._invalidate)
166
+
167
+ async def fetch_batch(self, size: int) -> list[tuple[object, ...]]:
168
+ try:
169
+ self._monitor.checkpoint()
170
+ rows = await self._cursor.fetchmany(size)
171
+ self._monitor.checkpoint()
172
+ except psycopg.Error as error:
173
+ self._monitor.translate_driver_error(error, self._invalidate)
174
+ return rows
175
+
176
+ async def close(self) -> None:
177
+ if self._closed:
178
+ return
179
+ self._closed = True
180
+ await _close_cursor(self._cursor, self._invalidate)
181
+ await self._monitor.close(self._connection, self._invalidate)
182
+
183
+
184
+ async def run_sql(
185
+ connection: AsyncConnection,
186
+ sql: str,
187
+ parameters: tuple[PostgresParameter, ...],
188
+ operation: Callable[[AsyncCursor], Awaitable[OperationResult]],
189
+ control: ExecutionControl | None,
190
+ invalidate: Invalidator,
191
+ ) -> OperationResult:
192
+ session = await open_cursor(connection, sql, parameters, control, invalidate)
193
+ try:
194
+ result = await operation(session.cursor)
195
+ session.checkpoint()
196
+ return result
197
+ except psycopg.Error as error:
198
+ session.translate_driver_error(error)
199
+ finally:
200
+ await session.close()
201
+
202
+
203
+ async def open_cursor(
204
+ connection: AsyncConnection,
205
+ sql: str,
206
+ parameters: tuple[PostgresParameter, ...],
207
+ control: ExecutionControl | None,
208
+ invalidate: Invalidator,
209
+ *,
210
+ server_side_name: str | None = None,
211
+ ) -> PostgresAsyncCursorSession:
212
+ monitor = PostgresAsyncExecutionMonitor(control)
213
+ cursor: AsyncCursor | None = None
214
+ try:
215
+ await monitor.install(connection, invalidate)
216
+ cursor = connection.cursor(name=server_side_name or "")
217
+ await cursor.execute(sql.encode(), parameters or None)
218
+ monitor.checkpoint()
219
+ return PostgresAsyncCursorSession(connection, cursor, monitor, invalidate)
220
+ except psycopg.Error as error:
221
+ await _release(cursor, monitor, connection, invalidate)
222
+ monitor.translate_driver_error(error, invalidate)
223
+ except BaseException:
224
+ await _release(cursor, monitor, connection, invalidate)
225
+ raise
226
+
227
+
228
+ async def _release(
229
+ cursor: AsyncCursor | None,
230
+ monitor: PostgresAsyncExecutionMonitor,
231
+ connection: AsyncConnection,
232
+ invalidate: Invalidator,
233
+ ) -> None:
234
+ if cursor is not None:
235
+ await _close_cursor(cursor, invalidate)
236
+ await monitor.close(connection, invalidate)
237
+
238
+
239
+ async def _close_cursor(cursor: AsyncCursor, invalidate: Invalidator) -> None:
240
+ try:
241
+ await cursor.close()
242
+ except psycopg.Error:
243
+ invalidate()
244
+
245
+
246
+ async def _cancel_quietly(connection: AsyncConnection) -> None:
247
+ try:
248
+ await connection.cancel_safe()
249
+ except psycopg.Error:
250
+ return
251
+
252
+
253
+ def _raise_query_execution_error(error: psycopg.Error) -> NoReturn:
254
+ message = f"PostgreSQL query execution failed: {error}"
255
+ raise QueryExecutionError(message) from error
256
+
257
+
258
+ __all__ = (
259
+ "PostgresAsyncCursorSession",
260
+ "PostgresAsyncExecutionMonitor",
261
+ "await_watchdog",
262
+ "open_cursor",
263
+ "run_sql",
264
+ )
@@ -0,0 +1,152 @@
1
+ """Asynchronous typed PostgreSQL query execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import TypeVar
7
+
8
+ import psycopg
9
+
10
+ from pyoq.query.execution import (
11
+ BulkPlan,
12
+ CompiledQuery,
13
+ ExecuteResult,
14
+ ExecutionControl,
15
+ StreamPolicy,
16
+ )
17
+ from pyoq.query.execution.aio import AsyncStreamResource
18
+ from pyoq.query.execution.aio.pooling import AsyncConnectionLease
19
+ from pyoq.query.execution.aio.streaming import AsyncRowStream
20
+ from pyoq.query.postgres.aio.bulk import run_pipeline
21
+ from pyoq.query.postgres.aio.commands import open_cursor, run_sql
22
+ from pyoq.query.postgres.aio.operations import PostgresAsyncQueryOperations
23
+ from pyoq.query.postgres.aio.pool import PostgresAsyncPool
24
+ from pyoq.query.postgres.aio.transactions import PostgresAsyncTransaction
25
+ from pyoq.query.postgres.bulk import pipeline_supported
26
+ from pyoq.query.postgres.compiler import PostgresCompiler
27
+ from pyoq.query.postgres.parameters import adapt_parameters
28
+ from pyoq.query.postgres.transactions import TransactionMode
29
+ from pyoq.query.select import Query
30
+
31
+ OperationResult = TypeVar("OperationResult")
32
+ ResultRow = TypeVar("ResultRow")
33
+
34
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
35
+ AsyncCursor = psycopg.AsyncCursor[tuple[object, ...]]
36
+
37
+ _STREAM_CURSOR_PREFIX = "pyoq_stream_"
38
+
39
+
40
+ class PostgresAsyncExecutor(PostgresAsyncQueryOperations):
41
+ __slots__ = ("_pool", "_stream_sequence")
42
+
43
+ def __init__(
44
+ self,
45
+ pool: PostgresAsyncPool,
46
+ compiler: PostgresCompiler | None = None,
47
+ ) -> None:
48
+ self._pool = pool
49
+ self._stream_sequence = 0
50
+ super().__init__(compiler or PostgresCompiler())
51
+
52
+ @property
53
+ def pool(self) -> PostgresAsyncPool:
54
+ return self._pool
55
+
56
+ def stream(
57
+ self,
58
+ query: Query[ResultRow],
59
+ /,
60
+ *,
61
+ policy: StreamPolicy | None = None,
62
+ ) -> AsyncRowStream[ResultRow]:
63
+ selected = policy or StreamPolicy()
64
+ statement = self.compiler.compile(query)
65
+ self._stream_sequence += 1
66
+ name = f"{_STREAM_CURSOR_PREFIX}{self._stream_sequence}"
67
+ return AsyncRowStream(
68
+ lambda: self._open_stream(statement, selected, name),
69
+ selected,
70
+ statement.row_types,
71
+ )
72
+
73
+ async def execute_chunks(
74
+ self,
75
+ plan: BulkPlan,
76
+ control: ExecutionControl | None,
77
+ ) -> tuple[ExecuteResult, ...]:
78
+ if not pipeline_supported(self.compiler.capabilities.pipeline):
79
+ return await super().execute_chunks(plan, control)
80
+ lease = self.pool.checkout()
81
+ async with lease as connection:
82
+ return await run_pipeline(
83
+ connection, plan.chunks, control, lease.invalidate
84
+ )
85
+
86
+ def transaction(
87
+ self,
88
+ mode: TransactionMode = TransactionMode.READ_COMMITTED,
89
+ *,
90
+ control: ExecutionControl | None = None,
91
+ ) -> PostgresAsyncTransaction:
92
+ return PostgresAsyncTransaction(self.pool, self.compiler, mode, control)
93
+
94
+ async def _run(
95
+ self,
96
+ statement: CompiledQuery,
97
+ operation: Callable[[AsyncCursor], Awaitable[OperationResult]],
98
+ control: ExecutionControl | None,
99
+ ) -> OperationResult:
100
+ parameters = adapt_parameters(statement.parameters)
101
+ lease = self.pool.checkout()
102
+ async with lease as connection:
103
+ return await run_sql(
104
+ connection,
105
+ statement.sql,
106
+ parameters,
107
+ operation,
108
+ control,
109
+ lease.invalidate,
110
+ )
111
+
112
+ async def _open_stream(
113
+ self,
114
+ statement: CompiledQuery,
115
+ policy: StreamPolicy,
116
+ name: str,
117
+ ) -> AsyncStreamResource:
118
+ parameters = adapt_parameters(statement.parameters)
119
+ lease = self.pool.checkout()
120
+ connection = await lease.__aenter__()
121
+ try:
122
+ await connection.execute(b"BEGIN")
123
+ session = await open_cursor(
124
+ connection,
125
+ statement.sql,
126
+ parameters,
127
+ policy.control,
128
+ lease.invalidate,
129
+ server_side_name=name,
130
+ )
131
+ except BaseException:
132
+ await _end_stream_transaction(connection, lease)
133
+ raise
134
+ return AsyncStreamResource(
135
+ session,
136
+ lambda: _end_stream_transaction(connection, lease),
137
+ )
138
+
139
+
140
+ async def _end_stream_transaction(
141
+ connection: AsyncConnection,
142
+ lease: AsyncConnectionLease[AsyncConnection],
143
+ ) -> None:
144
+ try:
145
+ await connection.execute(b"ROLLBACK")
146
+ except psycopg.Error:
147
+ lease.invalidate()
148
+ finally:
149
+ await lease.__aexit__(None, None, None)
150
+
151
+
152
+ __all__ = ("PostgresAsyncExecutor",)
@@ -0,0 +1,26 @@
1
+ """Asynchronous PostgreSQL connection creation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import psycopg
8
+
9
+ from pyoq.errors import ConnectionPoolError
10
+
11
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class PostgresAsyncConnectionFactory:
16
+ dsn: str
17
+
18
+ async def __call__(self) -> AsyncConnection:
19
+ try:
20
+ return await psycopg.AsyncConnection.connect(self.dsn, autocommit=True)
21
+ except psycopg.Error as error:
22
+ message = f"unable to open PostgreSQL connection: {error}"
23
+ raise ConnectionPoolError(message) from error
24
+
25
+
26
+ __all__ = ("PostgresAsyncConnectionFactory",)
@@ -0,0 +1,26 @@
1
+ """Asynchronous PostgreSQL typed result operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.execution import BulkPlan, WriteProvider
6
+ from pyoq.query.execution.aio import AsyncQueryOperations
7
+ from pyoq.query.postgres.compiler import PostgresCompiler
8
+ from pyoq.query.postgres.planning import PostgresBulkPlanner
9
+ from pyoq.query.write_nodes import WriteNode
10
+
11
+
12
+ class PostgresAsyncQueryOperations(AsyncQueryOperations):
13
+ __slots__ = ("_compiler",)
14
+
15
+ def __init__(self, compiler: PostgresCompiler) -> None:
16
+ self._compiler = compiler
17
+
18
+ @property
19
+ def compiler(self) -> PostgresCompiler:
20
+ return self._compiler
21
+
22
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
23
+ return PostgresBulkPlanner(self.compiler).plan(statement)
24
+
25
+
26
+ __all__ = ("PostgresAsyncQueryOperations",)
@@ -0,0 +1,40 @@
1
+ """Exclusive asynchronous PostgreSQL connection pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeAlias, cast
6
+
7
+ import psycopg
8
+
9
+ from pyoq.errors import ConnectionPoolError
10
+ from pyoq.query.execution.aio.pooling import (
11
+ AsyncConnectionLease,
12
+ AsyncConnectionPool,
13
+ )
14
+
15
+ AsyncConnection = psycopg.AsyncConnection[tuple[object, ...]]
16
+ PostgresAsyncConnectionLease: TypeAlias = AsyncConnectionLease[AsyncConnection]
17
+
18
+
19
+ class PostgresAsyncPool(AsyncConnectionPool[AsyncConnection]):
20
+ __slots__ = ()
21
+
22
+ async def reset(self, connection: AsyncConnection) -> bool:
23
+ try:
24
+ await connection.rollback()
25
+ await connection.execute(b"SET statement_timeout = 0")
26
+ except psycopg.Error:
27
+ return False
28
+ return not connection.closed
29
+
30
+ def validate(self, value: object) -> AsyncConnection:
31
+ if not isinstance(value, psycopg.AsyncConnection):
32
+ message = "PostgreSQL connection factory returned an invalid connection"
33
+ raise ConnectionPoolError(message)
34
+ return cast("AsyncConnection", value)
35
+
36
+ async def dispose(self, connection: AsyncConnection) -> None:
37
+ await connection.close()
38
+
39
+
40
+ __all__ = ("PostgresAsyncConnectionLease", "PostgresAsyncPool")