pyoq-sql 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"""Shared scalar expression rendering with dialect hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Generic, TypeAlias, TypeVar
|
|
10
|
+
|
|
11
|
+
from pyoq.config import DatabaseDialect
|
|
12
|
+
from pyoq.errors import CompilationError, UnsupportedQueryError
|
|
13
|
+
from pyoq.query.casts import kind_for_target
|
|
14
|
+
from pyoq.query.execution import CompilationContext
|
|
15
|
+
from pyoq.query.nodes import (
|
|
16
|
+
AggregateName,
|
|
17
|
+
AggregateNode,
|
|
18
|
+
ArrayConstructNode,
|
|
19
|
+
ArrayDimensionsNode,
|
|
20
|
+
ArrayElementNode,
|
|
21
|
+
ArrayLengthNode,
|
|
22
|
+
ArrayNode,
|
|
23
|
+
BinaryNode,
|
|
24
|
+
BinaryOperator,
|
|
25
|
+
BoundValueNode,
|
|
26
|
+
CaseNode,
|
|
27
|
+
CastNode,
|
|
28
|
+
CollectionJoinNode,
|
|
29
|
+
CollectionNode,
|
|
30
|
+
ExistsNode,
|
|
31
|
+
ExpressionNode,
|
|
32
|
+
ExtractNode,
|
|
33
|
+
FieldNode,
|
|
34
|
+
FrameBound,
|
|
35
|
+
FrameBoundNode,
|
|
36
|
+
FrameExclusion,
|
|
37
|
+
FrameKind,
|
|
38
|
+
FrameNode,
|
|
39
|
+
FunctionName,
|
|
40
|
+
FunctionNode,
|
|
41
|
+
JsonBuild,
|
|
42
|
+
JsonBuildNode,
|
|
43
|
+
JsonContainsNode,
|
|
44
|
+
JsonKeysNode,
|
|
45
|
+
JsonNode,
|
|
46
|
+
JsonWriteNode,
|
|
47
|
+
RawNode,
|
|
48
|
+
RowValueNode,
|
|
49
|
+
ScalarFamily,
|
|
50
|
+
UnaryNode,
|
|
51
|
+
UnaryOperator,
|
|
52
|
+
VariadicNode,
|
|
53
|
+
VariadicOperator,
|
|
54
|
+
VendorFunctionNode,
|
|
55
|
+
WindowFunctionNode,
|
|
56
|
+
WindowNode,
|
|
57
|
+
WindowReferenceNode,
|
|
58
|
+
WindowSpecificationNode,
|
|
59
|
+
)
|
|
60
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
61
|
+
from pyoq.query.rendering.sources import OrderTextRenderer, QueryTextRenderer
|
|
62
|
+
from pyoq.query.select_nodes import JsonElementsSourceNode, OrderNode
|
|
63
|
+
from pyoq.query.vendor import PLAIN_NAME
|
|
64
|
+
from pyoq.schema.models import TypeKind
|
|
65
|
+
|
|
66
|
+
ContextType = TypeVar("ContextType", bound=CompilationContext)
|
|
67
|
+
|
|
68
|
+
_UNSAFE_RAW_TEXT = re.compile(r"\?|;|--|/\*|\*/|(?<![A-Za-z0-9_])[:@$][A-Za-z_]")
|
|
69
|
+
|
|
70
|
+
_UNARY_SUFFIX = {
|
|
71
|
+
UnaryOperator.IS_NULL: "IS NULL",
|
|
72
|
+
UnaryOperator.IS_NOT_NULL: "IS NOT NULL",
|
|
73
|
+
UnaryOperator.IS_TRUE: "IS TRUE",
|
|
74
|
+
UnaryOperator.IS_FALSE: "IS FALSE",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_STANDARD_BINARY_SQL = {
|
|
78
|
+
BinaryOperator.EQUAL: "=",
|
|
79
|
+
BinaryOperator.NOT_EQUAL: "<>",
|
|
80
|
+
BinaryOperator.LESS_THAN: "<",
|
|
81
|
+
BinaryOperator.LESS_OR_EQUAL: "<=",
|
|
82
|
+
BinaryOperator.GREATER_THAN: ">",
|
|
83
|
+
BinaryOperator.GREATER_OR_EQUAL: ">=",
|
|
84
|
+
BinaryOperator.ADD: "+",
|
|
85
|
+
BinaryOperator.SUBTRACT: "-",
|
|
86
|
+
BinaryOperator.MULTIPLY: "*",
|
|
87
|
+
BinaryOperator.DIVIDE: "/",
|
|
88
|
+
BinaryOperator.MODULO: "%",
|
|
89
|
+
BinaryOperator.CONCAT: "||",
|
|
90
|
+
BinaryOperator.LIKE: "LIKE",
|
|
91
|
+
BinaryOperator.NOT_LIKE: "NOT LIKE",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_AGGREGATE_SQL = {
|
|
95
|
+
AggregateName.COUNT: "COUNT",
|
|
96
|
+
AggregateName.SUM: "SUM",
|
|
97
|
+
AggregateName.AVERAGE: "AVG",
|
|
98
|
+
AggregateName.MINIMUM: "MIN",
|
|
99
|
+
AggregateName.MAXIMUM: "MAX",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_FRAME_KINDS: Mapping[FrameKind, str] = {
|
|
103
|
+
FrameKind.ROWS: "ROWS",
|
|
104
|
+
FrameKind.RANGE: "RANGE",
|
|
105
|
+
FrameKind.GROUPS: "GROUPS",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_FRAME_BOUNDS: Mapping[FrameBound, str] = {
|
|
109
|
+
FrameBound.UNBOUNDED_PRECEDING: "UNBOUNDED PRECEDING",
|
|
110
|
+
FrameBound.PRECEDING: "PRECEDING",
|
|
111
|
+
FrameBound.CURRENT_ROW: "CURRENT ROW",
|
|
112
|
+
FrameBound.FOLLOWING: "FOLLOWING",
|
|
113
|
+
FrameBound.UNBOUNDED_FOLLOWING: "UNBOUNDED FOLLOWING",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_FRAME_EXCLUSIONS: Mapping[FrameExclusion, str] = {
|
|
117
|
+
FrameExclusion.CURRENT_ROW: "CURRENT ROW",
|
|
118
|
+
FrameExclusion.GROUP: "GROUP",
|
|
119
|
+
FrameExclusion.TIES: "TIES",
|
|
120
|
+
FrameExclusion.NO_OTHERS: "NO OTHERS",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_STANDARD_CAST_SQL: Mapping[TypeKind, str] = {
|
|
124
|
+
TypeKind.BOOLEAN: "BOOLEAN",
|
|
125
|
+
TypeKind.SMALL_INTEGER: "SMALLINT",
|
|
126
|
+
TypeKind.INTEGER: "INTEGER",
|
|
127
|
+
TypeKind.BIG_INTEGER: "BIGINT",
|
|
128
|
+
TypeKind.DECIMAL: "NUMERIC",
|
|
129
|
+
TypeKind.REAL: "REAL",
|
|
130
|
+
TypeKind.DOUBLE: "DOUBLE PRECISION",
|
|
131
|
+
TypeKind.STRING: "TEXT",
|
|
132
|
+
TypeKind.BINARY: "BYTEA",
|
|
133
|
+
TypeKind.DATE: "DATE",
|
|
134
|
+
TypeKind.TIME: "TIME",
|
|
135
|
+
TypeKind.DATETIME: "TIMESTAMP",
|
|
136
|
+
TypeKind.INTERVAL: "INTERVAL",
|
|
137
|
+
TypeKind.UUID: "UUID",
|
|
138
|
+
TypeKind.JSON: "JSONB",
|
|
139
|
+
}
|
|
140
|
+
"""Verified against a running PostgreSQL server, whose names these are."""
|
|
141
|
+
|
|
142
|
+
_STANDARD_CHOOSING_SQL: Mapping[FunctionName, str] = {
|
|
143
|
+
FunctionName.COALESCE: "COALESCE",
|
|
144
|
+
FunctionName.NULLIF: "NULLIF",
|
|
145
|
+
FunctionName.GREATEST: "GREATEST",
|
|
146
|
+
FunctionName.LEAST: "LEAST",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_CHOOSING_FUNCTIONS = frozenset(
|
|
150
|
+
{
|
|
151
|
+
FunctionName.COALESCE,
|
|
152
|
+
FunctionName.NULLIF,
|
|
153
|
+
FunctionName.GREATEST,
|
|
154
|
+
FunctionName.LEAST,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
"""Functions that take a list and answer with one of it."""
|
|
158
|
+
|
|
159
|
+
_SCALAR_TRANSFORMS = frozenset(
|
|
160
|
+
{FunctionName.LOWER, FunctionName.UPPER, FunctionName.LENGTH}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True, slots=True)
|
|
165
|
+
class CollectionFunctions:
|
|
166
|
+
"""How one dialect gathers rows into a single value, and builds one."""
|
|
167
|
+
|
|
168
|
+
array_function: str
|
|
169
|
+
object_function: str
|
|
170
|
+
empty_array: str
|
|
171
|
+
array_constructor: str
|
|
172
|
+
object_aggregate: str
|
|
173
|
+
ordered: bool = True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_Call: TypeAlias = (
|
|
177
|
+
CaseNode
|
|
178
|
+
| CastNode
|
|
179
|
+
| WindowNode
|
|
180
|
+
| WindowFunctionNode
|
|
181
|
+
| ExistsNode
|
|
182
|
+
| VendorFunctionNode
|
|
183
|
+
| ArrayNode
|
|
184
|
+
| ArrayElementNode
|
|
185
|
+
| ArrayLengthNode
|
|
186
|
+
| ArrayConstructNode
|
|
187
|
+
| ArrayDimensionsNode
|
|
188
|
+
| JsonContainsNode
|
|
189
|
+
| JsonWriteNode
|
|
190
|
+
| JsonNode
|
|
191
|
+
| JsonBuildNode
|
|
192
|
+
| JsonKeysNode
|
|
193
|
+
| RowValueNode
|
|
194
|
+
| FunctionNode
|
|
195
|
+
| ExtractNode
|
|
196
|
+
| AggregateNode
|
|
197
|
+
| CollectionNode
|
|
198
|
+
| RawNode
|
|
199
|
+
)
|
|
200
|
+
"""Everything not a leaf and not an operator, which is everything named."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
_KEPT = "pyoq_kept"
|
|
204
|
+
"""What a limited collection calls the rows its order chose."""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _kept_entry(position: int) -> str:
|
|
208
|
+
"""What one value a row carries is called inside that set."""
|
|
209
|
+
return f"pyoq_value_{position}"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _kept_order(position: int) -> str:
|
|
213
|
+
"""What one of the keys it is ordered by is called inside that set."""
|
|
214
|
+
return f"pyoq_order_{position}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _text_literal(value: str) -> str:
|
|
218
|
+
return "'" + value.replace("'", "''") + "'"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ExpressionRenderer(ABC, Generic[ContextType]):
|
|
222
|
+
__slots__ = ("_context", "_declared_windows", "_render_order", "_render_query")
|
|
223
|
+
_context: ContextType
|
|
224
|
+
_declared_windows: frozenset[str]
|
|
225
|
+
_render_order: OrderTextRenderer | None
|
|
226
|
+
_render_query: QueryTextRenderer | None
|
|
227
|
+
|
|
228
|
+
def __init__(self, context: ContextType) -> None:
|
|
229
|
+
self._context = context
|
|
230
|
+
self._render_order = None
|
|
231
|
+
self._render_query = None
|
|
232
|
+
self._declared_windows = frozenset()
|
|
233
|
+
|
|
234
|
+
def declares_windows(self, names: frozenset[str], /) -> frozenset[str]:
|
|
235
|
+
"""The windows named by the query being written, and the ones before it.
|
|
236
|
+
|
|
237
|
+
A subquery declares its own, so the caller puts back what it replaced
|
|
238
|
+
once that query has been written.
|
|
239
|
+
"""
|
|
240
|
+
previous = self._declared_windows
|
|
241
|
+
self._declared_windows = names
|
|
242
|
+
return previous
|
|
243
|
+
|
|
244
|
+
def orders_rows_with(self, render_order: OrderTextRenderer, /) -> None:
|
|
245
|
+
"""How to write one term of an ORDER BY, direction and nulls included.
|
|
246
|
+
|
|
247
|
+
A window orders rows the way a query does, and only one place knows
|
|
248
|
+
what a dialect accepts there, so it hands the answer back rather than
|
|
249
|
+
being reached for.
|
|
250
|
+
"""
|
|
251
|
+
self._render_order = render_order
|
|
252
|
+
|
|
253
|
+
def reads_queries_with(self, render_query: QueryTextRenderer, /) -> None:
|
|
254
|
+
"""How to write a query that appears inside an expression.
|
|
255
|
+
|
|
256
|
+
Only the query renderer knows how, and it is built from this one, so
|
|
257
|
+
it hands the answer back rather than being reached for.
|
|
258
|
+
"""
|
|
259
|
+
self._render_query = render_query
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
@abstractmethod
|
|
263
|
+
def dialect(self) -> str: ...
|
|
264
|
+
|
|
265
|
+
@abstractmethod
|
|
266
|
+
def qualified_field(self, node: FieldNode) -> str: ...
|
|
267
|
+
|
|
268
|
+
@abstractmethod
|
|
269
|
+
def render_extract(self, node: ExtractNode) -> str: ...
|
|
270
|
+
|
|
271
|
+
@abstractmethod
|
|
272
|
+
def render_string_predicate(self, node: FunctionNode) -> str: ...
|
|
273
|
+
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def require_raw_expressions(self) -> None: ...
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
@abstractmethod
|
|
279
|
+
def collection_functions(self) -> CollectionFunctions:
|
|
280
|
+
"""How this dialect gathers rows into one value."""
|
|
281
|
+
|
|
282
|
+
def qualified_table(self, node: CollectionNode) -> str:
|
|
283
|
+
"""Name the table a collection reads, and the name it answers to.
|
|
284
|
+
|
|
285
|
+
A collection may read the table its parent is already reading, which
|
|
286
|
+
is what a relation to its own table is, so the alias is what keeps the
|
|
287
|
+
correlation naming two different things.
|
|
288
|
+
"""
|
|
289
|
+
written = self.quoting.qualified(
|
|
290
|
+
node.table, schema=node.schema, catalog=node.catalog
|
|
291
|
+
)
|
|
292
|
+
if node.alias is None:
|
|
293
|
+
return written
|
|
294
|
+
return f"{written} AS {self.quoting.quote(node.alias)}"
|
|
295
|
+
|
|
296
|
+
def qualified_join(self, join: CollectionJoinNode) -> str:
|
|
297
|
+
"""Name a table a collection reads alongside its own.
|
|
298
|
+
|
|
299
|
+
Quoted the same way any other name is, and aliased where the join says
|
|
300
|
+
so, because a collection may read a table it has already read.
|
|
301
|
+
"""
|
|
302
|
+
written = self.quoting.qualified(
|
|
303
|
+
join.table, schema=join.schema, catalog=join.catalog
|
|
304
|
+
)
|
|
305
|
+
if join.alias is None:
|
|
306
|
+
return written
|
|
307
|
+
return f"{written} AS {self.quoting.quote(join.alias)}"
|
|
308
|
+
|
|
309
|
+
@abstractmethod
|
|
310
|
+
def validate_binary(self, node: BinaryNode) -> None: ...
|
|
311
|
+
|
|
312
|
+
def binary_operator_sql(self, operator: BinaryOperator) -> str | None:
|
|
313
|
+
return _STANDARD_BINARY_SQL.get(operator)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def cast_type_names(self) -> Mapping[TypeKind, str]:
|
|
317
|
+
"""What this dialect calls each type a cast can ask for."""
|
|
318
|
+
return _STANDARD_CAST_SQL
|
|
319
|
+
|
|
320
|
+
def cast_type_sql(self, value_type: type[object]) -> str:
|
|
321
|
+
"""How to name a cast target, or a refusal when there is no way to.
|
|
322
|
+
|
|
323
|
+
A dialect that has no such type must say so. Emitting a name it
|
|
324
|
+
merely tolerates would answer with a value of the wrong type and call
|
|
325
|
+
it the requested one.
|
|
326
|
+
"""
|
|
327
|
+
kind = kind_for_target(value_type)
|
|
328
|
+
name = self.cast_type_names.get(kind)
|
|
329
|
+
if name is None:
|
|
330
|
+
message = (
|
|
331
|
+
f"{self.dialect} cannot cast to {kind.value}, "
|
|
332
|
+
f"because it has no such type"
|
|
333
|
+
)
|
|
334
|
+
raise UnsupportedQueryError(message)
|
|
335
|
+
return name
|
|
336
|
+
|
|
337
|
+
def require_window_functions(self) -> None:
|
|
338
|
+
"""Whether this dialect can measure a row against its neighbours."""
|
|
339
|
+
|
|
340
|
+
def _compile_window_function(self, node: WindowFunctionNode) -> str:
|
|
341
|
+
arguments = ", ".join(self.compile(argument) for argument in node.arguments)
|
|
342
|
+
return f"{node.name.value.replace('-', '_').upper()}({arguments})"
|
|
343
|
+
|
|
344
|
+
def _compile_window(self, node: WindowNode) -> str:
|
|
345
|
+
self.require_window_functions()
|
|
346
|
+
looked_through = node.window
|
|
347
|
+
if isinstance(looked_through, WindowReferenceNode):
|
|
348
|
+
self._require_declared(looked_through.name)
|
|
349
|
+
return f"{self.compile(node.expression)} OVER {looked_through.name}"
|
|
350
|
+
specification = self.compile_window_specification(looked_through)
|
|
351
|
+
return f"{self.compile(node.expression)} OVER ({specification})"
|
|
352
|
+
|
|
353
|
+
def _require_declared(self, name: str) -> None:
|
|
354
|
+
if name not in self._declared_windows:
|
|
355
|
+
message = f"window {name} is read through but the query does not declare it"
|
|
356
|
+
raise CompilationError(message)
|
|
357
|
+
|
|
358
|
+
def compile_window_specification(self, node: WindowSpecificationNode) -> str:
|
|
359
|
+
"""How a window divides and orders the rows it covers."""
|
|
360
|
+
clauses: list[str] = []
|
|
361
|
+
if node.partition_by:
|
|
362
|
+
partitions = ", ".join(
|
|
363
|
+
self.compile(expression) for expression in node.partition_by
|
|
364
|
+
)
|
|
365
|
+
clauses.append(f"PARTITION BY {partitions}")
|
|
366
|
+
if node.order_by:
|
|
367
|
+
clauses.append(f"ORDER BY {self._ordered_rows(node.order_by)}")
|
|
368
|
+
if node.frame is not None:
|
|
369
|
+
clauses.append(self._compile_frame(node.frame))
|
|
370
|
+
return " ".join(clauses)
|
|
371
|
+
|
|
372
|
+
def _compile_frame(self, node: FrameNode) -> str:
|
|
373
|
+
"""The rows a window function reads, where this dialect can narrow them."""
|
|
374
|
+
self.require_frame(node)
|
|
375
|
+
start = self._frame_bound(node.start)
|
|
376
|
+
if node.end is None:
|
|
377
|
+
written = f"{_FRAME_KINDS[node.kind]} {start}"
|
|
378
|
+
else:
|
|
379
|
+
written = (
|
|
380
|
+
f"{_FRAME_KINDS[node.kind]} BETWEEN {start} "
|
|
381
|
+
f"AND {self._frame_bound(node.end)}"
|
|
382
|
+
)
|
|
383
|
+
if node.exclusion is None:
|
|
384
|
+
return written
|
|
385
|
+
return f"{written} EXCLUDE {_FRAME_EXCLUSIONS[node.exclusion]}"
|
|
386
|
+
|
|
387
|
+
def _frame_bound(self, node: FrameBoundNode) -> str:
|
|
388
|
+
written = _FRAME_BOUNDS[node.bound]
|
|
389
|
+
if node.offset is None:
|
|
390
|
+
return written
|
|
391
|
+
return f"{self.compile(node.offset)} {written}"
|
|
392
|
+
|
|
393
|
+
def require_frame(self, node: FrameNode) -> None:
|
|
394
|
+
"""Whether this dialect can narrow a window the way it was asked to."""
|
|
395
|
+
|
|
396
|
+
def _ordered_rows(self, orders: tuple[OrderNode, ...]) -> str:
|
|
397
|
+
if self._render_order is None:
|
|
398
|
+
message = f"{self.dialect} cannot order the rows a window covers"
|
|
399
|
+
raise CompilationError(message)
|
|
400
|
+
render_order = self._render_order
|
|
401
|
+
return ", ".join(render_order(order) for order in orders)
|
|
402
|
+
|
|
403
|
+
def _compile_cast(self, node: CastNode) -> str:
|
|
404
|
+
target = self.cast_type_sql(node.value_type)
|
|
405
|
+
return f"CAST({self.compile(node.expression)} AS {target})"
|
|
406
|
+
|
|
407
|
+
def choosing_function_sql(self, name: FunctionName) -> str:
|
|
408
|
+
"""What this dialect calls the function that picks one of its arguments."""
|
|
409
|
+
return _STANDARD_CHOOSING_SQL[name]
|
|
410
|
+
|
|
411
|
+
def compile(self, node: ExpressionNode) -> str:
|
|
412
|
+
"""Whatever this node is, written the way this dialect writes it."""
|
|
413
|
+
if isinstance(node, (BoundValueNode, FieldNode)):
|
|
414
|
+
return self._compile_leaf(node)
|
|
415
|
+
if isinstance(node, (UnaryNode, BinaryNode, VariadicNode)):
|
|
416
|
+
return self._compile_operator(node)
|
|
417
|
+
return self._compile_call(node)
|
|
418
|
+
|
|
419
|
+
def _compile_leaf(self, node: BoundValueNode | FieldNode) -> str:
|
|
420
|
+
"""A value or a column, which nothing inside has to be written first."""
|
|
421
|
+
if isinstance(node, BoundValueNode):
|
|
422
|
+
return self._context.bind(node)
|
|
423
|
+
return self.qualified_field(node)
|
|
424
|
+
|
|
425
|
+
def _compile_operator(
|
|
426
|
+
self,
|
|
427
|
+
node: UnaryNode | BinaryNode | VariadicNode,
|
|
428
|
+
) -> str:
|
|
429
|
+
"""The nodes written as operators between or before their operands."""
|
|
430
|
+
if isinstance(node, UnaryNode):
|
|
431
|
+
return self._compile_unary(node)
|
|
432
|
+
if isinstance(node, BinaryNode):
|
|
433
|
+
return self._compile_binary(node)
|
|
434
|
+
return self._compile_variadic(node)
|
|
435
|
+
|
|
436
|
+
def _compile_call(self, node: _Call) -> str:
|
|
437
|
+
"""The nodes written as a name, a bracket, or a query of their own."""
|
|
438
|
+
if isinstance(node, CaseNode):
|
|
439
|
+
return self._compile_case(node)
|
|
440
|
+
if isinstance(node, CastNode):
|
|
441
|
+
return self._compile_cast(node)
|
|
442
|
+
if isinstance(node, (WindowNode, WindowFunctionNode)):
|
|
443
|
+
return self._compile_windowed(node)
|
|
444
|
+
if isinstance(node, ExistsNode):
|
|
445
|
+
return self._compile_exists(node)
|
|
446
|
+
if isinstance(node, RowValueNode):
|
|
447
|
+
return self._compile_row_value(node)
|
|
448
|
+
if isinstance(node, VendorFunctionNode):
|
|
449
|
+
return self._compile_vendor_function(node)
|
|
450
|
+
if isinstance(
|
|
451
|
+
node,
|
|
452
|
+
(
|
|
453
|
+
ArrayNode,
|
|
454
|
+
ArrayElementNode,
|
|
455
|
+
ArrayLengthNode,
|
|
456
|
+
ArrayConstructNode,
|
|
457
|
+
ArrayDimensionsNode,
|
|
458
|
+
),
|
|
459
|
+
):
|
|
460
|
+
return self._compile_array(node)
|
|
461
|
+
if isinstance(node, JsonBuildNode):
|
|
462
|
+
return self.render_json_build(node)
|
|
463
|
+
if isinstance(node, (JsonNode, JsonContainsNode, JsonWriteNode, JsonKeysNode)):
|
|
464
|
+
return self._compile_json(node)
|
|
465
|
+
if isinstance(node, FunctionNode):
|
|
466
|
+
return self._compile_function(node)
|
|
467
|
+
if isinstance(node, ExtractNode):
|
|
468
|
+
return self.render_extract(node)
|
|
469
|
+
if isinstance(node, AggregateNode):
|
|
470
|
+
return self._compile_aggregate(node)
|
|
471
|
+
if isinstance(node, CollectionNode):
|
|
472
|
+
return self._compile_collection(node)
|
|
473
|
+
return self._compile_raw(node)
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
@abstractmethod
|
|
477
|
+
def quoting(self) -> IdentifierQuoting:
|
|
478
|
+
"""How this dialect writes a name that has to be written out."""
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
@abstractmethod
|
|
482
|
+
def database_dialect(self) -> DatabaseDialect:
|
|
483
|
+
"""Which database this writes for, as the configuration names it."""
|
|
484
|
+
|
|
485
|
+
def routine_name(
|
|
486
|
+
self,
|
|
487
|
+
name: str,
|
|
488
|
+
schema: str | None = None,
|
|
489
|
+
catalog: str | None = None,
|
|
490
|
+
) -> str:
|
|
491
|
+
"""A routine's name as written, or quoted when it is not plain.
|
|
492
|
+
|
|
493
|
+
A name a database reads as written is left alone, so a call reads the
|
|
494
|
+
way it would be typed, and a built-in keeps the spelling the database
|
|
495
|
+
knows it by. Anything else is quoted, which is what makes a name with a
|
|
496
|
+
space usable and a name with a quote in it harmless.
|
|
497
|
+
|
|
498
|
+
Where the routine says which schema it is kept in, each part is quoted
|
|
499
|
+
as itself, because a schema may hold a dot and a flattened name cannot
|
|
500
|
+
say where one part ends.
|
|
501
|
+
"""
|
|
502
|
+
if schema is None and catalog is None:
|
|
503
|
+
if PLAIN_NAME.match(name):
|
|
504
|
+
return name
|
|
505
|
+
return ".".join(self.quoting.quote(part) for part in name.split("."))
|
|
506
|
+
return self.quoting.qualified(name, schema=schema, catalog=catalog)
|
|
507
|
+
|
|
508
|
+
def _compile_vendor_function(self, node: VendorFunctionNode) -> str:
|
|
509
|
+
"""A function this database has, refused where it has not got it.
|
|
510
|
+
|
|
511
|
+
A caller who said which databases a function exists on is told here
|
|
512
|
+
rather than by the server, because a query that cannot run is better
|
|
513
|
+
found while it is being written.
|
|
514
|
+
"""
|
|
515
|
+
if node.dialects and self.database_dialect not in node.dialects:
|
|
516
|
+
named = ", ".join(sorted(node.dialects))
|
|
517
|
+
message = (
|
|
518
|
+
f"{node.name} was declared for {named}, and this is "
|
|
519
|
+
f"{self.database_dialect.value}"
|
|
520
|
+
)
|
|
521
|
+
raise UnsupportedQueryError(message)
|
|
522
|
+
arguments = ", ".join(self.compile(argument) for argument in node.arguments)
|
|
523
|
+
written = self.routine_name(node.name, node.schema, node.catalog)
|
|
524
|
+
return f"{written}({arguments})"
|
|
525
|
+
|
|
526
|
+
def _compile_json(
|
|
527
|
+
self,
|
|
528
|
+
node: JsonNode | JsonContainsNode | JsonWriteNode | JsonKeysNode,
|
|
529
|
+
) -> str:
|
|
530
|
+
if isinstance(node, JsonNode):
|
|
531
|
+
return self.render_json(node)
|
|
532
|
+
if isinstance(node, JsonContainsNode):
|
|
533
|
+
return self.render_json_contains(node)
|
|
534
|
+
if isinstance(node, JsonKeysNode):
|
|
535
|
+
return self.render_json_keys(node)
|
|
536
|
+
return self.render_json_write(node)
|
|
537
|
+
|
|
538
|
+
def _compile_array(
|
|
539
|
+
self,
|
|
540
|
+
node: (
|
|
541
|
+
ArrayNode
|
|
542
|
+
| ArrayElementNode
|
|
543
|
+
| ArrayLengthNode
|
|
544
|
+
| ArrayConstructNode
|
|
545
|
+
| ArrayDimensionsNode
|
|
546
|
+
),
|
|
547
|
+
) -> str:
|
|
548
|
+
if isinstance(node, ArrayNode):
|
|
549
|
+
return self.render_array(node)
|
|
550
|
+
if isinstance(node, ArrayElementNode):
|
|
551
|
+
return self.render_array_element(node)
|
|
552
|
+
if isinstance(node, ArrayConstructNode):
|
|
553
|
+
return self.render_array_construct(node)
|
|
554
|
+
if isinstance(node, ArrayDimensionsNode):
|
|
555
|
+
return self.render_array_dimensions(node)
|
|
556
|
+
return self.render_array_length(node)
|
|
557
|
+
|
|
558
|
+
def render_array(self, node: ArrayNode) -> str:
|
|
559
|
+
"""An array asked about, where this dialect has arrays."""
|
|
560
|
+
del node
|
|
561
|
+
raise UnsupportedQueryError(self._no_arrays())
|
|
562
|
+
|
|
563
|
+
def render_array_element(self, node: ArrayElementNode) -> str:
|
|
564
|
+
"""One element of an array, where this dialect has arrays."""
|
|
565
|
+
del node
|
|
566
|
+
raise UnsupportedQueryError(self._no_arrays())
|
|
567
|
+
|
|
568
|
+
def render_array_length(self, node: ArrayLengthNode) -> str:
|
|
569
|
+
"""How many elements an array holds, where this dialect has arrays."""
|
|
570
|
+
del node
|
|
571
|
+
raise UnsupportedQueryError(self._no_arrays())
|
|
572
|
+
|
|
573
|
+
def render_array_construct(self, node: ArrayConstructNode) -> str:
|
|
574
|
+
"""An array built from values, where this dialect has arrays."""
|
|
575
|
+
del node
|
|
576
|
+
raise UnsupportedQueryError(self._no_arrays())
|
|
577
|
+
|
|
578
|
+
def render_array_dimensions(self, node: ArrayDimensionsNode) -> str:
|
|
579
|
+
"""How many dimensions an array has, where this dialect has arrays."""
|
|
580
|
+
del node
|
|
581
|
+
raise UnsupportedQueryError(self._no_arrays())
|
|
582
|
+
|
|
583
|
+
def _no_arrays(self) -> str:
|
|
584
|
+
return (
|
|
585
|
+
f"{self.dialect} has no array type; hold a collection as JSON "
|
|
586
|
+
f"or in a table of its own"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
@abstractmethod
|
|
590
|
+
def render_json_elements(self, node: JsonElementsSourceNode) -> str:
|
|
591
|
+
"""A JSON array read as rows, which every dialect spells its own way."""
|
|
592
|
+
|
|
593
|
+
@abstractmethod
|
|
594
|
+
def render_json_keys(self, node: JsonKeysNode) -> str:
|
|
595
|
+
"""The members an object has, as a JSON array of their names."""
|
|
596
|
+
|
|
597
|
+
@abstractmethod
|
|
598
|
+
def render_json(self, node: JsonNode) -> str:
|
|
599
|
+
"""How this dialect reads inside a JSON value.
|
|
600
|
+
|
|
601
|
+
There is no shared spelling to fall back on: PostgreSQL takes a path
|
|
602
|
+
as a list of steps, while SQLite and MySQL take one written string,
|
|
603
|
+
and PostgreSQL reads that written string as a member name that is
|
|
604
|
+
simply not there. So every dialect answers this itself.
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
@abstractmethod
|
|
608
|
+
def render_json_write(self, node: JsonWriteNode) -> str:
|
|
609
|
+
"""How this dialect writes a value into a JSON document."""
|
|
610
|
+
|
|
611
|
+
@abstractmethod
|
|
612
|
+
def render_json_contains(self, node: JsonContainsNode) -> str:
|
|
613
|
+
"""Whether one JSON value holds another, where a dialect can ask."""
|
|
614
|
+
|
|
615
|
+
def json_path_text(self, path: tuple[str | int, ...]) -> str:
|
|
616
|
+
"""A path as SQLite and MySQL write one, bound rather than written in."""
|
|
617
|
+
steps = "".join(
|
|
618
|
+
f"[{step}]" if isinstance(step, int) else f".{step}" for step in path
|
|
619
|
+
)
|
|
620
|
+
return self._context.bind(BoundValueNode(f"${steps}", ScalarFamily.STRING))
|
|
621
|
+
|
|
622
|
+
def _compile_row_value(self, node: RowValueNode) -> str:
|
|
623
|
+
values = ", ".join(self.compile(expression) for expression in node.expressions)
|
|
624
|
+
return f"({values})"
|
|
625
|
+
|
|
626
|
+
def _compile_windowed(self, node: WindowNode | WindowFunctionNode) -> str:
|
|
627
|
+
if isinstance(node, WindowNode):
|
|
628
|
+
return self._compile_window(node)
|
|
629
|
+
return self._compile_window_function(node)
|
|
630
|
+
|
|
631
|
+
def _compile_collection(self, node: CollectionNode) -> str:
|
|
632
|
+
"""Render a correlated collection as one value beside its parent.
|
|
633
|
+
|
|
634
|
+
The empty case is coalesced rather than left null, because a relation
|
|
635
|
+
with no rows is an empty collection and a caller should not have to tell
|
|
636
|
+
the difference from a fetch that did not happen.
|
|
637
|
+
"""
|
|
638
|
+
functions = self.collection_functions
|
|
639
|
+
self._require_collection_order(node, functions)
|
|
640
|
+
if node.limit is not None:
|
|
641
|
+
return self._compile_limited_collection(node, functions)
|
|
642
|
+
aggregate = (
|
|
643
|
+
f"{functions.array_function}"
|
|
644
|
+
f"({self._collection_row(node)}{self._collection_order(node)})"
|
|
645
|
+
)
|
|
646
|
+
return "".join(
|
|
647
|
+
(
|
|
648
|
+
"(SELECT COALESCE(",
|
|
649
|
+
aggregate,
|
|
650
|
+
", ",
|
|
651
|
+
functions.empty_array,
|
|
652
|
+
") FROM ",
|
|
653
|
+
self._collection_reading(node),
|
|
654
|
+
" WHERE ",
|
|
655
|
+
self.compile(node.condition),
|
|
656
|
+
")",
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def _compile_limited_collection(
|
|
661
|
+
self,
|
|
662
|
+
node: CollectionNode,
|
|
663
|
+
functions: CollectionFunctions,
|
|
664
|
+
) -> str:
|
|
665
|
+
"""A collection of only the rows its order put first.
|
|
666
|
+
|
|
667
|
+
The derived table answers with the values each row carries rather than
|
|
668
|
+
with the row already built, for two reasons. A join's columns are not
|
|
669
|
+
reachable through the one name a derived table answers under. And
|
|
670
|
+
SQLite loses the JSON subtype of a value that passes through one, so an
|
|
671
|
+
object built inside comes back quoted as text rather than as an object.
|
|
672
|
+
"""
|
|
673
|
+
kept = self.quoting.quote(_KEPT)
|
|
674
|
+
carried = ", ".join(
|
|
675
|
+
(
|
|
676
|
+
*(
|
|
677
|
+
f"{self.compile(entry.expression)} AS "
|
|
678
|
+
f"{self.quoting.quote(_kept_entry(position))}"
|
|
679
|
+
for position, entry in enumerate(node.entries)
|
|
680
|
+
),
|
|
681
|
+
*(
|
|
682
|
+
f"{self.compile(order.expression)} AS "
|
|
683
|
+
f"{self.quoting.quote(_kept_order(position))}"
|
|
684
|
+
for position, order in enumerate(node.orders)
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
inner = "".join(
|
|
689
|
+
(
|
|
690
|
+
"SELECT ",
|
|
691
|
+
carried,
|
|
692
|
+
" FROM ",
|
|
693
|
+
self._collection_reading(node),
|
|
694
|
+
" WHERE ",
|
|
695
|
+
self.compile(node.condition),
|
|
696
|
+
self._collection_order(node),
|
|
697
|
+
f" LIMIT {node.limit:d}",
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
pairs = ", ".join(
|
|
701
|
+
f"{_text_literal(entry.key)}, "
|
|
702
|
+
f"{kept}.{self.quoting.quote(_kept_entry(position))}"
|
|
703
|
+
for position, entry in enumerate(node.entries)
|
|
704
|
+
)
|
|
705
|
+
ordering = ", ".join(
|
|
706
|
+
f"{kept}.{self.quoting.quote(_kept_order(position))}"
|
|
707
|
+
f"{' DESC' if order.descending else ''}"
|
|
708
|
+
for position, order in enumerate(node.orders)
|
|
709
|
+
)
|
|
710
|
+
row = f"{functions.object_function}({pairs})"
|
|
711
|
+
aggregate = f"{functions.array_function}({row} ORDER BY {ordering})"
|
|
712
|
+
return "".join(
|
|
713
|
+
(
|
|
714
|
+
"(SELECT COALESCE(",
|
|
715
|
+
aggregate,
|
|
716
|
+
", ",
|
|
717
|
+
functions.empty_array,
|
|
718
|
+
") FROM (",
|
|
719
|
+
inner,
|
|
720
|
+
f") AS {kept})",
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def _collection_row(self, node: CollectionNode) -> str:
|
|
725
|
+
"""One row of the collection, as the object it is gathered into."""
|
|
726
|
+
pairs = ", ".join(
|
|
727
|
+
f"{_text_literal(entry.key)}, {self.compile(entry.expression)}"
|
|
728
|
+
for entry in node.entries
|
|
729
|
+
)
|
|
730
|
+
return f"{self.collection_functions.object_function}({pairs})"
|
|
731
|
+
|
|
732
|
+
def _collection_reading(self, node: CollectionNode) -> str:
|
|
733
|
+
"""The table a collection reads, and whatever it reads alongside it.
|
|
734
|
+
|
|
735
|
+
Every join is a left join. A join widens what each row of a collection
|
|
736
|
+
carries and does not decide which rows there are, and an inner one
|
|
737
|
+
would drop a row whose join matched nothing: a null foreign key, or a
|
|
738
|
+
row pointing at one that is not there. The row is the collection's
|
|
739
|
+
answer, so it stays and the value it could not reach reads as null.
|
|
740
|
+
"""
|
|
741
|
+
joined = "".join(
|
|
742
|
+
f" LEFT JOIN {self.qualified_join(join)} ON {self.compile(join.condition)}"
|
|
743
|
+
for join in node.joins
|
|
744
|
+
)
|
|
745
|
+
return f"{self.qualified_table(node)}{joined}"
|
|
746
|
+
|
|
747
|
+
def _collection_order(self, node: CollectionNode) -> str:
|
|
748
|
+
if not node.orders:
|
|
749
|
+
return ""
|
|
750
|
+
terms = ", ".join(
|
|
751
|
+
f"{self.compile(order.expression)}{' DESC' if order.descending else ''}"
|
|
752
|
+
for order in node.orders
|
|
753
|
+
)
|
|
754
|
+
return f" ORDER BY {terms}"
|
|
755
|
+
|
|
756
|
+
def _require_collection_order(
|
|
757
|
+
self,
|
|
758
|
+
node: CollectionNode,
|
|
759
|
+
functions: CollectionFunctions,
|
|
760
|
+
) -> None:
|
|
761
|
+
if not node.orders or functions.ordered:
|
|
762
|
+
return
|
|
763
|
+
message = (
|
|
764
|
+
f"{self.dialect} cannot order rows inside a collection; fetch this "
|
|
765
|
+
f"relation with select-in instead"
|
|
766
|
+
)
|
|
767
|
+
raise UnsupportedQueryError(message)
|
|
768
|
+
|
|
769
|
+
def _compile_exists(self, node: ExistsNode) -> str:
|
|
770
|
+
if self._render_query is None:
|
|
771
|
+
message = f"{self.dialect} cannot read a query inside an expression"
|
|
772
|
+
raise CompilationError(message)
|
|
773
|
+
prefix = "NOT EXISTS" if node.negated else "EXISTS"
|
|
774
|
+
return f"{prefix} ({self._render_query(node.query)})"
|
|
775
|
+
|
|
776
|
+
def _compile_unary(self, node: UnaryNode) -> str:
|
|
777
|
+
operand = self.compile(node.operand)
|
|
778
|
+
if node.operator is UnaryOperator.NOT:
|
|
779
|
+
return f"(NOT {operand})"
|
|
780
|
+
if node.operator is UnaryOperator.NEGATE:
|
|
781
|
+
return f"(-{operand})"
|
|
782
|
+
suffix = _UNARY_SUFFIX.get(node.operator)
|
|
783
|
+
if suffix is None:
|
|
784
|
+
message = f"unsupported {self.dialect} unary operator: {node.operator}"
|
|
785
|
+
raise UnsupportedQueryError(message)
|
|
786
|
+
return f"({operand} {suffix})"
|
|
787
|
+
|
|
788
|
+
def render_binary(self, node: BinaryNode, left: str, right: str) -> str:
|
|
789
|
+
operator = self.binary_operator_sql(node.operator)
|
|
790
|
+
if operator is None:
|
|
791
|
+
message = f"unsupported {self.dialect} binary operator: {node.operator}"
|
|
792
|
+
raise UnsupportedQueryError(message)
|
|
793
|
+
rendered = self._context.structural_text(operator)
|
|
794
|
+
return f"({left} {rendered} {right})"
|
|
795
|
+
|
|
796
|
+
def _compile_binary(self, node: BinaryNode) -> str:
|
|
797
|
+
self.validate_binary(node)
|
|
798
|
+
return self.render_binary(
|
|
799
|
+
node, self.compile(node.left), self.compile(node.right)
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def _compile_variadic(self, node: VariadicNode) -> str:
|
|
803
|
+
if node.operator in {VariadicOperator.IN, VariadicOperator.NOT_IN}:
|
|
804
|
+
return self._compile_membership(node)
|
|
805
|
+
if node.operator in {VariadicOperator.BETWEEN, VariadicOperator.NOT_BETWEEN}:
|
|
806
|
+
return self._compile_range(node)
|
|
807
|
+
if node.operator is VariadicOperator.AND:
|
|
808
|
+
separator = " AND "
|
|
809
|
+
elif node.operator is VariadicOperator.OR:
|
|
810
|
+
separator = " OR "
|
|
811
|
+
else:
|
|
812
|
+
message = f"unsupported {self.dialect} variadic operator: {node.operator}"
|
|
813
|
+
raise UnsupportedQueryError(message)
|
|
814
|
+
return f"({separator.join(self.compile(item) for item in node.operands)})"
|
|
815
|
+
|
|
816
|
+
def _compile_membership(self, node: VariadicNode) -> str:
|
|
817
|
+
expression, *values = node.operands
|
|
818
|
+
if not values:
|
|
819
|
+
return "(1 = 1)" if node.operator is VariadicOperator.NOT_IN else "(0 = 1)"
|
|
820
|
+
operator = "NOT IN" if node.operator is VariadicOperator.NOT_IN else "IN"
|
|
821
|
+
rendered_values = ", ".join(self.compile(value) for value in values)
|
|
822
|
+
return f"({self.compile(expression)} {operator} ({rendered_values}))"
|
|
823
|
+
|
|
824
|
+
def _compile_range(self, node: VariadicNode) -> str:
|
|
825
|
+
if len(node.operands) != 3:
|
|
826
|
+
message = f"{self.dialect} range predicate requires exactly three operands"
|
|
827
|
+
raise CompilationError(message)
|
|
828
|
+
expression, lower, upper = node.operands
|
|
829
|
+
operator = (
|
|
830
|
+
"NOT BETWEEN"
|
|
831
|
+
if node.operator is VariadicOperator.NOT_BETWEEN
|
|
832
|
+
else "BETWEEN"
|
|
833
|
+
)
|
|
834
|
+
return (
|
|
835
|
+
f"({self.compile(expression)} {operator} {self.compile(lower)} "
|
|
836
|
+
f"AND {self.compile(upper)})"
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def _compile_case(self, node: CaseNode) -> str:
|
|
840
|
+
branches = " ".join(
|
|
841
|
+
f"WHEN {self.compile(when)} THEN {self.compile(then)}"
|
|
842
|
+
for when, then in node.branches
|
|
843
|
+
)
|
|
844
|
+
if node.otherwise is None:
|
|
845
|
+
return f"CASE {branches} END"
|
|
846
|
+
return f"CASE {branches} ELSE {self.compile(node.otherwise)} END"
|
|
847
|
+
|
|
848
|
+
def _compile_function(self, node: FunctionNode) -> str:
|
|
849
|
+
if node.name in _CHOOSING_FUNCTIONS:
|
|
850
|
+
arguments = ", ".join(self.compile(argument) for argument in node.arguments)
|
|
851
|
+
return f"{self.choosing_function_sql(node.name)}({arguments})"
|
|
852
|
+
if node.name in _SCALAR_TRANSFORMS:
|
|
853
|
+
if len(node.arguments) != 1:
|
|
854
|
+
message = f"{self.dialect} scalar transform requires one argument"
|
|
855
|
+
raise CompilationError(message)
|
|
856
|
+
name = node.name.value.upper()
|
|
857
|
+
return f"{name}({self.compile(node.arguments[0])})"
|
|
858
|
+
if len(node.arguments) != 2:
|
|
859
|
+
message = f"{self.dialect} string predicate requires two arguments"
|
|
860
|
+
raise CompilationError(message)
|
|
861
|
+
return self.render_string_predicate(node)
|
|
862
|
+
|
|
863
|
+
def _compile_aggregate(self, node: AggregateNode) -> str:
|
|
864
|
+
name = _AGGREGATE_SQL.get(node.name) or self._json_aggregate_sql(node.name)
|
|
865
|
+
if name is None:
|
|
866
|
+
message = f"unsupported {self.dialect} aggregate: {node.name}"
|
|
867
|
+
raise UnsupportedQueryError(message)
|
|
868
|
+
if not node.arguments:
|
|
869
|
+
return f"{name}(*)"
|
|
870
|
+
distinct = "DISTINCT " if node.distinct else ""
|
|
871
|
+
arguments = ", ".join(self.compile(argument) for argument in node.arguments)
|
|
872
|
+
return f"{name}({distinct}{arguments})"
|
|
873
|
+
|
|
874
|
+
def _json_aggregate_sql(self, name: AggregateName) -> str | None:
|
|
875
|
+
"""Gathering rows into JSON, which each dialect calls its own thing."""
|
|
876
|
+
functions = self.collection_functions
|
|
877
|
+
if name is AggregateName.JSON_ARRAY_AGG:
|
|
878
|
+
return functions.array_function
|
|
879
|
+
if name is AggregateName.JSON_OBJECT_AGG:
|
|
880
|
+
return functions.object_aggregate
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
def render_json_build(self, node: JsonBuildNode) -> str:
|
|
884
|
+
"""A JSON value built from the values around it, named per dialect."""
|
|
885
|
+
functions = self.collection_functions
|
|
886
|
+
if node.kind is JsonBuild.ARRAY:
|
|
887
|
+
built = functions.array_constructor
|
|
888
|
+
else:
|
|
889
|
+
built = functions.object_function
|
|
890
|
+
arguments = ", ".join(self.compile(argument) for argument in node.arguments)
|
|
891
|
+
return f"{built}({arguments})"
|
|
892
|
+
|
|
893
|
+
def _compile_raw(self, node: RawNode) -> str:
|
|
894
|
+
self.require_raw_expressions()
|
|
895
|
+
fragments: list[str] = []
|
|
896
|
+
for part in node.parts:
|
|
897
|
+
if isinstance(part, str):
|
|
898
|
+
self._validate_raw_text(part)
|
|
899
|
+
fragments.append(self._context.structural_text(part))
|
|
900
|
+
else:
|
|
901
|
+
fragments.append(self.compile(part))
|
|
902
|
+
return f"({''.join(fragments)})"
|
|
903
|
+
|
|
904
|
+
def _validate_raw_text(self, value: str) -> None:
|
|
905
|
+
if _UNSAFE_RAW_TEXT.search(value):
|
|
906
|
+
message = (
|
|
907
|
+
f"{self.dialect} raw text cannot contain bind markers "
|
|
908
|
+
"or statement syntax"
|
|
909
|
+
)
|
|
910
|
+
raise CompilationError(message)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
__all__ = ("ExpressionRenderer",)
|