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