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
pyoq/query/select.py
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""Typed immutable SELECT query construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
|
|
8
|
+
|
|
9
|
+
from pyoq.errors import QueryStateError, QueryValidationError
|
|
10
|
+
from pyoq.query.expressions import Condition, Expression, require_boolean
|
|
11
|
+
from pyoq.query.nodes import (
|
|
12
|
+
BinaryNode,
|
|
13
|
+
BinaryOperator,
|
|
14
|
+
BoundValueNode,
|
|
15
|
+
ExistsNode,
|
|
16
|
+
ExpressionNode,
|
|
17
|
+
FieldNode,
|
|
18
|
+
ScalarFamily,
|
|
19
|
+
VariadicNode,
|
|
20
|
+
VariadicOperator,
|
|
21
|
+
)
|
|
22
|
+
from pyoq.query.projections import Projection, order_node, projection_node
|
|
23
|
+
from pyoq.query.raw import NodeProvider
|
|
24
|
+
from pyoq.query.select_nodes import (
|
|
25
|
+
JoinKind,
|
|
26
|
+
JoinNode,
|
|
27
|
+
LockNode,
|
|
28
|
+
LockStrength,
|
|
29
|
+
LockWait,
|
|
30
|
+
OrderNode,
|
|
31
|
+
ProjectionNode,
|
|
32
|
+
QueryNode,
|
|
33
|
+
SelectNode,
|
|
34
|
+
SetNode,
|
|
35
|
+
SetOperator,
|
|
36
|
+
SourceNode,
|
|
37
|
+
)
|
|
38
|
+
from pyoq.query.sources import (
|
|
39
|
+
ColumnedTable,
|
|
40
|
+
CommonTable,
|
|
41
|
+
SourceBase,
|
|
42
|
+
Subquery,
|
|
43
|
+
TableLike,
|
|
44
|
+
common_table,
|
|
45
|
+
resolve_source,
|
|
46
|
+
subquery_source,
|
|
47
|
+
table_source,
|
|
48
|
+
write_target,
|
|
49
|
+
)
|
|
50
|
+
from pyoq.query.windows import WindowSpecification
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from pyoq.descriptors import RelationshipDescriptor
|
|
54
|
+
from pyoq.query.writes import ColumnReference
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
ResultRow = TypeVar("ResultRow")
|
|
58
|
+
Value1 = TypeVar("Value1")
|
|
59
|
+
Value2 = TypeVar("Value2")
|
|
60
|
+
Value3 = TypeVar("Value3")
|
|
61
|
+
Value4 = TypeVar("Value4")
|
|
62
|
+
Value5 = TypeVar("Value5")
|
|
63
|
+
Value6 = TypeVar("Value6")
|
|
64
|
+
Value7 = TypeVar("Value7")
|
|
65
|
+
Value8 = TypeVar("Value8")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Query(Generic[ResultRow]):
|
|
69
|
+
__slots__ = ("_node",)
|
|
70
|
+
_node: QueryNode
|
|
71
|
+
|
|
72
|
+
def __init__(self, node: QueryNode) -> None:
|
|
73
|
+
object.__setattr__(self, "_node", node)
|
|
74
|
+
|
|
75
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
76
|
+
message = "queries are immutable"
|
|
77
|
+
raise AttributeError(message)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def node(self) -> QueryNode:
|
|
81
|
+
return self._node
|
|
82
|
+
|
|
83
|
+
def as_subquery(self, alias: str, /) -> Subquery[ResultRow]:
|
|
84
|
+
return cast(Subquery[ResultRow], subquery_source(self.node, alias))
|
|
85
|
+
|
|
86
|
+
def as_lateral(self, alias: str, /) -> Subquery[ResultRow]:
|
|
87
|
+
"""A subquery that may read the rows to its left, row by row.
|
|
88
|
+
|
|
89
|
+
Not every dialect has one, so a dialect without it refuses the query
|
|
90
|
+
rather than evaluating the subquery once and quietly meaning
|
|
91
|
+
something else.
|
|
92
|
+
"""
|
|
93
|
+
return cast(
|
|
94
|
+
Subquery[ResultRow],
|
|
95
|
+
subquery_source(self.node, alias, lateral=True),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def as_common_table(
|
|
99
|
+
self,
|
|
100
|
+
name: str,
|
|
101
|
+
/,
|
|
102
|
+
*,
|
|
103
|
+
columns: tuple[str, ...] = (),
|
|
104
|
+
recursive: bool = False,
|
|
105
|
+
) -> CommonTable[ResultRow]:
|
|
106
|
+
if columns and len(columns) != _projection_count(self.node):
|
|
107
|
+
message = "common table column count must match its projection count"
|
|
108
|
+
raise QueryValidationError(message)
|
|
109
|
+
return cast(
|
|
110
|
+
CommonTable[ResultRow],
|
|
111
|
+
common_table(
|
|
112
|
+
name,
|
|
113
|
+
self.node,
|
|
114
|
+
columns=columns,
|
|
115
|
+
recursive=recursive,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def order_by(self, *terms: object) -> Query[ResultRow]:
|
|
120
|
+
if not terms:
|
|
121
|
+
message = "ORDER BY requires at least one term"
|
|
122
|
+
raise QueryValidationError(message)
|
|
123
|
+
nodes = tuple(order_node(term) for term in terms)
|
|
124
|
+
return self._replace_order(nodes)
|
|
125
|
+
|
|
126
|
+
def limit(self, value: int, /) -> Query[ResultRow]:
|
|
127
|
+
return self._replace_limit(_page_value(value, "limit"))
|
|
128
|
+
|
|
129
|
+
def offset(self, value: int, /) -> Query[ResultRow]:
|
|
130
|
+
return self._replace_offset(_page_value(value, "offset"))
|
|
131
|
+
|
|
132
|
+
def union(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
|
|
133
|
+
return self._set(SetOperator.UNION, other)
|
|
134
|
+
|
|
135
|
+
def union_all(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
|
|
136
|
+
return self._set(SetOperator.UNION_ALL, other)
|
|
137
|
+
|
|
138
|
+
def intersect(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
|
|
139
|
+
return self._set(SetOperator.INTERSECT, other)
|
|
140
|
+
|
|
141
|
+
def except_(self, other: Query[ResultRow], /) -> SetQuery[ResultRow]:
|
|
142
|
+
return self._set(SetOperator.EXCEPT, other)
|
|
143
|
+
|
|
144
|
+
def _set(
|
|
145
|
+
self,
|
|
146
|
+
operator: SetOperator,
|
|
147
|
+
other: Query[ResultRow],
|
|
148
|
+
) -> SetQuery[ResultRow]:
|
|
149
|
+
if _projection_count(self.node) != _projection_count(other.node):
|
|
150
|
+
message = "set operation projection counts must match"
|
|
151
|
+
raise QueryValidationError(message)
|
|
152
|
+
return SetQuery(SetNode(self.node, operator, other.node))
|
|
153
|
+
|
|
154
|
+
def _replace_order(self, terms: tuple[OrderNode, ...]) -> Query[ResultRow]:
|
|
155
|
+
if isinstance(self.node, SelectNode):
|
|
156
|
+
return SelectQuery[ResultRow](replace(self.node, orders=terms))
|
|
157
|
+
return SetQuery[ResultRow](replace(self.node, orders=terms))
|
|
158
|
+
|
|
159
|
+
def _replace_limit(self, value: int) -> Query[ResultRow]:
|
|
160
|
+
if isinstance(self.node, SelectNode):
|
|
161
|
+
return SelectQuery[ResultRow](replace(self.node, limit=value))
|
|
162
|
+
return SetQuery[ResultRow](replace(self.node, limit=value))
|
|
163
|
+
|
|
164
|
+
def _replace_offset(self, value: int) -> Query[ResultRow]:
|
|
165
|
+
if isinstance(self.node, SelectNode):
|
|
166
|
+
return SelectQuery[ResultRow](replace(self.node, offset=value))
|
|
167
|
+
return SetQuery[ResultRow](replace(self.node, offset=value))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SelectQuery(Query[ResultRow], Generic[ResultRow]):
|
|
171
|
+
__slots__ = ()
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def select_node(self) -> SelectNode:
|
|
175
|
+
node = self.node
|
|
176
|
+
if not isinstance(node, SelectNode):
|
|
177
|
+
message = "SELECT query contains an invalid node"
|
|
178
|
+
raise QueryStateError(message)
|
|
179
|
+
return node
|
|
180
|
+
|
|
181
|
+
def for_update(self) -> SelectQuery[ResultRow]:
|
|
182
|
+
"""Hold the rows this reads, so nobody else changes them first."""
|
|
183
|
+
return self._locked(LockStrength.UPDATE)
|
|
184
|
+
|
|
185
|
+
def for_no_key_update(self) -> SelectQuery[ResultRow]:
|
|
186
|
+
"""Hold the rows, but let their keys still be referenced."""
|
|
187
|
+
return self._locked(LockStrength.NO_KEY_UPDATE)
|
|
188
|
+
|
|
189
|
+
def for_share(self) -> SelectQuery[ResultRow]:
|
|
190
|
+
"""Hold the rows against change, while letting others read them too."""
|
|
191
|
+
return self._locked(LockStrength.SHARE)
|
|
192
|
+
|
|
193
|
+
def for_key_share(self) -> SelectQuery[ResultRow]:
|
|
194
|
+
"""Hold only the keys, against a change that would break a reference."""
|
|
195
|
+
return self._locked(LockStrength.KEY_SHARE)
|
|
196
|
+
|
|
197
|
+
def nowait(self) -> SelectQuery[ResultRow]:
|
|
198
|
+
"""Fail rather than wait for a row somebody else already holds."""
|
|
199
|
+
return self._waiting(LockWait.NOWAIT)
|
|
200
|
+
|
|
201
|
+
def skip_locked(self) -> SelectQuery[ResultRow]:
|
|
202
|
+
"""Pass over a row somebody else already holds, rather than wait."""
|
|
203
|
+
return self._waiting(LockWait.SKIP_LOCKED)
|
|
204
|
+
|
|
205
|
+
def of(self, *targets: TableLike | NodeProvider) -> SelectQuery[ResultRow]:
|
|
206
|
+
"""Hold rows of these tables only, and not everything read.
|
|
207
|
+
|
|
208
|
+
A column names the table that holds it, because a lock is taken on
|
|
209
|
+
rows and a caller who knows the column should not have to say the
|
|
210
|
+
table again.
|
|
211
|
+
"""
|
|
212
|
+
if not targets:
|
|
213
|
+
message = "a lock names at least one table or none at all"
|
|
214
|
+
raise QueryValidationError(message)
|
|
215
|
+
named = tuple(_locked_table(target) for target in targets)
|
|
216
|
+
return SelectQuery(
|
|
217
|
+
replace(self.select_node, lock=replace(self._lock, tables=named))
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _locked(self, strength: LockStrength) -> SelectQuery[ResultRow]:
|
|
221
|
+
node = self.select_node
|
|
222
|
+
current = node.lock
|
|
223
|
+
wait = LockWait.WAIT if current is None else current.wait
|
|
224
|
+
tables = () if current is None else current.tables
|
|
225
|
+
return SelectQuery(replace(node, lock=LockNode(strength, wait, tables)))
|
|
226
|
+
|
|
227
|
+
def _waiting(self, wait: LockWait) -> SelectQuery[ResultRow]:
|
|
228
|
+
return SelectQuery(
|
|
229
|
+
replace(self.select_node, lock=replace(self._lock, wait=wait))
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def _lock(self) -> LockNode:
|
|
234
|
+
lock = self.select_node.lock
|
|
235
|
+
if lock is None:
|
|
236
|
+
message = "how a lock waits is said after what it locks"
|
|
237
|
+
raise QueryStateError(message)
|
|
238
|
+
return lock
|
|
239
|
+
|
|
240
|
+
def distinct(self, enabled: bool = True, /) -> SelectQuery[ResultRow]:
|
|
241
|
+
return SelectQuery(
|
|
242
|
+
replace(self.select_node, distinct=_boolean_setting(enabled, "distinct"))
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def from_(
|
|
246
|
+
self,
|
|
247
|
+
source: SourceBase | TableLike,
|
|
248
|
+
/,
|
|
249
|
+
) -> SelectQuery[ResultRow]:
|
|
250
|
+
if self.select_node.source is not None:
|
|
251
|
+
message = "SELECT source is already defined"
|
|
252
|
+
raise QueryStateError(message)
|
|
253
|
+
return SelectQuery(replace(self.select_node, source=resolve_source(source)))
|
|
254
|
+
|
|
255
|
+
def join(self, source: SourceBase | TableLike, /) -> JoinStep[ResultRow]:
|
|
256
|
+
"""An inner join, qualified by the clause that follows it."""
|
|
257
|
+
return self._joining(JoinKind.INNER, source)
|
|
258
|
+
|
|
259
|
+
def inner_join(
|
|
260
|
+
self,
|
|
261
|
+
source: SourceBase | TableLike,
|
|
262
|
+
/,
|
|
263
|
+
) -> JoinStep[ResultRow]:
|
|
264
|
+
return self._joining(JoinKind.INNER, source)
|
|
265
|
+
|
|
266
|
+
def left_join(
|
|
267
|
+
self,
|
|
268
|
+
source: SourceBase | TableLike,
|
|
269
|
+
/,
|
|
270
|
+
) -> JoinStep[ResultRow]:
|
|
271
|
+
return self._joining(JoinKind.LEFT, source)
|
|
272
|
+
|
|
273
|
+
def right_join(
|
|
274
|
+
self,
|
|
275
|
+
source: SourceBase | TableLike,
|
|
276
|
+
/,
|
|
277
|
+
) -> JoinStep[ResultRow]:
|
|
278
|
+
return self._joining(JoinKind.RIGHT, source)
|
|
279
|
+
|
|
280
|
+
def full_join(
|
|
281
|
+
self,
|
|
282
|
+
source: SourceBase | TableLike,
|
|
283
|
+
/,
|
|
284
|
+
) -> JoinStep[ResultRow]:
|
|
285
|
+
return self._joining(JoinKind.FULL, source)
|
|
286
|
+
|
|
287
|
+
def semi_join(
|
|
288
|
+
self,
|
|
289
|
+
source: SourceBase | TableLike,
|
|
290
|
+
/,
|
|
291
|
+
) -> SemiJoinStep[ResultRow]:
|
|
292
|
+
"""Rows that have a match, without bringing the match back.
|
|
293
|
+
|
|
294
|
+
No database writes it as a join, so neither does the SQL: it becomes
|
|
295
|
+
`EXISTS`, which is what a semi join means and what every dialect here
|
|
296
|
+
already understands.
|
|
297
|
+
"""
|
|
298
|
+
return SemiJoinStep(self, resolve_source(source), negated=False)
|
|
299
|
+
|
|
300
|
+
def anti_join(
|
|
301
|
+
self,
|
|
302
|
+
source: SourceBase | TableLike,
|
|
303
|
+
/,
|
|
304
|
+
) -> SemiJoinStep[ResultRow]:
|
|
305
|
+
"""Rows that have no match, which is `NOT EXISTS`."""
|
|
306
|
+
return SemiJoinStep(self, resolve_source(source), negated=True)
|
|
307
|
+
|
|
308
|
+
def cross_join(
|
|
309
|
+
self,
|
|
310
|
+
source: SourceBase | TableLike,
|
|
311
|
+
/,
|
|
312
|
+
) -> SelectQuery[ResultRow]:
|
|
313
|
+
"""Every row against every row, which nothing qualifies."""
|
|
314
|
+
return self._joined(JoinNode(JoinKind.CROSS, self._source(source)))
|
|
315
|
+
|
|
316
|
+
def natural_join(
|
|
317
|
+
self,
|
|
318
|
+
source: SourceBase | TableLike,
|
|
319
|
+
/,
|
|
320
|
+
) -> SelectQuery[ResultRow]:
|
|
321
|
+
"""Joined on every column the two share, whatever those turn out to be."""
|
|
322
|
+
return self._natural(JoinKind.INNER, source)
|
|
323
|
+
|
|
324
|
+
def natural_left_join(
|
|
325
|
+
self,
|
|
326
|
+
source: SourceBase | TableLike,
|
|
327
|
+
/,
|
|
328
|
+
) -> SelectQuery[ResultRow]:
|
|
329
|
+
return self._natural(JoinKind.LEFT, source)
|
|
330
|
+
|
|
331
|
+
def natural_right_join(
|
|
332
|
+
self,
|
|
333
|
+
source: SourceBase | TableLike,
|
|
334
|
+
/,
|
|
335
|
+
) -> SelectQuery[ResultRow]:
|
|
336
|
+
return self._natural(JoinKind.RIGHT, source)
|
|
337
|
+
|
|
338
|
+
def natural_full_join(
|
|
339
|
+
self,
|
|
340
|
+
source: SourceBase | TableLike,
|
|
341
|
+
/,
|
|
342
|
+
) -> SelectQuery[ResultRow]:
|
|
343
|
+
return self._natural(JoinKind.FULL, source)
|
|
344
|
+
|
|
345
|
+
def _joining(
|
|
346
|
+
self,
|
|
347
|
+
kind: JoinKind,
|
|
348
|
+
source: SourceBase | TableLike,
|
|
349
|
+
) -> JoinStep[ResultRow]:
|
|
350
|
+
return JoinStep(self._joined, kind, self._source(source))
|
|
351
|
+
|
|
352
|
+
def _natural(
|
|
353
|
+
self,
|
|
354
|
+
kind: JoinKind,
|
|
355
|
+
source: SourceBase | TableLike,
|
|
356
|
+
) -> SelectQuery[ResultRow]:
|
|
357
|
+
return self._joined(JoinNode(kind, self._source(source), natural=True))
|
|
358
|
+
|
|
359
|
+
def _source(self, source: SourceBase | TableLike) -> SourceNode:
|
|
360
|
+
_require_from(self.select_node)
|
|
361
|
+
return resolve_source(source)
|
|
362
|
+
|
|
363
|
+
def _joined(self, join: JoinNode) -> SelectQuery[ResultRow]:
|
|
364
|
+
return SelectQuery(
|
|
365
|
+
replace(self.select_node, joins=(*self.select_node.joins, join))
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def where(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
|
|
369
|
+
require_boolean(condition, "WHERE")
|
|
370
|
+
if self.select_node.condition is not None:
|
|
371
|
+
message = "WHERE condition is already defined"
|
|
372
|
+
raise QueryStateError(message)
|
|
373
|
+
return SelectQuery(replace(self.select_node, condition=condition.node))
|
|
374
|
+
|
|
375
|
+
def group_by(self, *expressions: object) -> SelectQuery[ResultRow]:
|
|
376
|
+
if not expressions:
|
|
377
|
+
message = "GROUP BY requires at least one expression"
|
|
378
|
+
raise QueryValidationError(message)
|
|
379
|
+
nodes = tuple(_expression_node(value, "GROUP BY") for value in expressions)
|
|
380
|
+
return SelectQuery(replace(self.select_node, groups=nodes))
|
|
381
|
+
|
|
382
|
+
def having(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
|
|
383
|
+
require_boolean(condition, "HAVING")
|
|
384
|
+
if self.select_node.having is not None:
|
|
385
|
+
message = "HAVING condition is already defined"
|
|
386
|
+
raise QueryStateError(message)
|
|
387
|
+
return SelectQuery(replace(self.select_node, having=condition.node))
|
|
388
|
+
|
|
389
|
+
def window(
|
|
390
|
+
self,
|
|
391
|
+
*specifications: WindowSpecification,
|
|
392
|
+
) -> SelectQuery[ResultRow]:
|
|
393
|
+
"""Declare the windows this query's functions look through by name."""
|
|
394
|
+
if not specifications:
|
|
395
|
+
message = "WINDOW requires at least one named window"
|
|
396
|
+
raise QueryValidationError(message)
|
|
397
|
+
additions = tuple(item.declaration for item in specifications)
|
|
398
|
+
existing = {item.name for item in self.select_node.windows}
|
|
399
|
+
names = [item.name for item in additions]
|
|
400
|
+
if existing.intersection(names) or len(names) != len(set(names)):
|
|
401
|
+
message = "window names must be unique within a query"
|
|
402
|
+
raise QueryValidationError(message)
|
|
403
|
+
return SelectQuery(
|
|
404
|
+
replace(self.select_node, windows=self.select_node.windows + additions)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def with_(
|
|
408
|
+
self,
|
|
409
|
+
*common_tables: CommonTable[object],
|
|
410
|
+
) -> SelectQuery[ResultRow]:
|
|
411
|
+
if not common_tables:
|
|
412
|
+
message = "WITH requires at least one common table"
|
|
413
|
+
raise QueryValidationError(message)
|
|
414
|
+
existing = {item.name for item in self.select_node.common_tables}
|
|
415
|
+
additions = tuple(item.definition for item in common_tables)
|
|
416
|
+
names = [item.name for item in additions]
|
|
417
|
+
if existing.intersection(names) or len(names) != len(set(names)):
|
|
418
|
+
message = "common table names must be unique within a query"
|
|
419
|
+
raise QueryValidationError(message)
|
|
420
|
+
return SelectQuery(
|
|
421
|
+
replace(
|
|
422
|
+
self.select_node,
|
|
423
|
+
common_tables=(*self.select_node.common_tables, *additions),
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def order_by(self, *terms: object) -> SelectQuery[ResultRow]:
|
|
428
|
+
return _as_select(super().order_by(*terms))
|
|
429
|
+
|
|
430
|
+
def limit(self, value: int, /) -> SelectQuery[ResultRow]:
|
|
431
|
+
return _as_select(super().limit(value))
|
|
432
|
+
|
|
433
|
+
def offset(self, value: int, /) -> SelectQuery[ResultRow]:
|
|
434
|
+
return _as_select(super().offset(value))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class SetQuery(Query[ResultRow], Generic[ResultRow]):
|
|
438
|
+
__slots__ = ()
|
|
439
|
+
|
|
440
|
+
def order_by(self, *terms: object) -> SetQuery[ResultRow]:
|
|
441
|
+
return _as_set(super().order_by(*terms))
|
|
442
|
+
|
|
443
|
+
def limit(self, value: int, /) -> SetQuery[ResultRow]:
|
|
444
|
+
return _as_set(super().limit(value))
|
|
445
|
+
|
|
446
|
+
def offset(self, value: int, /) -> SetQuery[ResultRow]:
|
|
447
|
+
return _as_set(super().offset(value))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@overload
|
|
451
|
+
def select(
|
|
452
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
453
|
+
/,
|
|
454
|
+
) -> SelectQuery[tuple[Value1]]: ...
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@overload
|
|
458
|
+
def select(
|
|
459
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
460
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
461
|
+
/,
|
|
462
|
+
) -> SelectQuery[tuple[Value1, Value2]]: ...
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@overload
|
|
466
|
+
def select(
|
|
467
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
468
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
469
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
470
|
+
/,
|
|
471
|
+
) -> SelectQuery[tuple[Value1, Value2, Value3]]: ...
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@overload
|
|
475
|
+
def select(
|
|
476
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
477
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
478
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
479
|
+
value4: Expression[Value4] | Projection[Value4],
|
|
480
|
+
/,
|
|
481
|
+
) -> SelectQuery[tuple[Value1, Value2, Value3, Value4]]: ...
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@overload
|
|
485
|
+
def select(
|
|
486
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
487
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
488
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
489
|
+
value4: Expression[Value4] | Projection[Value4],
|
|
490
|
+
value5: Expression[Value5] | Projection[Value5],
|
|
491
|
+
/,
|
|
492
|
+
) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@overload
|
|
496
|
+
def select(
|
|
497
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
498
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
499
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
500
|
+
value4: Expression[Value4] | Projection[Value4],
|
|
501
|
+
value5: Expression[Value5] | Projection[Value5],
|
|
502
|
+
value6: Expression[Value6] | Projection[Value6],
|
|
503
|
+
/,
|
|
504
|
+
) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@overload
|
|
508
|
+
def select(
|
|
509
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
510
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
511
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
512
|
+
value4: Expression[Value4] | Projection[Value4],
|
|
513
|
+
value5: Expression[Value5] | Projection[Value5],
|
|
514
|
+
value6: Expression[Value6] | Projection[Value6],
|
|
515
|
+
value7: Expression[Value7] | Projection[Value7],
|
|
516
|
+
/,
|
|
517
|
+
) -> SelectQuery[tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7]]: ...
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@overload
|
|
521
|
+
def select(
|
|
522
|
+
value1: Expression[Value1] | Projection[Value1],
|
|
523
|
+
value2: Expression[Value2] | Projection[Value2],
|
|
524
|
+
value3: Expression[Value3] | Projection[Value3],
|
|
525
|
+
value4: Expression[Value4] | Projection[Value4],
|
|
526
|
+
value5: Expression[Value5] | Projection[Value5],
|
|
527
|
+
value6: Expression[Value6] | Projection[Value6],
|
|
528
|
+
value7: Expression[Value7] | Projection[Value7],
|
|
529
|
+
value8: Expression[Value8] | Projection[Value8],
|
|
530
|
+
/,
|
|
531
|
+
) -> SelectQuery[
|
|
532
|
+
tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8]
|
|
533
|
+
]: ...
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@overload
|
|
537
|
+
def select(
|
|
538
|
+
value1: object,
|
|
539
|
+
value2: object,
|
|
540
|
+
value3: object,
|
|
541
|
+
value4: object,
|
|
542
|
+
value5: object,
|
|
543
|
+
value6: object,
|
|
544
|
+
value7: object,
|
|
545
|
+
value8: object,
|
|
546
|
+
/,
|
|
547
|
+
*values: object,
|
|
548
|
+
) -> SelectQuery[tuple[object, ...]]:
|
|
549
|
+
"""Wider than the overloads above name, where the element types stop.
|
|
550
|
+
|
|
551
|
+
A query is still a query past eight projections, and matching no overload
|
|
552
|
+
would take the whole chain down with it rather than just the tuple. Past
|
|
553
|
+
here a projection is checked when it is turned into one, and the row is
|
|
554
|
+
`object` per column, which is what is still known about it.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def select( # type: ignore[misc]
|
|
559
|
+
*values: object,
|
|
560
|
+
) -> SelectQuery[object]:
|
|
561
|
+
if not values:
|
|
562
|
+
message = "SELECT requires at least one projection"
|
|
563
|
+
raise QueryValidationError(message)
|
|
564
|
+
return SelectQuery(SelectNode(tuple(projection_node(value) for value in values)))
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def select_from(table: ColumnedTable, /) -> SelectQuery[tuple[object, ...]]:
|
|
568
|
+
"""Every column the table declares, in the order it declares them.
|
|
569
|
+
|
|
570
|
+
The column list is written out rather than left as a star, because a star
|
|
571
|
+
changes meaning the moment a column is added and the caller reading the
|
|
572
|
+
row would not know it had. Values arrive as a tuple in that order, which
|
|
573
|
+
is the order the generated row type takes its arguments in.
|
|
574
|
+
"""
|
|
575
|
+
columns = table.COLUMNS
|
|
576
|
+
if not columns:
|
|
577
|
+
message = (
|
|
578
|
+
"table names no columns to select; generated tables declare them, "
|
|
579
|
+
"and any other table has to be selected from by naming its columns"
|
|
580
|
+
)
|
|
581
|
+
raise QueryValidationError(message)
|
|
582
|
+
projections = tuple(projection_node(column) for column in columns)
|
|
583
|
+
node = SelectNode(projections, every_column=True)
|
|
584
|
+
query: SelectQuery[tuple[object, ...]] = SelectQuery(node)
|
|
585
|
+
return query.from_(table_source(table))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class SemiJoinStep(Generic[ResultRow]):
|
|
589
|
+
"""A semi or anti join waiting for the predicate that matches the rows."""
|
|
590
|
+
|
|
591
|
+
__slots__ = ("_negated", "_query", "_source")
|
|
592
|
+
_query: SelectQuery[ResultRow]
|
|
593
|
+
_source: SourceNode
|
|
594
|
+
_negated: bool
|
|
595
|
+
|
|
596
|
+
def __init__(
|
|
597
|
+
self,
|
|
598
|
+
query: SelectQuery[ResultRow],
|
|
599
|
+
source: SourceNode,
|
|
600
|
+
*,
|
|
601
|
+
negated: bool,
|
|
602
|
+
) -> None:
|
|
603
|
+
object.__setattr__(self, "_query", query)
|
|
604
|
+
object.__setattr__(self, "_source", source)
|
|
605
|
+
object.__setattr__(self, "_negated", negated)
|
|
606
|
+
|
|
607
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
608
|
+
message = "join steps are immutable"
|
|
609
|
+
raise AttributeError(message, name, value)
|
|
610
|
+
|
|
611
|
+
def on(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
|
|
612
|
+
require_boolean(condition, "JOIN")
|
|
613
|
+
inner = SelectNode(
|
|
614
|
+
(ProjectionNode(BoundValueNode(1, ScalarFamily.NUMERIC)),),
|
|
615
|
+
source=self._source,
|
|
616
|
+
condition=condition.node,
|
|
617
|
+
)
|
|
618
|
+
matched = ExistsNode(inner, negated=self._negated)
|
|
619
|
+
node = self._query.select_node
|
|
620
|
+
return SelectQuery(replace(node, matches=(*node.matches, matched)))
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class JoinStep(Generic[ResultRow]):
|
|
624
|
+
"""A join waiting to be told how the two tables line up.
|
|
625
|
+
|
|
626
|
+
SQL names the table first and qualifies it after, and so does this. The
|
|
627
|
+
step cannot be run or read: until it is qualified there is no query.
|
|
628
|
+
"""
|
|
629
|
+
|
|
630
|
+
__slots__ = ("_join", "_kind", "_source")
|
|
631
|
+
_join: Callable[[JoinNode], SelectQuery[ResultRow]]
|
|
632
|
+
_kind: JoinKind
|
|
633
|
+
_source: SourceNode
|
|
634
|
+
|
|
635
|
+
def __init__(
|
|
636
|
+
self,
|
|
637
|
+
join: Callable[[JoinNode], SelectQuery[ResultRow]],
|
|
638
|
+
kind: JoinKind,
|
|
639
|
+
source: SourceNode,
|
|
640
|
+
) -> None:
|
|
641
|
+
object.__setattr__(self, "_join", join)
|
|
642
|
+
object.__setattr__(self, "_kind", kind)
|
|
643
|
+
object.__setattr__(self, "_source", source)
|
|
644
|
+
|
|
645
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
646
|
+
message = "join steps are immutable"
|
|
647
|
+
raise AttributeError(message, name, value)
|
|
648
|
+
|
|
649
|
+
def on(self, condition: Expression[bool], /) -> SelectQuery[ResultRow]:
|
|
650
|
+
"""The predicate the rows are matched by."""
|
|
651
|
+
require_boolean(condition, "JOIN")
|
|
652
|
+
return self._join(JoinNode(self._kind, self._source, condition.node))
|
|
653
|
+
|
|
654
|
+
def using(self, *columns: str | ColumnReference) -> SelectQuery[ResultRow]:
|
|
655
|
+
"""Matched on columns both tables name the same way."""
|
|
656
|
+
names = tuple(_using_name(column) for column in columns)
|
|
657
|
+
if not names:
|
|
658
|
+
message = "USING requires at least one column"
|
|
659
|
+
raise QueryValidationError(message)
|
|
660
|
+
return self._join(JoinNode(self._kind, self._source, using=names))
|
|
661
|
+
|
|
662
|
+
def on_key(
|
|
663
|
+
self, relation: RelationshipDescriptor[object, object], /
|
|
664
|
+
) -> SelectQuery[ResultRow]:
|
|
665
|
+
"""Matched on the foreign key the schema already declares.
|
|
666
|
+
|
|
667
|
+
Generation writes a descriptor for every key in both directions, so
|
|
668
|
+
the columns are a fact rather than something to restate here and get
|
|
669
|
+
wrong.
|
|
670
|
+
"""
|
|
671
|
+
return self._join(JoinNode(self._kind, self._source, _key_condition(relation)))
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _using_name(column: object) -> str:
|
|
675
|
+
"""The column's name in the database, which is what USING matches on.
|
|
676
|
+
|
|
677
|
+
A generated column and a hand written field both carry one, and both say
|
|
678
|
+
it the same way once the expression is a node.
|
|
679
|
+
"""
|
|
680
|
+
if isinstance(column, str):
|
|
681
|
+
return column
|
|
682
|
+
node = getattr(column, "node", None)
|
|
683
|
+
if isinstance(node, FieldNode):
|
|
684
|
+
return node.name
|
|
685
|
+
message = "USING takes columns or their names"
|
|
686
|
+
raise QueryValidationError(message)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _key_condition(
|
|
690
|
+
relation: RelationshipDescriptor[object, object],
|
|
691
|
+
) -> ExpressionNode:
|
|
692
|
+
"""One equality per key column, and the same shape for a composite key.
|
|
693
|
+
|
|
694
|
+
A descriptor validates that its two sides line up when it is built, so
|
|
695
|
+
the pairs here are known to match.
|
|
696
|
+
"""
|
|
697
|
+
equalities = [
|
|
698
|
+
BinaryNode(
|
|
699
|
+
BinaryOperator.EQUAL,
|
|
700
|
+
FieldNode(source, relation.source_table),
|
|
701
|
+
FieldNode(target, relation.target_table),
|
|
702
|
+
)
|
|
703
|
+
for source, target in zip(
|
|
704
|
+
relation.source_columns, relation.target_columns, strict=True
|
|
705
|
+
)
|
|
706
|
+
]
|
|
707
|
+
if len(equalities) == 1:
|
|
708
|
+
return equalities[0]
|
|
709
|
+
return VariadicNode(VariadicOperator.AND, tuple(equalities))
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def exists(query: Query[ResultRow], /) -> Condition:
|
|
713
|
+
"""Whether the query returns any row at all.
|
|
714
|
+
|
|
715
|
+
What is projected inside does not matter, so nothing is read out of it.
|
|
716
|
+
Databases understand that, and so do readers.
|
|
717
|
+
"""
|
|
718
|
+
return Condition(ExistsNode(query.node))
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def not_exists(query: Query[ResultRow], /) -> Condition:
|
|
722
|
+
"""Whether the query returns nothing at all."""
|
|
723
|
+
return Condition(ExistsNode(query.node, negated=True))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _expression_node(value: object, clause: str) -> ExpressionNode:
|
|
727
|
+
if isinstance(value, Expression):
|
|
728
|
+
return value.node
|
|
729
|
+
message = f"{clause} requires typed expressions"
|
|
730
|
+
raise TypeError(message)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _require_from(node: SelectNode) -> None:
|
|
734
|
+
if node.source is None:
|
|
735
|
+
message = "JOIN requires a FROM source"
|
|
736
|
+
raise QueryStateError(message)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _page_value(value: object, label: str) -> int:
|
|
740
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
741
|
+
message = f"{label} must be a non-negative integer"
|
|
742
|
+
raise QueryValidationError(message)
|
|
743
|
+
return value
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _locked_table(target: TableLike | NodeProvider) -> str:
|
|
747
|
+
if isinstance(target, TableLike):
|
|
748
|
+
return write_target(target).name
|
|
749
|
+
node = target.node
|
|
750
|
+
if not isinstance(node, FieldNode) or node.table is None:
|
|
751
|
+
message = "a lock names a table, or a column that names one"
|
|
752
|
+
raise QueryValidationError(message)
|
|
753
|
+
return node.table
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _boolean_setting(value: object, label: str) -> bool:
|
|
757
|
+
if not isinstance(value, bool):
|
|
758
|
+
message = f"{label} setting must be boolean"
|
|
759
|
+
raise QueryValidationError(message)
|
|
760
|
+
return value
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _projection_count(node: QueryNode) -> int:
|
|
764
|
+
if isinstance(node, SelectNode):
|
|
765
|
+
return len(node.projections)
|
|
766
|
+
return _projection_count(node.left)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _as_select(query: Query[ResultRow]) -> SelectQuery[ResultRow]:
|
|
770
|
+
if isinstance(query, SelectQuery):
|
|
771
|
+
return query
|
|
772
|
+
message = "query operation did not preserve SELECT state"
|
|
773
|
+
raise QueryStateError(message)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _as_set(query: Query[ResultRow]) -> SetQuery[ResultRow]:
|
|
777
|
+
if isinstance(query, SetQuery):
|
|
778
|
+
return query
|
|
779
|
+
message = "query operation did not preserve set state"
|
|
780
|
+
raise QueryStateError(message)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
__all__ = (
|
|
784
|
+
"JoinStep",
|
|
785
|
+
"Query",
|
|
786
|
+
"SelectQuery",
|
|
787
|
+
"SemiJoinStep",
|
|
788
|
+
"SetQuery",
|
|
789
|
+
"exists",
|
|
790
|
+
"not_exists",
|
|
791
|
+
"select",
|
|
792
|
+
"select_from",
|
|
793
|
+
)
|