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/aggregates.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Typed aggregate expressions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TypeAlias, TypeVar, cast, overload
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import ExpressionTypeError
|
|
9
|
+
from pyoq.query.expressions import ComputedExpression, Expression
|
|
10
|
+
from pyoq.query.nodes import AggregateName, AggregateNode, ScalarFamily
|
|
11
|
+
from pyoq.query.raw import NodeProvider
|
|
12
|
+
from pyoq.query.windows import Window, WindowSpecification, looking_through
|
|
13
|
+
from pyoq.schema.models import JsonValue
|
|
14
|
+
|
|
15
|
+
Value = TypeVar("Value")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AggregateExpression(ComputedExpression[Value]):
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
def over(self, window: WindowSpecification | None = None, /) -> Window[Value]:
|
|
22
|
+
"""This aggregate computed per row over its window, not per group.
|
|
23
|
+
|
|
24
|
+
The rows stay as they are and each one is given the aggregate of the
|
|
25
|
+
window around it, so a running total sits beside the row it counts.
|
|
26
|
+
"""
|
|
27
|
+
return Window[Value](looking_through(self.node, window), self.family)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SumResult: TypeAlias = (
|
|
31
|
+
AggregateExpression[int | None]
|
|
32
|
+
| AggregateExpression[float | None]
|
|
33
|
+
| AggregateExpression[Decimal | None]
|
|
34
|
+
)
|
|
35
|
+
AverageResult: TypeAlias = (
|
|
36
|
+
AggregateExpression[float | None] | AggregateExpression[Decimal | None]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count(
|
|
41
|
+
expression: Expression[Value] | None = None,
|
|
42
|
+
/,
|
|
43
|
+
*,
|
|
44
|
+
distinct: bool = False,
|
|
45
|
+
) -> AggregateExpression[int]:
|
|
46
|
+
arguments = () if expression is None else (expression.node,)
|
|
47
|
+
if distinct and expression is None:
|
|
48
|
+
message = "distinct count requires an expression"
|
|
49
|
+
raise ValueError(message)
|
|
50
|
+
return AggregateExpression(
|
|
51
|
+
AggregateNode(AggregateName.COUNT, arguments, distinct=distinct),
|
|
52
|
+
ScalarFamily.NUMERIC,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
def sum_(expression: Expression[int], /) -> AggregateExpression[int | None]: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def sum_(expression: Expression[float], /) -> AggregateExpression[float | None]: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def sum_(
|
|
66
|
+
expression: Expression[Decimal],
|
|
67
|
+
/,
|
|
68
|
+
) -> AggregateExpression[Decimal | None]: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sum_(expression: object, /) -> SumResult:
|
|
72
|
+
typed_expression = _require_expression(expression)
|
|
73
|
+
_require_numeric(typed_expression)
|
|
74
|
+
return cast(
|
|
75
|
+
SumResult,
|
|
76
|
+
AggregateExpression(
|
|
77
|
+
AggregateNode(AggregateName.SUM, (typed_expression.node,)),
|
|
78
|
+
ScalarFamily.NUMERIC,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@overload
|
|
84
|
+
def average(expression: Expression[int], /) -> AggregateExpression[float | None]: ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
def average(expression: Expression[float], /) -> AggregateExpression[float | None]: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@overload
|
|
92
|
+
def average(
|
|
93
|
+
expression: Expression[Decimal],
|
|
94
|
+
/,
|
|
95
|
+
) -> AggregateExpression[Decimal | None]: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def average(expression: object, /) -> AverageResult:
|
|
99
|
+
typed_expression = _require_expression(expression)
|
|
100
|
+
_require_numeric(typed_expression)
|
|
101
|
+
return cast(
|
|
102
|
+
AverageResult,
|
|
103
|
+
AggregateExpression(
|
|
104
|
+
AggregateNode(AggregateName.AVERAGE, (typed_expression.node,)),
|
|
105
|
+
ScalarFamily.NUMERIC,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def minimum(expression: Expression[Value], /) -> AggregateExpression[Value | None]:
|
|
111
|
+
return AggregateExpression(
|
|
112
|
+
AggregateNode(AggregateName.MINIMUM, (expression.node,)),
|
|
113
|
+
expression.family,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def maximum(expression: Expression[Value], /) -> AggregateExpression[Value | None]:
|
|
118
|
+
return AggregateExpression(
|
|
119
|
+
AggregateNode(AggregateName.MAXIMUM, (expression.node,)),
|
|
120
|
+
expression.family,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def json_agg(expression: NodeProvider, /) -> AggregateExpression[JsonValue]:
|
|
125
|
+
"""The rows of a group gathered into one JSON array.
|
|
126
|
+
|
|
127
|
+
Only PostgreSQL and SQLite keep the order the rows were read in. MySQL
|
|
128
|
+
gathers them in whatever order it reached them.
|
|
129
|
+
"""
|
|
130
|
+
return AggregateExpression[JsonValue](
|
|
131
|
+
AggregateNode(AggregateName.JSON_ARRAY_AGG, (expression.node,)),
|
|
132
|
+
ScalarFamily.JSON,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def json_object_agg(
|
|
137
|
+
member: Expression[str],
|
|
138
|
+
value: NodeProvider,
|
|
139
|
+
/,
|
|
140
|
+
) -> AggregateExpression[JsonValue]:
|
|
141
|
+
"""The rows of a group gathered into one JSON object, named by a column."""
|
|
142
|
+
return AggregateExpression[JsonValue](
|
|
143
|
+
AggregateNode(AggregateName.JSON_OBJECT_AGG, (member.node, value.node)),
|
|
144
|
+
ScalarFamily.JSON,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _require_numeric(expression: Expression[object]) -> None:
|
|
149
|
+
if expression.family is not ScalarFamily.NUMERIC:
|
|
150
|
+
message = "aggregate requires a numeric expression"
|
|
151
|
+
raise ExpressionTypeError(message)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _require_expression(value: object) -> Expression[object]:
|
|
155
|
+
if isinstance(value, Expression):
|
|
156
|
+
return cast(Expression[object], value)
|
|
157
|
+
message = "aggregate requires a typed expression"
|
|
158
|
+
raise TypeError(message)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = (
|
|
162
|
+
"AggregateExpression",
|
|
163
|
+
"AggregateName",
|
|
164
|
+
"AggregateNode",
|
|
165
|
+
"average",
|
|
166
|
+
"count",
|
|
167
|
+
"json_agg",
|
|
168
|
+
"json_object_agg",
|
|
169
|
+
"maximum",
|
|
170
|
+
"minimum",
|
|
171
|
+
"sum_",
|
|
172
|
+
)
|
pyoq/query/arrays.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Building an array out of the values around it.
|
|
2
|
+
|
|
3
|
+
Asking about an array is asked of the array, so those questions are methods
|
|
4
|
+
on the value itself. Building one starts from values that are not an array
|
|
5
|
+
yet, so it starts here.
|
|
6
|
+
|
|
7
|
+
Only PostgreSQL keeps arrays as a type of their own. The other dialects
|
|
8
|
+
refuse an array before the statement reaches them rather than storing one
|
|
9
|
+
under a name that does not mean the same thing.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from types import GenericAlias
|
|
15
|
+
from typing import TypeVar, cast
|
|
16
|
+
|
|
17
|
+
from pyoq.errors import QueryValidationError
|
|
18
|
+
from pyoq.query.expressions import (
|
|
19
|
+
ComputedExpression,
|
|
20
|
+
Expression,
|
|
21
|
+
family_for_value,
|
|
22
|
+
operand,
|
|
23
|
+
)
|
|
24
|
+
from pyoq.query.nodes import ArrayConstructNode, ExpressionNode, ScalarFamily
|
|
25
|
+
|
|
26
|
+
Element = TypeVar("Element")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def array_of(*values: Element) -> ComputedExpression[tuple[Element, ...]]:
|
|
30
|
+
"""An array holding these values, in the order they are written.
|
|
31
|
+
|
|
32
|
+
At least one value, because an array built from none has no element type
|
|
33
|
+
and a database will not store a value whose type it cannot name.
|
|
34
|
+
"""
|
|
35
|
+
if not values:
|
|
36
|
+
message = "an array is built from at least one value, which gives it a type"
|
|
37
|
+
raise QueryValidationError(message)
|
|
38
|
+
elements: tuple[ExpressionNode, ...] = tuple(
|
|
39
|
+
operand(value, family_for_value(value)) for value in values
|
|
40
|
+
)
|
|
41
|
+
return ComputedExpression[tuple[Element, ...]](
|
|
42
|
+
ArrayConstructNode(elements, _element_type(values)), ScalarFamily.OTHER
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _element_type(values: tuple[object, ...]) -> type[object] | None:
|
|
47
|
+
"""What an array of these values holds, where they all hold one thing.
|
|
48
|
+
|
|
49
|
+
A column of an array is declared as a tuple of its element type, and an
|
|
50
|
+
array built here is declared the same way, so both read back alike. The
|
|
51
|
+
tuple type is built rather than written, which is the one place a checker
|
|
52
|
+
cannot see the type being made, so it is named here.
|
|
53
|
+
|
|
54
|
+
Values of more than one type, or values that are expressions, say nothing
|
|
55
|
+
about the element type, and the family speaks for them instead.
|
|
56
|
+
"""
|
|
57
|
+
if any(isinstance(value, Expression) for value in values):
|
|
58
|
+
return None
|
|
59
|
+
held = {type(value) for value in values}
|
|
60
|
+
if len(held) != 1:
|
|
61
|
+
return None
|
|
62
|
+
return cast("type[object]", GenericAlias(tuple, (held.pop(), Ellipsis)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ("array_of",)
|
pyoq/query/binding.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Bound parameter accounting for query and write structures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.nodes import (
|
|
6
|
+
AggregateNode,
|
|
7
|
+
BinaryNode,
|
|
8
|
+
BoundValueNode,
|
|
9
|
+
ExpressionNode,
|
|
10
|
+
ExtractNode,
|
|
11
|
+
FunctionNode,
|
|
12
|
+
RawNode,
|
|
13
|
+
UnaryNode,
|
|
14
|
+
VariadicNode,
|
|
15
|
+
)
|
|
16
|
+
from pyoq.query.select_nodes import ProjectionNode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bound_value_count(node: ExpressionNode) -> int:
|
|
20
|
+
direct = 1 if isinstance(node, BoundValueNode) else 0
|
|
21
|
+
return direct + sum(bound_value_count(child) for child in _children(node))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def row_bound_value_count(row: tuple[ExpressionNode, ...]) -> int:
|
|
25
|
+
return sum(bound_value_count(value) for value in row)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def projection_bound_value_count(projections: tuple[ProjectionNode, ...]) -> int:
|
|
29
|
+
return sum(bound_value_count(node.expression) for node in projections)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _children(node: ExpressionNode) -> tuple[ExpressionNode, ...]:
|
|
33
|
+
if isinstance(node, UnaryNode):
|
|
34
|
+
return (node.operand,)
|
|
35
|
+
if isinstance(node, BinaryNode):
|
|
36
|
+
return (node.left, node.right)
|
|
37
|
+
if isinstance(node, VariadicNode):
|
|
38
|
+
return node.operands
|
|
39
|
+
if isinstance(node, (FunctionNode, AggregateNode)):
|
|
40
|
+
return node.arguments
|
|
41
|
+
if isinstance(node, ExtractNode):
|
|
42
|
+
return (node.expression,)
|
|
43
|
+
if isinstance(node, RawNode):
|
|
44
|
+
return tuple(part for part in node.parts if not isinstance(part, str))
|
|
45
|
+
return ()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = (
|
|
49
|
+
"bound_value_count",
|
|
50
|
+
"projection_bound_value_count",
|
|
51
|
+
"row_bound_value_count",
|
|
52
|
+
)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""What each dialect can be asked for, one operation at a time.
|
|
2
|
+
|
|
3
|
+
A broad name like JSON or windows says nothing about which questions a
|
|
4
|
+
dialect will answer. This says it for each operation: written by the dialect
|
|
5
|
+
itself, written by PyOQ out of what the dialect has, or refused before the
|
|
6
|
+
statement reaches a server.
|
|
7
|
+
|
|
8
|
+
This is the one place that claim is made. The documentation is written from
|
|
9
|
+
it and a contract compiles every row against every dialect, so a claim here
|
|
10
|
+
that is not true stops the test suite rather than reaching a caller.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import StrEnum
|
|
18
|
+
from types import MappingProxyType
|
|
19
|
+
|
|
20
|
+
from pyoq.config import DatabaseDialect
|
|
21
|
+
from pyoq.query.nodes import ScalarFamily
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Support(StrEnum):
|
|
25
|
+
"""How a dialect answers one operation."""
|
|
26
|
+
|
|
27
|
+
NATIVE = "native"
|
|
28
|
+
"""The dialect has it, and PyOQ writes what the dialect calls it."""
|
|
29
|
+
|
|
30
|
+
EMULATED = "emulated"
|
|
31
|
+
"""The dialect lacks it, and PyOQ writes it out of what the dialect has."""
|
|
32
|
+
|
|
33
|
+
REJECTED = "rejected"
|
|
34
|
+
"""No dialect support, refused while the query is compiled."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Operation(StrEnum):
|
|
38
|
+
"""One thing a caller can ask for, named at the grain support is decided."""
|
|
39
|
+
|
|
40
|
+
WINDOW_ROW_NUMBER = "window: row number"
|
|
41
|
+
WINDOW_RANK = "window: rank"
|
|
42
|
+
WINDOW_DENSE_RANK = "window: dense rank"
|
|
43
|
+
WINDOW_PERCENT_RANK = "window: percent rank"
|
|
44
|
+
WINDOW_CUME_DIST = "window: cumulative distribution"
|
|
45
|
+
WINDOW_NTILE = "window: ntile"
|
|
46
|
+
WINDOW_LAG = "window: lag"
|
|
47
|
+
WINDOW_LEAD = "window: lead"
|
|
48
|
+
WINDOW_FIRST_VALUE = "window: first value"
|
|
49
|
+
WINDOW_LAST_VALUE = "window: last value"
|
|
50
|
+
WINDOW_NTH_VALUE = "window: nth value"
|
|
51
|
+
WINDOW_AGGREGATE = "window: an aggregate over a window"
|
|
52
|
+
WINDOW_PARTITION = "window: partition by"
|
|
53
|
+
WINDOW_ORDER = "window: order by"
|
|
54
|
+
WINDOW_FRAME_ROWS = "window frame: rows"
|
|
55
|
+
WINDOW_FRAME_RANGE = "window frame: range"
|
|
56
|
+
WINDOW_FRAME_GROUPS = "window frame: groups"
|
|
57
|
+
WINDOW_FRAME_EXCLUDE = "window frame: exclude"
|
|
58
|
+
WINDOW_NAMED = "window: declared by name"
|
|
59
|
+
|
|
60
|
+
JSON_VALUE = "json: read a value"
|
|
61
|
+
JSON_TEXT = "json: read text"
|
|
62
|
+
JSON_LENGTH = "json: how long an array is"
|
|
63
|
+
JSON_EXISTS = "json: whether a path is there"
|
|
64
|
+
JSON_CONTAINS = "json: whether it holds a value"
|
|
65
|
+
JSON_SET = "json: set"
|
|
66
|
+
JSON_INSERT = "json: insert"
|
|
67
|
+
JSON_REPLACE = "json: replace"
|
|
68
|
+
JSON_MERGE = "json: merge all the way down"
|
|
69
|
+
JSON_CONCAT = "json: write over members, one level"
|
|
70
|
+
JSON_REMOVE = "json: remove"
|
|
71
|
+
JSON_OBJECT = "json: build an object"
|
|
72
|
+
JSON_ARRAY = "json: build an array"
|
|
73
|
+
JSON_KEYS = "json: name the members"
|
|
74
|
+
JSON_AGG = "json: gather rows into an array"
|
|
75
|
+
JSON_OBJECT_AGG = "json: gather rows into an object"
|
|
76
|
+
JSON_ELEMENTS = "json: read an array as rows"
|
|
77
|
+
|
|
78
|
+
ARRAY_HAS = "array: holds a value"
|
|
79
|
+
ARRAY_LACKS = "array: holds a value nowhere"
|
|
80
|
+
ARRAY_CONTAINS_ALL = "array: holds every value"
|
|
81
|
+
ARRAY_CONTAINED_BY = "array: is among"
|
|
82
|
+
ARRAY_OVERLAPS = "array: shares a value"
|
|
83
|
+
ARRAY_APPEND = "array: append"
|
|
84
|
+
ARRAY_PREPEND = "array: prepend"
|
|
85
|
+
ARRAY_CONCAT = "array: concatenate"
|
|
86
|
+
ARRAY_REMOVE = "array: without a value"
|
|
87
|
+
ARRAY_REPLACE = "array: replace a value"
|
|
88
|
+
ARRAY_ELEMENT = "array: one element"
|
|
89
|
+
ARRAY_LENGTH = "array: how many elements"
|
|
90
|
+
ARRAY_LENGTH_ALONG = "array: how many along a dimension"
|
|
91
|
+
ARRAY_DIMENSIONS = "array: how many dimensions"
|
|
92
|
+
ARRAY_CONSTRUCT = "array: built from values"
|
|
93
|
+
|
|
94
|
+
LOCK_FOR_UPDATE = "lock: for update"
|
|
95
|
+
LOCK_FOR_SHARE = "lock: for share"
|
|
96
|
+
LOCK_NOWAIT = "lock: fail rather than wait"
|
|
97
|
+
LOCK_SKIP_LOCKED = "lock: skip what is locked"
|
|
98
|
+
LOCK_OF_TABLES = "lock: named tables"
|
|
99
|
+
LOCK_OF_FIELDS = "lock: named fields"
|
|
100
|
+
|
|
101
|
+
QUERY_LATERAL = "query: lateral source"
|
|
102
|
+
QUERY_RECURSIVE = "query: recursive common table"
|
|
103
|
+
QUERY_ROW_VALUE = "query: row value"
|
|
104
|
+
QUERY_CAST = "query: cast"
|
|
105
|
+
QUERY_NULL_ORDERING = "query: nulls first or last"
|
|
106
|
+
QUERY_RIGHT_JOIN = "query: right join"
|
|
107
|
+
QUERY_FULL_JOIN = "query: full join"
|
|
108
|
+
|
|
109
|
+
ROUTINE_PROCEDURE = "routine: run a stored procedure"
|
|
110
|
+
ROUTINE_FUNCTION = "routine: ask a function by name"
|
|
111
|
+
ROUTINE_MANY_RESULT_SETS = "routine: more than one result set"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_P = DatabaseDialect.POSTGRES
|
|
115
|
+
_M = DatabaseDialect.MYSQL
|
|
116
|
+
_S = DatabaseDialect.SQLITE
|
|
117
|
+
|
|
118
|
+
_N = Support.NATIVE
|
|
119
|
+
_E = Support.EMULATED
|
|
120
|
+
_R = Support.REJECTED
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class OperationSupport:
|
|
125
|
+
"""One operation: what each dialect does, what it reads back, what runs it.
|
|
126
|
+
|
|
127
|
+
``reads_back_as`` is the kind of value the operation answers with, which
|
|
128
|
+
is what decides how a driver's answer is read back. It is none where the
|
|
129
|
+
operation answers with rows rather than with a value.
|
|
130
|
+
|
|
131
|
+
``evidence`` names the dialect-neutral contract that executes it against
|
|
132
|
+
every server that claims it, so a row points at the run that proves it
|
|
133
|
+
rather than at a compiled string.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
dialects: Mapping[DatabaseDialect, Support]
|
|
137
|
+
reads_back_as: ScalarFamily | None
|
|
138
|
+
evidence: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _row(
|
|
142
|
+
postgres: Support,
|
|
143
|
+
mysql: Support,
|
|
144
|
+
sqlite: Support,
|
|
145
|
+
reads_back_as: ScalarFamily | None,
|
|
146
|
+
evidence: str,
|
|
147
|
+
) -> OperationSupport:
|
|
148
|
+
return OperationSupport(
|
|
149
|
+
MappingProxyType({_P: postgres, _M: mysql, _S: sqlite}),
|
|
150
|
+
reads_back_as,
|
|
151
|
+
evidence,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
SUPPORT: Mapping[Operation, OperationSupport] = MappingProxyType(
|
|
156
|
+
{
|
|
157
|
+
Operation.WINDOW_ROW_NUMBER: _row(
|
|
158
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
159
|
+
),
|
|
160
|
+
Operation.WINDOW_RANK: _row(
|
|
161
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
162
|
+
),
|
|
163
|
+
Operation.WINDOW_DENSE_RANK: _row(
|
|
164
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
165
|
+
),
|
|
166
|
+
Operation.WINDOW_PERCENT_RANK: _row(
|
|
167
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
168
|
+
),
|
|
169
|
+
Operation.WINDOW_CUME_DIST: _row(
|
|
170
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
171
|
+
),
|
|
172
|
+
Operation.WINDOW_NTILE: _row(
|
|
173
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
174
|
+
),
|
|
175
|
+
Operation.WINDOW_LAG: _row(_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"),
|
|
176
|
+
Operation.WINDOW_LEAD: _row(
|
|
177
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
178
|
+
),
|
|
179
|
+
Operation.WINDOW_FIRST_VALUE: _row(
|
|
180
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
181
|
+
),
|
|
182
|
+
Operation.WINDOW_LAST_VALUE: _row(
|
|
183
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
184
|
+
),
|
|
185
|
+
Operation.WINDOW_NTH_VALUE: _row(
|
|
186
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
187
|
+
),
|
|
188
|
+
Operation.WINDOW_AGGREGATE: _row(
|
|
189
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
190
|
+
),
|
|
191
|
+
Operation.WINDOW_PARTITION: _row(
|
|
192
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
193
|
+
),
|
|
194
|
+
Operation.WINDOW_ORDER: _row(
|
|
195
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
196
|
+
),
|
|
197
|
+
Operation.WINDOW_FRAME_ROWS: _row(
|
|
198
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
199
|
+
),
|
|
200
|
+
Operation.WINDOW_FRAME_RANGE: _row(
|
|
201
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
202
|
+
),
|
|
203
|
+
Operation.WINDOW_FRAME_GROUPS: _row(
|
|
204
|
+
_N, _R, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
205
|
+
),
|
|
206
|
+
Operation.WINDOW_FRAME_EXCLUDE: _row(
|
|
207
|
+
_N, _R, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
208
|
+
),
|
|
209
|
+
Operation.WINDOW_NAMED: _row(
|
|
210
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
|
|
211
|
+
),
|
|
212
|
+
Operation.JSON_VALUE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
213
|
+
Operation.JSON_TEXT: _row(_N, _N, _N, ScalarFamily.STRING, "json_contract"),
|
|
214
|
+
Operation.JSON_LENGTH: _row(_N, _N, _N, ScalarFamily.NUMERIC, "json_contract"),
|
|
215
|
+
Operation.JSON_EXISTS: _row(_N, _N, _N, ScalarFamily.BOOLEAN, "json_contract"),
|
|
216
|
+
Operation.JSON_CONTAINS: _row(
|
|
217
|
+
_N, _N, _R, ScalarFamily.BOOLEAN, "json_contract"
|
|
218
|
+
),
|
|
219
|
+
Operation.JSON_SET: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
220
|
+
Operation.JSON_INSERT: _row(_E, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
221
|
+
Operation.JSON_REPLACE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
222
|
+
Operation.JSON_MERGE: _row(_R, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
223
|
+
Operation.JSON_CONCAT: _row(_N, _R, _R, ScalarFamily.JSON, "json_contract"),
|
|
224
|
+
Operation.JSON_REMOVE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
225
|
+
Operation.JSON_OBJECT: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
226
|
+
Operation.JSON_ARRAY: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
227
|
+
Operation.JSON_KEYS: _row(_E, _N, _E, ScalarFamily.JSON, "json_contract"),
|
|
228
|
+
Operation.JSON_AGG: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
229
|
+
Operation.JSON_OBJECT_AGG: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
|
|
230
|
+
Operation.JSON_ELEMENTS: _row(_N, _N, _N, ScalarFamily.STRING, "json_contract"),
|
|
231
|
+
Operation.ARRAY_HAS: _row(_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"),
|
|
232
|
+
Operation.ARRAY_LACKS: _row(_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"),
|
|
233
|
+
Operation.ARRAY_CONTAINS_ALL: _row(
|
|
234
|
+
_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
|
|
235
|
+
),
|
|
236
|
+
Operation.ARRAY_CONTAINED_BY: _row(
|
|
237
|
+
_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
|
|
238
|
+
),
|
|
239
|
+
Operation.ARRAY_OVERLAPS: _row(
|
|
240
|
+
_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
|
|
241
|
+
),
|
|
242
|
+
Operation.ARRAY_APPEND: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
243
|
+
Operation.ARRAY_PREPEND: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
244
|
+
Operation.ARRAY_CONCAT: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
245
|
+
Operation.ARRAY_REMOVE: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
246
|
+
Operation.ARRAY_REPLACE: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
247
|
+
Operation.ARRAY_ELEMENT: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
|
|
248
|
+
Operation.ARRAY_LENGTH: _row(
|
|
249
|
+
_N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
|
|
250
|
+
),
|
|
251
|
+
Operation.ARRAY_LENGTH_ALONG: _row(
|
|
252
|
+
_N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
|
|
253
|
+
),
|
|
254
|
+
Operation.ARRAY_DIMENSIONS: _row(
|
|
255
|
+
_N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
|
|
256
|
+
),
|
|
257
|
+
Operation.ARRAY_CONSTRUCT: _row(
|
|
258
|
+
_N, _R, _R, ScalarFamily.OTHER, "array_contract"
|
|
259
|
+
),
|
|
260
|
+
Operation.LOCK_FOR_UPDATE: _row(
|
|
261
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
262
|
+
),
|
|
263
|
+
Operation.LOCK_FOR_SHARE: _row(
|
|
264
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
265
|
+
),
|
|
266
|
+
Operation.LOCK_NOWAIT: _row(
|
|
267
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
268
|
+
),
|
|
269
|
+
Operation.LOCK_SKIP_LOCKED: _row(
|
|
270
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
271
|
+
),
|
|
272
|
+
Operation.LOCK_OF_TABLES: _row(
|
|
273
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
274
|
+
),
|
|
275
|
+
Operation.LOCK_OF_FIELDS: _row(
|
|
276
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
|
|
277
|
+
),
|
|
278
|
+
Operation.QUERY_LATERAL: _row(
|
|
279
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "lateral_contract"
|
|
280
|
+
),
|
|
281
|
+
Operation.QUERY_RECURSIVE: _row(
|
|
282
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "recursion_contract"
|
|
283
|
+
),
|
|
284
|
+
Operation.QUERY_ROW_VALUE: _row(
|
|
285
|
+
_N, _N, _N, ScalarFamily.BOOLEAN, "row_value_contract"
|
|
286
|
+
),
|
|
287
|
+
Operation.QUERY_CAST: _row(_N, _N, _N, ScalarFamily.STRING, "cast_contract"),
|
|
288
|
+
Operation.QUERY_NULL_ORDERING: _row(
|
|
289
|
+
_N, _E, _N, ScalarFamily.NUMERIC, "null_order_contract"
|
|
290
|
+
),
|
|
291
|
+
Operation.QUERY_RIGHT_JOIN: _row(
|
|
292
|
+
_N, _N, _R, ScalarFamily.NUMERIC, "joined_contract"
|
|
293
|
+
),
|
|
294
|
+
Operation.QUERY_FULL_JOIN: _row(
|
|
295
|
+
_N, _R, _R, ScalarFamily.NUMERIC, "joined_contract"
|
|
296
|
+
),
|
|
297
|
+
Operation.ROUTINE_PROCEDURE: _row(_N, _N, _R, None, "routine_contract"),
|
|
298
|
+
Operation.ROUTINE_FUNCTION: _row(
|
|
299
|
+
_N, _N, _N, ScalarFamily.NUMERIC, "routine_contract"
|
|
300
|
+
),
|
|
301
|
+
Operation.ROUTINE_MANY_RESULT_SETS: _row(_R, _R, _R, None, "routine_contract"),
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
"""Every operation, and how each dialect answers it.
|
|
305
|
+
|
|
306
|
+
Measured against running servers rather than read from documentation. A row
|
|
307
|
+
that says rejected is refused while the query is compiled, so a caller finds
|
|
308
|
+
out while writing rather than in production.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def support_for(operation: Operation, dialect: DatabaseDialect, /) -> Support:
|
|
313
|
+
"""How that dialect answers that operation."""
|
|
314
|
+
return SUPPORT[operation].dialects[dialect]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
__all__ = ("SUPPORT", "Operation", "OperationSupport", "Support", "support_for")
|
pyoq/query/casts.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Asking the database for a value as another type.
|
|
2
|
+
|
|
3
|
+
A cast is the one computed expression that declares what it answers with, so
|
|
4
|
+
it is also the one the row decoder can honour. The Python type given here
|
|
5
|
+
decides three things at once: the SQL each dialect emits, the family the
|
|
6
|
+
expression carries, and what reads the value back.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import date, datetime, time, timedelta
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from pyoq.query.expressions import ComputedExpression
|
|
17
|
+
from pyoq.query.fields import family_for_type
|
|
18
|
+
from pyoq.query.nodes import CastNode
|
|
19
|
+
from pyoq.query.raw import NodeProvider
|
|
20
|
+
from pyoq.schema.models import TypeKind
|
|
21
|
+
|
|
22
|
+
Target = TypeVar("Target")
|
|
23
|
+
|
|
24
|
+
_TARGET_KINDS: dict[type[object], TypeKind] = {
|
|
25
|
+
bool: TypeKind.BOOLEAN,
|
|
26
|
+
int: TypeKind.INTEGER,
|
|
27
|
+
float: TypeKind.DOUBLE,
|
|
28
|
+
Decimal: TypeKind.DECIMAL,
|
|
29
|
+
str: TypeKind.STRING,
|
|
30
|
+
bytes: TypeKind.BINARY,
|
|
31
|
+
date: TypeKind.DATE,
|
|
32
|
+
time: TypeKind.TIME,
|
|
33
|
+
datetime: TypeKind.DATETIME,
|
|
34
|
+
timedelta: TypeKind.INTERVAL,
|
|
35
|
+
UUID: TypeKind.UUID,
|
|
36
|
+
dict: TypeKind.JSON,
|
|
37
|
+
list: TypeKind.JSON,
|
|
38
|
+
}
|
|
39
|
+
"""The database type each Python type asks for, which a dialect then names."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cast(
|
|
43
|
+
expression: NodeProvider,
|
|
44
|
+
target: type[Target],
|
|
45
|
+
/,
|
|
46
|
+
) -> ComputedExpression[Target]:
|
|
47
|
+
"""`expression` as `target`, computed by the database.
|
|
48
|
+
|
|
49
|
+
A target no database type answers to is refused here rather than at
|
|
50
|
+
compilation, so it is reported where it was written.
|
|
51
|
+
"""
|
|
52
|
+
kind_for_target(target)
|
|
53
|
+
return ComputedExpression[Target](
|
|
54
|
+
CastNode(expression.node, target), family_for_type(target)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def kind_for_target(target: type[object], /) -> TypeKind:
|
|
59
|
+
"""The database type a Python type asks for, or a refusal naming it.
|
|
60
|
+
|
|
61
|
+
`datetime` is settled first because it is also a `date`, and asking for
|
|
62
|
+
one is not asking for the other.
|
|
63
|
+
"""
|
|
64
|
+
if target is datetime:
|
|
65
|
+
return TypeKind.DATETIME
|
|
66
|
+
for value_type, kind in _TARGET_KINDS.items():
|
|
67
|
+
if issubclass(target, value_type):
|
|
68
|
+
return kind
|
|
69
|
+
message = f"{getattr(target, '__name__', target)} is not a cast target"
|
|
70
|
+
raise TypeError(message)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ("cast", "kind_for_target")
|