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,430 @@
1
+ """A database that holds every statement to its policies.
2
+
3
+ A compiler is the one thing every statement built from nodes passes through, so
4
+ that is where policies are applied. Nothing reaches a driver another way except
5
+ SQL that was already compiled, which is the one case a node cannot govern, so
6
+ that case is refused here rather than left open.
7
+
8
+ Both are one object to install, because installing half of it would be a hole
9
+ that looked like a policy.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextlib import (
15
+ AbstractAsyncContextManager,
16
+ AbstractContextManager,
17
+ asynccontextmanager,
18
+ contextmanager,
19
+ )
20
+ from typing import TYPE_CHECKING, Protocol, TypeVar, cast, runtime_checkable
21
+
22
+ from pyoq.errors import OperationUnavailableError, PolicyViolationError
23
+ from pyoq.policies.model import Policies, Policy
24
+ from pyoq.policies.rewriting import govern
25
+ from pyoq.query.execution import (
26
+ BulkResult,
27
+ CompiledQuery,
28
+ QueryOperations,
29
+ RowStream,
30
+ )
31
+ from pyoq.query.execution.aio import AsyncQueryOperations, AsyncRowStream
32
+ from pyoq.serving import SupportsAsyncTransaction, SupportsTransaction
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import (
36
+ AsyncGenerator,
37
+ Awaitable,
38
+ Callable,
39
+ Generator,
40
+ Iterable,
41
+ )
42
+
43
+ from pyoq.query.execution import (
44
+ BulkPlan,
45
+ DatabaseCursor,
46
+ ExecuteResult,
47
+ ExecutionControl,
48
+ WriteProvider,
49
+ )
50
+ from pyoq.query.execution.aio import AsyncDatabaseCursor
51
+ from pyoq.query.select import Query
52
+ from pyoq.query.select_nodes import QueryNode
53
+ from pyoq.query.statements import (
54
+ StatementCompiler,
55
+ StatementNode,
56
+ StatementProvider,
57
+ )
58
+ from pyoq.query.write_nodes import WriteNode
59
+
60
+ OperationResult = TypeVar("OperationResult")
61
+ ResultRow = TypeVar("ResultRow")
62
+
63
+
64
+ class GovernedCompiler:
65
+ """A compiler that only ever sees a statement its policies allow."""
66
+
67
+ __slots__ = ("_inner", "_policies", "_raw_sql")
68
+
69
+ def __init__(
70
+ self,
71
+ inner: StatementCompiler,
72
+ policies: Policies,
73
+ *,
74
+ raw_sql: bool = False,
75
+ ) -> None:
76
+ self._inner = inner
77
+ self._policies = policies
78
+ self._raw_sql = raw_sql
79
+
80
+ @property
81
+ def capabilities(self) -> object:
82
+ """Whatever the dialect's own compiler says it can do.
83
+
84
+ Not every compiler declares one, so this asks rather than assumes.
85
+ """
86
+ return getattr(self._inner, "capabilities", None)
87
+
88
+ def compile(
89
+ self,
90
+ statement: StatementProvider | StatementNode,
91
+ /,
92
+ ) -> CompiledQuery:
93
+ node = getattr(statement, "node", statement)
94
+ governed = govern(node, self._policies, raw_sql=self._raw_sql)
95
+ return self._inner.compile(cast("StatementNode", governed))
96
+
97
+
98
+ @runtime_checkable
99
+ class SupportsStreaming(Protocol):
100
+ """A database that can read a result a piece at a time."""
101
+
102
+ def stream(self, query: Query[ResultRow], /) -> RowStream[ResultRow]: ...
103
+
104
+
105
+ @runtime_checkable
106
+ class SupportsAsyncStreaming(Protocol):
107
+ """The same, for a database that is awaited."""
108
+
109
+ def stream(self, query: Query[ResultRow], /) -> AsyncRowStream[ResultRow]: ...
110
+
111
+
112
+ class GovernedOperations(QueryOperations):
113
+ """A database whose statements are held to a policy set."""
114
+
115
+ __slots__ = ("_compiler", "_inner", "_policies", "_raw_sql")
116
+
117
+ def __init__(
118
+ self,
119
+ inner: QueryOperations,
120
+ policies: Policies,
121
+ *,
122
+ raw_sql: bool = False,
123
+ ) -> None:
124
+ self._inner = inner
125
+ self._policies = policies
126
+ self._raw_sql = raw_sql
127
+ self._compiler = GovernedCompiler(inner.compiler, policies, raw_sql=raw_sql)
128
+
129
+ @property
130
+ def policies(self) -> Policies:
131
+ return self._policies
132
+
133
+ @property
134
+ def released(self) -> QueryOperations:
135
+ """The database underneath, which nothing here holds to a policy."""
136
+ return self._inner
137
+
138
+ @property
139
+ def compiler(self) -> StatementCompiler:
140
+ return self._compiler
141
+
142
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
143
+ """A bulk write is planned from a governed statement, not the given one."""
144
+ return self._inner.plan_bulk(self._governed_write(statement))
145
+
146
+ def last_inserted_id(self, cursor: DatabaseCursor) -> int | None:
147
+ return self._inner.last_inserted_id(cursor)
148
+
149
+ def _governed_write(
150
+ self,
151
+ statement: WriteProvider | WriteNode,
152
+ ) -> WriteProvider | WriteNode:
153
+ node = getattr(statement, "node", statement)
154
+ governed = govern(node, self._policies, raw_sql=self._raw_sql)
155
+ return cast("WriteNode", governed)
156
+
157
+ def execute(
158
+ self,
159
+ statement: CompiledQuery | object,
160
+ /,
161
+ *,
162
+ control: ExecutionControl | None = None,
163
+ ) -> ExecuteResult:
164
+ _require_governable(statement, raw_sql=self._raw_sql)
165
+ return super().execute(statement, control=control) # type: ignore[arg-type]
166
+
167
+ def execute_bulk(
168
+ self,
169
+ statement: WriteProvider | WriteNode,
170
+ /,
171
+ *,
172
+ control: ExecutionControl | None = None,
173
+ ) -> BulkResult:
174
+ """The chunks of a plan this database made from a governed statement.
175
+
176
+ They are compiled SQL by the time they run, so they are handed to the
177
+ database underneath rather than back through the refusal that exists for
178
+ SQL a caller compiled elsewhere.
179
+ """
180
+ plan = self.plan_bulk(statement)
181
+ return BulkResult(self._inner.execute_chunks(plan, control), plan.row_counts)
182
+
183
+ def execute_chunks(
184
+ self,
185
+ plan: BulkPlan,
186
+ control: ExecutionControl | None,
187
+ ) -> tuple[ExecuteResult, ...]:
188
+ """A plan handed in from elsewhere is SQL from elsewhere."""
189
+ _require_governable(_first_statement(plan), raw_sql=self._raw_sql)
190
+ return self._inner.execute_chunks(plan, control)
191
+
192
+ def transaction(self) -> AbstractContextManager[QueryOperations]:
193
+ """A transaction held to the same policies the database is.
194
+
195
+ Without this a caller would reach for the database underneath to open
196
+ one, and everything inside it would be ungoverned.
197
+ """
198
+ return _governed_transaction(
199
+ _transactional(self._inner), self._policies, raw_sql=self._raw_sql
200
+ )
201
+
202
+ def stream(self, query: Query[ResultRow], /) -> RowStream[ResultRow]:
203
+ """A streamed read, governed the way any other read is.
204
+
205
+ A dialect's own streaming settings belong to the database underneath,
206
+ which is where it was given them.
207
+ """
208
+ return _streaming(self._inner).stream(self._governed_query(query))
209
+
210
+ def _governed_query(self, query: Query[ResultRow]) -> Query[ResultRow]:
211
+ governed_node = govern(query.node, self._policies, raw_sql=self._raw_sql)
212
+ return _rebuilt_query(query, governed_node)
213
+
214
+ def _run(
215
+ self,
216
+ statement: CompiledQuery,
217
+ operation: Callable[[DatabaseCursor], OperationResult],
218
+ control: ExecutionControl | None,
219
+ ) -> OperationResult:
220
+ return self._inner._run(statement, operation, control)
221
+
222
+
223
+ class AsyncGovernedOperations(AsyncQueryOperations):
224
+ """The asynchronous counterpart, governed at the same point."""
225
+
226
+ __slots__ = ("_compiler", "_inner", "_policies", "_raw_sql")
227
+
228
+ def __init__(
229
+ self,
230
+ inner: AsyncQueryOperations,
231
+ policies: Policies,
232
+ *,
233
+ raw_sql: bool = False,
234
+ ) -> None:
235
+ self._inner = inner
236
+ self._policies = policies
237
+ self._raw_sql = raw_sql
238
+ self._compiler = GovernedCompiler(inner.compiler, policies, raw_sql=raw_sql)
239
+
240
+ @property
241
+ def policies(self) -> Policies:
242
+ return self._policies
243
+
244
+ @property
245
+ def released(self) -> AsyncQueryOperations:
246
+ """The database underneath, which nothing here holds to a policy."""
247
+ return self._inner
248
+
249
+ @property
250
+ def compiler(self) -> StatementCompiler:
251
+ return self._compiler
252
+
253
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
254
+ return self._inner.plan_bulk(self._governed_write(statement))
255
+
256
+ def last_inserted_id(self, cursor: AsyncDatabaseCursor) -> int | None:
257
+ return self._inner.last_inserted_id(cursor)
258
+
259
+ def _governed_write(
260
+ self,
261
+ statement: WriteProvider | WriteNode,
262
+ ) -> WriteProvider | WriteNode:
263
+ node = getattr(statement, "node", statement)
264
+ governed = govern(node, self._policies, raw_sql=self._raw_sql)
265
+ return cast("WriteNode", governed)
266
+
267
+ async def execute(
268
+ self,
269
+ statement: CompiledQuery | object,
270
+ /,
271
+ *,
272
+ control: ExecutionControl | None = None,
273
+ ) -> ExecuteResult:
274
+ _require_governable(statement, raw_sql=self._raw_sql)
275
+ return await super().execute(statement, control=control) # type: ignore[arg-type]
276
+
277
+ async def execute_bulk(
278
+ self,
279
+ statement: WriteProvider | WriteNode,
280
+ /,
281
+ *,
282
+ control: ExecutionControl | None = None,
283
+ ) -> BulkResult:
284
+ """The chunks of a plan this database made from a governed statement."""
285
+ plan = self.plan_bulk(statement)
286
+ return BulkResult(
287
+ await self._inner.execute_chunks(plan, control), plan.row_counts
288
+ )
289
+
290
+ async def execute_chunks(
291
+ self,
292
+ plan: BulkPlan,
293
+ control: ExecutionControl | None,
294
+ ) -> tuple[ExecuteResult, ...]:
295
+ """A plan handed in from elsewhere is SQL from elsewhere."""
296
+ _require_governable(_first_statement(plan), raw_sql=self._raw_sql)
297
+ return await self._inner.execute_chunks(plan, control)
298
+
299
+ def transaction(self) -> AbstractAsyncContextManager[AsyncQueryOperations]:
300
+ """A transaction held to the same policies the database is."""
301
+ return _async_governed_transaction(
302
+ _async_transactional(self._inner), self._policies, raw_sql=self._raw_sql
303
+ )
304
+
305
+ def stream(self, query: Query[ResultRow], /) -> AsyncRowStream[ResultRow]:
306
+ """A streamed read, governed the way any other read is."""
307
+ return _async_streaming(self._inner).stream(self._governed_query(query))
308
+
309
+ def _governed_query(self, query: Query[ResultRow]) -> Query[ResultRow]:
310
+ governed_node = govern(query.node, self._policies, raw_sql=self._raw_sql)
311
+ return _rebuilt_query(query, governed_node)
312
+
313
+ async def _run(
314
+ self,
315
+ statement: CompiledQuery,
316
+ operation: Callable[[AsyncDatabaseCursor], Awaitable[OperationResult]],
317
+ control: ExecutionControl | None,
318
+ ) -> OperationResult:
319
+ return await self._inner._run(statement, operation, control)
320
+
321
+
322
+ def governed(
323
+ operations: QueryOperations,
324
+ policies: Iterable[Policy],
325
+ /,
326
+ *,
327
+ raw_sql: bool = False,
328
+ ) -> GovernedOperations:
329
+ """A database held to these policies."""
330
+ return GovernedOperations(operations, _policies_of(policies), raw_sql=raw_sql)
331
+
332
+
333
+ def async_governed(
334
+ operations: AsyncQueryOperations,
335
+ policies: Iterable[Policy],
336
+ /,
337
+ *,
338
+ raw_sql: bool = False,
339
+ ) -> AsyncGovernedOperations:
340
+ return AsyncGovernedOperations(operations, _policies_of(policies), raw_sql=raw_sql)
341
+
342
+
343
+ def _policies_of(policies: Iterable[object]) -> Policies:
344
+ from typing import cast
345
+
346
+ return Policies(tuple(cast("Policy", policy) for policy in policies))
347
+
348
+
349
+ @contextmanager
350
+ def _governed_transaction(
351
+ inner: SupportsTransaction,
352
+ policies: Policies,
353
+ *,
354
+ raw_sql: bool,
355
+ ) -> Generator[QueryOperations]:
356
+ with inner.transaction() as active:
357
+ yield GovernedOperations(active, policies, raw_sql=raw_sql)
358
+
359
+
360
+ @asynccontextmanager
361
+ async def _async_governed_transaction(
362
+ inner: SupportsAsyncTransaction,
363
+ policies: Policies,
364
+ *,
365
+ raw_sql: bool,
366
+ ) -> AsyncGenerator[AsyncQueryOperations]:
367
+ async with inner.transaction() as active:
368
+ yield AsyncGovernedOperations(active, policies, raw_sql=raw_sql)
369
+
370
+
371
+ def _rebuilt_query(query: Query[ResultRow], node: object) -> Query[ResultRow]:
372
+ """The same kind of query, around the statement a policy allows."""
373
+ return type(query)(cast("QueryNode", node))
374
+
375
+
376
+ def _transactional(inner: QueryOperations) -> SupportsTransaction:
377
+ if isinstance(inner, SupportsTransaction):
378
+ return inner
379
+ raise _cannot("open a transaction")
380
+
381
+
382
+ def _async_transactional(inner: AsyncQueryOperations) -> SupportsAsyncTransaction:
383
+ if isinstance(inner, SupportsAsyncTransaction):
384
+ return inner
385
+ raise _cannot("open a transaction")
386
+
387
+
388
+ def _streaming(inner: QueryOperations) -> SupportsStreaming:
389
+ if isinstance(inner, SupportsStreaming):
390
+ return inner
391
+ raise _cannot("stream a result")
392
+
393
+
394
+ def _async_streaming(inner: AsyncQueryOperations) -> SupportsAsyncStreaming:
395
+ if isinstance(inner, SupportsAsyncStreaming):
396
+ return inner
397
+ raise _cannot("stream a result")
398
+
399
+
400
+ def _cannot(what: str) -> OperationUnavailableError:
401
+ message = f"the database underneath cannot {what}"
402
+ return OperationUnavailableError(message)
403
+
404
+
405
+ def _first_statement(plan: BulkPlan) -> object:
406
+ """What the plan would run, judged the way one statement is.
407
+
408
+ A plan always has a chunk, because one with none is refused when it is made.
409
+ """
410
+ return plan.chunks[0].statement
411
+
412
+
413
+ def _require_governable(statement: object, *, raw_sql: bool) -> None:
414
+ """SQL that was already compiled has no statement left to hold."""
415
+ if not isinstance(statement, CompiledQuery) or raw_sql:
416
+ return
417
+ message = (
418
+ "already compiled SQL cannot be held to a policy, because there is no "
419
+ "statement to read; allow it deliberately if this connection may run it"
420
+ )
421
+ raise PolicyViolationError(message)
422
+
423
+
424
+ __all__ = (
425
+ "AsyncGovernedOperations",
426
+ "GovernedCompiler",
427
+ "GovernedOperations",
428
+ "async_governed",
429
+ "governed",
430
+ )
pyoq/policies/model.py ADDED
@@ -0,0 +1,242 @@
1
+ """What a policy says about a table a statement touches.
2
+
3
+ A policy answers three questions about one table: whether a statement may touch
4
+ it at all, what must always be true of the rows it sees, and what must always be
5
+ set on the rows it writes. Everything else follows from those three.
6
+
7
+ Policies work on the statement's own nodes, before any SQL exists. A rule
8
+ written against SQL text can be walked around with an alias, a subquery, or a
9
+ common table; a rule written against the node cannot, because the node is what
10
+ the alias, the subquery, and the common table are made of.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import TYPE_CHECKING, Protocol
17
+
18
+ from pyoq.query.fields import family_for_type
19
+ from pyoq.query.nodes import (
20
+ BinaryNode,
21
+ BinaryOperator,
22
+ BoundValueNode,
23
+ FieldNode,
24
+ ScalarFamily,
25
+ UnaryNode,
26
+ UnaryOperator,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Callable, Iterable
31
+
32
+ from pyoq.query.nodes import ExpressionNode
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class TableReference:
37
+ """A table a statement touches, and the name its columns answer to.
38
+
39
+ A statement that gave the table an alias refers to its columns by that
40
+ alias, so a condition added for it has to as well.
41
+ """
42
+
43
+ name: str
44
+ schema: str | None = None
45
+ catalog: str | None = None
46
+ alias: str | None = None
47
+
48
+ @property
49
+ def qualifier(self) -> str:
50
+ """What a column of this table is written under."""
51
+ return self.alias or self.name
52
+
53
+ def column(self, name: str, /) -> FieldNode:
54
+ """A column of this table, written the way the statement writes it."""
55
+ return FieldNode(name, self.qualifier)
56
+
57
+
58
+ class Policy(Protocol):
59
+ """What a policy is asked about every table a statement touches.
60
+
61
+ `refuses` says why the table may not be touched at all, or nothing when it
62
+ may. `restricts` says what must always be true of the rows a statement may
63
+ see. `assigns` says what must always be set on the rows it writes.
64
+ """
65
+
66
+ def refuses(self, table: TableReference, /) -> str | None: ...
67
+
68
+ def restricts(self, table: TableReference, /) -> ExpressionNode | None: ...
69
+
70
+ def assigns(
71
+ self,
72
+ table: TableReference,
73
+ /,
74
+ ) -> tuple[tuple[str, object], ...]: ...
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class TenantScope:
79
+ """Every row read or written belongs to one tenant.
80
+
81
+ The column is required on writes as well as reads, so a row cannot be
82
+ created outside the scope that would then be unable to see it.
83
+ """
84
+
85
+ column: str
86
+ value: object
87
+ tables: frozenset[str] | None = None
88
+
89
+ def refuses(self, table: TableReference, /) -> str | None:
90
+ del table
91
+ return None
92
+
93
+ def restricts(self, table: TableReference, /) -> ExpressionNode | None:
94
+ if not self._governs(table):
95
+ return None
96
+ return BinaryNode(
97
+ BinaryOperator.EQUAL,
98
+ table.column(self.column),
99
+ BoundValueNode(self.value, family_for_value(self.value)),
100
+ )
101
+
102
+ def assigns(
103
+ self,
104
+ table: TableReference,
105
+ /,
106
+ ) -> tuple[tuple[str, object], ...]:
107
+ if not self._governs(table):
108
+ return ()
109
+ return ((self.column, self.value),)
110
+
111
+ def _governs(self, table: TableReference) -> bool:
112
+ return self.tables is None or table.name in self.tables
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class SoftDelete:
117
+ """A row marked deleted is a row nothing reads."""
118
+
119
+ column: str
120
+ tables: frozenset[str] | None = None
121
+
122
+ def refuses(self, table: TableReference, /) -> str | None:
123
+ del table
124
+ return None
125
+
126
+ def restricts(self, table: TableReference, /) -> ExpressionNode | None:
127
+ if self.tables is not None and table.name not in self.tables:
128
+ return None
129
+ return UnaryNode(UnaryOperator.IS_NULL, table.column(self.column))
130
+
131
+ def assigns(
132
+ self,
133
+ table: TableReference,
134
+ /,
135
+ ) -> tuple[tuple[str, object], ...]:
136
+ del table
137
+ return ()
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class AllowedTables:
142
+ """Only these tables may be touched at all."""
143
+
144
+ names: frozenset[str]
145
+
146
+ def refuses(self, table: TableReference, /) -> str | None:
147
+ if table.name in self.names:
148
+ return None
149
+ return (
150
+ f"the table {table.name!r} is not one this connection may touch; "
151
+ f"allowed: {', '.join(sorted(self.names)) or 'none'}"
152
+ )
153
+
154
+ def restricts(self, table: TableReference, /) -> ExpressionNode | None:
155
+ del table
156
+ return None
157
+
158
+ def assigns(
159
+ self,
160
+ table: TableReference,
161
+ /,
162
+ ) -> tuple[tuple[str, object], ...]:
163
+ del table
164
+ return ()
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class RowConstraint:
169
+ """One table's rows are limited to those a condition admits."""
170
+
171
+ table: str
172
+ build: Callable[[TableReference], ExpressionNode]
173
+
174
+ def refuses(self, table: TableReference, /) -> str | None:
175
+ del table
176
+ return None
177
+
178
+ def restricts(self, table: TableReference, /) -> ExpressionNode | None:
179
+ return None if table.name != self.table else self.build(table)
180
+
181
+ def assigns(
182
+ self,
183
+ table: TableReference,
184
+ /,
185
+ ) -> tuple[tuple[str, object], ...]:
186
+ del table
187
+ return ()
188
+
189
+
190
+ @dataclass(frozen=True, slots=True)
191
+ class Policies:
192
+ """Every policy a statement is held to, asked as one."""
193
+
194
+ members: tuple[Policy, ...] = ()
195
+
196
+ @classmethod
197
+ def of(cls, policies: Iterable[Policy], /) -> Policies:
198
+ return cls(tuple(policies))
199
+
200
+ def refusal(self, table: TableReference, /) -> str | None:
201
+ for policy in self.members:
202
+ reason = policy.refuses(table)
203
+ if reason is not None:
204
+ return reason
205
+ return None
206
+
207
+ def conditions(self, table: TableReference, /) -> tuple[ExpressionNode, ...]:
208
+ return tuple(
209
+ condition
210
+ for condition in (policy.restricts(table) for policy in self.members)
211
+ if condition is not None
212
+ )
213
+
214
+ def assignments(
215
+ self,
216
+ table: TableReference,
217
+ /,
218
+ ) -> tuple[tuple[str, object], ...]:
219
+ return tuple(
220
+ assignment
221
+ for policy in self.members
222
+ for assignment in policy.assigns(table)
223
+ )
224
+
225
+ def __bool__(self) -> bool:
226
+ return bool(self.members)
227
+
228
+
229
+ __all__ = (
230
+ "AllowedTables",
231
+ "Policies",
232
+ "Policy",
233
+ "RowConstraint",
234
+ "SoftDelete",
235
+ "TableReference",
236
+ "TenantScope",
237
+ )
238
+
239
+
240
+ def family_for_value(value: object, /) -> ScalarFamily:
241
+ """What a policy's own value is, so it binds like any other."""
242
+ return ScalarFamily.NULL if value is None else family_for_type(type(value))