pyoq-sql 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
|
@@ -0,0 +1,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))
|