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.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. 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")