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/nodes.py
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"""Immutable query expression nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING, Protocol, TypeAlias, Union, runtime_checkable
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pyoq.query.select_nodes import OrderNode, QueryNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_ROW_VALUE_WIDTH = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class NodeProvider(Protocol):
|
|
18
|
+
"""Anything that can stand in an expression tree.
|
|
19
|
+
|
|
20
|
+
It lives here because both the expression layer and the raw templates
|
|
21
|
+
above it need the same idea, and one of them cannot import the other.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def node(self) -> ExpressionNode: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ScalarFamily(StrEnum):
|
|
29
|
+
BOOLEAN = "boolean"
|
|
30
|
+
NUMERIC = "numeric"
|
|
31
|
+
STRING = "string"
|
|
32
|
+
BINARY = "binary"
|
|
33
|
+
TEMPORAL = "temporal"
|
|
34
|
+
JSON = "json"
|
|
35
|
+
OTHER = "other"
|
|
36
|
+
NULL = "null"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UnaryOperator(StrEnum):
|
|
40
|
+
NOT = "not"
|
|
41
|
+
NEGATE = "negate"
|
|
42
|
+
IS_NULL = "is-null"
|
|
43
|
+
IS_NOT_NULL = "is-not-null"
|
|
44
|
+
IS_TRUE = "is-true"
|
|
45
|
+
IS_FALSE = "is-false"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BinaryOperator(StrEnum):
|
|
49
|
+
EQUAL = "equal"
|
|
50
|
+
NOT_EQUAL = "not-equal"
|
|
51
|
+
LESS_THAN = "less-than"
|
|
52
|
+
LESS_OR_EQUAL = "less-or-equal"
|
|
53
|
+
GREATER_THAN = "greater-than"
|
|
54
|
+
GREATER_OR_EQUAL = "greater-or-equal"
|
|
55
|
+
IS_DISTINCT_FROM = "is-distinct-from"
|
|
56
|
+
IS_NOT_DISTINCT_FROM = "is-not-distinct-from"
|
|
57
|
+
ADD = "add"
|
|
58
|
+
SUBTRACT = "subtract"
|
|
59
|
+
MULTIPLY = "multiply"
|
|
60
|
+
DIVIDE = "divide"
|
|
61
|
+
MODULO = "modulo"
|
|
62
|
+
CONCAT = "concat"
|
|
63
|
+
LIKE = "like"
|
|
64
|
+
NOT_LIKE = "not-like"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class VariadicOperator(StrEnum):
|
|
68
|
+
AND = "and"
|
|
69
|
+
OR = "or"
|
|
70
|
+
IN = "in"
|
|
71
|
+
NOT_IN = "not-in"
|
|
72
|
+
BETWEEN = "between"
|
|
73
|
+
NOT_BETWEEN = "not-between"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FunctionName(StrEnum):
|
|
77
|
+
LOWER = "lower"
|
|
78
|
+
UPPER = "upper"
|
|
79
|
+
LENGTH = "length"
|
|
80
|
+
CONTAINS = "contains"
|
|
81
|
+
STARTS_WITH = "starts-with"
|
|
82
|
+
ENDS_WITH = "ends-with"
|
|
83
|
+
COALESCE = "coalesce"
|
|
84
|
+
NULLIF = "nullif"
|
|
85
|
+
GREATEST = "greatest"
|
|
86
|
+
LEAST = "least"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AggregateName(StrEnum):
|
|
90
|
+
COUNT = "count"
|
|
91
|
+
SUM = "sum"
|
|
92
|
+
AVERAGE = "average"
|
|
93
|
+
MINIMUM = "minimum"
|
|
94
|
+
MAXIMUM = "maximum"
|
|
95
|
+
JSON_ARRAY_AGG = "json-array-agg"
|
|
96
|
+
JSON_OBJECT_AGG = "json-object-agg"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DatePart(StrEnum):
|
|
100
|
+
YEAR = "year"
|
|
101
|
+
MONTH = "month"
|
|
102
|
+
DAY = "day"
|
|
103
|
+
HOUR = "hour"
|
|
104
|
+
MINUTE = "minute"
|
|
105
|
+
SECOND = "second"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class BoundValueNode:
|
|
110
|
+
value: object
|
|
111
|
+
family: ScalarFamily
|
|
112
|
+
sensitive: bool = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True, slots=True)
|
|
116
|
+
class FieldNode:
|
|
117
|
+
name: str
|
|
118
|
+
table: str | None = None
|
|
119
|
+
schema: str | None = None
|
|
120
|
+
catalog: str | None = None
|
|
121
|
+
family: ScalarFamily = ScalarFamily.OTHER
|
|
122
|
+
# What the column was declared to hold, which a family cannot say: an
|
|
123
|
+
# integer, a float, and a decimal are all one family and three types.
|
|
124
|
+
value_type: type[object] | None = None
|
|
125
|
+
# Whether the column may hold no value, which decides whether anything
|
|
126
|
+
# read through it may answer with none.
|
|
127
|
+
nullable: bool = True
|
|
128
|
+
|
|
129
|
+
def __post_init__(self) -> None:
|
|
130
|
+
_require_identifier(self.name, "field")
|
|
131
|
+
_require_optional_identifier(self.table, "table")
|
|
132
|
+
_require_optional_identifier(self.schema, "schema")
|
|
133
|
+
_require_optional_identifier(self.catalog, "catalog")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class UnaryNode:
|
|
138
|
+
operator: UnaryOperator
|
|
139
|
+
operand: ExpressionNode
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class BinaryNode:
|
|
144
|
+
operator: BinaryOperator
|
|
145
|
+
left: ExpressionNode
|
|
146
|
+
right: ExpressionNode
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class VariadicNode:
|
|
151
|
+
operator: VariadicOperator
|
|
152
|
+
operands: tuple[ExpressionNode, ...]
|
|
153
|
+
|
|
154
|
+
def __post_init__(self) -> None:
|
|
155
|
+
if not self.operands:
|
|
156
|
+
message = "variadic expression requires at least one operand"
|
|
157
|
+
raise ValueError(message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True)
|
|
161
|
+
class FunctionNode:
|
|
162
|
+
name: FunctionName
|
|
163
|
+
arguments: tuple[ExpressionNode, ...]
|
|
164
|
+
|
|
165
|
+
def __post_init__(self) -> None:
|
|
166
|
+
if not self.arguments:
|
|
167
|
+
message = "function expression requires at least one argument"
|
|
168
|
+
raise ValueError(message)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class ExtractNode:
|
|
173
|
+
part: DatePart
|
|
174
|
+
expression: ExpressionNode
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_AGGREGATE_ARITY = {AggregateName.JSON_OBJECT_AGG: 2}
|
|
178
|
+
"""Aggregates that read more than the one value the rest of them read."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True, slots=True)
|
|
182
|
+
class AggregateNode:
|
|
183
|
+
name: AggregateName
|
|
184
|
+
arguments: tuple[ExpressionNode, ...]
|
|
185
|
+
distinct: bool = False
|
|
186
|
+
|
|
187
|
+
def __post_init__(self) -> None:
|
|
188
|
+
if self.name is AggregateName.COUNT:
|
|
189
|
+
if len(self.arguments) > 1:
|
|
190
|
+
message = "count aggregate accepts at most one argument"
|
|
191
|
+
raise ValueError(message)
|
|
192
|
+
elif len(self.arguments) != _AGGREGATE_ARITY.get(self.name, 1):
|
|
193
|
+
message = "aggregate requires exactly one argument"
|
|
194
|
+
raise ValueError(message)
|
|
195
|
+
if self.distinct and not self.arguments:
|
|
196
|
+
message = "distinct aggregate requires an argument"
|
|
197
|
+
raise ValueError(message)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
RawPart: TypeAlias = Union[str, "ExpressionNode"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True, slots=True)
|
|
204
|
+
class RawNode:
|
|
205
|
+
parts: tuple[RawPart, ...]
|
|
206
|
+
family: ScalarFamily = ScalarFamily.OTHER
|
|
207
|
+
|
|
208
|
+
def __post_init__(self) -> None:
|
|
209
|
+
if not self.parts or not any(
|
|
210
|
+
isinstance(part, str) and part for part in self.parts
|
|
211
|
+
):
|
|
212
|
+
message = "raw expression requires structural SQL text"
|
|
213
|
+
raise ValueError(message)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True, slots=True)
|
|
217
|
+
class CollectionEntry:
|
|
218
|
+
"""One named value each row of a collection carries."""
|
|
219
|
+
|
|
220
|
+
key: str
|
|
221
|
+
expression: ExpressionNode
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
if not self.key:
|
|
225
|
+
message = "a collection entry needs a key"
|
|
226
|
+
raise ValueError(message)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True, slots=True)
|
|
230
|
+
class CollectionOrderNode:
|
|
231
|
+
expression: ExpressionNode
|
|
232
|
+
descending: bool = False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _require_bounded_collection(
|
|
236
|
+
limit: int | None,
|
|
237
|
+
orders: tuple[CollectionOrderNode, ...],
|
|
238
|
+
) -> None:
|
|
239
|
+
"""A limit takes the first rows, so something has to say which are first."""
|
|
240
|
+
if limit is None:
|
|
241
|
+
return
|
|
242
|
+
if limit < 1:
|
|
243
|
+
message = "a collection carries at least one row or is not limited"
|
|
244
|
+
raise ValueError(message)
|
|
245
|
+
if not orders:
|
|
246
|
+
message = "a limited collection is ordered, so the rows it keeps are known"
|
|
247
|
+
raise ValueError(message)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass(frozen=True, slots=True)
|
|
251
|
+
class CollectionJoinNode:
|
|
252
|
+
"""A table a collection reads alongside the one the relation names.
|
|
253
|
+
|
|
254
|
+
A collection is still the rows of one relation. A join widens what each of
|
|
255
|
+
those rows carries and does not change which rows there are, so it is a
|
|
256
|
+
left join: an inner one would drop a row whose join matched nothing, which
|
|
257
|
+
is a row the collection was asked for going missing. A row that matched
|
|
258
|
+
nothing carries null for what the join would have added.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
table: str
|
|
262
|
+
condition: ExpressionNode
|
|
263
|
+
schema: str | None = None
|
|
264
|
+
catalog: str | None = None
|
|
265
|
+
alias: str | None = None
|
|
266
|
+
|
|
267
|
+
def __post_init__(self) -> None:
|
|
268
|
+
if not self.table:
|
|
269
|
+
message = "a collection join needs a table to read"
|
|
270
|
+
raise ValueError(message)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(frozen=True, slots=True)
|
|
274
|
+
class CollectionNode:
|
|
275
|
+
"""A related table's rows, gathered into one value beside their parent.
|
|
276
|
+
|
|
277
|
+
The condition correlates the collection with the row it belongs to, so it
|
|
278
|
+
reads columns of both tables. The relation names one table, and a join
|
|
279
|
+
widens what each of its rows carries without changing which rows there
|
|
280
|
+
are, so a collection stays the rows of one relation.
|
|
281
|
+
|
|
282
|
+
``limit`` bounds how many rows the collection carries. The rows are chosen
|
|
283
|
+
by the order, so a limit without one would take an arbitrary few, and it
|
|
284
|
+
is refused here rather than left to the dialect to decide. That is also
|
|
285
|
+
why a dialect that cannot order inside a collection cannot carry a limited
|
|
286
|
+
one: the order refusal already covers both.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
table: str
|
|
290
|
+
entries: tuple[CollectionEntry, ...]
|
|
291
|
+
condition: ExpressionNode
|
|
292
|
+
orders: tuple[CollectionOrderNode, ...] = ()
|
|
293
|
+
schema: str | None = None
|
|
294
|
+
catalog: str | None = None
|
|
295
|
+
alias: str | None = None
|
|
296
|
+
joins: tuple[CollectionJoinNode, ...] = ()
|
|
297
|
+
limit: int | None = None
|
|
298
|
+
|
|
299
|
+
def __post_init__(self) -> None:
|
|
300
|
+
if not self.table:
|
|
301
|
+
message = "a collection needs a table to read"
|
|
302
|
+
raise ValueError(message)
|
|
303
|
+
if not self.entries:
|
|
304
|
+
message = "a collection needs at least one value per row"
|
|
305
|
+
raise ValueError(message)
|
|
306
|
+
_require_bounded_collection(self.limit, self.orders)
|
|
307
|
+
keys = tuple(entry.key for entry in self.entries)
|
|
308
|
+
if len(set(keys)) != len(keys):
|
|
309
|
+
message = "a collection cannot carry one key twice"
|
|
310
|
+
raise ValueError(message)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True, slots=True)
|
|
314
|
+
class VendorFunctionNode:
|
|
315
|
+
"""A function this database has that PyOQ does not name itself.
|
|
316
|
+
|
|
317
|
+
The name is written into the SQL rather than bound, so it is checked to
|
|
318
|
+
be an identifier and nothing else. The arguments are expressions and
|
|
319
|
+
travel the way every other expression does.
|
|
320
|
+
|
|
321
|
+
``dialects`` is the databases the function exists on. Empty means the
|
|
322
|
+
caller did not say, and it is written wherever it is asked for.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
name: str
|
|
326
|
+
arguments: tuple[ExpressionNode, ...]
|
|
327
|
+
family: ScalarFamily
|
|
328
|
+
dialects: frozenset[str] = frozenset()
|
|
329
|
+
# Where the routine is kept, held apart so each part is quoted as itself
|
|
330
|
+
# rather than flattened into a name a dot could be part of.
|
|
331
|
+
schema: str | None = None
|
|
332
|
+
catalog: str | None = None
|
|
333
|
+
# What the caller declared it answers with, which a family cannot say and
|
|
334
|
+
# which is what reads the value back as the type it was promised to be.
|
|
335
|
+
value_type: type[object] | None = None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ArrayOperation(StrEnum):
|
|
339
|
+
"""What is being asked of an array and one other thing."""
|
|
340
|
+
|
|
341
|
+
HAS = "has"
|
|
342
|
+
LACKS = "lacks"
|
|
343
|
+
CONTAINS = "contains"
|
|
344
|
+
CONTAINED_BY = "contained-by"
|
|
345
|
+
OVERLAPS = "overlaps"
|
|
346
|
+
APPEND = "append"
|
|
347
|
+
PREPEND = "prepend"
|
|
348
|
+
CONCAT = "concat"
|
|
349
|
+
REMOVE = "remove"
|
|
350
|
+
REPLACE = "replace"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
_ARRAY_ARITY = {ArrayOperation.REPLACE: 2}
|
|
354
|
+
"""Operations that read more than the one value the rest of them read."""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclass(frozen=True, slots=True)
|
|
358
|
+
class ArrayNode:
|
|
359
|
+
"""An array asked about against something else."""
|
|
360
|
+
|
|
361
|
+
operation: ArrayOperation
|
|
362
|
+
expression: ExpressionNode
|
|
363
|
+
operands: tuple[ExpressionNode, ...]
|
|
364
|
+
|
|
365
|
+
def __post_init__(self) -> None:
|
|
366
|
+
if len(self.operands) != _ARRAY_ARITY.get(self.operation, 1):
|
|
367
|
+
message = "array operation was given the wrong number of values"
|
|
368
|
+
raise ValueError(message)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@dataclass(frozen=True, slots=True)
|
|
372
|
+
class ArrayElementNode:
|
|
373
|
+
"""One element of an array, counted the way the value it reads back is.
|
|
374
|
+
|
|
375
|
+
A column of an array is read back as a tuple, and a tuple counts from
|
|
376
|
+
zero, so this does too. SQL counts arrays from one, which is a difference
|
|
377
|
+
written out here rather than left for a caller to remember.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
expression: ExpressionNode
|
|
381
|
+
indexes: tuple[ExpressionNode, ...]
|
|
382
|
+
|
|
383
|
+
def __post_init__(self) -> None:
|
|
384
|
+
if not self.indexes:
|
|
385
|
+
message = "an element of an array is read at a position"
|
|
386
|
+
raise ValueError(message)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@dataclass(frozen=True, slots=True)
|
|
390
|
+
class ArrayLengthNode:
|
|
391
|
+
"""How many elements an array holds, in one dimension or in all of them."""
|
|
392
|
+
|
|
393
|
+
expression: ExpressionNode
|
|
394
|
+
dimension: int | None = None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclass(frozen=True, slots=True)
|
|
398
|
+
class ArrayConstructNode:
|
|
399
|
+
"""An array built out of the values around it."""
|
|
400
|
+
|
|
401
|
+
elements: tuple[ExpressionNode, ...] = ()
|
|
402
|
+
# What one element holds, where every element holds the same thing.
|
|
403
|
+
element_type: type[object] | None = None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@dataclass(frozen=True, slots=True)
|
|
407
|
+
class ArrayDimensionsNode:
|
|
408
|
+
"""How many dimensions an array has, which an empty array does not say."""
|
|
409
|
+
|
|
410
|
+
expression: ExpressionNode
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class JsonOperation(StrEnum):
|
|
414
|
+
"""What is being asked of a JSON value at a path."""
|
|
415
|
+
|
|
416
|
+
VALUE = "value"
|
|
417
|
+
TEXT = "text"
|
|
418
|
+
LENGTH = "length"
|
|
419
|
+
EXISTS = "exists"
|
|
420
|
+
REMOVE = "remove"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@dataclass(frozen=True, slots=True)
|
|
424
|
+
class JsonNode:
|
|
425
|
+
"""Reading inside a JSON value, by a path each dialect spells its own way.
|
|
426
|
+
|
|
427
|
+
The path is steps rather than text, because the three dialects do not
|
|
428
|
+
agree on how a path is written and one of them reads another's spelling
|
|
429
|
+
as a key that is simply absent. Steps are a string for a member and an
|
|
430
|
+
integer for an element.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
operation: JsonOperation
|
|
434
|
+
expression: ExpressionNode
|
|
435
|
+
path: tuple[str | int, ...] = ()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class JsonBuild(StrEnum):
|
|
439
|
+
"""Whether a JSON value is being built as an object or as an array."""
|
|
440
|
+
|
|
441
|
+
OBJECT = "object"
|
|
442
|
+
ARRAY = "array"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@dataclass(frozen=True, slots=True)
|
|
446
|
+
class JsonBuildNode:
|
|
447
|
+
"""A JSON value built out of the values around it.
|
|
448
|
+
|
|
449
|
+
An object reads its arguments as alternating members and values, so it
|
|
450
|
+
takes an even number of them and every other one names a member.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
kind: JsonBuild
|
|
454
|
+
arguments: tuple[ExpressionNode, ...] = ()
|
|
455
|
+
|
|
456
|
+
def __post_init__(self) -> None:
|
|
457
|
+
if self.kind is JsonBuild.OBJECT and len(self.arguments) % 2:
|
|
458
|
+
message = "a JSON object is built from members and their values"
|
|
459
|
+
raise ValueError(message)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@dataclass(frozen=True, slots=True)
|
|
463
|
+
class JsonKeysNode:
|
|
464
|
+
"""The members an object has, as a JSON array of their names.
|
|
465
|
+
|
|
466
|
+
The dialects do not agree on the order the names come back in, because
|
|
467
|
+
each one stores an object the way it stores an object.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
expression: ExpressionNode
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class JsonWrite(StrEnum):
|
|
474
|
+
"""How a value is put into a JSON document."""
|
|
475
|
+
|
|
476
|
+
SET = "set"
|
|
477
|
+
INSERT = "insert"
|
|
478
|
+
REPLACE = "replace"
|
|
479
|
+
MERGE = "merge"
|
|
480
|
+
"""Merge-patch: a null removes a member, and objects merge all the way down."""
|
|
481
|
+
|
|
482
|
+
CONCAT = "concat"
|
|
483
|
+
"""One level only: a member is replaced whole, and a null is kept as one."""
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@dataclass(frozen=True, slots=True)
|
|
487
|
+
class JsonWriteNode:
|
|
488
|
+
"""A JSON document with something written into it.
|
|
489
|
+
|
|
490
|
+
The answer is a new document; nothing is changed in place, because an
|
|
491
|
+
expression cannot change anything. A merge has no path, because it is
|
|
492
|
+
written over the whole document.
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
operation: JsonWrite
|
|
496
|
+
expression: ExpressionNode
|
|
497
|
+
value: ExpressionNode
|
|
498
|
+
path: tuple[str | int, ...] = ()
|
|
499
|
+
|
|
500
|
+
def __post_init__(self) -> None:
|
|
501
|
+
if self.operation is JsonWrite.MERGE and self.path:
|
|
502
|
+
message = "a merge is written over the whole document, not at a path"
|
|
503
|
+
raise ValueError(message)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@dataclass(frozen=True, slots=True)
|
|
507
|
+
class JsonContainsNode:
|
|
508
|
+
"""Whether one JSON value holds another, which is not a question of path."""
|
|
509
|
+
|
|
510
|
+
expression: ExpressionNode
|
|
511
|
+
value: ExpressionNode
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@dataclass(frozen=True, slots=True)
|
|
515
|
+
class RowValueNode:
|
|
516
|
+
"""Several values compared as one, the way a composite key is.
|
|
517
|
+
|
|
518
|
+
A row of one value is that value, and SQL reads it as one, so a row
|
|
519
|
+
value is only a row value from two columns up.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
expressions: tuple[ExpressionNode, ...]
|
|
523
|
+
|
|
524
|
+
def __post_init__(self) -> None:
|
|
525
|
+
if len(self.expressions) < _ROW_VALUE_WIDTH:
|
|
526
|
+
message = "a row value needs at least two expressions"
|
|
527
|
+
raise ValueError(message)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class FrameKind(StrEnum):
|
|
531
|
+
"""What a frame counts: rows, values, or peer groups."""
|
|
532
|
+
|
|
533
|
+
ROWS = "rows"
|
|
534
|
+
RANGE = "range"
|
|
535
|
+
GROUPS = "groups"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class FrameBound(StrEnum):
|
|
539
|
+
"""Where a frame starts or ends, relative to the row being measured."""
|
|
540
|
+
|
|
541
|
+
UNBOUNDED_PRECEDING = "unbounded-preceding"
|
|
542
|
+
PRECEDING = "preceding"
|
|
543
|
+
CURRENT_ROW = "current-row"
|
|
544
|
+
FOLLOWING = "following"
|
|
545
|
+
UNBOUNDED_FOLLOWING = "unbounded-following"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class FrameExclusion(StrEnum):
|
|
549
|
+
"""Which rows a frame leaves out even though it reaches them."""
|
|
550
|
+
|
|
551
|
+
CURRENT_ROW = "current-row"
|
|
552
|
+
GROUP = "group"
|
|
553
|
+
TIES = "ties"
|
|
554
|
+
NO_OTHERS = "no-others"
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@dataclass(frozen=True, slots=True)
|
|
558
|
+
class FrameBoundNode:
|
|
559
|
+
"""One edge of a frame, and how far from the row it sits."""
|
|
560
|
+
|
|
561
|
+
bound: FrameBound
|
|
562
|
+
offset: ExpressionNode | None = None
|
|
563
|
+
|
|
564
|
+
def __post_init__(self) -> None:
|
|
565
|
+
counted = self.bound in {FrameBound.PRECEDING, FrameBound.FOLLOWING}
|
|
566
|
+
if counted != (self.offset is not None):
|
|
567
|
+
message = "a counted frame edge needs a distance and no other does"
|
|
568
|
+
raise ValueError(message)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@dataclass(frozen=True, slots=True)
|
|
572
|
+
class FrameNode:
|
|
573
|
+
"""The rows a window function reads, narrower than the whole partition."""
|
|
574
|
+
|
|
575
|
+
kind: FrameKind
|
|
576
|
+
start: FrameBoundNode
|
|
577
|
+
end: FrameBoundNode | None = None
|
|
578
|
+
exclusion: FrameExclusion | None = None
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class WindowName(StrEnum):
|
|
582
|
+
ROW_NUMBER = "row-number"
|
|
583
|
+
RANK = "rank"
|
|
584
|
+
DENSE_RANK = "dense-rank"
|
|
585
|
+
PERCENT_RANK = "percent-rank"
|
|
586
|
+
CUME_DIST = "cume-dist"
|
|
587
|
+
NTILE = "ntile"
|
|
588
|
+
LAG = "lag"
|
|
589
|
+
LEAD = "lead"
|
|
590
|
+
FIRST_VALUE = "first-value"
|
|
591
|
+
LAST_VALUE = "last-value"
|
|
592
|
+
NTH_VALUE = "nth-value"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@dataclass(frozen=True, slots=True)
|
|
596
|
+
class WindowFunctionNode:
|
|
597
|
+
"""A function that only means anything with a window behind it."""
|
|
598
|
+
|
|
599
|
+
name: WindowName
|
|
600
|
+
arguments: tuple[ExpressionNode, ...] = ()
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@dataclass(frozen=True, slots=True)
|
|
604
|
+
class WindowSpecificationNode:
|
|
605
|
+
"""What a row is measured against: its partition, in its order.
|
|
606
|
+
|
|
607
|
+
The rows a window covers are named the way a query names them, so the
|
|
608
|
+
order terms here are the same ones an ORDER BY is built from.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
partition_by: tuple[ExpressionNode, ...] = ()
|
|
612
|
+
order_by: tuple[OrderNode, ...] = ()
|
|
613
|
+
frame: FrameNode | None = None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@dataclass(frozen=True, slots=True)
|
|
617
|
+
class WindowReferenceNode:
|
|
618
|
+
"""A window the query declared once under a name."""
|
|
619
|
+
|
|
620
|
+
name: str
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@dataclass(frozen=True, slots=True)
|
|
624
|
+
class NamedWindowNode:
|
|
625
|
+
"""A window written once in a query and looked through by several rows."""
|
|
626
|
+
|
|
627
|
+
name: str
|
|
628
|
+
specification: WindowSpecificationNode
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
EVERY_ROW = WindowSpecificationNode()
|
|
632
|
+
"""The window a function reads before anything narrows it."""
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@dataclass(frozen=True, slots=True)
|
|
636
|
+
class WindowNode:
|
|
637
|
+
"""A function and the window it looks through, spelled out or named."""
|
|
638
|
+
|
|
639
|
+
expression: ExpressionNode
|
|
640
|
+
window: WindowSpecificationNode | WindowReferenceNode = EVERY_ROW
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@dataclass(frozen=True, slots=True)
|
|
644
|
+
class CastNode:
|
|
645
|
+
"""A value asked for as another type, by the database rather than Python.
|
|
646
|
+
|
|
647
|
+
The Python type is the one fact stored. What SQL names it is a dialect
|
|
648
|
+
question, and what reads it back is a decoding question, and both are
|
|
649
|
+
answered from here rather than repeated alongside it.
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
expression: ExpressionNode
|
|
653
|
+
value_type: type[object]
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@dataclass(frozen=True, slots=True)
|
|
657
|
+
class CaseNode:
|
|
658
|
+
"""One value chosen from several, by the first condition that holds.
|
|
659
|
+
|
|
660
|
+
The branches are ordered, because SQL reads them in order and so does a
|
|
661
|
+
reader. A case with no branches chooses nothing and is not a value.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
branches: tuple[tuple[ExpressionNode, ExpressionNode], ...]
|
|
665
|
+
otherwise: ExpressionNode | None = None
|
|
666
|
+
|
|
667
|
+
def __post_init__(self) -> None:
|
|
668
|
+
if not self.branches:
|
|
669
|
+
message = "case expression requires at least one branch"
|
|
670
|
+
raise ValueError(message)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@dataclass(frozen=True, slots=True)
|
|
674
|
+
class ExistsNode:
|
|
675
|
+
"""Whether a query returns anything at all.
|
|
676
|
+
|
|
677
|
+
The query is held rather than a source, because what EXISTS asks about is
|
|
678
|
+
a whole statement. The import is type only, so an expression may contain a
|
|
679
|
+
query without the node layers depending on each other at runtime.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
query: QueryNode
|
|
683
|
+
negated: bool = False
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
ExpressionNode = (
|
|
687
|
+
BoundValueNode
|
|
688
|
+
| FieldNode
|
|
689
|
+
| UnaryNode
|
|
690
|
+
| BinaryNode
|
|
691
|
+
| VariadicNode
|
|
692
|
+
| FunctionNode
|
|
693
|
+
| ExtractNode
|
|
694
|
+
| AggregateNode
|
|
695
|
+
| CaseNode
|
|
696
|
+
| CastNode
|
|
697
|
+
| VendorFunctionNode
|
|
698
|
+
| ArrayNode
|
|
699
|
+
| ArrayElementNode
|
|
700
|
+
| ArrayLengthNode
|
|
701
|
+
| ArrayConstructNode
|
|
702
|
+
| ArrayDimensionsNode
|
|
703
|
+
| JsonContainsNode
|
|
704
|
+
| JsonWriteNode
|
|
705
|
+
| JsonNode
|
|
706
|
+
| JsonBuildNode
|
|
707
|
+
| JsonKeysNode
|
|
708
|
+
| RowValueNode
|
|
709
|
+
| WindowFunctionNode
|
|
710
|
+
| WindowNode
|
|
711
|
+
| CollectionNode
|
|
712
|
+
| ExistsNode
|
|
713
|
+
| RawNode
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _require_identifier(value: str, label: str) -> None:
|
|
718
|
+
if not value or "\x00" in value:
|
|
719
|
+
message = f"{label} identifier cannot be empty or contain a null character"
|
|
720
|
+
raise ValueError(message)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _require_optional_identifier(value: str | None, label: str) -> None:
|
|
724
|
+
if value is not None:
|
|
725
|
+
_require_identifier(value, label)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
__all__ = (
|
|
729
|
+
"AggregateName",
|
|
730
|
+
"AggregateNode",
|
|
731
|
+
"BinaryNode",
|
|
732
|
+
"BinaryOperator",
|
|
733
|
+
"BoundValueNode",
|
|
734
|
+
"CollectionEntry",
|
|
735
|
+
"CollectionNode",
|
|
736
|
+
"CollectionOrderNode",
|
|
737
|
+
"DatePart",
|
|
738
|
+
"ExpressionNode",
|
|
739
|
+
"ExtractNode",
|
|
740
|
+
"FieldNode",
|
|
741
|
+
"FunctionName",
|
|
742
|
+
"FunctionNode",
|
|
743
|
+
"RawNode",
|
|
744
|
+
"RawPart",
|
|
745
|
+
"ScalarFamily",
|
|
746
|
+
"UnaryNode",
|
|
747
|
+
"UnaryOperator",
|
|
748
|
+
"VariadicNode",
|
|
749
|
+
"VariadicOperator",
|
|
750
|
+
)
|