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,107 @@
|
|
|
1
|
+
"""Bulk write planning bounded by a dialect parameter budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import CompilationError, ParameterLimitError, QueryValidationError
|
|
8
|
+
from pyoq.query.binding import (
|
|
9
|
+
bound_value_count,
|
|
10
|
+
projection_bound_value_count,
|
|
11
|
+
row_bound_value_count,
|
|
12
|
+
)
|
|
13
|
+
from pyoq.query.execution.batch import BulkChunk, BulkPlan, WriteProvider
|
|
14
|
+
from pyoq.query.nodes import ExpressionNode
|
|
15
|
+
from pyoq.query.statements import StatementCompiler
|
|
16
|
+
from pyoq.query.write_nodes import DeleteNode, InsertNode, UpdateNode, WriteNode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BulkPlanner:
|
|
20
|
+
__slots__ = ("_compiler", "_dialect", "_maximum_parameters")
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
compiler: StatementCompiler,
|
|
25
|
+
maximum_parameters: int,
|
|
26
|
+
dialect: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._compiler = compiler
|
|
29
|
+
self._maximum_parameters = maximum_parameters
|
|
30
|
+
self._dialect = dialect
|
|
31
|
+
|
|
32
|
+
def plan(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
|
|
33
|
+
node = _insert_node(statement)
|
|
34
|
+
if not node.rows:
|
|
35
|
+
message = "a bulk INSERT requires at least one row of values"
|
|
36
|
+
raise CompilationError(message)
|
|
37
|
+
limit = self._maximum_parameters
|
|
38
|
+
overhead = _suffix_bound_values(node)
|
|
39
|
+
if overhead > limit:
|
|
40
|
+
message = f"INSERT clauses alone exceed the {self._dialect} parameter limit"
|
|
41
|
+
raise ParameterLimitError(message)
|
|
42
|
+
return BulkPlan(tuple(self._chunks(node, limit, overhead)))
|
|
43
|
+
|
|
44
|
+
def _chunks(
|
|
45
|
+
self,
|
|
46
|
+
node: InsertNode,
|
|
47
|
+
limit: int,
|
|
48
|
+
overhead: int,
|
|
49
|
+
) -> list[BulkChunk]:
|
|
50
|
+
chunks: list[BulkChunk] = []
|
|
51
|
+
start = 0
|
|
52
|
+
while start < len(node.rows):
|
|
53
|
+
length = _chunk_length(node.rows, start, limit, overhead, self._dialect)
|
|
54
|
+
rows = node.rows[start : start + length]
|
|
55
|
+
statement = self._compiler.compile(replace(node, rows=rows))
|
|
56
|
+
chunks.append(BulkChunk(statement, start, length))
|
|
57
|
+
start += length
|
|
58
|
+
return chunks
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _insert_node(statement: WriteProvider | WriteNode) -> InsertNode:
|
|
62
|
+
node = (
|
|
63
|
+
statement
|
|
64
|
+
if isinstance(statement, (InsertNode, UpdateNode, DeleteNode))
|
|
65
|
+
else statement.node
|
|
66
|
+
)
|
|
67
|
+
if not isinstance(node, InsertNode):
|
|
68
|
+
message = "bulk planning requires an INSERT statement"
|
|
69
|
+
raise QueryValidationError(message)
|
|
70
|
+
return node
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _chunk_length(
|
|
74
|
+
rows: tuple[tuple[ExpressionNode, ...], ...],
|
|
75
|
+
start: int,
|
|
76
|
+
limit: int,
|
|
77
|
+
overhead: int,
|
|
78
|
+
dialect: str,
|
|
79
|
+
) -> int:
|
|
80
|
+
total = overhead
|
|
81
|
+
length = 0
|
|
82
|
+
for row in rows[start:]:
|
|
83
|
+
cost = row_bound_value_count(row)
|
|
84
|
+
if total + cost > limit:
|
|
85
|
+
if length == 0:
|
|
86
|
+
message = f"a single INSERT row exceeds the {dialect} parameter limit"
|
|
87
|
+
raise ParameterLimitError(message)
|
|
88
|
+
break
|
|
89
|
+
total += cost
|
|
90
|
+
length += 1
|
|
91
|
+
return length
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _suffix_bound_values(node: InsertNode) -> int:
|
|
95
|
+
total = projection_bound_value_count(node.returning)
|
|
96
|
+
conflict = node.conflict
|
|
97
|
+
if conflict is None:
|
|
98
|
+
return total
|
|
99
|
+
total += sum(
|
|
100
|
+
bound_value_count(assignment.value) for assignment in conflict.assignments
|
|
101
|
+
)
|
|
102
|
+
if conflict.condition is not None:
|
|
103
|
+
total += bound_value_count(conflict.condition)
|
|
104
|
+
return total
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ("BulkPlanner",)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Exclusive synchronous connection pooling shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from threading import Condition, Lock
|
|
10
|
+
from time import monotonic
|
|
11
|
+
from typing import Generic, Self, TypeVar
|
|
12
|
+
|
|
13
|
+
from pyoq.errors import (
|
|
14
|
+
ConnectionPoolClosedError,
|
|
15
|
+
ConnectionPoolError,
|
|
16
|
+
ConnectionPoolTimeoutError,
|
|
17
|
+
QueryValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ConnectionType = TypeVar("ConnectionType")
|
|
21
|
+
ConnectionFactory = Callable[[], ConnectionType]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class PoolPolicy:
|
|
26
|
+
maximum_size: int = 5
|
|
27
|
+
minimum_size: int = 0
|
|
28
|
+
checkout_timeout: float | None = 30.0
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
if not _positive_integer(self.maximum_size):
|
|
32
|
+
message = "connection pool maximum size must be a positive integer"
|
|
33
|
+
raise QueryValidationError(message)
|
|
34
|
+
if not _non_negative_integer(self.minimum_size):
|
|
35
|
+
message = "connection pool minimum size must be a non-negative integer"
|
|
36
|
+
raise QueryValidationError(message)
|
|
37
|
+
if self.minimum_size > self.maximum_size:
|
|
38
|
+
message = "connection pool minimum size cannot exceed maximum size"
|
|
39
|
+
raise QueryValidationError(message)
|
|
40
|
+
if not _valid_timeout(self.checkout_timeout):
|
|
41
|
+
message = "connection pool checkout timeout must be positive or None"
|
|
42
|
+
raise QueryValidationError(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class PoolStats:
|
|
47
|
+
open_connections: int
|
|
48
|
+
idle_connections: int
|
|
49
|
+
checked_out_connections: int
|
|
50
|
+
closed: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ConnectionLease(Generic[ConnectionType]):
|
|
54
|
+
__slots__ = ("_connection", "_entered", "_invalid", "_pool")
|
|
55
|
+
|
|
56
|
+
def __init__(self, pool: ConnectionPool[ConnectionType]) -> None:
|
|
57
|
+
self._pool = pool
|
|
58
|
+
self._connection: ConnectionType | None = None
|
|
59
|
+
self._entered = False
|
|
60
|
+
self._invalid = False
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def connection(self) -> ConnectionType:
|
|
64
|
+
if self._connection is None or not self._entered:
|
|
65
|
+
message = "connection lease is not active"
|
|
66
|
+
raise ConnectionPoolError(message)
|
|
67
|
+
return self._connection
|
|
68
|
+
|
|
69
|
+
def invalidate(self) -> None:
|
|
70
|
+
if not self._entered:
|
|
71
|
+
message = "inactive connection lease cannot be invalidated"
|
|
72
|
+
raise ConnectionPoolError(message)
|
|
73
|
+
self._invalid = True
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> ConnectionType:
|
|
76
|
+
if self._entered:
|
|
77
|
+
message = "connection lease cannot be entered twice"
|
|
78
|
+
raise ConnectionPoolError(message)
|
|
79
|
+
connection = self._pool.acquire_for_lease()
|
|
80
|
+
self._connection = connection
|
|
81
|
+
self._entered = True
|
|
82
|
+
return connection
|
|
83
|
+
|
|
84
|
+
def __exit__(
|
|
85
|
+
self,
|
|
86
|
+
exception_type: type[BaseException] | None,
|
|
87
|
+
exception: BaseException | None,
|
|
88
|
+
traceback: object,
|
|
89
|
+
) -> None:
|
|
90
|
+
connection = self._connection
|
|
91
|
+
if connection is None or not self._entered:
|
|
92
|
+
return
|
|
93
|
+
self._entered = False
|
|
94
|
+
self._connection = None
|
|
95
|
+
self._pool.release_from_lease(connection, invalid=self._invalid)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ConnectionPool(ABC, Generic[ConnectionType]):
|
|
99
|
+
__slots__ = (
|
|
100
|
+
"_checked_out",
|
|
101
|
+
"_closed",
|
|
102
|
+
"_condition",
|
|
103
|
+
"_factory",
|
|
104
|
+
"_idle",
|
|
105
|
+
"_open_connections",
|
|
106
|
+
"_policy",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
factory: ConnectionFactory[ConnectionType],
|
|
112
|
+
policy: PoolPolicy | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
self._factory = factory
|
|
115
|
+
self._policy = policy or PoolPolicy()
|
|
116
|
+
self._condition = Condition(Lock())
|
|
117
|
+
self._idle: deque[ConnectionType] = deque()
|
|
118
|
+
self._checked_out: set[int] = set()
|
|
119
|
+
self._open_connections = 0
|
|
120
|
+
self._closed = False
|
|
121
|
+
self._initialize_minimum()
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def reset(self, connection: ConnectionType) -> bool: ...
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def validate(self, value: object) -> ConnectionType: ...
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def dispose(self, connection: ConnectionType) -> None: ...
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def policy(self) -> PoolPolicy:
|
|
134
|
+
return self._policy
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def stats(self) -> PoolStats:
|
|
138
|
+
with self._condition:
|
|
139
|
+
return PoolStats(
|
|
140
|
+
self._open_connections,
|
|
141
|
+
len(self._idle),
|
|
142
|
+
len(self._checked_out),
|
|
143
|
+
self._closed,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def checkout(self) -> ConnectionLease[ConnectionType]:
|
|
147
|
+
return ConnectionLease(self)
|
|
148
|
+
|
|
149
|
+
def close(self) -> None:
|
|
150
|
+
with self._condition:
|
|
151
|
+
if self._closed:
|
|
152
|
+
return
|
|
153
|
+
self._closed = True
|
|
154
|
+
idle = tuple(self._idle)
|
|
155
|
+
self._idle.clear()
|
|
156
|
+
self._open_connections -= len(idle)
|
|
157
|
+
self._condition.notify_all()
|
|
158
|
+
for connection in idle:
|
|
159
|
+
self.dispose(connection)
|
|
160
|
+
|
|
161
|
+
def __enter__(self) -> Self:
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(
|
|
165
|
+
self,
|
|
166
|
+
exception_type: type[BaseException] | None,
|
|
167
|
+
exception: BaseException | None,
|
|
168
|
+
traceback: object,
|
|
169
|
+
) -> None:
|
|
170
|
+
self.close()
|
|
171
|
+
|
|
172
|
+
def acquire_for_lease(self) -> ConnectionType:
|
|
173
|
+
deadline = _deadline(self.policy.checkout_timeout)
|
|
174
|
+
with self._condition:
|
|
175
|
+
while True:
|
|
176
|
+
self._require_open()
|
|
177
|
+
if self._idle:
|
|
178
|
+
connection = self._idle.popleft()
|
|
179
|
+
self._checked_out.add(id(connection))
|
|
180
|
+
return connection
|
|
181
|
+
if self._open_connections < self.policy.maximum_size:
|
|
182
|
+
self._open_connections += 1
|
|
183
|
+
break
|
|
184
|
+
self._wait_for_connection(deadline)
|
|
185
|
+
return self._create_reserved_connection()
|
|
186
|
+
|
|
187
|
+
def release_from_lease(
|
|
188
|
+
self,
|
|
189
|
+
connection: ConnectionType,
|
|
190
|
+
*,
|
|
191
|
+
invalid: bool,
|
|
192
|
+
) -> None:
|
|
193
|
+
reusable = not invalid and self.reset(connection)
|
|
194
|
+
with self._condition:
|
|
195
|
+
self._checked_out.discard(id(connection))
|
|
196
|
+
if reusable and not self._closed:
|
|
197
|
+
self._idle.append(connection)
|
|
198
|
+
else:
|
|
199
|
+
self._open_connections -= 1
|
|
200
|
+
self._condition.notify()
|
|
201
|
+
if not reusable or self._closed:
|
|
202
|
+
self.dispose(connection)
|
|
203
|
+
|
|
204
|
+
def _initialize_minimum(self) -> None:
|
|
205
|
+
created: list[ConnectionType] = []
|
|
206
|
+
try:
|
|
207
|
+
for _ in range(self.policy.minimum_size):
|
|
208
|
+
created.append(self._create_connection())
|
|
209
|
+
except Exception:
|
|
210
|
+
for connection in created:
|
|
211
|
+
self.dispose(connection)
|
|
212
|
+
raise
|
|
213
|
+
self._idle.extend(created)
|
|
214
|
+
self._open_connections = len(created)
|
|
215
|
+
|
|
216
|
+
def _create_reserved_connection(self) -> ConnectionType:
|
|
217
|
+
try:
|
|
218
|
+
connection = self._create_connection()
|
|
219
|
+
except Exception:
|
|
220
|
+
with self._condition:
|
|
221
|
+
self._open_connections -= 1
|
|
222
|
+
self._condition.notify()
|
|
223
|
+
raise
|
|
224
|
+
with self._condition:
|
|
225
|
+
if self._closed:
|
|
226
|
+
self._open_connections -= 1
|
|
227
|
+
self.dispose(connection)
|
|
228
|
+
raise ConnectionPoolClosedError("connection pool is closed")
|
|
229
|
+
self._checked_out.add(id(connection))
|
|
230
|
+
return connection
|
|
231
|
+
|
|
232
|
+
def _create_connection(self) -> ConnectionType:
|
|
233
|
+
try:
|
|
234
|
+
connection_value: object = self._factory()
|
|
235
|
+
except ConnectionPoolError:
|
|
236
|
+
raise
|
|
237
|
+
except Exception as error:
|
|
238
|
+
message = f"unable to create pooled connection: {error}"
|
|
239
|
+
raise ConnectionPoolError(message) from error
|
|
240
|
+
return self.validate(connection_value)
|
|
241
|
+
|
|
242
|
+
def _wait_for_connection(self, deadline: float | None) -> None:
|
|
243
|
+
remaining = None if deadline is None else deadline - monotonic()
|
|
244
|
+
if remaining is not None and remaining <= 0:
|
|
245
|
+
message = "connection checkout exceeded its timeout"
|
|
246
|
+
raise ConnectionPoolTimeoutError(message)
|
|
247
|
+
self._condition.wait(remaining)
|
|
248
|
+
|
|
249
|
+
def _require_open(self) -> None:
|
|
250
|
+
if self._closed:
|
|
251
|
+
message = "connection pool is closed"
|
|
252
|
+
raise ConnectionPoolClosedError(message)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _deadline(timeout: float | None) -> float | None:
|
|
256
|
+
return None if timeout is None else monotonic() + timeout
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _positive_integer(value: object) -> bool:
|
|
260
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _non_negative_integer(value: object) -> bool:
|
|
264
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _valid_timeout(value: object) -> bool:
|
|
268
|
+
return value is None or (
|
|
269
|
+
isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = (
|
|
274
|
+
"ConnectionFactory",
|
|
275
|
+
"ConnectionLease",
|
|
276
|
+
"ConnectionPool",
|
|
277
|
+
"PoolPolicy",
|
|
278
|
+
"PoolStats",
|
|
279
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Immutable execution result contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import QueryValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StatementKind(Enum):
|
|
12
|
+
UNKNOWN = "unknown"
|
|
13
|
+
SELECT = "select"
|
|
14
|
+
INSERT = "insert"
|
|
15
|
+
UPDATE = "update"
|
|
16
|
+
DELETE = "delete"
|
|
17
|
+
DEFINITION = "definition"
|
|
18
|
+
CALL = "call"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ExecuteResult:
|
|
23
|
+
rows_affected: int
|
|
24
|
+
last_inserted_id: int | None = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
if not _is_non_negative_integer(self.rows_affected):
|
|
28
|
+
message = "affected row count must be a non-negative integer"
|
|
29
|
+
raise QueryValidationError(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_non_negative_integer(value: object) -> bool:
|
|
33
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ("ExecuteResult", "StatementKind")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Bounded synchronous row streaming shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from threading import get_ident
|
|
9
|
+
from typing import Generic, Protocol, Self, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
from pyoq.errors import QueryValidationError, StreamStateError
|
|
12
|
+
from pyoq.query.decoding import decode_rows
|
|
13
|
+
from pyoq.query.execution.control import ExecutionControl
|
|
14
|
+
|
|
15
|
+
ResultRow = TypeVar("ResultRow")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CursorSession(Protocol):
|
|
19
|
+
def checkpoint(self) -> None: ...
|
|
20
|
+
|
|
21
|
+
def fetch_batch(self, size: int) -> list[tuple[object, ...]]: ...
|
|
22
|
+
|
|
23
|
+
def close(self) -> None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class StreamPolicy:
|
|
28
|
+
batch_size: int = 256
|
|
29
|
+
control: ExecutionControl | None = None
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
if not _positive_integer(self.batch_size):
|
|
33
|
+
message = "stream batch size must be a positive integer"
|
|
34
|
+
raise QueryValidationError(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StreamResource:
|
|
38
|
+
__slots__ = ("_closed", "_release", "session")
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
session: CursorSession,
|
|
43
|
+
release: Callable[[], None],
|
|
44
|
+
) -> None:
|
|
45
|
+
self.session = session
|
|
46
|
+
self._release = release
|
|
47
|
+
self._closed = False
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
if self._closed:
|
|
51
|
+
return
|
|
52
|
+
self._closed = True
|
|
53
|
+
try:
|
|
54
|
+
self.session.close()
|
|
55
|
+
finally:
|
|
56
|
+
self._release()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RowStream(Iterator[ResultRow], Generic[ResultRow]):
|
|
60
|
+
__slots__ = (
|
|
61
|
+
"_buffer",
|
|
62
|
+
"_closed",
|
|
63
|
+
"_opened",
|
|
64
|
+
"_opener",
|
|
65
|
+
"_owner_thread",
|
|
66
|
+
"_policy",
|
|
67
|
+
"_resource",
|
|
68
|
+
"_row_types",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
opener: Callable[[], StreamResource],
|
|
74
|
+
policy: StreamPolicy,
|
|
75
|
+
row_types: tuple[type[object] | None, ...] = (),
|
|
76
|
+
) -> None:
|
|
77
|
+
self._opener = opener
|
|
78
|
+
self._policy = policy
|
|
79
|
+
self._row_types = row_types
|
|
80
|
+
self._buffer: deque[tuple[object, ...]] = deque()
|
|
81
|
+
self._resource: StreamResource | None = None
|
|
82
|
+
self._owner_thread: int | None = None
|
|
83
|
+
self._opened = False
|
|
84
|
+
self._closed = False
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def closed(self) -> bool:
|
|
88
|
+
return self._closed
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def buffered_rows(self) -> int:
|
|
92
|
+
return len(self._buffer)
|
|
93
|
+
|
|
94
|
+
def __enter__(self) -> Self:
|
|
95
|
+
if self._closed:
|
|
96
|
+
raise StreamStateError("closed row stream cannot be entered")
|
|
97
|
+
self._ensure_open()
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(
|
|
101
|
+
self,
|
|
102
|
+
exception_type: type[BaseException] | None,
|
|
103
|
+
exception: BaseException | None,
|
|
104
|
+
traceback: object,
|
|
105
|
+
) -> None:
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
def __iter__(self) -> Self:
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __next__(self) -> ResultRow:
|
|
112
|
+
if self._closed:
|
|
113
|
+
raise StopIteration
|
|
114
|
+
self._ensure_open()
|
|
115
|
+
self._require_owner()
|
|
116
|
+
try:
|
|
117
|
+
resource = self._require_resource()
|
|
118
|
+
resource.session.checkpoint()
|
|
119
|
+
if not self._buffer:
|
|
120
|
+
self._load_batch()
|
|
121
|
+
return cast(ResultRow, self._buffer.popleft())
|
|
122
|
+
except BaseException:
|
|
123
|
+
self.close()
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
if self._closed:
|
|
128
|
+
return
|
|
129
|
+
if self._opened:
|
|
130
|
+
self._require_owner()
|
|
131
|
+
self._closed = True
|
|
132
|
+
self._buffer.clear()
|
|
133
|
+
resource = self._resource
|
|
134
|
+
self._resource = None
|
|
135
|
+
if resource is not None:
|
|
136
|
+
resource.close()
|
|
137
|
+
|
|
138
|
+
def _ensure_open(self) -> None:
|
|
139
|
+
if self._opened:
|
|
140
|
+
self._require_owner()
|
|
141
|
+
return
|
|
142
|
+
self._owner_thread = get_ident()
|
|
143
|
+
try:
|
|
144
|
+
self._resource = self._opener()
|
|
145
|
+
except BaseException:
|
|
146
|
+
self._closed = True
|
|
147
|
+
raise
|
|
148
|
+
self._opened = True
|
|
149
|
+
|
|
150
|
+
def _load_batch(self) -> None:
|
|
151
|
+
resource = self._require_resource()
|
|
152
|
+
rows = resource.session.fetch_batch(self._policy.batch_size)
|
|
153
|
+
if not rows:
|
|
154
|
+
raise StopIteration
|
|
155
|
+
self._buffer.extend(
|
|
156
|
+
cast("list[tuple[object, ...]]", decode_rows(rows, self._row_types))
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _require_resource(self) -> StreamResource:
|
|
160
|
+
if self._resource is None:
|
|
161
|
+
raise StreamStateError("row stream has no active cursor")
|
|
162
|
+
return self._resource
|
|
163
|
+
|
|
164
|
+
def _require_owner(self) -> None:
|
|
165
|
+
if self._owner_thread != get_ident():
|
|
166
|
+
raise StreamStateError("row stream used outside its owner thread")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _positive_integer(value: object) -> bool:
|
|
170
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = (
|
|
174
|
+
"CursorSession",
|
|
175
|
+
"RowStream",
|
|
176
|
+
"StreamPolicy",
|
|
177
|
+
"StreamResource",
|
|
178
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Transaction scope, ownership, and savepoint state shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from threading import get_ident
|
|
6
|
+
from typing import Generic, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import TransactionStateError
|
|
9
|
+
from pyoq.query.execution.pooling import ConnectionLease
|
|
10
|
+
from pyoq.query.execution.streaming import RowStream
|
|
11
|
+
|
|
12
|
+
ConnectionType = TypeVar("ConnectionType")
|
|
13
|
+
ScopeType = TypeVar("ScopeType")
|
|
14
|
+
|
|
15
|
+
_SAVEPOINT_PREFIX = "pyoq_savepoint_"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransactionState(Generic[ConnectionType, ScopeType]):
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"active_stream",
|
|
21
|
+
"broken",
|
|
22
|
+
"connection",
|
|
23
|
+
"current_scope",
|
|
24
|
+
"lease",
|
|
25
|
+
"owner_thread",
|
|
26
|
+
"savepoint_sequence",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.lease: ConnectionLease[ConnectionType] | None = None
|
|
31
|
+
self.connection: ConnectionType | None = None
|
|
32
|
+
self.owner_thread: int | None = None
|
|
33
|
+
self.current_scope: ScopeType | None = None
|
|
34
|
+
self.active_stream: RowStream[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_thread != get_ident():
|
|
56
|
+
raise TransactionStateError("transaction used outside its owner thread")
|
|
57
|
+
return self.connection
|
|
58
|
+
|
|
59
|
+
def require_owner(self) -> ConnectionType:
|
|
60
|
+
if self.owner_thread != get_ident():
|
|
61
|
+
raise TransactionStateError("transaction is not active")
|
|
62
|
+
return cast(ConnectionType, self.connection)
|
|
63
|
+
|
|
64
|
+
def invalidate(self) -> None:
|
|
65
|
+
cast(ConnectionLease[ConnectionType], self.lease).invalidate()
|
|
66
|
+
self.broken = True
|
|
67
|
+
|
|
68
|
+
def next_savepoint(self) -> str:
|
|
69
|
+
self.savepoint_sequence += 1
|
|
70
|
+
return f"{_SAVEPOINT_PREFIX}{self.savepoint_sequence}"
|
|
71
|
+
|
|
72
|
+
def close_stream(self) -> None:
|
|
73
|
+
stream = self.active_stream
|
|
74
|
+
if stream is not None:
|
|
75
|
+
stream.close()
|
|
76
|
+
|
|
77
|
+
def release_stream(self) -> None:
|
|
78
|
+
self.active_stream = None
|
|
79
|
+
|
|
80
|
+
def adopt(self, lease: ConnectionLease[ConnectionType], scope: ScopeType) -> None:
|
|
81
|
+
self.lease = lease
|
|
82
|
+
self.connection = lease.__enter__()
|
|
83
|
+
self.owner_thread = get_ident()
|
|
84
|
+
self.current_scope = scope
|
|
85
|
+
|
|
86
|
+
def release(self) -> None:
|
|
87
|
+
lease = self.lease
|
|
88
|
+
self.current_scope = None
|
|
89
|
+
self.connection = None
|
|
90
|
+
self.lease = None
|
|
91
|
+
self.owner_thread = None
|
|
92
|
+
cast(ConnectionLease[ConnectionType], lease).__exit__(None, None, None)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = ("TransactionState",)
|