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/results.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""What an expression answers with, in one place.
|
|
2
|
+
|
|
3
|
+
Three facts, one home: the kind of value it is, the Python type it holds where
|
|
4
|
+
that can be known, and whether it may answer with nothing. They are derived
|
|
5
|
+
from the same tree by the same walk, so nothing holds two answers to one of
|
|
6
|
+
them, and nothing above this decides any of them again.
|
|
7
|
+
|
|
8
|
+
A family says what kind of value an expression is. This says the rest: which
|
|
9
|
+
Python type it is where that can be known, and whether it may answer with no
|
|
10
|
+
value at all. All three are derived from the same tree, so there is one answer
|
|
11
|
+
to what an expression results in rather than one per layer that wants to know.
|
|
12
|
+
|
|
13
|
+
Without it a caller is promised a Decimal by the type checker and handed a
|
|
14
|
+
float by the driver, which is the promise the decoder exists to keep.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from datetime import date, time, timedelta
|
|
22
|
+
from decimal import Decimal
|
|
23
|
+
from typing import TypeAlias, get_args, get_origin
|
|
24
|
+
|
|
25
|
+
from pyoq.query.nodes import (
|
|
26
|
+
AggregateName,
|
|
27
|
+
AggregateNode,
|
|
28
|
+
ArrayConstructNode,
|
|
29
|
+
ArrayDimensionsNode,
|
|
30
|
+
ArrayElementNode,
|
|
31
|
+
ArrayLengthNode,
|
|
32
|
+
ArrayNode,
|
|
33
|
+
ArrayOperation,
|
|
34
|
+
BinaryNode,
|
|
35
|
+
BinaryOperator,
|
|
36
|
+
BoundValueNode,
|
|
37
|
+
CaseNode,
|
|
38
|
+
CastNode,
|
|
39
|
+
CollectionNode,
|
|
40
|
+
ExistsNode,
|
|
41
|
+
ExpressionNode,
|
|
42
|
+
ExtractNode,
|
|
43
|
+
FieldNode,
|
|
44
|
+
FunctionName,
|
|
45
|
+
FunctionNode,
|
|
46
|
+
JsonBuildNode,
|
|
47
|
+
JsonContainsNode,
|
|
48
|
+
JsonKeysNode,
|
|
49
|
+
JsonNode,
|
|
50
|
+
JsonOperation,
|
|
51
|
+
JsonWriteNode,
|
|
52
|
+
RawNode,
|
|
53
|
+
RowValueNode,
|
|
54
|
+
ScalarFamily,
|
|
55
|
+
UnaryNode,
|
|
56
|
+
UnaryOperator,
|
|
57
|
+
VariadicNode,
|
|
58
|
+
VendorFunctionNode,
|
|
59
|
+
WindowFunctionNode,
|
|
60
|
+
WindowName,
|
|
61
|
+
WindowNode,
|
|
62
|
+
)
|
|
63
|
+
from pyoq.schema.models import TypeKind
|
|
64
|
+
|
|
65
|
+
_NUMERIC_KINDS = frozenset(
|
|
66
|
+
{
|
|
67
|
+
TypeKind.BIG_INTEGER,
|
|
68
|
+
TypeKind.DECIMAL,
|
|
69
|
+
TypeKind.DOUBLE,
|
|
70
|
+
TypeKind.INTEGER,
|
|
71
|
+
TypeKind.REAL,
|
|
72
|
+
TypeKind.SMALL_INTEGER,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
_TEMPORAL_KINDS = frozenset(
|
|
76
|
+
{TypeKind.DATE, TypeKind.DATETIME, TypeKind.INTERVAL, TypeKind.TIME}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def family_for_kind(kind: TypeKind, /) -> ScalarFamily:
|
|
81
|
+
"""What kind of value a column declared with this SQL type holds.
|
|
82
|
+
|
|
83
|
+
The companion to `family_for_type`: one reads a Python type and this reads
|
|
84
|
+
a schema's own. Both answer the same question, so they live together and a
|
|
85
|
+
kind gains a family in one place rather than in each layer that asks.
|
|
86
|
+
"""
|
|
87
|
+
if kind is TypeKind.BOOLEAN:
|
|
88
|
+
return ScalarFamily.BOOLEAN
|
|
89
|
+
if kind in _NUMERIC_KINDS:
|
|
90
|
+
return ScalarFamily.NUMERIC
|
|
91
|
+
if kind in {TypeKind.STRING, TypeKind.ENUM}:
|
|
92
|
+
return ScalarFamily.STRING
|
|
93
|
+
if kind is TypeKind.BINARY:
|
|
94
|
+
return ScalarFamily.BINARY
|
|
95
|
+
if kind in _TEMPORAL_KINDS:
|
|
96
|
+
return ScalarFamily.TEMPORAL
|
|
97
|
+
if kind is TypeKind.JSON:
|
|
98
|
+
return ScalarFamily.JSON
|
|
99
|
+
return ScalarFamily.OTHER
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def family_for_type(value_type: type[object]) -> ScalarFamily:
|
|
103
|
+
"""What kind of value a column of this type holds.
|
|
104
|
+
|
|
105
|
+
A column that holds a collection is written with what it holds, so the
|
|
106
|
+
type given here may be a parameterised one. What decides the family is
|
|
107
|
+
what it is a collection of, which is the type it is written from.
|
|
108
|
+
"""
|
|
109
|
+
written = get_origin(value_type) or value_type
|
|
110
|
+
if not isinstance(written, type):
|
|
111
|
+
return ScalarFamily.OTHER
|
|
112
|
+
return _family_of(written)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _family_of(value_type: type[object]) -> ScalarFamily:
|
|
116
|
+
if issubclass(value_type, bool):
|
|
117
|
+
return ScalarFamily.BOOLEAN
|
|
118
|
+
if issubclass(value_type, (int, float, Decimal)):
|
|
119
|
+
return ScalarFamily.NUMERIC
|
|
120
|
+
if issubclass(value_type, str):
|
|
121
|
+
return ScalarFamily.STRING
|
|
122
|
+
if issubclass(value_type, bytes):
|
|
123
|
+
return ScalarFamily.BINARY
|
|
124
|
+
if issubclass(value_type, (date, time, timedelta)):
|
|
125
|
+
return ScalarFamily.TEMPORAL
|
|
126
|
+
if issubclass(value_type, (list, dict)):
|
|
127
|
+
return ScalarFamily.JSON
|
|
128
|
+
return ScalarFamily.OTHER
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def expression_family(node: ExpressionNode) -> ScalarFamily:
|
|
132
|
+
"""What kind of value this node answers with."""
|
|
133
|
+
if isinstance(node, (BoundValueNode, FieldNode, RawNode, VendorFunctionNode)):
|
|
134
|
+
return node.family
|
|
135
|
+
if isinstance(node, UnaryNode):
|
|
136
|
+
if node.operator is UnaryOperator.NEGATE:
|
|
137
|
+
return expression_family(node.operand)
|
|
138
|
+
return ScalarFamily.BOOLEAN
|
|
139
|
+
if isinstance(node, BinaryNode):
|
|
140
|
+
return _binary_family(node)
|
|
141
|
+
if isinstance(node, VariadicNode):
|
|
142
|
+
return ScalarFamily.BOOLEAN
|
|
143
|
+
if isinstance(node, FunctionNode):
|
|
144
|
+
if node.name in _CHOOSING:
|
|
145
|
+
return expression_family(node.arguments[0])
|
|
146
|
+
return _function_family(node.name)
|
|
147
|
+
if isinstance(node, ExtractNode):
|
|
148
|
+
return ScalarFamily.NUMERIC
|
|
149
|
+
if isinstance(node, RowValueNode):
|
|
150
|
+
return ScalarFamily.OTHER
|
|
151
|
+
if isinstance(node, _Shaped):
|
|
152
|
+
return _shaped_family(node)
|
|
153
|
+
return _borrowed_family(node)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_ARRAY_ANSWERS_AN_ARRAY = frozenset(
|
|
157
|
+
{
|
|
158
|
+
ArrayOperation.APPEND,
|
|
159
|
+
ArrayOperation.PREPEND,
|
|
160
|
+
ArrayOperation.CONCAT,
|
|
161
|
+
ArrayOperation.REMOVE,
|
|
162
|
+
ArrayOperation.REPLACE,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
"""The ways of asking about an array that answer with another array."""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _shaped_family(node: _Shaped) -> ScalarFamily:
|
|
169
|
+
"""The kinds whose family their own shape decides, not what is inside."""
|
|
170
|
+
if isinstance(node, JsonNode):
|
|
171
|
+
return _JSON_FAMILIES[node.operation]
|
|
172
|
+
if isinstance(node, (JsonWriteNode, JsonBuildNode, JsonKeysNode)):
|
|
173
|
+
return ScalarFamily.JSON
|
|
174
|
+
if isinstance(node, JsonContainsNode):
|
|
175
|
+
return ScalarFamily.BOOLEAN
|
|
176
|
+
if isinstance(node, ArrayNode):
|
|
177
|
+
return (
|
|
178
|
+
ScalarFamily.OTHER
|
|
179
|
+
if node.operation in _ARRAY_ANSWERS_AN_ARRAY
|
|
180
|
+
else ScalarFamily.BOOLEAN
|
|
181
|
+
)
|
|
182
|
+
if isinstance(node, (ArrayLengthNode, ArrayDimensionsNode)):
|
|
183
|
+
return ScalarFamily.NUMERIC
|
|
184
|
+
if isinstance(node, ArrayConstructNode):
|
|
185
|
+
return ScalarFamily.OTHER
|
|
186
|
+
return ScalarFamily.OTHER
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
_JSON_AGGREGATES = frozenset(
|
|
190
|
+
{AggregateName.JSON_ARRAY_AGG, AggregateName.JSON_OBJECT_AGG}
|
|
191
|
+
)
|
|
192
|
+
"""Aggregates that gather rows into a JSON value rather than a number."""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _borrowed_family(node: _Borrowing) -> ScalarFamily:
|
|
196
|
+
"""The kinds that take their family from something inside them."""
|
|
197
|
+
if isinstance(node, CollectionNode):
|
|
198
|
+
return ScalarFamily.JSON
|
|
199
|
+
if isinstance(node, ExistsNode):
|
|
200
|
+
return ScalarFamily.BOOLEAN
|
|
201
|
+
if isinstance(node, CaseNode):
|
|
202
|
+
return expression_family(node.branches[0][1])
|
|
203
|
+
if isinstance(node, CastNode):
|
|
204
|
+
return family_for_type(node.value_type)
|
|
205
|
+
if isinstance(node, WindowNode):
|
|
206
|
+
return expression_family(node.expression)
|
|
207
|
+
if isinstance(node, WindowFunctionNode):
|
|
208
|
+
return _window_family(node)
|
|
209
|
+
if node.name in {AggregateName.MINIMUM, AggregateName.MAXIMUM}:
|
|
210
|
+
return expression_family(node.arguments[0])
|
|
211
|
+
if node.name in _JSON_AGGREGATES:
|
|
212
|
+
return ScalarFamily.JSON
|
|
213
|
+
return ScalarFamily.NUMERIC
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_Shaped: TypeAlias = (
|
|
217
|
+
JsonNode
|
|
218
|
+
| JsonWriteNode
|
|
219
|
+
| JsonBuildNode
|
|
220
|
+
| JsonKeysNode
|
|
221
|
+
| JsonContainsNode
|
|
222
|
+
| ArrayNode
|
|
223
|
+
| ArrayLengthNode
|
|
224
|
+
| ArrayElementNode
|
|
225
|
+
| ArrayConstructNode
|
|
226
|
+
| ArrayDimensionsNode
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
_Borrowing: TypeAlias = (
|
|
230
|
+
CollectionNode
|
|
231
|
+
| ExistsNode
|
|
232
|
+
| CaseNode
|
|
233
|
+
| CastNode
|
|
234
|
+
| WindowNode
|
|
235
|
+
| WindowFunctionNode
|
|
236
|
+
| AggregateNode
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_JSON_FAMILIES: Mapping[JsonOperation, ScalarFamily] = {
|
|
241
|
+
JsonOperation.VALUE: ScalarFamily.JSON,
|
|
242
|
+
JsonOperation.TEXT: ScalarFamily.STRING,
|
|
243
|
+
JsonOperation.LENGTH: ScalarFamily.NUMERIC,
|
|
244
|
+
JsonOperation.EXISTS: ScalarFamily.BOOLEAN,
|
|
245
|
+
JsonOperation.REMOVE: ScalarFamily.JSON,
|
|
246
|
+
}
|
|
247
|
+
"""What each question about a JSON value answers with."""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
_NAVIGATING_WINDOWS = frozenset(
|
|
251
|
+
{
|
|
252
|
+
WindowName.LAG,
|
|
253
|
+
WindowName.LEAD,
|
|
254
|
+
WindowName.FIRST_VALUE,
|
|
255
|
+
WindowName.LAST_VALUE,
|
|
256
|
+
WindowName.NTH_VALUE,
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
"""Windows that answer with a value already in the window."""
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _window_family(node: WindowFunctionNode) -> ScalarFamily:
|
|
263
|
+
"""A position is a number; a neighbour's value is whatever that value is."""
|
|
264
|
+
if node.name in _NAVIGATING_WINDOWS:
|
|
265
|
+
return expression_family(node.arguments[0])
|
|
266
|
+
return ScalarFamily.NUMERIC
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _binary_family(node: BinaryNode) -> ScalarFamily:
|
|
270
|
+
boolean_operators = {
|
|
271
|
+
BinaryOperator.EQUAL,
|
|
272
|
+
BinaryOperator.NOT_EQUAL,
|
|
273
|
+
BinaryOperator.LESS_THAN,
|
|
274
|
+
BinaryOperator.LESS_OR_EQUAL,
|
|
275
|
+
BinaryOperator.GREATER_THAN,
|
|
276
|
+
BinaryOperator.GREATER_OR_EQUAL,
|
|
277
|
+
BinaryOperator.IS_DISTINCT_FROM,
|
|
278
|
+
BinaryOperator.IS_NOT_DISTINCT_FROM,
|
|
279
|
+
BinaryOperator.LIKE,
|
|
280
|
+
BinaryOperator.NOT_LIKE,
|
|
281
|
+
}
|
|
282
|
+
if node.operator in boolean_operators:
|
|
283
|
+
return ScalarFamily.BOOLEAN
|
|
284
|
+
if node.operator is BinaryOperator.CONCAT:
|
|
285
|
+
return ScalarFamily.STRING
|
|
286
|
+
families = {expression_family(node.left), expression_family(node.right)}
|
|
287
|
+
if ScalarFamily.TEMPORAL in families:
|
|
288
|
+
return ScalarFamily.TEMPORAL
|
|
289
|
+
return ScalarFamily.NUMERIC
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
_CHOOSING = frozenset(
|
|
293
|
+
{
|
|
294
|
+
FunctionName.COALESCE,
|
|
295
|
+
FunctionName.NULLIF,
|
|
296
|
+
FunctionName.GREATEST,
|
|
297
|
+
FunctionName.LEAST,
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _function_family(name: FunctionName) -> ScalarFamily:
|
|
303
|
+
if name in {FunctionName.LOWER, FunctionName.UPPER}:
|
|
304
|
+
return ScalarFamily.STRING
|
|
305
|
+
if name is FunctionName.LENGTH:
|
|
306
|
+
return ScalarFamily.NUMERIC
|
|
307
|
+
return ScalarFamily.BOOLEAN
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
_CARRYING_FUNCTIONS = frozenset(
|
|
311
|
+
{
|
|
312
|
+
FunctionName.COALESCE,
|
|
313
|
+
FunctionName.NULLIF,
|
|
314
|
+
FunctionName.GREATEST,
|
|
315
|
+
FunctionName.LEAST,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
"""Functions that answer with one of the values they were given."""
|
|
319
|
+
|
|
320
|
+
_CARRYING_AGGREGATES = frozenset({AggregateName.MINIMUM, AggregateName.MAXIMUM})
|
|
321
|
+
|
|
322
|
+
_CARRYING_WINDOWS = frozenset(
|
|
323
|
+
{
|
|
324
|
+
WindowName.LAG,
|
|
325
|
+
WindowName.LEAD,
|
|
326
|
+
WindowName.FIRST_VALUE,
|
|
327
|
+
WindowName.LAST_VALUE,
|
|
328
|
+
WindowName.NTH_VALUE,
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
"""Windows that answer with a value one of their rows already held."""
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def declared_type(node: ExpressionNode, /) -> type[object] | None:
|
|
335
|
+
"""The Python type this answers with, where the tree knows one.
|
|
336
|
+
|
|
337
|
+
A node that carries a value through answers with whatever that value was
|
|
338
|
+
declared to be. A node that computes something new answers with nothing
|
|
339
|
+
here, and its family speaks for it instead.
|
|
340
|
+
"""
|
|
341
|
+
if isinstance(node, (FieldNode, CastNode)):
|
|
342
|
+
return node.value_type
|
|
343
|
+
if isinstance(node, CaseNode):
|
|
344
|
+
return declared_type(node.branches[0][1])
|
|
345
|
+
if isinstance(node, WindowNode):
|
|
346
|
+
return declared_type(node.expression)
|
|
347
|
+
if isinstance(node, ArrayElementNode):
|
|
348
|
+
return _element_type(declared_type(node.expression))
|
|
349
|
+
if isinstance(node, ArrayConstructNode):
|
|
350
|
+
return node.element_type
|
|
351
|
+
if isinstance(node, VendorFunctionNode):
|
|
352
|
+
return node.value_type
|
|
353
|
+
if isinstance(node, ArrayNode) and node.operation in _ARRAY_ANSWERS_AN_ARRAY:
|
|
354
|
+
return declared_type(node.expression)
|
|
355
|
+
return _carried(node)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _carried(node: ExpressionNode) -> type[object] | None:
|
|
359
|
+
"""The kinds that answer with one of their own arguments."""
|
|
360
|
+
if isinstance(node, FunctionNode) and node.name in _CARRYING_FUNCTIONS:
|
|
361
|
+
return declared_type(node.arguments[0])
|
|
362
|
+
if isinstance(node, AggregateNode) and node.name in _CARRYING_AGGREGATES:
|
|
363
|
+
return declared_type(node.arguments[0])
|
|
364
|
+
if isinstance(node, WindowFunctionNode) and node.name in _CARRYING_WINDOWS:
|
|
365
|
+
return declared_type(node.arguments[0])
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _element_type(array_type: type[object] | None) -> type[object] | None:
|
|
370
|
+
"""What one element of an array was declared to hold.
|
|
371
|
+
|
|
372
|
+
An array column is declared as a tuple of its element type, so the element
|
|
373
|
+
type is inside it. A bare tuple says nothing about what it holds.
|
|
374
|
+
"""
|
|
375
|
+
if array_type is None or get_origin(array_type) is not tuple:
|
|
376
|
+
return None
|
|
377
|
+
arguments = get_args(array_type)
|
|
378
|
+
return arguments[0] if arguments else None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
_NEVER_NULL_AGGREGATES = frozenset({AggregateName.COUNT})
|
|
382
|
+
"""Aggregates that answer with a value even where there are no rows."""
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@dataclass(frozen=True, slots=True)
|
|
386
|
+
class ResultDescriptor:
|
|
387
|
+
"""Everything one expression answers with, derived from the tree once.
|
|
388
|
+
|
|
389
|
+
``family`` is the kind of value, which every expression has. ``declared``
|
|
390
|
+
is the Python type where the tree knows one, and is none where an
|
|
391
|
+
expression computes something the tree cannot name. ``nullable`` is
|
|
392
|
+
whether it may answer with no value.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
family: ScalarFamily
|
|
396
|
+
declared: type[object] | None
|
|
397
|
+
nullable: bool
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def result_descriptor(node: ExpressionNode, /) -> ResultDescriptor:
|
|
401
|
+
"""What this expression answers with, which one place decides."""
|
|
402
|
+
return ResultDescriptor(
|
|
403
|
+
expression_family(node), declared_type(node), nullable(node)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def nullable(node: ExpressionNode, /) -> bool:
|
|
408
|
+
"""Whether this may answer with no value at all.
|
|
409
|
+
|
|
410
|
+
A column says so itself. Everything else says so through what it reads:
|
|
411
|
+
a value carried through is as nullable as the value was, and a question
|
|
412
|
+
answered about rows that may not be there is nullable whatever it reads.
|
|
413
|
+
"""
|
|
414
|
+
if isinstance(node, FieldNode):
|
|
415
|
+
return node.nullable
|
|
416
|
+
if isinstance(node, BoundValueNode):
|
|
417
|
+
return node.value is None
|
|
418
|
+
if isinstance(node, (ExistsNode, RowValueNode)):
|
|
419
|
+
return False
|
|
420
|
+
if isinstance(node, AggregateNode):
|
|
421
|
+
return node.name not in _NEVER_NULL_AGGREGATES
|
|
422
|
+
if isinstance(node, WindowFunctionNode):
|
|
423
|
+
return node.name in _CARRYING_WINDOWS
|
|
424
|
+
return _nullable_through(node)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _nullable_through(node: ExpressionNode) -> bool:
|
|
428
|
+
"""The kinds that are nullable because what they read is."""
|
|
429
|
+
if isinstance(node, (CastNode, WindowNode)):
|
|
430
|
+
return nullable(node.expression)
|
|
431
|
+
if isinstance(node, UnaryNode):
|
|
432
|
+
return nullable(node.operand)
|
|
433
|
+
if isinstance(node, BinaryNode):
|
|
434
|
+
return nullable(node.left) or nullable(node.right)
|
|
435
|
+
if isinstance(node, CaseNode):
|
|
436
|
+
return _nullable_case(node)
|
|
437
|
+
if isinstance(node, FunctionNode) and node.name is FunctionName.COALESCE:
|
|
438
|
+
return nullable(node.arguments[-1])
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _nullable_case(node: CaseNode) -> bool:
|
|
443
|
+
"""A case answers with null where no branch holds and none was given."""
|
|
444
|
+
if node.otherwise is None:
|
|
445
|
+
return True
|
|
446
|
+
return any(nullable(result) for _, result in node.branches) or nullable(
|
|
447
|
+
node.otherwise
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
__all__ = (
|
|
452
|
+
"ResultDescriptor",
|
|
453
|
+
"declared_type",
|
|
454
|
+
"expression_family",
|
|
455
|
+
"family_for_kind",
|
|
456
|
+
"family_for_type",
|
|
457
|
+
"nullable",
|
|
458
|
+
"result_descriptor",
|
|
459
|
+
)
|
pyoq/query/routines.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Running a stored procedure.
|
|
2
|
+
|
|
3
|
+
A procedure is invoked rather than selected from, and it may answer with rows
|
|
4
|
+
or with nothing at all, so it is a statement rather than an expression. A
|
|
5
|
+
stored function is a function and is declared with `vendor_function` instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
from typing import TYPE_CHECKING, Generic, Literal, TypeVar, cast, overload
|
|
12
|
+
|
|
13
|
+
from pyoq.errors import QueryValidationError
|
|
14
|
+
from pyoq.query.expressions import ComputedExpression, Expression
|
|
15
|
+
from pyoq.query.nodes import FieldNode
|
|
16
|
+
from pyoq.query.results import family_for_type
|
|
17
|
+
from pyoq.query.select_nodes import ProjectionNode
|
|
18
|
+
from pyoq.query.values import bind
|
|
19
|
+
from pyoq.query.vendor import checked_routine_name, checked_routine_qualifier
|
|
20
|
+
from pyoq.query.write_nodes import CallNode
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pyoq.query.nodes import ExpressionNode
|
|
24
|
+
from pyoq.query.raw import NodeProvider
|
|
25
|
+
|
|
26
|
+
ResultRow = TypeVar("ResultRow")
|
|
27
|
+
Value1 = TypeVar("Value1")
|
|
28
|
+
Value2 = TypeVar("Value2")
|
|
29
|
+
Value3 = TypeVar("Value3")
|
|
30
|
+
Value4 = TypeVar("Value4")
|
|
31
|
+
Value5 = TypeVar("Value5")
|
|
32
|
+
Value6 = TypeVar("Value6")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Call(Generic[ResultRow]):
|
|
36
|
+
"""A procedure asked to run, and whatever it answers with.
|
|
37
|
+
|
|
38
|
+
A procedure's rows are described by the expressions passed to returning,
|
|
39
|
+
so their tuple type and runtime decoder come from the same declarations.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
__slots__ = ("_node",)
|
|
43
|
+
|
|
44
|
+
def __init__(self, node: CallNode) -> None:
|
|
45
|
+
self._node = node
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def node(self) -> CallNode:
|
|
49
|
+
return self._node
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def returning(self, value1: Expression[Value1], /) -> Call[tuple[Value1]]: ...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def returning(
|
|
56
|
+
self,
|
|
57
|
+
value1: Expression[Value1],
|
|
58
|
+
value2: Expression[Value2],
|
|
59
|
+
/,
|
|
60
|
+
) -> Call[tuple[Value1, Value2]]: ...
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def returning(
|
|
64
|
+
self,
|
|
65
|
+
value1: Expression[Value1],
|
|
66
|
+
value2: Expression[Value2],
|
|
67
|
+
value3: Expression[Value3],
|
|
68
|
+
/,
|
|
69
|
+
) -> Call[tuple[Value1, Value2, Value3]]: ...
|
|
70
|
+
|
|
71
|
+
@overload
|
|
72
|
+
def returning(
|
|
73
|
+
self,
|
|
74
|
+
value1: Expression[Value1],
|
|
75
|
+
value2: Expression[Value2],
|
|
76
|
+
value3: Expression[Value3],
|
|
77
|
+
value4: Expression[Value4],
|
|
78
|
+
/,
|
|
79
|
+
) -> Call[tuple[Value1, Value2, Value3, Value4]]: ...
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def returning(
|
|
83
|
+
self,
|
|
84
|
+
value1: Expression[Value1],
|
|
85
|
+
value2: Expression[Value2],
|
|
86
|
+
value3: Expression[Value3],
|
|
87
|
+
value4: Expression[Value4],
|
|
88
|
+
value5: Expression[Value5],
|
|
89
|
+
/,
|
|
90
|
+
) -> Call[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def returning(
|
|
94
|
+
self,
|
|
95
|
+
value1: Expression[Value1],
|
|
96
|
+
value2: Expression[Value2],
|
|
97
|
+
value3: Expression[Value3],
|
|
98
|
+
value4: Expression[Value4],
|
|
99
|
+
value5: Expression[Value5],
|
|
100
|
+
value6: Expression[Value6],
|
|
101
|
+
/,
|
|
102
|
+
) -> Call[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
|
|
103
|
+
|
|
104
|
+
def returning( # type: ignore[misc]
|
|
105
|
+
self,
|
|
106
|
+
*columns: Expression[object],
|
|
107
|
+
) -> Call[object]:
|
|
108
|
+
"""Say what the rows this answers with hold.
|
|
109
|
+
|
|
110
|
+
Naming the columns supplies both the tuple type and runtime decoder.
|
|
111
|
+
"""
|
|
112
|
+
if not columns:
|
|
113
|
+
message = "a procedure returning rows names at least one column"
|
|
114
|
+
raise QueryValidationError(message)
|
|
115
|
+
return Call[object](
|
|
116
|
+
replace(
|
|
117
|
+
self._node,
|
|
118
|
+
returning=tuple(ProjectionNode(column.node) for column in columns),
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def argument(value: object, /) -> NodeProvider:
|
|
124
|
+
"""A routine argument, whether it was given as an expression or a value.
|
|
125
|
+
|
|
126
|
+
Generated call surfaces take either, because a caller passing a literal
|
|
127
|
+
should not have to say so, and a value still travels bound.
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(value, Expression):
|
|
130
|
+
return cast("Expression[object]", value)
|
|
131
|
+
return bind(value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def routine_output(
|
|
136
|
+
value_type: type[ResultRow],
|
|
137
|
+
name: str,
|
|
138
|
+
/,
|
|
139
|
+
*,
|
|
140
|
+
nullable: Literal[False],
|
|
141
|
+
) -> ComputedExpression[ResultRow]: ...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@overload
|
|
145
|
+
def routine_output(
|
|
146
|
+
value_type: type[ResultRow],
|
|
147
|
+
name: str,
|
|
148
|
+
/,
|
|
149
|
+
*,
|
|
150
|
+
nullable: Literal[True],
|
|
151
|
+
) -> ComputedExpression[ResultRow | None]: ...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def routine_output(
|
|
155
|
+
value_type: type[ResultRow],
|
|
156
|
+
name: str,
|
|
157
|
+
/,
|
|
158
|
+
*,
|
|
159
|
+
nullable: bool,
|
|
160
|
+
) -> ComputedExpression[ResultRow] | ComputedExpression[ResultRow | None]:
|
|
161
|
+
"""Describe a reflected routine output without losing its nullability."""
|
|
162
|
+
family = family_for_type(value_type)
|
|
163
|
+
expression: ComputedExpression[ResultRow] | ComputedExpression[ResultRow | None]
|
|
164
|
+
expression = ComputedExpression(
|
|
165
|
+
FieldNode(name, family=family, value_type=value_type, nullable=nullable),
|
|
166
|
+
family,
|
|
167
|
+
)
|
|
168
|
+
return expression
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def call(
|
|
172
|
+
name: str,
|
|
173
|
+
/,
|
|
174
|
+
*arguments: NodeProvider,
|
|
175
|
+
schema: str | None = None,
|
|
176
|
+
catalog: str | None = None,
|
|
177
|
+
) -> Call[tuple[object, ...]]:
|
|
178
|
+
"""Run a stored procedure, with its arguments bound like any other value.
|
|
179
|
+
|
|
180
|
+
The name is written into the SQL rather than bound, because no database
|
|
181
|
+
takes a routine name as a parameter, so it is held to being a name. Saying
|
|
182
|
+
which schema keeps it is what makes the call reach that one rather than
|
|
183
|
+
whichever the search path finds first.
|
|
184
|
+
"""
|
|
185
|
+
nodes: tuple[ExpressionNode, ...] = tuple(argument.node for argument in arguments)
|
|
186
|
+
return Call[tuple[object, ...]](
|
|
187
|
+
CallNode(
|
|
188
|
+
checked_routine_name(name),
|
|
189
|
+
checked_routine_qualifier(schema, "routine schema"),
|
|
190
|
+
checked_routine_qualifier(catalog, "routine catalog"),
|
|
191
|
+
nodes,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = ("Call", "argument", "call", "routine_output")
|