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,71 @@
|
|
|
1
|
+
"""PostgreSQL driver parameter adaptation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime, time, timedelta
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import cast
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from psycopg.types.json import Jsonb
|
|
12
|
+
|
|
13
|
+
from pyoq.errors import ParameterBindingError
|
|
14
|
+
|
|
15
|
+
PostgresParameter = (
|
|
16
|
+
str
|
|
17
|
+
| int
|
|
18
|
+
| float
|
|
19
|
+
| bool
|
|
20
|
+
| bytes
|
|
21
|
+
| Decimal
|
|
22
|
+
| date
|
|
23
|
+
| datetime
|
|
24
|
+
| time
|
|
25
|
+
| timedelta
|
|
26
|
+
| UUID
|
|
27
|
+
| list[object]
|
|
28
|
+
| Jsonb
|
|
29
|
+
| None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_SCALAR_TYPES = (
|
|
33
|
+
str,
|
|
34
|
+
int,
|
|
35
|
+
float,
|
|
36
|
+
bool,
|
|
37
|
+
bytes,
|
|
38
|
+
Decimal,
|
|
39
|
+
datetime,
|
|
40
|
+
date,
|
|
41
|
+
time,
|
|
42
|
+
timedelta,
|
|
43
|
+
UUID,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def adapt_parameters(values: tuple[object, ...]) -> tuple[PostgresParameter, ...]:
|
|
48
|
+
return tuple(adapt_parameter(value) for value in values)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def adapt_parameter(value: object) -> PostgresParameter:
|
|
52
|
+
"""One value as its driver carries it.
|
|
53
|
+
|
|
54
|
+
An array is a tuple and JSON is a dict or a list, which is the mapping
|
|
55
|
+
generation writes and the one the expression families agree with. The two
|
|
56
|
+
are told apart here because the driver sends a sequence as an array and
|
|
57
|
+
has no way to guess that a list was meant as JSON.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(value, Enum):
|
|
60
|
+
return adapt_parameter(value.value)
|
|
61
|
+
if value is None or isinstance(value, _SCALAR_TYPES):
|
|
62
|
+
return value
|
|
63
|
+
if isinstance(value, tuple):
|
|
64
|
+
return [adapt_parameter(item) for item in cast("tuple[object, ...]", value)]
|
|
65
|
+
if isinstance(value, (dict, list)):
|
|
66
|
+
return Jsonb(value)
|
|
67
|
+
message = f"unsupported PostgreSQL parameter type: {type(value).__name__}"
|
|
68
|
+
raise ParameterBindingError(message)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ("PostgresParameter", "adapt_parameter", "adapt_parameters")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PostgreSQL bulk write planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.execution.planning import BulkPlanner
|
|
6
|
+
from pyoq.query.postgres.compiler import PostgresCompiler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostgresBulkPlanner(BulkPlanner):
|
|
10
|
+
__slots__ = ()
|
|
11
|
+
|
|
12
|
+
def __init__(self, compiler: PostgresCompiler) -> None:
|
|
13
|
+
super().__init__(
|
|
14
|
+
compiler,
|
|
15
|
+
compiler.capabilities.maximum_parameters,
|
|
16
|
+
"PostgreSQL",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("PostgresBulkPlanner",)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Exclusive synchronous PostgreSQL connection pool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TypeAlias, cast
|
|
7
|
+
|
|
8
|
+
import psycopg
|
|
9
|
+
|
|
10
|
+
from pyoq.errors import ConnectionPoolError
|
|
11
|
+
from pyoq.query.execution.pooling import (
|
|
12
|
+
ConnectionLease,
|
|
13
|
+
ConnectionPool,
|
|
14
|
+
PoolPolicy,
|
|
15
|
+
PoolStats,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
Connection = psycopg.Connection[tuple[object, ...]]
|
|
19
|
+
PostgresConnectionFactoryProtocol = Callable[[], Connection]
|
|
20
|
+
PostgresConnectionLease: TypeAlias = ConnectionLease[Connection]
|
|
21
|
+
PostgresPoolPolicy: TypeAlias = PoolPolicy
|
|
22
|
+
PostgresPoolStats: TypeAlias = PoolStats
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PostgresPool(ConnectionPool[Connection]):
|
|
26
|
+
__slots__ = ()
|
|
27
|
+
|
|
28
|
+
def reset(self, connection: Connection) -> bool:
|
|
29
|
+
try:
|
|
30
|
+
connection.rollback()
|
|
31
|
+
connection.execute(b"SET statement_timeout = 0")
|
|
32
|
+
except psycopg.Error:
|
|
33
|
+
return False
|
|
34
|
+
return not connection.closed
|
|
35
|
+
|
|
36
|
+
def validate(self, value: object) -> Connection:
|
|
37
|
+
if not isinstance(value, psycopg.Connection):
|
|
38
|
+
message = "PostgreSQL connection factory returned an invalid connection"
|
|
39
|
+
raise ConnectionPoolError(message)
|
|
40
|
+
return cast("Connection", value)
|
|
41
|
+
|
|
42
|
+
def dispose(self, connection: Connection) -> None:
|
|
43
|
+
connection.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = (
|
|
47
|
+
"PostgresConnectionFactoryProtocol",
|
|
48
|
+
"PostgresConnectionLease",
|
|
49
|
+
"PostgresPool",
|
|
50
|
+
"PostgresPoolPolicy",
|
|
51
|
+
"PostgresPoolStats",
|
|
52
|
+
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Owned PostgreSQL transactions and nested savepoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import psycopg
|
|
10
|
+
|
|
11
|
+
from pyoq.errors import ExecutionError, QueryValidationError, TransactionStateError
|
|
12
|
+
from pyoq.query.execution import (
|
|
13
|
+
BulkPlan,
|
|
14
|
+
CompiledQuery,
|
|
15
|
+
ExecuteResult,
|
|
16
|
+
ExecutionControl,
|
|
17
|
+
RowStream,
|
|
18
|
+
StreamPolicy,
|
|
19
|
+
StreamResource,
|
|
20
|
+
TransactionState,
|
|
21
|
+
)
|
|
22
|
+
from pyoq.query.postgres.bulk import pipeline_supported, run_pipeline
|
|
23
|
+
from pyoq.query.postgres.commands import open_cursor, run_sql
|
|
24
|
+
from pyoq.query.postgres.compiler import PostgresCompiler
|
|
25
|
+
from pyoq.query.postgres.operations import PostgresQueryOperations
|
|
26
|
+
from pyoq.query.postgres.parameters import adapt_parameters
|
|
27
|
+
from pyoq.query.postgres.pool import PostgresPool
|
|
28
|
+
from pyoq.query.select import Query
|
|
29
|
+
|
|
30
|
+
OperationResult = TypeVar("OperationResult")
|
|
31
|
+
ResultRow = TypeVar("ResultRow")
|
|
32
|
+
|
|
33
|
+
Connection = psycopg.Connection[tuple[object, ...]]
|
|
34
|
+
_PostgresTransactionState = TransactionState[Connection, "PostgresTransaction"]
|
|
35
|
+
|
|
36
|
+
_STREAM_CURSOR_PREFIX = "pyoq_stream_"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TransactionMode(Enum):
|
|
40
|
+
READ_COMMITTED = "ISOLATION LEVEL READ COMMITTED"
|
|
41
|
+
REPEATABLE_READ = "ISOLATION LEVEL REPEATABLE READ"
|
|
42
|
+
SERIALIZABLE = "ISOLATION LEVEL SERIALIZABLE"
|
|
43
|
+
READ_ONLY = "ISOLATION LEVEL READ COMMITTED READ ONLY"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PostgresTransaction(PostgresQueryOperations):
|
|
47
|
+
__slots__ = (
|
|
48
|
+
"_control",
|
|
49
|
+
"_entered",
|
|
50
|
+
"_finished",
|
|
51
|
+
"_mode",
|
|
52
|
+
"_parent",
|
|
53
|
+
"_pool",
|
|
54
|
+
"_savepoint_name",
|
|
55
|
+
"_state",
|
|
56
|
+
"_stream_sequence",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
pool: PostgresPool,
|
|
62
|
+
compiler: PostgresCompiler,
|
|
63
|
+
mode: TransactionMode = TransactionMode.READ_COMMITTED,
|
|
64
|
+
control: ExecutionControl | None = None,
|
|
65
|
+
*,
|
|
66
|
+
parent: PostgresTransaction | None = None,
|
|
67
|
+
state: _PostgresTransactionState | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
super().__init__(compiler)
|
|
70
|
+
if not _is_transaction_mode(mode):
|
|
71
|
+
raise QueryValidationError("PostgreSQL transaction mode is invalid")
|
|
72
|
+
self._pool = pool
|
|
73
|
+
self._mode = mode
|
|
74
|
+
self._control = control
|
|
75
|
+
self._parent = parent
|
|
76
|
+
self._state = state or TransactionState()
|
|
77
|
+
self._savepoint_name: str | None = None
|
|
78
|
+
self._stream_sequence = 0
|
|
79
|
+
self._entered = False
|
|
80
|
+
self._finished = False
|
|
81
|
+
|
|
82
|
+
def __enter__(self) -> PostgresTransaction:
|
|
83
|
+
if self._entered or self._finished:
|
|
84
|
+
raise TransactionStateError("transaction cannot be entered twice")
|
|
85
|
+
if self._parent is None:
|
|
86
|
+
self._enter_outer()
|
|
87
|
+
else:
|
|
88
|
+
self._enter_savepoint()
|
|
89
|
+
self._entered = True
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __exit__(
|
|
93
|
+
self,
|
|
94
|
+
exception_type: type[BaseException] | None,
|
|
95
|
+
exception: BaseException | None,
|
|
96
|
+
traceback: object,
|
|
97
|
+
) -> None:
|
|
98
|
+
if not self._entered or self._finished:
|
|
99
|
+
return
|
|
100
|
+
if self._parent is None:
|
|
101
|
+
self._exit_outer(exception_type)
|
|
102
|
+
else:
|
|
103
|
+
self._exit_savepoint(exception_type)
|
|
104
|
+
|
|
105
|
+
def savepoint(
|
|
106
|
+
self,
|
|
107
|
+
*,
|
|
108
|
+
control: ExecutionControl | None = None,
|
|
109
|
+
) -> PostgresTransaction:
|
|
110
|
+
self._state.require_current(self)
|
|
111
|
+
return PostgresTransaction(
|
|
112
|
+
self._pool,
|
|
113
|
+
self.compiler,
|
|
114
|
+
self._mode,
|
|
115
|
+
control or self._control,
|
|
116
|
+
parent=self,
|
|
117
|
+
state=self._state,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def execute_chunks(
|
|
121
|
+
self,
|
|
122
|
+
plan: BulkPlan,
|
|
123
|
+
control: ExecutionControl | None,
|
|
124
|
+
) -> tuple[ExecuteResult, ...]:
|
|
125
|
+
if not pipeline_supported(self.compiler.capabilities.pipeline):
|
|
126
|
+
return super().execute_chunks(plan, control)
|
|
127
|
+
connection = self._state.require_current(self)
|
|
128
|
+
return run_pipeline(
|
|
129
|
+
connection,
|
|
130
|
+
plan.chunks,
|
|
131
|
+
control or self._control,
|
|
132
|
+
self._state.invalidate,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def stream(
|
|
136
|
+
self,
|
|
137
|
+
query: Query[ResultRow],
|
|
138
|
+
/,
|
|
139
|
+
*,
|
|
140
|
+
policy: StreamPolicy | None = None,
|
|
141
|
+
) -> RowStream[ResultRow]:
|
|
142
|
+
self._state.require_current(self)
|
|
143
|
+
selected = policy or StreamPolicy(control=self._control)
|
|
144
|
+
if selected.control is None and self._control is not None:
|
|
145
|
+
selected = StreamPolicy(selected.batch_size, self._control)
|
|
146
|
+
statement = self.compiler.compile(query)
|
|
147
|
+
stream: RowStream[ResultRow]
|
|
148
|
+
stream = RowStream(
|
|
149
|
+
lambda: self._open_stream(stream, statement, selected),
|
|
150
|
+
selected,
|
|
151
|
+
statement.row_types,
|
|
152
|
+
)
|
|
153
|
+
return stream
|
|
154
|
+
|
|
155
|
+
def _run(
|
|
156
|
+
self,
|
|
157
|
+
statement: CompiledQuery,
|
|
158
|
+
operation: Callable[[psycopg.Cursor[tuple[object, ...]]], OperationResult],
|
|
159
|
+
control: ExecutionControl | None,
|
|
160
|
+
) -> OperationResult:
|
|
161
|
+
parameters = adapt_parameters(statement.parameters)
|
|
162
|
+
connection = self._state.require_current(self)
|
|
163
|
+
return run_sql(
|
|
164
|
+
connection,
|
|
165
|
+
statement.sql,
|
|
166
|
+
parameters,
|
|
167
|
+
operation,
|
|
168
|
+
control or self._control,
|
|
169
|
+
self._state.invalidate,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _enter_outer(self) -> None:
|
|
173
|
+
self._state.adopt(self._pool.checkout(), self)
|
|
174
|
+
try:
|
|
175
|
+
self._run_transaction_sql(f"BEGIN {self._mode.value}")
|
|
176
|
+
except BaseException:
|
|
177
|
+
self._release_outer()
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def _enter_savepoint(self) -> None:
|
|
181
|
+
self._state.require_current(cast("PostgresTransaction", self._parent))
|
|
182
|
+
name = self._state.next_savepoint()
|
|
183
|
+
self._run_transaction_sql(f'SAVEPOINT "{name}"', scope=self._parent)
|
|
184
|
+
self._savepoint_name = name
|
|
185
|
+
self._state.current_scope = self
|
|
186
|
+
|
|
187
|
+
def _exit_outer(self, exception_type: type[BaseException] | None) -> None:
|
|
188
|
+
try:
|
|
189
|
+
self._state.require_scope(self)
|
|
190
|
+
except TransactionStateError:
|
|
191
|
+
self._abort_misnested_outer(exception_type)
|
|
192
|
+
return
|
|
193
|
+
self._state.close_stream()
|
|
194
|
+
command = "ROLLBACK" if exception_type is not None else "COMMIT"
|
|
195
|
+
try:
|
|
196
|
+
if not self._state.broken:
|
|
197
|
+
self._run_transaction_sql(command)
|
|
198
|
+
except ExecutionError:
|
|
199
|
+
self._state.invalidate()
|
|
200
|
+
if exception_type is None:
|
|
201
|
+
raise
|
|
202
|
+
finally:
|
|
203
|
+
self._release_outer()
|
|
204
|
+
|
|
205
|
+
def _exit_savepoint(self, exception_type: type[BaseException] | None) -> None:
|
|
206
|
+
self._state.require_scope(self)
|
|
207
|
+
self._state.close_stream()
|
|
208
|
+
name = cast("str", self._savepoint_name)
|
|
209
|
+
try:
|
|
210
|
+
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
|
+
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
|
+
self._run_transaction_sql(f'ROLLBACK TO SAVEPOINT "{name}"')
|
|
228
|
+
self._run_transaction_sql(f'RELEASE SAVEPOINT "{name}"')
|
|
229
|
+
|
|
230
|
+
def _run_transaction_sql(
|
|
231
|
+
self,
|
|
232
|
+
sql: str,
|
|
233
|
+
*,
|
|
234
|
+
scope: PostgresTransaction | None = None,
|
|
235
|
+
) -> None:
|
|
236
|
+
connection = self._state.require_current(scope or self, allow_stream=True)
|
|
237
|
+
run_sql(
|
|
238
|
+
connection,
|
|
239
|
+
sql,
|
|
240
|
+
(),
|
|
241
|
+
_discard_cursor,
|
|
242
|
+
self._control,
|
|
243
|
+
self._state.invalidate,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _open_stream(
|
|
247
|
+
self,
|
|
248
|
+
stream: RowStream[ResultRow],
|
|
249
|
+
statement: CompiledQuery,
|
|
250
|
+
policy: StreamPolicy,
|
|
251
|
+
) -> StreamResource:
|
|
252
|
+
parameters = adapt_parameters(statement.parameters)
|
|
253
|
+
connection = self._state.require_current(self)
|
|
254
|
+
self._stream_sequence += 1
|
|
255
|
+
session = 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("RowStream[object]", stream)
|
|
264
|
+
return StreamResource(session, self._state.release_stream)
|
|
265
|
+
|
|
266
|
+
def _release_outer(self) -> None:
|
|
267
|
+
self._finished = True
|
|
268
|
+
self._state.release()
|
|
269
|
+
|
|
270
|
+
def _abort_misnested_outer(
|
|
271
|
+
self,
|
|
272
|
+
exception_type: type[BaseException] | None,
|
|
273
|
+
) -> None:
|
|
274
|
+
connection = self._state.require_owner()
|
|
275
|
+
self._state.close_stream()
|
|
276
|
+
try:
|
|
277
|
+
connection.rollback()
|
|
278
|
+
except psycopg.Error:
|
|
279
|
+
self._state.invalidate()
|
|
280
|
+
finally:
|
|
281
|
+
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
|
+
def _is_transaction_mode(value: object) -> bool:
|
|
288
|
+
return isinstance(value, TransactionMode)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _discard_cursor(cursor: psycopg.Cursor[tuple[object, ...]]) -> None:
|
|
292
|
+
del cursor
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
__all__ = ("PostgresTransaction", "TransactionMode")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""PostgreSQL write statement compilation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.errors import UnsupportedQueryError
|
|
6
|
+
from pyoq.query.postgres.context import PostgresCompilationContext
|
|
7
|
+
from pyoq.query.postgres.expressions import PostgresExpressionCompiler
|
|
8
|
+
from pyoq.query.postgres.identifiers import QUOTING
|
|
9
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
10
|
+
from pyoq.query.rendering.writes import WriteRenderer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PostgresWriteCompilation(WriteRenderer):
|
|
14
|
+
__slots__ = ("_context",)
|
|
15
|
+
|
|
16
|
+
def __init__(self, context: PostgresCompilationContext) -> None:
|
|
17
|
+
self._context = context
|
|
18
|
+
super().__init__(PostgresExpressionCompiler(context))
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def quoting(self) -> IdentifierQuoting:
|
|
22
|
+
return QUOTING
|
|
23
|
+
|
|
24
|
+
def require_returning(self) -> None:
|
|
25
|
+
if not self._context.capabilities.returning:
|
|
26
|
+
message = "PostgreSQL RETURNING is disabled by compiler capabilities"
|
|
27
|
+
raise UnsupportedQueryError(message)
|
|
28
|
+
|
|
29
|
+
def require_conflict_resolution(self) -> None:
|
|
30
|
+
if not self._context.capabilities.upsert:
|
|
31
|
+
message = (
|
|
32
|
+
"PostgreSQL conflict resolution is disabled by compiler capabilities"
|
|
33
|
+
)
|
|
34
|
+
raise UnsupportedQueryError(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ("PostgresWriteCompilation",)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Typed SELECT projections and ordering terms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pyoq.query.expressions import Expression
|
|
8
|
+
from pyoq.query.select_nodes import (
|
|
9
|
+
NullOrder,
|
|
10
|
+
OrderDirection,
|
|
11
|
+
OrderNode,
|
|
12
|
+
ProjectionNode,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Value = TypeVar("Value")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Projection(Generic[Value]):
|
|
19
|
+
__slots__ = ("_node",)
|
|
20
|
+
_node: ProjectionNode
|
|
21
|
+
|
|
22
|
+
def __init__(self, node: ProjectionNode) -> None:
|
|
23
|
+
object.__setattr__(self, "_node", node)
|
|
24
|
+
|
|
25
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
26
|
+
message = "projections are immutable"
|
|
27
|
+
raise AttributeError(message)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def node(self) -> ProjectionNode:
|
|
31
|
+
return self._node
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OrderTerm:
|
|
35
|
+
__slots__ = ("_node",)
|
|
36
|
+
_node: OrderNode
|
|
37
|
+
|
|
38
|
+
def __init__(self, node: OrderNode) -> None:
|
|
39
|
+
object.__setattr__(self, "_node", node)
|
|
40
|
+
|
|
41
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
42
|
+
message = "order terms are immutable"
|
|
43
|
+
raise AttributeError(message)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def node(self) -> OrderNode:
|
|
47
|
+
return self._node
|
|
48
|
+
|
|
49
|
+
def nulls_first(self) -> OrderTerm:
|
|
50
|
+
return OrderTerm(
|
|
51
|
+
OrderNode(self.node.expression, self.node.direction, NullOrder.FIRST)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def nulls_last(self) -> OrderTerm:
|
|
55
|
+
return OrderTerm(
|
|
56
|
+
OrderNode(self.node.expression, self.node.direction, NullOrder.LAST)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def alias(expression: Expression[Value], name: str, /) -> Projection[Value]:
|
|
61
|
+
_require_identifier(name, "projection alias")
|
|
62
|
+
return Projection(ProjectionNode(expression.node, name))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ascending(expression: Expression[Value], /) -> OrderTerm:
|
|
66
|
+
return OrderTerm(OrderNode(expression.node, OrderDirection.ASCENDING))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def descending(expression: Expression[Value], /) -> OrderTerm:
|
|
70
|
+
return OrderTerm(OrderNode(expression.node, OrderDirection.DESCENDING))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def projection_node(value: object) -> ProjectionNode:
|
|
74
|
+
if isinstance(value, Projection):
|
|
75
|
+
return value.node
|
|
76
|
+
if isinstance(value, Expression):
|
|
77
|
+
return ProjectionNode(value.node)
|
|
78
|
+
message = "SELECT projections must be typed expressions"
|
|
79
|
+
raise TypeError(message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def order_node(value: object) -> OrderNode:
|
|
83
|
+
if isinstance(value, OrderTerm):
|
|
84
|
+
return value.node
|
|
85
|
+
if isinstance(value, Expression):
|
|
86
|
+
return OrderNode(value.node, OrderDirection.ASCENDING)
|
|
87
|
+
message = "ORDER BY terms must be typed expressions or order terms"
|
|
88
|
+
raise TypeError(message)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _require_identifier(value: str, label: str) -> None:
|
|
92
|
+
if not value or "\x00" in value:
|
|
93
|
+
message = f"{label} cannot be empty or contain a null character"
|
|
94
|
+
raise ValueError(message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = (
|
|
98
|
+
"OrderTerm",
|
|
99
|
+
"Projection",
|
|
100
|
+
"alias",
|
|
101
|
+
"ascending",
|
|
102
|
+
"descending",
|
|
103
|
+
"order_node",
|
|
104
|
+
"projection_node",
|
|
105
|
+
)
|
pyoq/query/raw.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Explicit typed raw expression templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from string import Formatter
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import RawExpressionError
|
|
9
|
+
from pyoq.query.expressions import ComputedExpression, Condition
|
|
10
|
+
from pyoq.query.fields import family_for_type
|
|
11
|
+
from pyoq.query.nodes import (
|
|
12
|
+
NodeProvider,
|
|
13
|
+
RawNode,
|
|
14
|
+
RawPart,
|
|
15
|
+
ScalarFamily,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
Value = TypeVar("Value")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def raw(
|
|
22
|
+
value_type: type[Value],
|
|
23
|
+
template: str,
|
|
24
|
+
/,
|
|
25
|
+
**expressions: NodeProvider,
|
|
26
|
+
) -> ComputedExpression[Value]:
|
|
27
|
+
parts = _template_parts(template, expressions)
|
|
28
|
+
family = family_for_type(value_type)
|
|
29
|
+
node = RawNode(parts, family)
|
|
30
|
+
return ComputedExpression(node, family)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def raw_condition(template: str, /, **expressions: NodeProvider) -> Condition:
|
|
34
|
+
return Condition(
|
|
35
|
+
RawNode(_template_parts(template, expressions), ScalarFamily.BOOLEAN)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _template_parts(
|
|
40
|
+
template: str,
|
|
41
|
+
expressions: dict[str, NodeProvider],
|
|
42
|
+
) -> tuple[RawPart, ...]:
|
|
43
|
+
if not template or "\x00" in template:
|
|
44
|
+
message = "raw expression template cannot be empty or contain a null character"
|
|
45
|
+
raise RawExpressionError(message)
|
|
46
|
+
try:
|
|
47
|
+
parsed = tuple(Formatter().parse(template))
|
|
48
|
+
except ValueError as error:
|
|
49
|
+
message = "raw expression template has invalid braces"
|
|
50
|
+
raise RawExpressionError(message) from error
|
|
51
|
+
parts: list[RawPart] = []
|
|
52
|
+
referenced: set[str] = set()
|
|
53
|
+
for literal, name, format_spec, conversion in parsed:
|
|
54
|
+
_append_literal(parts, literal)
|
|
55
|
+
if name is None:
|
|
56
|
+
continue
|
|
57
|
+
_require_plain_name(name, format_spec or "", conversion)
|
|
58
|
+
expression: object = expressions.get(name)
|
|
59
|
+
if not isinstance(expression, NodeProvider):
|
|
60
|
+
message = f"raw expression is missing expression argument: {name}"
|
|
61
|
+
raise RawExpressionError(message)
|
|
62
|
+
parts.append(expression.node)
|
|
63
|
+
referenced.add(name)
|
|
64
|
+
unused = sorted(expressions.keys() - referenced)
|
|
65
|
+
if unused:
|
|
66
|
+
message = f"raw expression has unused arguments: {', '.join(unused)}"
|
|
67
|
+
raise RawExpressionError(message)
|
|
68
|
+
return tuple(parts)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _append_literal(parts: list[RawPart], literal: str) -> None:
|
|
72
|
+
if not literal:
|
|
73
|
+
return
|
|
74
|
+
if parts and isinstance(parts[-1], str):
|
|
75
|
+
parts[-1] += literal
|
|
76
|
+
return
|
|
77
|
+
parts.append(literal)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _require_plain_name(
|
|
81
|
+
name: str,
|
|
82
|
+
format_spec: str,
|
|
83
|
+
conversion: str | None,
|
|
84
|
+
) -> None:
|
|
85
|
+
if not name.isidentifier() or format_spec or conversion is not None:
|
|
86
|
+
message = "raw placeholders must be plain Python identifiers"
|
|
87
|
+
raise RawExpressionError(message)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = ("NodeProvider", "raw", "raw_condition")
|