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,277 @@
|
|
|
1
|
+
"""Immutable structural nodes for SELECT queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from pyoq.query.nodes import ExpressionNode, NamedWindowNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JoinKind(StrEnum):
|
|
13
|
+
INNER = "inner"
|
|
14
|
+
LEFT = "left"
|
|
15
|
+
RIGHT = "right"
|
|
16
|
+
FULL = "full"
|
|
17
|
+
CROSS = "cross"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OrderDirection(StrEnum):
|
|
21
|
+
ASCENDING = "ascending"
|
|
22
|
+
DESCENDING = "descending"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NullOrder(StrEnum):
|
|
26
|
+
FIRST = "first"
|
|
27
|
+
LAST = "last"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SetOperator(StrEnum):
|
|
31
|
+
UNION = "union"
|
|
32
|
+
UNION_ALL = "union-all"
|
|
33
|
+
INTERSECT = "intersect"
|
|
34
|
+
EXCEPT = "except"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class TableSourceNode:
|
|
39
|
+
name: str
|
|
40
|
+
schema: str | None = None
|
|
41
|
+
catalog: str | None = None
|
|
42
|
+
alias: str | None = None
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
_require_identifier(self.name, "table")
|
|
46
|
+
_require_optional_identifier(self.schema, "schema")
|
|
47
|
+
_require_optional_identifier(self.catalog, "catalog")
|
|
48
|
+
_require_optional_identifier(self.alias, "table alias")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ProjectionNode:
|
|
53
|
+
expression: ExpressionNode
|
|
54
|
+
alias: str | None = None
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
_require_optional_identifier(self.alias, "projection alias")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class OrderNode:
|
|
62
|
+
expression: ExpressionNode
|
|
63
|
+
direction: OrderDirection
|
|
64
|
+
nulls: NullOrder | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LockStrength(StrEnum):
|
|
68
|
+
"""How much of a row a lock holds, from the whole row down to its key."""
|
|
69
|
+
|
|
70
|
+
UPDATE = "update"
|
|
71
|
+
NO_KEY_UPDATE = "no-key-update"
|
|
72
|
+
SHARE = "share"
|
|
73
|
+
KEY_SHARE = "key-share"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LockWait(StrEnum):
|
|
77
|
+
"""What a statement does about rows somebody else has already locked."""
|
|
78
|
+
|
|
79
|
+
WAIT = "wait"
|
|
80
|
+
NOWAIT = "nowait"
|
|
81
|
+
SKIP_LOCKED = "skip-locked"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class LockNode:
|
|
86
|
+
"""Rows held for this transaction, so nobody else changes them first.
|
|
87
|
+
|
|
88
|
+
``tables`` narrows the lock to some of what the query reads, which is
|
|
89
|
+
what keeps a join from locking rows it only looked at.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
strength: LockStrength
|
|
93
|
+
wait: LockWait = LockWait.WAIT
|
|
94
|
+
tables: tuple[str, ...] = ()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class SelectNode:
|
|
99
|
+
projections: tuple[ProjectionNode, ...]
|
|
100
|
+
source: SourceNode | None = None
|
|
101
|
+
joins: tuple[JoinNode, ...] = ()
|
|
102
|
+
condition: ExpressionNode | None = None
|
|
103
|
+
matches: tuple[ExpressionNode, ...] = ()
|
|
104
|
+
"""What a semi or anti join asks of each row.
|
|
105
|
+
|
|
106
|
+
Kept apart from the condition so that `where` stays a clause a caller
|
|
107
|
+
defines once, and a semi join neither collides with it nor depends on
|
|
108
|
+
having been written after it.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
groups: tuple[ExpressionNode, ...] = ()
|
|
112
|
+
having: ExpressionNode | None = None
|
|
113
|
+
orders: tuple[OrderNode, ...] = ()
|
|
114
|
+
limit: int | None = None
|
|
115
|
+
offset: int | None = None
|
|
116
|
+
common_tables: tuple[CommonTableNode, ...] = ()
|
|
117
|
+
windows: tuple[NamedWindowNode, ...] = ()
|
|
118
|
+
"""Windows written once here and looked through by name."""
|
|
119
|
+
|
|
120
|
+
distinct: bool = False
|
|
121
|
+
lock: LockNode | None = None
|
|
122
|
+
every_column: bool = False
|
|
123
|
+
"""Whether the projection is written as a star.
|
|
124
|
+
|
|
125
|
+
The columns are still carried, because they are what names a row's fields
|
|
126
|
+
and says what type each one reads back as. Only the SQL is shorter.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __post_init__(self) -> None:
|
|
130
|
+
if not self.projections:
|
|
131
|
+
message = "SELECT requires at least one projection"
|
|
132
|
+
raise ValueError(message)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class SetNode:
|
|
137
|
+
left: QueryNode
|
|
138
|
+
operator: SetOperator
|
|
139
|
+
right: QueryNode
|
|
140
|
+
orders: tuple[OrderNode, ...] = ()
|
|
141
|
+
limit: int | None = None
|
|
142
|
+
offset: int | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class SubquerySourceNode:
|
|
147
|
+
query: QueryNode
|
|
148
|
+
alias: str
|
|
149
|
+
lateral: bool = False
|
|
150
|
+
"""Whether the subquery may read the rows to its left.
|
|
151
|
+
|
|
152
|
+
An ordinary subquery in a FROM clause is evaluated on its own. A lateral
|
|
153
|
+
one is evaluated per row of what came before it, which is the whole point
|
|
154
|
+
and also why not every dialect has it.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __post_init__(self) -> None:
|
|
158
|
+
_require_identifier(self.alias, "subquery alias")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class CommonTableSourceNode:
|
|
163
|
+
name: str
|
|
164
|
+
|
|
165
|
+
def __post_init__(self) -> None:
|
|
166
|
+
_require_identifier(self.name, "common table")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True, slots=True)
|
|
170
|
+
class CommonTableNode:
|
|
171
|
+
name: str
|
|
172
|
+
query: QueryNode
|
|
173
|
+
columns: tuple[str, ...] = ()
|
|
174
|
+
recursive: bool = False
|
|
175
|
+
|
|
176
|
+
def __post_init__(self) -> None:
|
|
177
|
+
_require_identifier(self.name, "common table")
|
|
178
|
+
for column in self.columns:
|
|
179
|
+
_require_identifier(column, "common table column")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True, slots=True)
|
|
183
|
+
class JoinNode:
|
|
184
|
+
kind: JoinKind
|
|
185
|
+
source: SourceNode
|
|
186
|
+
condition: ExpressionNode | None = None
|
|
187
|
+
using: tuple[str, ...] = ()
|
|
188
|
+
natural: bool = False
|
|
189
|
+
|
|
190
|
+
def __post_init__(self) -> None:
|
|
191
|
+
_validate_join(self)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_join(node: JoinNode) -> None:
|
|
195
|
+
"""A join is qualified once, by the one clause SQL allows it.
|
|
196
|
+
|
|
197
|
+
`ON`, `USING`, and `NATURAL` each say how two tables line up, and a join
|
|
198
|
+
that named two of them would be asking the database to decide which.
|
|
199
|
+
"""
|
|
200
|
+
for column in node.using:
|
|
201
|
+
_require_identifier(column, "join column")
|
|
202
|
+
qualifiers = (
|
|
203
|
+
node.condition is not None,
|
|
204
|
+
bool(node.using),
|
|
205
|
+
node.natural,
|
|
206
|
+
)
|
|
207
|
+
named = sum(qualifiers)
|
|
208
|
+
if node.kind is JoinKind.CROSS:
|
|
209
|
+
if named:
|
|
210
|
+
message = "cross join cannot be qualified"
|
|
211
|
+
raise ValueError(message)
|
|
212
|
+
return
|
|
213
|
+
if named == 0:
|
|
214
|
+
message = "qualified join requires ON, USING, or NATURAL"
|
|
215
|
+
raise ValueError(message)
|
|
216
|
+
if named > 1:
|
|
217
|
+
message = "qualified join takes one of ON, USING, or NATURAL"
|
|
218
|
+
raise ValueError(message)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
QueryNode: TypeAlias = SelectNode | SetNode
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True, slots=True)
|
|
225
|
+
class JsonElementsSourceNode:
|
|
226
|
+
"""The elements of a JSON array, read one row at a time.
|
|
227
|
+
|
|
228
|
+
Every dialect can turn an array into rows, and each spells it so
|
|
229
|
+
differently that only the alias and the one column are shared.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
expression: ExpressionNode
|
|
233
|
+
alias: str
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
ELEMENT_COLUMN = "value"
|
|
237
|
+
"""What the one column of an expanded JSON array is called on every dialect."""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
SourceNode: TypeAlias = (
|
|
241
|
+
TableSourceNode
|
|
242
|
+
| SubquerySourceNode
|
|
243
|
+
| CommonTableSourceNode
|
|
244
|
+
| JsonElementsSourceNode
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _require_identifier(value: str, label: str) -> None:
|
|
249
|
+
if not value or "\x00" in value:
|
|
250
|
+
message = f"{label} identifier cannot be empty or contain a null character"
|
|
251
|
+
raise ValueError(message)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _require_optional_identifier(value: str | None, label: str) -> None:
|
|
255
|
+
if value is not None:
|
|
256
|
+
_require_identifier(value, label)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = (
|
|
260
|
+
"ELEMENT_COLUMN",
|
|
261
|
+
"CommonTableNode",
|
|
262
|
+
"CommonTableSourceNode",
|
|
263
|
+
"JoinKind",
|
|
264
|
+
"JoinNode",
|
|
265
|
+
"JsonElementsSourceNode",
|
|
266
|
+
"NullOrder",
|
|
267
|
+
"OrderDirection",
|
|
268
|
+
"OrderNode",
|
|
269
|
+
"ProjectionNode",
|
|
270
|
+
"QueryNode",
|
|
271
|
+
"SelectNode",
|
|
272
|
+
"SetNode",
|
|
273
|
+
"SetOperator",
|
|
274
|
+
"SourceNode",
|
|
275
|
+
"SubquerySourceNode",
|
|
276
|
+
"TableSourceNode",
|
|
277
|
+
)
|
pyoq/query/sources.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Typed query sources and write targets for generated table descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar, Generic, Protocol, TypeVar, cast, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import QueryValidationError
|
|
8
|
+
from pyoq.query.fields import Field, field
|
|
9
|
+
from pyoq.query.nodes import FieldNode
|
|
10
|
+
from pyoq.query.raw import NodeProvider
|
|
11
|
+
from pyoq.query.select_nodes import (
|
|
12
|
+
ELEMENT_COLUMN,
|
|
13
|
+
CommonTableNode,
|
|
14
|
+
CommonTableSourceNode,
|
|
15
|
+
JsonElementsSourceNode,
|
|
16
|
+
QueryNode,
|
|
17
|
+
SourceNode,
|
|
18
|
+
SubquerySourceNode,
|
|
19
|
+
TableSourceNode,
|
|
20
|
+
)
|
|
21
|
+
from pyoq.query.write_nodes import WriteTargetNode
|
|
22
|
+
|
|
23
|
+
SourceRow = TypeVar("SourceRow", covariant=True)
|
|
24
|
+
FieldValue = TypeVar("FieldValue")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class TableLike(Protocol):
|
|
29
|
+
@property
|
|
30
|
+
def database_name(self) -> str: ...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def schema_name(self) -> str | None: ...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def catalog_name(self) -> str | None: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class ColumnedTable(TableLike, Protocol):
|
|
41
|
+
"""A table that names its own columns, which a generated table does."""
|
|
42
|
+
|
|
43
|
+
COLUMNS: ClassVar[tuple[object, ...]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SourceBase:
|
|
47
|
+
__slots__ = ("_node", "_qualifier")
|
|
48
|
+
_node: SourceNode
|
|
49
|
+
_qualifier: str
|
|
50
|
+
|
|
51
|
+
def __init__(self, node: SourceNode, qualifier: str) -> None:
|
|
52
|
+
object.__setattr__(self, "_node", node)
|
|
53
|
+
object.__setattr__(self, "_qualifier", qualifier)
|
|
54
|
+
|
|
55
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
56
|
+
message = "query sources are immutable"
|
|
57
|
+
raise AttributeError(message)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def node(self) -> SourceNode:
|
|
61
|
+
return self._node
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Source(SourceBase, Generic[SourceRow]):
|
|
65
|
+
__slots__ = ()
|
|
66
|
+
|
|
67
|
+
def field(
|
|
68
|
+
self,
|
|
69
|
+
value_type: type[FieldValue],
|
|
70
|
+
database_name: str,
|
|
71
|
+
/,
|
|
72
|
+
) -> Field[FieldValue]:
|
|
73
|
+
schema_name, catalog_name = _source_qualification(self.node)
|
|
74
|
+
return field(
|
|
75
|
+
value_type,
|
|
76
|
+
database_name,
|
|
77
|
+
table_name=self._qualifier,
|
|
78
|
+
schema_name=schema_name,
|
|
79
|
+
catalog_name=catalog_name,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Subquery(Source[SourceRow], Generic[SourceRow]):
|
|
84
|
+
__slots__ = ()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CommonTable(Source[SourceRow], Generic[SourceRow]):
|
|
88
|
+
__slots__ = ("_definition",)
|
|
89
|
+
_definition: CommonTableNode
|
|
90
|
+
|
|
91
|
+
def __init__(self, definition: CommonTableNode) -> None:
|
|
92
|
+
super().__init__(CommonTableSourceNode(definition.name), definition.name)
|
|
93
|
+
object.__setattr__(self, "_definition", definition)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def definition(self) -> CommonTableNode:
|
|
97
|
+
return self._definition
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def table_source(table: TableLike, /, *, alias: str | None = None) -> Source[object]:
|
|
101
|
+
_require_table_instance(table)
|
|
102
|
+
require_identifier(table.database_name, "table")
|
|
103
|
+
_require_optional_identifier(table.schema_name, "schema")
|
|
104
|
+
_require_optional_identifier(table.catalog_name, "catalog")
|
|
105
|
+
_require_optional_identifier(alias, "table alias")
|
|
106
|
+
qualifier = alias or table.database_name
|
|
107
|
+
return Source(
|
|
108
|
+
TableSourceNode(
|
|
109
|
+
table.database_name,
|
|
110
|
+
table.schema_name,
|
|
111
|
+
table.catalog_name,
|
|
112
|
+
alias,
|
|
113
|
+
),
|
|
114
|
+
qualifier,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def json_elements(
|
|
119
|
+
document: NodeProvider,
|
|
120
|
+
/,
|
|
121
|
+
*,
|
|
122
|
+
alias: str,
|
|
123
|
+
) -> Source[object]:
|
|
124
|
+
"""The elements of a JSON array, read one row at a time.
|
|
125
|
+
|
|
126
|
+
The rows have one column, named the same on every dialect, so a document
|
|
127
|
+
is joined to and filtered like any other source.
|
|
128
|
+
"""
|
|
129
|
+
require_identifier(alias, "json elements alias")
|
|
130
|
+
return Source(JsonElementsSourceNode(document.node, alias), alias)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_target(table: TableLike, /) -> WriteTargetNode:
|
|
134
|
+
_require_table_instance(table)
|
|
135
|
+
require_identifier(table.database_name, "table")
|
|
136
|
+
_require_optional_identifier(table.schema_name, "schema")
|
|
137
|
+
_require_optional_identifier(table.catalog_name, "catalog")
|
|
138
|
+
return WriteTargetNode(
|
|
139
|
+
table.database_name,
|
|
140
|
+
table.schema_name,
|
|
141
|
+
table.catalog_name,
|
|
142
|
+
_declared_columns(table),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _declared_columns(table: TableLike) -> tuple[str, ...]:
|
|
147
|
+
"""What a generated table names, and nothing for one written by hand."""
|
|
148
|
+
declared: object = getattr(table, "COLUMNS", ())
|
|
149
|
+
if not isinstance(declared, tuple):
|
|
150
|
+
return ()
|
|
151
|
+
columns = cast("tuple[object, ...]", declared)
|
|
152
|
+
names: list[str] = []
|
|
153
|
+
for column in columns:
|
|
154
|
+
node = getattr(column, "node", None)
|
|
155
|
+
if isinstance(node, FieldNode):
|
|
156
|
+
names.append(node.name)
|
|
157
|
+
return tuple(names)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def subquery_source(
|
|
161
|
+
query: QueryNode,
|
|
162
|
+
alias: str,
|
|
163
|
+
/,
|
|
164
|
+
*,
|
|
165
|
+
lateral: bool = False,
|
|
166
|
+
) -> Subquery[object]:
|
|
167
|
+
require_identifier(alias, "subquery alias")
|
|
168
|
+
return Subquery(SubquerySourceNode(query, alias, lateral=lateral), alias)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def common_table(
|
|
172
|
+
name: str,
|
|
173
|
+
query: QueryNode,
|
|
174
|
+
/,
|
|
175
|
+
*,
|
|
176
|
+
columns: tuple[str, ...] = (),
|
|
177
|
+
recursive: bool = False,
|
|
178
|
+
) -> CommonTable[object]:
|
|
179
|
+
require_identifier(name, "common table")
|
|
180
|
+
for column in columns:
|
|
181
|
+
require_identifier(column, "common table column")
|
|
182
|
+
return CommonTable(CommonTableNode(name, query, columns, recursive))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolve_source(value: object) -> SourceNode:
|
|
186
|
+
if isinstance(value, SourceBase):
|
|
187
|
+
return value.node
|
|
188
|
+
if isinstance(value, TableLike):
|
|
189
|
+
return table_source(value).node
|
|
190
|
+
message = "query source must be a table, subquery, or common table"
|
|
191
|
+
raise TypeError(message)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _require_table_instance(table: TableLike) -> None:
|
|
195
|
+
if isinstance(table, type):
|
|
196
|
+
message = (
|
|
197
|
+
"query source must be a table descriptor instance; a generated table "
|
|
198
|
+
"class is the typed column and builder namespace, and the generated "
|
|
199
|
+
"table constant beside it is the query source"
|
|
200
|
+
)
|
|
201
|
+
raise QueryValidationError(message)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _source_qualification(node: SourceNode) -> tuple[str | None, str | None]:
|
|
205
|
+
if isinstance(node, TableSourceNode) and node.alias is None:
|
|
206
|
+
return node.schema, node.catalog
|
|
207
|
+
return None, None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def require_identifier(value: str, label: str) -> None:
|
|
211
|
+
"""A name written into the SQL, held to being one a database can hold."""
|
|
212
|
+
if not value or "\x00" in value:
|
|
213
|
+
message = f"{label} identifier cannot be empty or contain a null character"
|
|
214
|
+
raise ValueError(message)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _require_optional_identifier(value: str | None, label: str) -> None:
|
|
218
|
+
if value is not None:
|
|
219
|
+
require_identifier(value, label)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = (
|
|
223
|
+
"ELEMENT_COLUMN",
|
|
224
|
+
"ColumnedTable",
|
|
225
|
+
"CommonTable",
|
|
226
|
+
"Source",
|
|
227
|
+
"SourceBase",
|
|
228
|
+
"Subquery",
|
|
229
|
+
"TableLike",
|
|
230
|
+
"common_table",
|
|
231
|
+
"json_elements",
|
|
232
|
+
"resolve_source",
|
|
233
|
+
"subquery_source",
|
|
234
|
+
"table_source",
|
|
235
|
+
"write_target",
|
|
236
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""SQLite query compilation."""
|
|
2
|
+
|
|
3
|
+
from pyoq.query.sqlite.compiler import QueryProvider, SQLiteCompiler
|
|
4
|
+
from pyoq.query.sqlite.executor import SQLiteExecutor
|
|
5
|
+
from pyoq.query.sqlite.factory import SQLiteConnectionFactory
|
|
6
|
+
from pyoq.query.sqlite.model import CompiledQuery, SQLiteCapabilities
|
|
7
|
+
from pyoq.query.sqlite.parameters import (
|
|
8
|
+
SQLiteParameter,
|
|
9
|
+
adapt_parameter,
|
|
10
|
+
adapt_parameters,
|
|
11
|
+
)
|
|
12
|
+
from pyoq.query.sqlite.planning import SQLiteBulkPlanner
|
|
13
|
+
from pyoq.query.sqlite.pool import (
|
|
14
|
+
SQLiteConnectionFactoryProtocol,
|
|
15
|
+
SQLiteConnectionLease,
|
|
16
|
+
SQLitePool,
|
|
17
|
+
SQLitePoolPolicy,
|
|
18
|
+
SQLitePoolStats,
|
|
19
|
+
)
|
|
20
|
+
from pyoq.query.sqlite.streaming import SQLiteRowStream, SQLiteStreamPolicy
|
|
21
|
+
from pyoq.query.sqlite.transactions import SQLiteTransaction, TransactionMode
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
"CompiledQuery",
|
|
25
|
+
"QueryProvider",
|
|
26
|
+
"SQLiteBulkPlanner",
|
|
27
|
+
"SQLiteCapabilities",
|
|
28
|
+
"SQLiteCompiler",
|
|
29
|
+
"SQLiteConnectionFactory",
|
|
30
|
+
"SQLiteConnectionFactoryProtocol",
|
|
31
|
+
"SQLiteConnectionLease",
|
|
32
|
+
"SQLiteExecutor",
|
|
33
|
+
"SQLiteParameter",
|
|
34
|
+
"SQLitePool",
|
|
35
|
+
"SQLitePoolPolicy",
|
|
36
|
+
"SQLitePoolStats",
|
|
37
|
+
"SQLiteRowStream",
|
|
38
|
+
"SQLiteStreamPolicy",
|
|
39
|
+
"SQLiteTransaction",
|
|
40
|
+
"TransactionMode",
|
|
41
|
+
"adapt_parameter",
|
|
42
|
+
"adapt_parameters",
|
|
43
|
+
)
|