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,105 @@
|
|
|
1
|
+
"""Asynchronous transaction scope, ownership, and savepoint state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from asyncio import current_task
|
|
6
|
+
from typing import Generic, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import TransactionStateError
|
|
9
|
+
from pyoq.query.execution.aio.pooling import AsyncConnectionLease
|
|
10
|
+
from pyoq.query.execution.aio.streaming import AsyncRowStream
|
|
11
|
+
|
|
12
|
+
ConnectionType = TypeVar("ConnectionType")
|
|
13
|
+
ScopeType = TypeVar("ScopeType")
|
|
14
|
+
|
|
15
|
+
_SAVEPOINT_PREFIX = "pyoq_savepoint_"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncTransactionState(Generic[ConnectionType, ScopeType]):
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"active_stream",
|
|
21
|
+
"broken",
|
|
22
|
+
"connection",
|
|
23
|
+
"current_scope",
|
|
24
|
+
"lease",
|
|
25
|
+
"owner_task",
|
|
26
|
+
"savepoint_sequence",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.lease: AsyncConnectionLease[ConnectionType] | None = None
|
|
31
|
+
self.connection: ConnectionType | None = None
|
|
32
|
+
self.owner_task: object | None = None
|
|
33
|
+
self.current_scope: ScopeType | None = None
|
|
34
|
+
self.active_stream: AsyncRowStream[object] | None = None
|
|
35
|
+
self.savepoint_sequence = 0
|
|
36
|
+
self.broken = False
|
|
37
|
+
|
|
38
|
+
def require_current(
|
|
39
|
+
self,
|
|
40
|
+
scope: ScopeType,
|
|
41
|
+
*,
|
|
42
|
+
allow_stream: bool = False,
|
|
43
|
+
) -> ConnectionType:
|
|
44
|
+
connection = self.require_scope(scope)
|
|
45
|
+
if self.broken:
|
|
46
|
+
raise TransactionStateError("transaction connection is unusable")
|
|
47
|
+
if self.active_stream is not None and not allow_stream:
|
|
48
|
+
raise TransactionStateError("transaction has an active row stream")
|
|
49
|
+
return connection
|
|
50
|
+
|
|
51
|
+
def require_scope(self, scope: ScopeType) -> ConnectionType:
|
|
52
|
+
if self.connection is None or self.current_scope is not scope:
|
|
53
|
+
message = "only the active innermost transaction may be used"
|
|
54
|
+
raise TransactionStateError(message)
|
|
55
|
+
if self.owner_task is not current_task():
|
|
56
|
+
raise TransactionStateError("transaction used outside its owner task")
|
|
57
|
+
return self.connection
|
|
58
|
+
|
|
59
|
+
def require_owner(self) -> ConnectionType:
|
|
60
|
+
if self.owner_task is not current_task():
|
|
61
|
+
raise TransactionStateError("transaction is not active")
|
|
62
|
+
return cast("ConnectionType", self.connection)
|
|
63
|
+
|
|
64
|
+
def invalidate(self) -> None:
|
|
65
|
+
cast(
|
|
66
|
+
"AsyncConnectionLease[ConnectionType]",
|
|
67
|
+
self.lease,
|
|
68
|
+
).invalidate()
|
|
69
|
+
self.broken = True
|
|
70
|
+
|
|
71
|
+
def next_savepoint(self) -> str:
|
|
72
|
+
self.savepoint_sequence += 1
|
|
73
|
+
return f"{_SAVEPOINT_PREFIX}{self.savepoint_sequence}"
|
|
74
|
+
|
|
75
|
+
async def close_stream(self) -> None:
|
|
76
|
+
stream = self.active_stream
|
|
77
|
+
if stream is not None:
|
|
78
|
+
await stream.close()
|
|
79
|
+
|
|
80
|
+
async def release_stream(self) -> None:
|
|
81
|
+
self.active_stream = None
|
|
82
|
+
|
|
83
|
+
async def adopt(
|
|
84
|
+
self,
|
|
85
|
+
lease: AsyncConnectionLease[ConnectionType],
|
|
86
|
+
scope: ScopeType,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.lease = lease
|
|
89
|
+
self.connection = await lease.__aenter__()
|
|
90
|
+
self.owner_task = current_task()
|
|
91
|
+
self.current_scope = scope
|
|
92
|
+
|
|
93
|
+
async def release(self) -> None:
|
|
94
|
+
lease = self.lease
|
|
95
|
+
self.current_scope = None
|
|
96
|
+
self.connection = None
|
|
97
|
+
self.lease = None
|
|
98
|
+
self.owner_task = None
|
|
99
|
+
await cast(
|
|
100
|
+
"AsyncConnectionLease[ConnectionType]",
|
|
101
|
+
lease,
|
|
102
|
+
).__aexit__(None, None, None)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ("AsyncTransactionState",)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Immutable bulk and multi-operation execution contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import QueryValidationError
|
|
9
|
+
from pyoq.query.execution.compilation import CompiledQuery
|
|
10
|
+
from pyoq.query.execution.results import ExecuteResult
|
|
11
|
+
from pyoq.query.write_nodes import WriteNode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class OperationBudget:
|
|
16
|
+
maximum_operations: int = 256
|
|
17
|
+
|
|
18
|
+
def __post_init__(self) -> None:
|
|
19
|
+
if not _is_positive_integer(self.maximum_operations):
|
|
20
|
+
message = "maximum operations must be a positive integer"
|
|
21
|
+
raise QueryValidationError(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class BulkResult:
|
|
26
|
+
results: tuple[ExecuteResult, ...]
|
|
27
|
+
row_counts: tuple[int, ...]
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if len(self.results) != len(self.row_counts):
|
|
31
|
+
message = "bulk results must align with their planned row counts"
|
|
32
|
+
raise QueryValidationError(message)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def rows_affected(self) -> int:
|
|
36
|
+
return sum(result.rows_affected for result in self.results)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def statement_count(self) -> int:
|
|
40
|
+
return len(self.results)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def input_rows(self) -> int:
|
|
44
|
+
return sum(self.row_counts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_positive_integer(value: object) -> bool:
|
|
48
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WriteProvider(Protocol):
|
|
52
|
+
@property
|
|
53
|
+
def node(self) -> WriteNode: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class BulkChunk:
|
|
58
|
+
statement: CompiledQuery
|
|
59
|
+
start: int
|
|
60
|
+
length: int
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if self.start < 0 or self.length <= 0:
|
|
64
|
+
message = "a bulk chunk must cover at least one input row"
|
|
65
|
+
raise QueryValidationError(message)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class BulkPlan:
|
|
70
|
+
chunks: tuple[BulkChunk, ...]
|
|
71
|
+
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
if not self.chunks:
|
|
74
|
+
message = "a bulk plan requires at least one chunk"
|
|
75
|
+
raise QueryValidationError(message)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def statement_count(self) -> int:
|
|
79
|
+
return len(self.chunks)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def input_rows(self) -> int:
|
|
83
|
+
return sum(chunk.length for chunk in self.chunks)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def row_counts(self) -> tuple[int, ...]:
|
|
87
|
+
return tuple(chunk.length for chunk in self.chunks)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = (
|
|
91
|
+
"BulkChunk",
|
|
92
|
+
"BulkPlan",
|
|
93
|
+
"BulkResult",
|
|
94
|
+
"OperationBudget",
|
|
95
|
+
"WriteProvider",
|
|
96
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Driver parameter placeholder styles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParameterStyle(StrEnum):
|
|
9
|
+
QMARK = "qmark"
|
|
10
|
+
NUMERIC = "numeric"
|
|
11
|
+
FORMAT = "format"
|
|
12
|
+
|
|
13
|
+
def placeholder(self, index: int, /) -> str:
|
|
14
|
+
if self is ParameterStyle.QMARK:
|
|
15
|
+
return "?"
|
|
16
|
+
if self is ParameterStyle.NUMERIC:
|
|
17
|
+
return f"${index + 1}"
|
|
18
|
+
return "%s"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def escapes_percent(self) -> bool:
|
|
22
|
+
return self is ParameterStyle.FORMAT
|
|
23
|
+
|
|
24
|
+
def structural_text(self, text: str, /) -> str:
|
|
25
|
+
if not self.escapes_percent:
|
|
26
|
+
return text
|
|
27
|
+
return text.replace("%", "%%")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ("ParameterStyle",)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Dialect-neutral compiled statement output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import QueryValidationError
|
|
8
|
+
from pyoq.query.execution.results import StatementKind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class CompiledQuery:
|
|
13
|
+
sql: str
|
|
14
|
+
parameters: tuple[object, ...] = ()
|
|
15
|
+
sensitive_parameter_indexes: frozenset[int] = frozenset()
|
|
16
|
+
statement_kind: StatementKind = StatementKind.UNKNOWN
|
|
17
|
+
# What each projected column was declared to hold, so a row can be given
|
|
18
|
+
# back as it was described rather than as a driver happened to answer.
|
|
19
|
+
row_types: tuple[type[object] | None, ...] = ()
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if not self.sql or "\x00" in self.sql:
|
|
23
|
+
message = "compiled SQL cannot be empty or contain a null character"
|
|
24
|
+
raise QueryValidationError(message)
|
|
25
|
+
if any(
|
|
26
|
+
not _is_valid_parameter_index(index, len(self.parameters))
|
|
27
|
+
for index in self.sensitive_parameter_indexes
|
|
28
|
+
):
|
|
29
|
+
message = "sensitive parameter index is outside the parameter set"
|
|
30
|
+
raise QueryValidationError(message)
|
|
31
|
+
if not _is_statement_kind(self.statement_kind):
|
|
32
|
+
message = "compiled statement kind is invalid"
|
|
33
|
+
raise QueryValidationError(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_valid_parameter_index(value: object, parameter_count: int) -> bool:
|
|
37
|
+
return (
|
|
38
|
+
isinstance(value, int)
|
|
39
|
+
and not isinstance(value, bool)
|
|
40
|
+
and 0 <= value < parameter_count
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_statement_kind(value: object) -> bool:
|
|
45
|
+
return isinstance(value, StatementKind)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ("CompiledQuery",)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Per-compilation parameter state shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.errors import ParameterLimitError
|
|
6
|
+
from pyoq.query.decoding import projected_types
|
|
7
|
+
from pyoq.query.execution.binding_style import ParameterStyle
|
|
8
|
+
from pyoq.query.execution.compilation import CompiledQuery
|
|
9
|
+
from pyoq.query.execution.results import StatementKind
|
|
10
|
+
from pyoq.query.nodes import BoundValueNode, ScalarFamily
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CompilationContext:
|
|
14
|
+
__slots__ = ("_maximum_parameters", "_style", "parameters", "sensitive_indexes")
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
maximum_parameters: int,
|
|
19
|
+
parameter_style: ParameterStyle,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._maximum_parameters = maximum_parameters
|
|
22
|
+
self._style = parameter_style
|
|
23
|
+
self.parameters: list[object] = []
|
|
24
|
+
self.sensitive_indexes: set[int] = set()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def parameter_style(self) -> ParameterStyle:
|
|
28
|
+
return self._style
|
|
29
|
+
|
|
30
|
+
def bind(self, node: BoundValueNode) -> str:
|
|
31
|
+
index = len(self.parameters)
|
|
32
|
+
if index >= self._maximum_parameters:
|
|
33
|
+
message = "statement exceeds the configured parameter limit"
|
|
34
|
+
raise ParameterLimitError(message)
|
|
35
|
+
self.parameters.append(node.value)
|
|
36
|
+
if node.sensitive:
|
|
37
|
+
self.sensitive_indexes.add(index)
|
|
38
|
+
return self._style.placeholder(index)
|
|
39
|
+
|
|
40
|
+
def bind_page_value(self, value: int) -> str:
|
|
41
|
+
return self.bind(BoundValueNode(value, family=ScalarFamily.NUMERIC))
|
|
42
|
+
|
|
43
|
+
def structural_text(self, text: str, /) -> str:
|
|
44
|
+
return self._style.structural_text(text)
|
|
45
|
+
|
|
46
|
+
def result(
|
|
47
|
+
self,
|
|
48
|
+
sql: str,
|
|
49
|
+
statement_kind: StatementKind,
|
|
50
|
+
node: object = None,
|
|
51
|
+
) -> CompiledQuery:
|
|
52
|
+
return CompiledQuery(
|
|
53
|
+
sql,
|
|
54
|
+
tuple(self.parameters),
|
|
55
|
+
frozenset(self.sensitive_indexes),
|
|
56
|
+
statement_kind,
|
|
57
|
+
projected_types(node) if node is not None else (),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ("CompilationContext",)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Cooperative execution timeout and cancellation contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from threading import Event
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import QueryValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CancellationToken:
|
|
12
|
+
__slots__ = ("_event",)
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._event = Event()
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def cancelled(self) -> bool:
|
|
19
|
+
return self._event.is_set()
|
|
20
|
+
|
|
21
|
+
def cancel(self) -> None:
|
|
22
|
+
self._event.set()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ExecutionControl:
|
|
27
|
+
timeout: float | None = None
|
|
28
|
+
cancellation_token: CancellationToken | None = None
|
|
29
|
+
progress_steps: int = 1_000
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
if not _valid_timeout(self.timeout):
|
|
33
|
+
message = "execution timeout must be positive or None"
|
|
34
|
+
raise QueryValidationError(message)
|
|
35
|
+
if not _positive_integer(self.progress_steps):
|
|
36
|
+
message = "execution progress steps must be a positive integer"
|
|
37
|
+
raise QueryValidationError(message)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _valid_timeout(value: object) -> bool:
|
|
41
|
+
return value is None or (
|
|
42
|
+
isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _positive_integer(value: object) -> bool:
|
|
47
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ("CancellationToken", "ExecutionControl")
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Dialect-neutral typed result operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
7
|
+
from typing import Protocol, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from pyoq.errors import (
|
|
10
|
+
MultipleResultsError,
|
|
11
|
+
NoResultError,
|
|
12
|
+
QueryValidationError,
|
|
13
|
+
ResultCardinalityError,
|
|
14
|
+
)
|
|
15
|
+
from pyoq.query.decoding import decode_row, decode_rows
|
|
16
|
+
from pyoq.query.execution.batch import (
|
|
17
|
+
BulkPlan,
|
|
18
|
+
BulkResult,
|
|
19
|
+
OperationBudget,
|
|
20
|
+
WriteProvider,
|
|
21
|
+
)
|
|
22
|
+
from pyoq.query.execution.compilation import CompiledQuery
|
|
23
|
+
from pyoq.query.execution.control import ExecutionControl
|
|
24
|
+
from pyoq.query.execution.results import ExecuteResult, StatementKind
|
|
25
|
+
from pyoq.query.routines import Call
|
|
26
|
+
from pyoq.query.select import Query
|
|
27
|
+
from pyoq.query.statements import StatementCompiler
|
|
28
|
+
from pyoq.query.write_nodes import WriteNode
|
|
29
|
+
from pyoq.query.writes import ReturningStatement, WriteStatement
|
|
30
|
+
|
|
31
|
+
ResultRow = TypeVar("ResultRow")
|
|
32
|
+
ScalarValue = TypeVar("ScalarValue")
|
|
33
|
+
OperationResult = TypeVar("OperationResult")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DatabaseCursor(Protocol):
|
|
37
|
+
@property
|
|
38
|
+
def rowcount(self) -> int: ...
|
|
39
|
+
|
|
40
|
+
def fetchall(self) -> Sequence[object]: ...
|
|
41
|
+
|
|
42
|
+
def fetchmany(self, size: int, /) -> Sequence[object]: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QueryOperations(ABC):
|
|
46
|
+
__slots__ = ()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def compiler(self) -> StatementCompiler: ...
|
|
51
|
+
|
|
52
|
+
def one(
|
|
53
|
+
self,
|
|
54
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
55
|
+
/,
|
|
56
|
+
*,
|
|
57
|
+
control: ExecutionControl | None = None,
|
|
58
|
+
) -> ResultRow:
|
|
59
|
+
compiled = self.compiler.compile(query)
|
|
60
|
+
rows = self._limited_rows(compiled, control)
|
|
61
|
+
if not rows:
|
|
62
|
+
raise NoResultError("query returned no rows")
|
|
63
|
+
if len(rows) > 1:
|
|
64
|
+
raise MultipleResultsError("query returned more than one row")
|
|
65
|
+
return cast(ResultRow, decode_row(rows[0], compiled.row_types))
|
|
66
|
+
|
|
67
|
+
def one_or_none(
|
|
68
|
+
self,
|
|
69
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
70
|
+
/,
|
|
71
|
+
*,
|
|
72
|
+
control: ExecutionControl | None = None,
|
|
73
|
+
) -> ResultRow | None:
|
|
74
|
+
compiled = self.compiler.compile(query)
|
|
75
|
+
rows = self._limited_rows(compiled, control)
|
|
76
|
+
if len(rows) > 1:
|
|
77
|
+
raise MultipleResultsError("query returned more than one row")
|
|
78
|
+
if not rows:
|
|
79
|
+
return None
|
|
80
|
+
return cast(ResultRow, decode_row(rows[0], compiled.row_types))
|
|
81
|
+
|
|
82
|
+
def many(
|
|
83
|
+
self,
|
|
84
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
85
|
+
/,
|
|
86
|
+
*,
|
|
87
|
+
control: ExecutionControl | None = None,
|
|
88
|
+
) -> list[ResultRow]:
|
|
89
|
+
compiled = self.compiler.compile(query)
|
|
90
|
+
rows = self._run(compiled, fetch_all, control)
|
|
91
|
+
return cast(list[ResultRow], decode_rows(rows, compiled.row_types))
|
|
92
|
+
|
|
93
|
+
def scalar(
|
|
94
|
+
self,
|
|
95
|
+
query: Query[tuple[ScalarValue]] | ReturningStatement[tuple[ScalarValue]],
|
|
96
|
+
/,
|
|
97
|
+
*,
|
|
98
|
+
control: ExecutionControl | None = None,
|
|
99
|
+
) -> ScalarValue:
|
|
100
|
+
row = cast(tuple[object, ...], self.one(query, control=control))
|
|
101
|
+
return cast(ScalarValue, _scalar_value(row))
|
|
102
|
+
|
|
103
|
+
def execute(
|
|
104
|
+
self,
|
|
105
|
+
statement: CompiledQuery | WriteStatement[WriteNode] | Call[tuple[object, ...]],
|
|
106
|
+
/,
|
|
107
|
+
*,
|
|
108
|
+
control: ExecutionControl | None = None,
|
|
109
|
+
) -> ExecuteResult:
|
|
110
|
+
compiled = (
|
|
111
|
+
statement
|
|
112
|
+
if isinstance(statement, CompiledQuery)
|
|
113
|
+
else self.compiler.compile(statement)
|
|
114
|
+
)
|
|
115
|
+
return self._run(
|
|
116
|
+
compiled,
|
|
117
|
+
lambda cursor: self._execution_result(cursor, compiled.statement_kind),
|
|
118
|
+
control,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def last_inserted_id(self, cursor: DatabaseCursor) -> int | None:
|
|
122
|
+
del cursor
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan: ...
|
|
127
|
+
|
|
128
|
+
def execute_bulk(
|
|
129
|
+
self,
|
|
130
|
+
statement: WriteProvider | WriteNode,
|
|
131
|
+
/,
|
|
132
|
+
*,
|
|
133
|
+
control: ExecutionControl | None = None,
|
|
134
|
+
) -> BulkResult:
|
|
135
|
+
plan = self.plan_bulk(statement)
|
|
136
|
+
return BulkResult(self.execute_chunks(plan, control), plan.row_counts)
|
|
137
|
+
|
|
138
|
+
def execute_chunks(
|
|
139
|
+
self,
|
|
140
|
+
plan: BulkPlan,
|
|
141
|
+
control: ExecutionControl | None,
|
|
142
|
+
) -> tuple[ExecuteResult, ...]:
|
|
143
|
+
return tuple(
|
|
144
|
+
self.execute(chunk.statement, control=control) for chunk in plan.chunks
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def many_bulk(
|
|
148
|
+
self,
|
|
149
|
+
statement: ReturningStatement[ResultRow],
|
|
150
|
+
/,
|
|
151
|
+
*,
|
|
152
|
+
control: ExecutionControl | None = None,
|
|
153
|
+
) -> list[ResultRow]:
|
|
154
|
+
plan = self.plan_bulk(statement)
|
|
155
|
+
rows: list[object] = []
|
|
156
|
+
for chunk in plan.chunks:
|
|
157
|
+
answered = self._run(chunk.statement, fetch_all, control)
|
|
158
|
+
rows.extend(decode_rows(answered, chunk.statement.row_types))
|
|
159
|
+
return cast(list[ResultRow], rows)
|
|
160
|
+
|
|
161
|
+
def execute_all(
|
|
162
|
+
self,
|
|
163
|
+
statements: Iterable[CompiledQuery | WriteStatement[WriteNode]],
|
|
164
|
+
/,
|
|
165
|
+
*,
|
|
166
|
+
budget: OperationBudget | None = None,
|
|
167
|
+
control: ExecutionControl | None = None,
|
|
168
|
+
) -> tuple[ExecuteResult, ...]:
|
|
169
|
+
operations: Sequence[CompiledQuery | WriteStatement[WriteNode]] = tuple(
|
|
170
|
+
statements
|
|
171
|
+
)
|
|
172
|
+
limit = (budget or OperationBudget()).maximum_operations
|
|
173
|
+
if len(operations) > limit:
|
|
174
|
+
message = f"operation batch exceeds its budget of {limit} operations"
|
|
175
|
+
raise QueryValidationError(message)
|
|
176
|
+
return tuple(
|
|
177
|
+
self.execute(operation, control=control) for operation in operations
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _execution_result(
|
|
181
|
+
self,
|
|
182
|
+
cursor: DatabaseCursor,
|
|
183
|
+
statement_kind: StatementKind,
|
|
184
|
+
) -> ExecuteResult:
|
|
185
|
+
row_count = max(cursor.rowcount, 0)
|
|
186
|
+
inserted = (
|
|
187
|
+
self.last_inserted_id(cursor)
|
|
188
|
+
if statement_kind is StatementKind.INSERT
|
|
189
|
+
else None
|
|
190
|
+
)
|
|
191
|
+
return ExecuteResult(row_count, inserted)
|
|
192
|
+
|
|
193
|
+
def _limited_rows(
|
|
194
|
+
self,
|
|
195
|
+
statement: CompiledQuery,
|
|
196
|
+
control: ExecutionControl | None,
|
|
197
|
+
) -> list[object]:
|
|
198
|
+
return self._run(statement, _fetch_up_to_two, control)
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
def _run(
|
|
202
|
+
self,
|
|
203
|
+
statement: CompiledQuery,
|
|
204
|
+
operation: Callable[[DatabaseCursor], OperationResult],
|
|
205
|
+
control: ExecutionControl | None,
|
|
206
|
+
) -> OperationResult: ...
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def fetch_all(cursor: DatabaseCursor) -> list[object]:
|
|
210
|
+
return list(cursor.fetchall())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _fetch_up_to_two(cursor: DatabaseCursor) -> list[object]:
|
|
214
|
+
return list(cursor.fetchmany(2))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _scalar_value(row: tuple[object, ...]) -> object:
|
|
218
|
+
if len(row) != 1:
|
|
219
|
+
message = "scalar query projection must contain exactly one value"
|
|
220
|
+
raise ResultCardinalityError(message)
|
|
221
|
+
return row[0]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = ("DatabaseCursor", "QueryOperations", "fetch_all")
|