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.
- pyoq/__init__.py +7 -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 +19 -0
- pyoq/config/loader.py +204 -0
- pyoq/config/models.py +243 -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 +72 -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 +62 -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 +19 -0
- pyoq/serving/databases.py +107 -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.0.dist-info/METADATA +3034 -0
- pyoq_sql-1.0.0.dist-info/RECORD +264 -0
- pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
- 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",)
|