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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- 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")
|