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,20 @@
|
|
|
1
|
+
"""PostgreSQL compilation context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.execution import CompilationContext
|
|
6
|
+
from pyoq.query.postgres.model import PostgresCapabilities
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostgresCompilationContext(CompilationContext):
|
|
10
|
+
__slots__ = ("capabilities",)
|
|
11
|
+
|
|
12
|
+
def __init__(self, capabilities: PostgresCapabilities) -> None:
|
|
13
|
+
super().__init__(
|
|
14
|
+
capabilities.maximum_parameters,
|
|
15
|
+
capabilities.parameter_style,
|
|
16
|
+
)
|
|
17
|
+
self.capabilities = capabilities
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("PostgresCompilationContext",)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Synchronous typed PostgreSQL query execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import 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
|
+
RowStream,
|
|
16
|
+
StreamPolicy,
|
|
17
|
+
StreamResource,
|
|
18
|
+
)
|
|
19
|
+
from pyoq.query.execution.pooling import ConnectionLease
|
|
20
|
+
from pyoq.query.postgres.bulk import pipeline_supported, run_pipeline
|
|
21
|
+
from pyoq.query.postgres.commands import open_cursor, run_sql
|
|
22
|
+
from pyoq.query.postgres.compiler import PostgresCompiler
|
|
23
|
+
from pyoq.query.postgres.operations import PostgresQueryOperations
|
|
24
|
+
from pyoq.query.postgres.parameters import adapt_parameters
|
|
25
|
+
from pyoq.query.postgres.pool import PostgresPool
|
|
26
|
+
from pyoq.query.postgres.transactions import PostgresTransaction, TransactionMode
|
|
27
|
+
from pyoq.query.select import Query
|
|
28
|
+
|
|
29
|
+
OperationResult = TypeVar("OperationResult")
|
|
30
|
+
ResultRow = TypeVar("ResultRow")
|
|
31
|
+
|
|
32
|
+
Connection = psycopg.Connection[tuple[object, ...]]
|
|
33
|
+
|
|
34
|
+
_STREAM_CURSOR_PREFIX = "pyoq_stream_"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PostgresExecutor(PostgresQueryOperations):
|
|
38
|
+
__slots__ = ("_pool", "_stream_sequence")
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
pool: PostgresPool,
|
|
43
|
+
compiler: PostgresCompiler | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._pool = pool
|
|
46
|
+
self._stream_sequence = 0
|
|
47
|
+
super().__init__(compiler or PostgresCompiler())
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def pool(self) -> PostgresPool:
|
|
51
|
+
return self._pool
|
|
52
|
+
|
|
53
|
+
def stream(
|
|
54
|
+
self,
|
|
55
|
+
query: Query[ResultRow],
|
|
56
|
+
/,
|
|
57
|
+
*,
|
|
58
|
+
policy: StreamPolicy | None = None,
|
|
59
|
+
) -> RowStream[ResultRow]:
|
|
60
|
+
selected = policy or StreamPolicy()
|
|
61
|
+
statement = self.compiler.compile(query)
|
|
62
|
+
self._stream_sequence += 1
|
|
63
|
+
name = f"{_STREAM_CURSOR_PREFIX}{self._stream_sequence}"
|
|
64
|
+
return RowStream(
|
|
65
|
+
lambda: self._open_stream(statement, selected, name),
|
|
66
|
+
selected,
|
|
67
|
+
statement.row_types,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def execute_chunks(
|
|
71
|
+
self,
|
|
72
|
+
plan: BulkPlan,
|
|
73
|
+
control: ExecutionControl | None,
|
|
74
|
+
) -> tuple[ExecuteResult, ...]:
|
|
75
|
+
if not pipeline_supported(self.compiler.capabilities.pipeline):
|
|
76
|
+
return super().execute_chunks(plan, control)
|
|
77
|
+
lease = self.pool.checkout()
|
|
78
|
+
with lease as connection:
|
|
79
|
+
return run_pipeline(connection, plan.chunks, control, lease.invalidate)
|
|
80
|
+
|
|
81
|
+
def transaction(
|
|
82
|
+
self,
|
|
83
|
+
mode: TransactionMode = TransactionMode.READ_COMMITTED,
|
|
84
|
+
*,
|
|
85
|
+
control: ExecutionControl | None = None,
|
|
86
|
+
) -> PostgresTransaction:
|
|
87
|
+
return PostgresTransaction(self.pool, self.compiler, mode, control)
|
|
88
|
+
|
|
89
|
+
def _run(
|
|
90
|
+
self,
|
|
91
|
+
statement: CompiledQuery,
|
|
92
|
+
operation: Callable[[psycopg.Cursor[tuple[object, ...]]], OperationResult],
|
|
93
|
+
control: ExecutionControl | None,
|
|
94
|
+
) -> OperationResult:
|
|
95
|
+
parameters = adapt_parameters(statement.parameters)
|
|
96
|
+
lease = self.pool.checkout()
|
|
97
|
+
with lease as connection:
|
|
98
|
+
return run_sql(
|
|
99
|
+
connection,
|
|
100
|
+
statement.sql,
|
|
101
|
+
parameters,
|
|
102
|
+
operation,
|
|
103
|
+
control,
|
|
104
|
+
lease.invalidate,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _open_stream(
|
|
108
|
+
self,
|
|
109
|
+
statement: CompiledQuery,
|
|
110
|
+
policy: StreamPolicy,
|
|
111
|
+
name: str,
|
|
112
|
+
) -> StreamResource:
|
|
113
|
+
parameters = adapt_parameters(statement.parameters)
|
|
114
|
+
lease = self.pool.checkout()
|
|
115
|
+
connection = lease.__enter__()
|
|
116
|
+
try:
|
|
117
|
+
connection.execute(b"BEGIN")
|
|
118
|
+
session = open_cursor(
|
|
119
|
+
connection,
|
|
120
|
+
statement.sql,
|
|
121
|
+
parameters,
|
|
122
|
+
policy.control,
|
|
123
|
+
lease.invalidate,
|
|
124
|
+
server_side_name=name,
|
|
125
|
+
)
|
|
126
|
+
except BaseException:
|
|
127
|
+
_end_stream_transaction(connection, lease)
|
|
128
|
+
raise
|
|
129
|
+
return StreamResource(
|
|
130
|
+
session,
|
|
131
|
+
lambda: _end_stream_transaction(connection, lease),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _end_stream_transaction(
|
|
136
|
+
connection: Connection,
|
|
137
|
+
lease: ConnectionLease[Connection],
|
|
138
|
+
) -> None:
|
|
139
|
+
try:
|
|
140
|
+
connection.execute(b"ROLLBACK")
|
|
141
|
+
except psycopg.Error:
|
|
142
|
+
lease.invalidate()
|
|
143
|
+
finally:
|
|
144
|
+
lease.__exit__(None, None, None)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = ("PostgresExecutor",)
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""PostgreSQL scalar expression compilation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from pyoq.config import DatabaseDialect
|
|
9
|
+
from pyoq.errors import UnsupportedQueryError
|
|
10
|
+
from pyoq.query.nodes import (
|
|
11
|
+
ArrayConstructNode,
|
|
12
|
+
ArrayDimensionsNode,
|
|
13
|
+
ArrayElementNode,
|
|
14
|
+
ArrayLengthNode,
|
|
15
|
+
ArrayNode,
|
|
16
|
+
ArrayOperation,
|
|
17
|
+
BinaryNode,
|
|
18
|
+
BinaryOperator,
|
|
19
|
+
BoundValueNode,
|
|
20
|
+
DatePart,
|
|
21
|
+
ExpressionNode,
|
|
22
|
+
ExtractNode,
|
|
23
|
+
FieldNode,
|
|
24
|
+
FunctionName,
|
|
25
|
+
FunctionNode,
|
|
26
|
+
JsonBuild,
|
|
27
|
+
JsonBuildNode,
|
|
28
|
+
JsonContainsNode,
|
|
29
|
+
JsonKeysNode,
|
|
30
|
+
JsonNode,
|
|
31
|
+
JsonOperation,
|
|
32
|
+
JsonWrite,
|
|
33
|
+
JsonWriteNode,
|
|
34
|
+
ScalarFamily,
|
|
35
|
+
)
|
|
36
|
+
from pyoq.query.postgres.context import PostgresCompilationContext
|
|
37
|
+
from pyoq.query.postgres.identifiers import QUOTING
|
|
38
|
+
from pyoq.query.rendering.expressions import (
|
|
39
|
+
CollectionFunctions,
|
|
40
|
+
ExpressionRenderer,
|
|
41
|
+
)
|
|
42
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
43
|
+
from pyoq.query.results import expression_family
|
|
44
|
+
from pyoq.query.select_nodes import ELEMENT_COLUMN, JsonElementsSourceNode
|
|
45
|
+
|
|
46
|
+
_POSTGRES_BINARY_SQL = {
|
|
47
|
+
BinaryOperator.IS_DISTINCT_FROM: "IS DISTINCT FROM",
|
|
48
|
+
BinaryOperator.IS_NOT_DISTINCT_FROM: "IS NOT DISTINCT FROM",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_DATE_FIELD = {
|
|
52
|
+
DatePart.YEAR: "YEAR",
|
|
53
|
+
DatePart.MONTH: "MONTH",
|
|
54
|
+
DatePart.DAY: "DAY",
|
|
55
|
+
DatePart.HOUR: "HOUR",
|
|
56
|
+
DatePart.MINUTE: "MINUTE",
|
|
57
|
+
DatePart.SECOND: "SECOND",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_ARRAY_FUNCTIONS: Mapping[ArrayOperation, str] = {
|
|
62
|
+
ArrayOperation.APPEND: "ARRAY_APPEND",
|
|
63
|
+
ArrayOperation.PREPEND: "ARRAY_PREPEND",
|
|
64
|
+
ArrayOperation.CONCAT: "ARRAY_CAT",
|
|
65
|
+
ArrayOperation.REMOVE: "ARRAY_REMOVE",
|
|
66
|
+
ArrayOperation.REPLACE: "ARRAY_REPLACE",
|
|
67
|
+
}
|
|
68
|
+
"""What PostgreSQL calls each way of making one array out of another."""
|
|
69
|
+
|
|
70
|
+
_ARRAY_OPERATORS: Mapping[ArrayOperation, str] = {
|
|
71
|
+
ArrayOperation.CONTAINS: "@>",
|
|
72
|
+
ArrayOperation.CONTAINED_BY: "<@",
|
|
73
|
+
ArrayOperation.OVERLAPS: "&&",
|
|
74
|
+
}
|
|
75
|
+
"""Verified against a running server, which is where these came from."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_NO_MERGE_PATCH = (
|
|
79
|
+
"PostgreSQL has no merge-patch; its concatenation replaces a member whole "
|
|
80
|
+
"and keeps a null rather than removing one, which json_concat says plainly"
|
|
81
|
+
)
|
|
82
|
+
"""Refused rather than emulated, because one level is not all the way down."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PostgresExpressionCompiler(ExpressionRenderer[PostgresCompilationContext]):
|
|
86
|
+
__slots__ = ()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def collection_functions(self) -> CollectionFunctions:
|
|
90
|
+
return CollectionFunctions(
|
|
91
|
+
"jsonb_agg",
|
|
92
|
+
"jsonb_build_object",
|
|
93
|
+
"'[]'::jsonb",
|
|
94
|
+
"jsonb_build_array",
|
|
95
|
+
"jsonb_object_agg",
|
|
96
|
+
ordered=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def dialect(self) -> str:
|
|
101
|
+
return "PostgreSQL"
|
|
102
|
+
|
|
103
|
+
def qualified_field(self, node: FieldNode) -> str:
|
|
104
|
+
return QUOTING.qualified(
|
|
105
|
+
node.name,
|
|
106
|
+
table=node.table,
|
|
107
|
+
schema=node.schema,
|
|
108
|
+
catalog=node.catalog,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def binary_operator_sql(self, operator: BinaryOperator) -> str | None:
|
|
112
|
+
override = _POSTGRES_BINARY_SQL.get(operator)
|
|
113
|
+
if override is not None:
|
|
114
|
+
return override
|
|
115
|
+
return super().binary_operator_sql(operator)
|
|
116
|
+
|
|
117
|
+
def validate_binary(self, node: BinaryNode) -> None:
|
|
118
|
+
if node.operator is not BinaryOperator.ADD:
|
|
119
|
+
return
|
|
120
|
+
families = (
|
|
121
|
+
expression_family(node.left),
|
|
122
|
+
expression_family(node.right),
|
|
123
|
+
)
|
|
124
|
+
if all(family is ScalarFamily.TEMPORAL for family in families):
|
|
125
|
+
message = "PostgreSQL cannot add two temporal values"
|
|
126
|
+
raise UnsupportedQueryError(message)
|
|
127
|
+
|
|
128
|
+
def render_array(self, node: ArrayNode) -> str:
|
|
129
|
+
subject = self.compile(node.expression)
|
|
130
|
+
written_operands = [self.compile(item) for item in node.operands]
|
|
131
|
+
operand = written_operands[0]
|
|
132
|
+
if node.operation is ArrayOperation.HAS:
|
|
133
|
+
return f"({operand} = ANY({subject}))"
|
|
134
|
+
if node.operation is ArrayOperation.LACKS:
|
|
135
|
+
return f"({operand} <> ALL({subject}))"
|
|
136
|
+
if node.operation in _ARRAY_FUNCTIONS:
|
|
137
|
+
written = _ARRAY_FUNCTIONS[node.operation]
|
|
138
|
+
if node.operation is ArrayOperation.PREPEND:
|
|
139
|
+
return f"{written}({operand}, {subject})"
|
|
140
|
+
values = ", ".join(written_operands)
|
|
141
|
+
return f"{written}({subject}, {values})"
|
|
142
|
+
return f"({subject} {_ARRAY_OPERATORS[node.operation]} {operand})"
|
|
143
|
+
|
|
144
|
+
def render_array_element(self, node: ArrayElementNode) -> str:
|
|
145
|
+
"""PostgreSQL counts an array from one, and a tuple counts from zero.
|
|
146
|
+
|
|
147
|
+
The array is bracketed because PostgreSQL will not subscript the
|
|
148
|
+
answer of a function without it, and the array here is often one.
|
|
149
|
+
Every dimension is subscripted at once, because PostgreSQL reads one
|
|
150
|
+
subscript of an array of arrays as nothing at all.
|
|
151
|
+
"""
|
|
152
|
+
subject = self.compile(node.expression)
|
|
153
|
+
positions = "".join(f"[{self.compile(index)} + 1]" for index in node.indexes)
|
|
154
|
+
return f"({subject}){positions}"
|
|
155
|
+
|
|
156
|
+
def render_array_length(self, node: ArrayLengthNode) -> str:
|
|
157
|
+
subject = self.compile(node.expression)
|
|
158
|
+
if node.dimension is None:
|
|
159
|
+
return f"CARDINALITY({subject})"
|
|
160
|
+
return f"ARRAY_LENGTH({subject}, {node.dimension})"
|
|
161
|
+
|
|
162
|
+
def render_array_construct(self, node: ArrayConstructNode) -> str:
|
|
163
|
+
values = ", ".join(self.compile(element) for element in node.elements)
|
|
164
|
+
return f"ARRAY[{values}]"
|
|
165
|
+
|
|
166
|
+
def render_array_dimensions(self, node: ArrayDimensionsNode) -> str:
|
|
167
|
+
return f"ARRAY_NDIMS({self.compile(node.expression)})"
|
|
168
|
+
|
|
169
|
+
def render_json_elements(self, node: JsonElementsSourceNode) -> str:
|
|
170
|
+
"""PostgreSQL expands an array into rows and lets the column be named."""
|
|
171
|
+
subject = self.compile(node.expression)
|
|
172
|
+
alias = QUOTING.quote(node.alias)
|
|
173
|
+
return f"JSONB_ARRAY_ELEMENTS({subject}) AS {alias}({ELEMENT_COLUMN})"
|
|
174
|
+
|
|
175
|
+
def render_json_keys(self, node: JsonKeysNode) -> str:
|
|
176
|
+
"""PostgreSQL gives back one row per key, and nothing for an empty object."""
|
|
177
|
+
subject = self.compile(node.expression)
|
|
178
|
+
return f"JSONB_PATH_QUERY_ARRAY({subject}, '$.keyvalue().key')"
|
|
179
|
+
|
|
180
|
+
def render_json(self, node: JsonNode) -> str:
|
|
181
|
+
"""PostgreSQL takes a path as steps, and reads a written one as a key.
|
|
182
|
+
|
|
183
|
+
`#>` and `#>>` walk a list of step names, which is why the steps are
|
|
184
|
+
bound as an array rather than joined into the `$.a.b` text the other
|
|
185
|
+
two dialects want. Given that text, PostgreSQL would look for a member
|
|
186
|
+
actually called `$.a.b` and answer null without complaining.
|
|
187
|
+
"""
|
|
188
|
+
subject = self.compile(node.expression)
|
|
189
|
+
if node.operation is JsonOperation.REMOVE:
|
|
190
|
+
return f"({subject} #- {self._json_steps(node.path)})"
|
|
191
|
+
if node.operation is JsonOperation.EXISTS:
|
|
192
|
+
return f"({subject} #> {self._json_steps(node.path)} IS NOT NULL)"
|
|
193
|
+
if node.operation is JsonOperation.LENGTH:
|
|
194
|
+
return f"JSONB_ARRAY_LENGTH({subject} #> {self._json_steps(node.path)})"
|
|
195
|
+
operator = "#>>" if node.operation is JsonOperation.TEXT else "#>"
|
|
196
|
+
return f"({subject} {operator} {self._json_steps(node.path)})"
|
|
197
|
+
|
|
198
|
+
def _json_steps(self, path: tuple[str | int, ...]) -> str:
|
|
199
|
+
steps = tuple(str(step) for step in path)
|
|
200
|
+
return self._context.bind(BoundValueNode(steps, ScalarFamily.OTHER))
|
|
201
|
+
|
|
202
|
+
def render_json_write(self, node: JsonWriteNode) -> str:
|
|
203
|
+
"""Compiled in the order it is written, because binding follows that."""
|
|
204
|
+
subject = self.compile(node.expression)
|
|
205
|
+
if node.operation is JsonWrite.MERGE:
|
|
206
|
+
raise UnsupportedQueryError(_NO_MERGE_PATCH)
|
|
207
|
+
if node.operation is JsonWrite.CONCAT:
|
|
208
|
+
return f"({subject} || {self._as_json(node.value)})"
|
|
209
|
+
if node.operation is JsonWrite.INSERT:
|
|
210
|
+
return self._json_inserted(node, subject)
|
|
211
|
+
steps = self._json_steps(node.path)
|
|
212
|
+
value = self._as_json(node.value)
|
|
213
|
+
created = "false" if node.operation is JsonWrite.REPLACE else "true"
|
|
214
|
+
return f"JSONB_SET({subject}, {steps}, {value}, {created})"
|
|
215
|
+
|
|
216
|
+
def _json_inserted(self, node: JsonWriteNode, subject: str) -> str:
|
|
217
|
+
"""Written only where nothing is, the way the other dialects insert.
|
|
218
|
+
|
|
219
|
+
PostgreSQL's own insert raises where a member already exists, while
|
|
220
|
+
SQLite and MySQL leave it. Asking first is what makes one word mean
|
|
221
|
+
one thing on all three.
|
|
222
|
+
"""
|
|
223
|
+
present = self._json_steps(node.path)
|
|
224
|
+
steps = self._json_steps(node.path)
|
|
225
|
+
value = self._as_json(node.value)
|
|
226
|
+
return (
|
|
227
|
+
f"CASE WHEN ({subject} #> {present}) IS NULL "
|
|
228
|
+
f"THEN JSONB_SET({subject}, {steps}, {value}, true) "
|
|
229
|
+
f"ELSE {subject} END"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _as_json(self, node: ExpressionNode) -> str:
|
|
233
|
+
"""A value as JSON, whatever it arrived as.
|
|
234
|
+
|
|
235
|
+
A cast will not turn a bound integer into JSON, because the driver
|
|
236
|
+
sends it as a smallint and PostgreSQL has no cast from one to the
|
|
237
|
+
other, and a bare parameter has no type to infer from. A value is
|
|
238
|
+
therefore written as the JSON text of itself and cast from there,
|
|
239
|
+
which every JSON value has.
|
|
240
|
+
"""
|
|
241
|
+
if not isinstance(node, BoundValueNode):
|
|
242
|
+
return f"({self.compile(node)})::jsonb"
|
|
243
|
+
written = json.dumps(node.value, ensure_ascii=False, separators=(",", ":"))
|
|
244
|
+
placeholder = self._context.bind(
|
|
245
|
+
BoundValueNode(written, ScalarFamily.STRING, node.sensitive)
|
|
246
|
+
)
|
|
247
|
+
return f"{placeholder}::jsonb"
|
|
248
|
+
|
|
249
|
+
def render_json_build(self, node: JsonBuildNode) -> str:
|
|
250
|
+
"""PostgreSQL infers nothing from a bare parameter, so each one says.
|
|
251
|
+
|
|
252
|
+
A builder takes any type at all, which leaves a bound value with
|
|
253
|
+
nothing to be inferred from. Members are written as text and values
|
|
254
|
+
as the JSON of themselves, which is what they are being built into.
|
|
255
|
+
"""
|
|
256
|
+
functions = self.collection_functions
|
|
257
|
+
if node.kind is JsonBuild.ARRAY:
|
|
258
|
+
values = ", ".join(self._as_json(item) for item in node.arguments)
|
|
259
|
+
return f"{functions.array_constructor}({values})"
|
|
260
|
+
members = ", ".join(
|
|
261
|
+
self._as_text(item) if position % 2 == 0 else self._as_json(item)
|
|
262
|
+
for position, item in enumerate(node.arguments)
|
|
263
|
+
)
|
|
264
|
+
return f"{functions.object_function}({members})"
|
|
265
|
+
|
|
266
|
+
def _as_text(self, node: ExpressionNode) -> str:
|
|
267
|
+
"""A member name, which PostgreSQL takes as text and nothing else."""
|
|
268
|
+
return f"({self.compile(node)})::text"
|
|
269
|
+
|
|
270
|
+
def render_json_contains(self, node: JsonContainsNode) -> str:
|
|
271
|
+
subject = self.compile(node.expression)
|
|
272
|
+
return f"({subject} @> {self.compile(node.value)}::jsonb)"
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def quoting(self) -> IdentifierQuoting:
|
|
276
|
+
return QUOTING
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def database_dialect(self) -> DatabaseDialect:
|
|
280
|
+
return DatabaseDialect.POSTGRES
|
|
281
|
+
|
|
282
|
+
def render_extract(self, node: ExtractNode) -> str:
|
|
283
|
+
field = _DATE_FIELD.get(node.part)
|
|
284
|
+
if field is None:
|
|
285
|
+
message = f"unsupported PostgreSQL date part: {node.part}"
|
|
286
|
+
raise UnsupportedQueryError(message)
|
|
287
|
+
expression = self.compile(node.expression)
|
|
288
|
+
return f"CAST(EXTRACT({field} FROM {expression}) AS INTEGER)"
|
|
289
|
+
|
|
290
|
+
def render_string_predicate(self, node: FunctionNode) -> str:
|
|
291
|
+
left_node, right_node = node.arguments
|
|
292
|
+
left = self.compile(left_node)
|
|
293
|
+
right = self.compile(right_node)
|
|
294
|
+
if node.name is FunctionName.CONTAINS:
|
|
295
|
+
return f"(POSITION({right} IN {left}) > 0)"
|
|
296
|
+
if node.name is FunctionName.STARTS_WITH:
|
|
297
|
+
comparison = self.compile(right_node)
|
|
298
|
+
return f"(LEFT({left}, LENGTH({right})) = {comparison})"
|
|
299
|
+
if node.name is FunctionName.ENDS_WITH:
|
|
300
|
+
comparison = self.compile(right_node)
|
|
301
|
+
return f"(RIGHT({left}, LENGTH({right})) = {comparison})"
|
|
302
|
+
message = f"unsupported PostgreSQL function: {node.name}"
|
|
303
|
+
raise UnsupportedQueryError(message)
|
|
304
|
+
|
|
305
|
+
def require_raw_expressions(self) -> None:
|
|
306
|
+
if not self._context.capabilities.raw_expressions:
|
|
307
|
+
message = "PostgreSQL raw expressions are disabled by compiler capabilities"
|
|
308
|
+
raise UnsupportedQueryError(message)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
__all__ = ("PostgresExpressionCompiler",)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PostgreSQL runtime 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
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class PostgresConnectionFactory:
|
|
14
|
+
dsn: str
|
|
15
|
+
|
|
16
|
+
def __call__(self) -> psycopg.Connection[tuple[object, ...]]:
|
|
17
|
+
try:
|
|
18
|
+
return psycopg.connect(self.dsn, autocommit=True)
|
|
19
|
+
except psycopg.Error as error:
|
|
20
|
+
message = f"unable to open PostgreSQL connection: {error}"
|
|
21
|
+
raise ConnectionPoolError(message) from error
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ("PostgresConnectionFactory",)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PostgreSQL connection failure classification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import psycopg
|
|
6
|
+
|
|
7
|
+
_FATAL_STATE_CLASSES = frozenset({"08", "53", "57P", "58", "XX"})
|
|
8
|
+
_CANCELLED_STATE = "57014"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_connection_failure(error: psycopg.Error) -> bool:
|
|
12
|
+
if isinstance(error, (psycopg.OperationalError, psycopg.InterfaceError)):
|
|
13
|
+
return not is_cancellation(error)
|
|
14
|
+
state = error.sqlstate
|
|
15
|
+
if state is None:
|
|
16
|
+
return False
|
|
17
|
+
return any(state.startswith(prefix) for prefix in _FATAL_STATE_CLASSES)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_cancellation(error: psycopg.Error) -> bool:
|
|
21
|
+
return error.sqlstate == _CANCELLED_STATE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ("is_cancellation", "is_connection_failure")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""PostgreSQL compilation capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import QueryValidationError
|
|
8
|
+
from pyoq.query.execution import ParameterStyle
|
|
9
|
+
from pyoq.relations.planning import FetchCapabilities
|
|
10
|
+
|
|
11
|
+
_EXTENDED_PROTOCOL_PARAMETERS = 65535
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class PostgresCapabilities:
|
|
16
|
+
maximum_parameters: int = _EXTENDED_PROTOCOL_PARAMETERS
|
|
17
|
+
parameter_style: ParameterStyle = ParameterStyle.FORMAT
|
|
18
|
+
right_join: bool = True
|
|
19
|
+
full_join: bool = True
|
|
20
|
+
null_ordering: bool = True
|
|
21
|
+
lateral_sources: bool = True
|
|
22
|
+
recursive_common_tables: bool = True
|
|
23
|
+
raw_expressions: bool = True
|
|
24
|
+
nested_collections: bool = True
|
|
25
|
+
ordered_nested_collections: bool = True
|
|
26
|
+
returning: bool = True
|
|
27
|
+
upsert: bool = True
|
|
28
|
+
pipeline: bool = True
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def fetch(self) -> FetchCapabilities:
|
|
32
|
+
"""What this dialect can do when fetching a collection in one query."""
|
|
33
|
+
return FetchCapabilities(
|
|
34
|
+
self.nested_collections,
|
|
35
|
+
self.ordered_nested_collections,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
if not _is_positive_integer(self.maximum_parameters):
|
|
40
|
+
message = "PostgreSQL maximum parameters must be a positive integer"
|
|
41
|
+
raise QueryValidationError(message)
|
|
42
|
+
if self.maximum_parameters > _EXTENDED_PROTOCOL_PARAMETERS:
|
|
43
|
+
message = (
|
|
44
|
+
"PostgreSQL cannot bind more than "
|
|
45
|
+
f"{_EXTENDED_PROTOCOL_PARAMETERS} parameters in one statement"
|
|
46
|
+
)
|
|
47
|
+
raise QueryValidationError(message)
|
|
48
|
+
if not _is_parameter_style(self.parameter_style):
|
|
49
|
+
message = "PostgreSQL parameter style must be a ParameterStyle value"
|
|
50
|
+
raise QueryValidationError(message)
|
|
51
|
+
feature_flags: tuple[object, ...] = (
|
|
52
|
+
self.right_join,
|
|
53
|
+
self.full_join,
|
|
54
|
+
self.null_ordering,
|
|
55
|
+
self.lateral_sources,
|
|
56
|
+
self.recursive_common_tables,
|
|
57
|
+
self.raw_expressions,
|
|
58
|
+
self.nested_collections,
|
|
59
|
+
self.ordered_nested_collections,
|
|
60
|
+
self.returning,
|
|
61
|
+
self.upsert,
|
|
62
|
+
self.pipeline,
|
|
63
|
+
)
|
|
64
|
+
if not _all_boolean(feature_flags):
|
|
65
|
+
message = "PostgreSQL capability flags must be boolean"
|
|
66
|
+
raise QueryValidationError(message)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_parameter_style(value: object) -> bool:
|
|
70
|
+
return isinstance(value, ParameterStyle)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_positive_integer(value: object) -> bool:
|
|
74
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _all_boolean(values: tuple[object, ...]) -> bool:
|
|
78
|
+
return all(isinstance(value, bool) for value in values)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ("PostgresCapabilities",)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PostgreSQL typed result operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.execution import BulkPlan, QueryOperations, WriteProvider
|
|
6
|
+
from pyoq.query.postgres.compiler import PostgresCompiler
|
|
7
|
+
from pyoq.query.postgres.planning import PostgresBulkPlanner
|
|
8
|
+
from pyoq.query.write_nodes import WriteNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PostgresQueryOperations(QueryOperations):
|
|
12
|
+
__slots__ = ("_compiler",)
|
|
13
|
+
|
|
14
|
+
def __init__(self, compiler: PostgresCompiler) -> None:
|
|
15
|
+
self._compiler = compiler
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def compiler(self) -> PostgresCompiler:
|
|
19
|
+
return self._compiler
|
|
20
|
+
|
|
21
|
+
def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
|
|
22
|
+
return PostgresBulkPlanner(self.compiler).plan(statement)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ("PostgresQueryOperations",)
|