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,228 @@
|
|
|
1
|
+
"""Dialect-neutral asynchronous typed result operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Awaitable, 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
|
+
|
|
32
|
+
class AsyncDatabaseCursor(Protocol):
|
|
33
|
+
@property
|
|
34
|
+
def rowcount(self) -> int: ...
|
|
35
|
+
|
|
36
|
+
async def fetchall(self) -> Sequence[object]: ...
|
|
37
|
+
|
|
38
|
+
async def fetchmany(self, size: int, /) -> Sequence[object]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
ResultRow = TypeVar("ResultRow")
|
|
42
|
+
ScalarValue = TypeVar("ScalarValue")
|
|
43
|
+
OperationResult = TypeVar("OperationResult")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncQueryOperations(ABC):
|
|
47
|
+
__slots__ = ()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def compiler(self) -> StatementCompiler: ...
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan: ...
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
async def _run(
|
|
58
|
+
self,
|
|
59
|
+
statement: CompiledQuery,
|
|
60
|
+
operation: Callable[[AsyncDatabaseCursor], Awaitable[OperationResult]],
|
|
61
|
+
control: ExecutionControl | None,
|
|
62
|
+
) -> OperationResult: ...
|
|
63
|
+
|
|
64
|
+
def last_inserted_id(self, cursor: AsyncDatabaseCursor) -> int | None:
|
|
65
|
+
del cursor
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
async def one(
|
|
69
|
+
self,
|
|
70
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
71
|
+
/,
|
|
72
|
+
*,
|
|
73
|
+
control: ExecutionControl | None = None,
|
|
74
|
+
) -> ResultRow:
|
|
75
|
+
compiled = self.compiler.compile(query)
|
|
76
|
+
rows = await self._limited_rows(compiled, control)
|
|
77
|
+
if not rows:
|
|
78
|
+
raise NoResultError("query returned no rows")
|
|
79
|
+
if len(rows) > 1:
|
|
80
|
+
raise MultipleResultsError("query returned more than one row")
|
|
81
|
+
return cast("ResultRow", decode_row(rows[0], compiled.row_types))
|
|
82
|
+
|
|
83
|
+
async def one_or_none(
|
|
84
|
+
self,
|
|
85
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
86
|
+
/,
|
|
87
|
+
*,
|
|
88
|
+
control: ExecutionControl | None = None,
|
|
89
|
+
) -> ResultRow | None:
|
|
90
|
+
compiled = self.compiler.compile(query)
|
|
91
|
+
rows = await self._limited_rows(compiled, control)
|
|
92
|
+
if len(rows) > 1:
|
|
93
|
+
raise MultipleResultsError("query returned more than one row")
|
|
94
|
+
if not rows:
|
|
95
|
+
return None
|
|
96
|
+
return cast("ResultRow", decode_row(rows[0], compiled.row_types))
|
|
97
|
+
|
|
98
|
+
async def many(
|
|
99
|
+
self,
|
|
100
|
+
query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
|
|
101
|
+
/,
|
|
102
|
+
*,
|
|
103
|
+
control: ExecutionControl | None = None,
|
|
104
|
+
) -> list[ResultRow]:
|
|
105
|
+
compiled = self.compiler.compile(query)
|
|
106
|
+
rows = await self._run(compiled, fetch_all, control)
|
|
107
|
+
return cast("list[ResultRow]", decode_rows(rows, compiled.row_types))
|
|
108
|
+
|
|
109
|
+
async def scalar(
|
|
110
|
+
self,
|
|
111
|
+
query: Query[tuple[ScalarValue]] | ReturningStatement[tuple[ScalarValue]],
|
|
112
|
+
/,
|
|
113
|
+
*,
|
|
114
|
+
control: ExecutionControl | None = None,
|
|
115
|
+
) -> ScalarValue:
|
|
116
|
+
row = cast("tuple[object, ...]", await self.one(query, control=control))
|
|
117
|
+
return cast("ScalarValue", _scalar_value(row))
|
|
118
|
+
|
|
119
|
+
async def execute(
|
|
120
|
+
self,
|
|
121
|
+
statement: CompiledQuery | WriteStatement[WriteNode] | Call[tuple[object, ...]],
|
|
122
|
+
/,
|
|
123
|
+
*,
|
|
124
|
+
control: ExecutionControl | None = None,
|
|
125
|
+
) -> ExecuteResult:
|
|
126
|
+
compiled = (
|
|
127
|
+
statement
|
|
128
|
+
if isinstance(statement, CompiledQuery)
|
|
129
|
+
else self.compiler.compile(statement)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def operation(cursor: AsyncDatabaseCursor) -> ExecuteResult:
|
|
133
|
+
return self._execution_result(cursor, compiled.statement_kind)
|
|
134
|
+
|
|
135
|
+
return await self._run(compiled, operation, control)
|
|
136
|
+
|
|
137
|
+
async def execute_bulk(
|
|
138
|
+
self,
|
|
139
|
+
statement: WriteProvider | WriteNode,
|
|
140
|
+
/,
|
|
141
|
+
*,
|
|
142
|
+
control: ExecutionControl | None = None,
|
|
143
|
+
) -> BulkResult:
|
|
144
|
+
plan = self.plan_bulk(statement)
|
|
145
|
+
return BulkResult(await self.execute_chunks(plan, control), plan.row_counts)
|
|
146
|
+
|
|
147
|
+
async def execute_chunks(
|
|
148
|
+
self,
|
|
149
|
+
plan: BulkPlan,
|
|
150
|
+
control: ExecutionControl | None,
|
|
151
|
+
) -> tuple[ExecuteResult, ...]:
|
|
152
|
+
results = [
|
|
153
|
+
await self.execute(chunk.statement, control=control)
|
|
154
|
+
for chunk in plan.chunks
|
|
155
|
+
]
|
|
156
|
+
return tuple(results)
|
|
157
|
+
|
|
158
|
+
async def many_bulk(
|
|
159
|
+
self,
|
|
160
|
+
statement: ReturningStatement[ResultRow],
|
|
161
|
+
/,
|
|
162
|
+
*,
|
|
163
|
+
control: ExecutionControl | None = None,
|
|
164
|
+
) -> list[ResultRow]:
|
|
165
|
+
plan = self.plan_bulk(statement)
|
|
166
|
+
rows: list[object] = []
|
|
167
|
+
for chunk in plan.chunks:
|
|
168
|
+
answered = await self._run(chunk.statement, fetch_all, control)
|
|
169
|
+
rows.extend(decode_rows(answered, chunk.statement.row_types))
|
|
170
|
+
return cast("list[ResultRow]", rows)
|
|
171
|
+
|
|
172
|
+
async def execute_all(
|
|
173
|
+
self,
|
|
174
|
+
statements: Iterable[CompiledQuery | WriteStatement[WriteNode]],
|
|
175
|
+
/,
|
|
176
|
+
*,
|
|
177
|
+
budget: OperationBudget | None = None,
|
|
178
|
+
control: ExecutionControl | None = None,
|
|
179
|
+
) -> tuple[ExecuteResult, ...]:
|
|
180
|
+
operations: Sequence[CompiledQuery | WriteStatement[WriteNode]] = tuple(
|
|
181
|
+
statements
|
|
182
|
+
)
|
|
183
|
+
limit = (budget or OperationBudget()).maximum_operations
|
|
184
|
+
if len(operations) > limit:
|
|
185
|
+
message = f"operation batch exceeds its budget of {limit} operations"
|
|
186
|
+
raise QueryValidationError(message)
|
|
187
|
+
results = [
|
|
188
|
+
await self.execute(operation, control=control) for operation in operations
|
|
189
|
+
]
|
|
190
|
+
return tuple(results)
|
|
191
|
+
|
|
192
|
+
def _execution_result(
|
|
193
|
+
self,
|
|
194
|
+
cursor: AsyncDatabaseCursor,
|
|
195
|
+
statement_kind: StatementKind,
|
|
196
|
+
) -> ExecuteResult:
|
|
197
|
+
row_count = max(cursor.rowcount, 0)
|
|
198
|
+
inserted = (
|
|
199
|
+
self.last_inserted_id(cursor)
|
|
200
|
+
if statement_kind is StatementKind.INSERT
|
|
201
|
+
else None
|
|
202
|
+
)
|
|
203
|
+
return ExecuteResult(row_count, inserted)
|
|
204
|
+
|
|
205
|
+
async def _limited_rows(
|
|
206
|
+
self,
|
|
207
|
+
statement: CompiledQuery,
|
|
208
|
+
control: ExecutionControl | None,
|
|
209
|
+
) -> list[object]:
|
|
210
|
+
return await self._run(statement, _fetch_up_to_two, control)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def fetch_all(cursor: AsyncDatabaseCursor) -> list[object]:
|
|
214
|
+
return list(await cursor.fetchall())
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def _fetch_up_to_two(cursor: AsyncDatabaseCursor) -> list[object]:
|
|
218
|
+
return list(await cursor.fetchmany(2))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _scalar_value(row: tuple[object, ...]) -> object:
|
|
222
|
+
if len(row) != 1:
|
|
223
|
+
message = "scalar query projection must contain exactly one value"
|
|
224
|
+
raise ResultCardinalityError(message)
|
|
225
|
+
return row[0]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
__all__ = ("AsyncDatabaseCursor", "AsyncQueryOperations", "fetch_all")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Exclusive asynchronous connection pooling shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from asyncio import Condition, wait_for
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Generic, Self, TypeVar
|
|
11
|
+
|
|
12
|
+
from pyoq.errors import (
|
|
13
|
+
ConnectionPoolClosedError,
|
|
14
|
+
ConnectionPoolError,
|
|
15
|
+
ConnectionPoolTimeoutError,
|
|
16
|
+
)
|
|
17
|
+
from pyoq.query.execution.pooling import PoolPolicy, PoolStats
|
|
18
|
+
|
|
19
|
+
ConnectionType = TypeVar("ConnectionType")
|
|
20
|
+
AsyncConnectionFactory = Callable[[], Awaitable[ConnectionType]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncConnectionLease(Generic[ConnectionType]):
|
|
24
|
+
__slots__ = ("_connection", "_entered", "_invalid", "_pool")
|
|
25
|
+
|
|
26
|
+
def __init__(self, pool: AsyncConnectionPool[ConnectionType]) -> None:
|
|
27
|
+
self._pool = pool
|
|
28
|
+
self._connection: ConnectionType | None = None
|
|
29
|
+
self._entered = False
|
|
30
|
+
self._invalid = False
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def connection(self) -> ConnectionType:
|
|
34
|
+
if self._connection is None or not self._entered:
|
|
35
|
+
message = "connection lease is not active"
|
|
36
|
+
raise ConnectionPoolError(message)
|
|
37
|
+
return self._connection
|
|
38
|
+
|
|
39
|
+
def invalidate(self) -> None:
|
|
40
|
+
if not self._entered:
|
|
41
|
+
message = "inactive connection lease cannot be invalidated"
|
|
42
|
+
raise ConnectionPoolError(message)
|
|
43
|
+
self._invalid = True
|
|
44
|
+
|
|
45
|
+
async def __aenter__(self) -> ConnectionType:
|
|
46
|
+
if self._entered:
|
|
47
|
+
message = "connection lease cannot be entered twice"
|
|
48
|
+
raise ConnectionPoolError(message)
|
|
49
|
+
connection = await self._pool.acquire_for_lease()
|
|
50
|
+
self._connection = connection
|
|
51
|
+
self._entered = True
|
|
52
|
+
return connection
|
|
53
|
+
|
|
54
|
+
async def __aexit__(
|
|
55
|
+
self,
|
|
56
|
+
exception_type: type[BaseException] | None,
|
|
57
|
+
exception: BaseException | None,
|
|
58
|
+
traceback: object,
|
|
59
|
+
) -> None:
|
|
60
|
+
connection = self._connection
|
|
61
|
+
if connection is None or not self._entered:
|
|
62
|
+
return
|
|
63
|
+
self._entered = False
|
|
64
|
+
self._connection = None
|
|
65
|
+
await self._pool.release_from_lease(connection, invalid=self._invalid)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AsyncConnectionPool(ABC, Generic[ConnectionType]):
|
|
69
|
+
__slots__ = (
|
|
70
|
+
"_checked_out",
|
|
71
|
+
"_closed",
|
|
72
|
+
"_condition",
|
|
73
|
+
"_factory",
|
|
74
|
+
"_idle",
|
|
75
|
+
"_open_connections",
|
|
76
|
+
"_policy",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
factory: AsyncConnectionFactory[ConnectionType],
|
|
82
|
+
policy: PoolPolicy | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._factory = factory
|
|
85
|
+
self._policy = policy or PoolPolicy()
|
|
86
|
+
self._condition = Condition()
|
|
87
|
+
self._idle: deque[ConnectionType] = deque()
|
|
88
|
+
self._checked_out: set[int] = set()
|
|
89
|
+
self._open_connections = 0
|
|
90
|
+
self._closed = False
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
async def reset(self, connection: ConnectionType) -> bool: ...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def validate(self, value: object) -> ConnectionType: ...
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def dispose(self, connection: ConnectionType) -> None: ...
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def policy(self) -> PoolPolicy:
|
|
103
|
+
return self._policy
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def stats(self) -> PoolStats:
|
|
107
|
+
return PoolStats(
|
|
108
|
+
self._open_connections,
|
|
109
|
+
len(self._idle),
|
|
110
|
+
len(self._checked_out),
|
|
111
|
+
self._closed,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def checkout(self) -> AsyncConnectionLease[ConnectionType]:
|
|
115
|
+
return AsyncConnectionLease(self)
|
|
116
|
+
|
|
117
|
+
async def open(self) -> Self:
|
|
118
|
+
created: list[ConnectionType] = []
|
|
119
|
+
try:
|
|
120
|
+
for _ in range(self.policy.minimum_size):
|
|
121
|
+
created.append(await self._create_connection())
|
|
122
|
+
except Exception:
|
|
123
|
+
for connection in created:
|
|
124
|
+
await self.dispose(connection)
|
|
125
|
+
raise
|
|
126
|
+
self._idle.extend(created)
|
|
127
|
+
self._open_connections += len(created)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
async with self._condition:
|
|
132
|
+
if self._closed:
|
|
133
|
+
return
|
|
134
|
+
self._closed = True
|
|
135
|
+
idle = tuple(self._idle)
|
|
136
|
+
self._idle.clear()
|
|
137
|
+
self._open_connections -= len(idle)
|
|
138
|
+
self._condition.notify_all()
|
|
139
|
+
for connection in idle:
|
|
140
|
+
await self.dispose(connection)
|
|
141
|
+
|
|
142
|
+
async def __aenter__(self) -> Self:
|
|
143
|
+
return await self.open()
|
|
144
|
+
|
|
145
|
+
async def __aexit__(
|
|
146
|
+
self,
|
|
147
|
+
exception_type: type[BaseException] | None,
|
|
148
|
+
exception: BaseException | None,
|
|
149
|
+
traceback: object,
|
|
150
|
+
) -> None:
|
|
151
|
+
await self.close()
|
|
152
|
+
|
|
153
|
+
async def acquire_for_lease(self) -> ConnectionType:
|
|
154
|
+
deadline = _deadline(self.policy.checkout_timeout)
|
|
155
|
+
async with self._condition:
|
|
156
|
+
while True:
|
|
157
|
+
self._require_open()
|
|
158
|
+
if self._idle:
|
|
159
|
+
connection = self._idle.popleft()
|
|
160
|
+
self._checked_out.add(id(connection))
|
|
161
|
+
return connection
|
|
162
|
+
if self._open_connections < self.policy.maximum_size:
|
|
163
|
+
self._open_connections += 1
|
|
164
|
+
break
|
|
165
|
+
await self._wait_for_connection(deadline)
|
|
166
|
+
return await self._create_reserved_connection()
|
|
167
|
+
|
|
168
|
+
async def release_from_lease(
|
|
169
|
+
self,
|
|
170
|
+
connection: ConnectionType,
|
|
171
|
+
*,
|
|
172
|
+
invalid: bool,
|
|
173
|
+
) -> None:
|
|
174
|
+
reusable = not invalid and await self.reset(connection)
|
|
175
|
+
async with self._condition:
|
|
176
|
+
self._checked_out.discard(id(connection))
|
|
177
|
+
if reusable and not self._closed:
|
|
178
|
+
self._idle.append(connection)
|
|
179
|
+
else:
|
|
180
|
+
self._open_connections -= 1
|
|
181
|
+
self._condition.notify()
|
|
182
|
+
if not reusable or self._closed:
|
|
183
|
+
await self.dispose(connection)
|
|
184
|
+
|
|
185
|
+
async def _create_reserved_connection(self) -> ConnectionType:
|
|
186
|
+
try:
|
|
187
|
+
connection = await self._create_connection()
|
|
188
|
+
except Exception:
|
|
189
|
+
async with self._condition:
|
|
190
|
+
self._open_connections -= 1
|
|
191
|
+
self._condition.notify()
|
|
192
|
+
raise
|
|
193
|
+
async with self._condition:
|
|
194
|
+
if self._closed:
|
|
195
|
+
self._open_connections -= 1
|
|
196
|
+
await self.dispose(connection)
|
|
197
|
+
raise ConnectionPoolClosedError("connection pool is closed")
|
|
198
|
+
self._checked_out.add(id(connection))
|
|
199
|
+
return connection
|
|
200
|
+
|
|
201
|
+
async def _create_connection(self) -> ConnectionType:
|
|
202
|
+
try:
|
|
203
|
+
connection_value: object = await self._factory()
|
|
204
|
+
except ConnectionPoolError:
|
|
205
|
+
raise
|
|
206
|
+
except Exception as error:
|
|
207
|
+
message = f"unable to create pooled connection: {error}"
|
|
208
|
+
raise ConnectionPoolError(message) from error
|
|
209
|
+
return self.validate(connection_value)
|
|
210
|
+
|
|
211
|
+
async def _wait_for_connection(self, deadline: float | None) -> None:
|
|
212
|
+
remaining = None if deadline is None else deadline - monotonic()
|
|
213
|
+
try:
|
|
214
|
+
await wait_for(self._condition.wait(), remaining)
|
|
215
|
+
except TimeoutError as error:
|
|
216
|
+
message = "connection checkout exceeded its timeout"
|
|
217
|
+
raise ConnectionPoolTimeoutError(message) from error
|
|
218
|
+
|
|
219
|
+
def _require_open(self) -> None:
|
|
220
|
+
if self._closed:
|
|
221
|
+
message = "connection pool is closed"
|
|
222
|
+
raise ConnectionPoolClosedError(message)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _deadline(timeout: float | None) -> float | None:
|
|
226
|
+
return None if timeout is None else monotonic() + timeout
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
__all__ = (
|
|
230
|
+
"AsyncConnectionFactory",
|
|
231
|
+
"AsyncConnectionLease",
|
|
232
|
+
"AsyncConnectionPool",
|
|
233
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Bounded asynchronous row streaming shared by every dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from asyncio import current_task
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
|
+
from typing import Generic, Protocol, Self, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from pyoq.errors import StreamStateError
|
|
11
|
+
from pyoq.query.decoding import decode_rows
|
|
12
|
+
from pyoq.query.execution.streaming import StreamPolicy
|
|
13
|
+
|
|
14
|
+
ResultRow = TypeVar("ResultRow")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncCursorSession(Protocol):
|
|
18
|
+
def checkpoint(self) -> None: ...
|
|
19
|
+
|
|
20
|
+
async def fetch_batch(self, size: int) -> list[tuple[object, ...]]: ...
|
|
21
|
+
|
|
22
|
+
async def close(self) -> None: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncStreamResource:
|
|
26
|
+
__slots__ = ("_closed", "_release", "session")
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
session: AsyncCursorSession,
|
|
31
|
+
release: Callable[[], Awaitable[None]],
|
|
32
|
+
) -> None:
|
|
33
|
+
self.session = session
|
|
34
|
+
self._release = release
|
|
35
|
+
self._closed = False
|
|
36
|
+
|
|
37
|
+
async def close(self) -> None:
|
|
38
|
+
if self._closed:
|
|
39
|
+
return
|
|
40
|
+
self._closed = True
|
|
41
|
+
try:
|
|
42
|
+
await self.session.close()
|
|
43
|
+
finally:
|
|
44
|
+
await self._release()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncRowStream(AsyncIterator[ResultRow], Generic[ResultRow]):
|
|
48
|
+
__slots__ = (
|
|
49
|
+
"_buffer",
|
|
50
|
+
"_closed",
|
|
51
|
+
"_opened",
|
|
52
|
+
"_opener",
|
|
53
|
+
"_owner_task",
|
|
54
|
+
"_policy",
|
|
55
|
+
"_resource",
|
|
56
|
+
"_row_types",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
opener: Callable[[], Awaitable[AsyncStreamResource]],
|
|
62
|
+
policy: StreamPolicy,
|
|
63
|
+
row_types: tuple[type[object] | None, ...] = (),
|
|
64
|
+
) -> None:
|
|
65
|
+
self._opener = opener
|
|
66
|
+
self._policy = policy
|
|
67
|
+
self._row_types = row_types
|
|
68
|
+
self._buffer: deque[tuple[object, ...]] = deque()
|
|
69
|
+
self._resource: AsyncStreamResource | None = None
|
|
70
|
+
self._owner_task: object | None = None
|
|
71
|
+
self._opened = False
|
|
72
|
+
self._closed = False
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def closed(self) -> bool:
|
|
76
|
+
return self._closed
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def buffered_rows(self) -> int:
|
|
80
|
+
return len(self._buffer)
|
|
81
|
+
|
|
82
|
+
async def __aenter__(self) -> Self:
|
|
83
|
+
if self._closed:
|
|
84
|
+
raise StreamStateError("closed row stream cannot be entered")
|
|
85
|
+
await self._ensure_open()
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
async def __aexit__(
|
|
89
|
+
self,
|
|
90
|
+
exception_type: type[BaseException] | None,
|
|
91
|
+
exception: BaseException | None,
|
|
92
|
+
traceback: object,
|
|
93
|
+
) -> None:
|
|
94
|
+
await self.close()
|
|
95
|
+
|
|
96
|
+
def __aiter__(self) -> Self:
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __anext__(self) -> ResultRow:
|
|
100
|
+
if self._closed:
|
|
101
|
+
raise StopAsyncIteration
|
|
102
|
+
await self._ensure_open()
|
|
103
|
+
self._require_owner()
|
|
104
|
+
try:
|
|
105
|
+
resource = self._require_resource()
|
|
106
|
+
resource.session.checkpoint()
|
|
107
|
+
if not self._buffer:
|
|
108
|
+
await self._load_batch()
|
|
109
|
+
return cast("ResultRow", self._buffer.popleft())
|
|
110
|
+
except BaseException:
|
|
111
|
+
await self.close()
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
async def close(self) -> None:
|
|
115
|
+
if self._closed:
|
|
116
|
+
return
|
|
117
|
+
if self._opened:
|
|
118
|
+
self._require_owner()
|
|
119
|
+
self._closed = True
|
|
120
|
+
self._buffer.clear()
|
|
121
|
+
resource = self._resource
|
|
122
|
+
self._resource = None
|
|
123
|
+
if resource is not None:
|
|
124
|
+
await resource.close()
|
|
125
|
+
|
|
126
|
+
async def _ensure_open(self) -> None:
|
|
127
|
+
if self._opened:
|
|
128
|
+
self._require_owner()
|
|
129
|
+
return
|
|
130
|
+
self._owner_task = current_task()
|
|
131
|
+
try:
|
|
132
|
+
self._resource = await self._opener()
|
|
133
|
+
except BaseException:
|
|
134
|
+
self._closed = True
|
|
135
|
+
raise
|
|
136
|
+
self._opened = True
|
|
137
|
+
|
|
138
|
+
async def _load_batch(self) -> None:
|
|
139
|
+
resource = self._require_resource()
|
|
140
|
+
rows = await resource.session.fetch_batch(self._policy.batch_size)
|
|
141
|
+
if not rows:
|
|
142
|
+
raise StopAsyncIteration
|
|
143
|
+
self._buffer.extend(
|
|
144
|
+
cast("list[tuple[object, ...]]", decode_rows(rows, self._row_types))
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _require_resource(self) -> AsyncStreamResource:
|
|
148
|
+
if self._resource is None:
|
|
149
|
+
raise StreamStateError("row stream has no active cursor")
|
|
150
|
+
return self._resource
|
|
151
|
+
|
|
152
|
+
def _require_owner(self) -> None:
|
|
153
|
+
if self._owner_task is not current_task():
|
|
154
|
+
raise StreamStateError("row stream used outside its owner task")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = (
|
|
158
|
+
"AsyncCursorSession",
|
|
159
|
+
"AsyncRowStream",
|
|
160
|
+
"AsyncStreamResource",
|
|
161
|
+
)
|