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/generation/model.py
ADDED
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
"""Canonical Python generation model derived from a schema snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pyoq.config import DatabaseDialect
|
|
8
|
+
from pyoq.naming import (
|
|
9
|
+
NameRequest,
|
|
10
|
+
NamingPolicy,
|
|
11
|
+
NamingResult,
|
|
12
|
+
NamingScope,
|
|
13
|
+
PythonNameStyle,
|
|
14
|
+
)
|
|
15
|
+
from pyoq.relations import (
|
|
16
|
+
RelationDirection,
|
|
17
|
+
RelationGraph,
|
|
18
|
+
TypedRelation,
|
|
19
|
+
build_relation_graph,
|
|
20
|
+
)
|
|
21
|
+
from pyoq.schema import (
|
|
22
|
+
Catalog,
|
|
23
|
+
Column,
|
|
24
|
+
Domain,
|
|
25
|
+
EnumType,
|
|
26
|
+
Identifier,
|
|
27
|
+
Key,
|
|
28
|
+
KeyKind,
|
|
29
|
+
ObjectReference,
|
|
30
|
+
ParameterMode,
|
|
31
|
+
Routine,
|
|
32
|
+
RoutineKind,
|
|
33
|
+
RoutineParameter,
|
|
34
|
+
Schema,
|
|
35
|
+
SchemaSnapshot,
|
|
36
|
+
SqlType,
|
|
37
|
+
Table,
|
|
38
|
+
TypeKind,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_FIELD_RESERVED_NAMES = (
|
|
42
|
+
"build",
|
|
43
|
+
"builder",
|
|
44
|
+
"update_builder",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class GeneratedColumn:
|
|
50
|
+
source: Column
|
|
51
|
+
field_name: str
|
|
52
|
+
constant_name: str
|
|
53
|
+
annotation: str
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def writable(self) -> bool:
|
|
57
|
+
return self.source.generated is None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def required(self) -> bool:
|
|
61
|
+
return (
|
|
62
|
+
self.writable and not self.source.nullable and self.source.default is None
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def value_annotation(self) -> str:
|
|
67
|
+
if self.source.nullable:
|
|
68
|
+
return f"{self.annotation} | None"
|
|
69
|
+
return self.annotation
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class GeneratedKey:
|
|
74
|
+
source: Key
|
|
75
|
+
type_name: str
|
|
76
|
+
constant_name: str
|
|
77
|
+
columns: tuple[GeneratedColumn, ...]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class GeneratedRelation:
|
|
82
|
+
source: TypedRelation
|
|
83
|
+
constant_name: str
|
|
84
|
+
target_row_name: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class GeneratedTable:
|
|
89
|
+
source: Table
|
|
90
|
+
schema_name: str | None
|
|
91
|
+
catalog_name: str | None
|
|
92
|
+
class_name: str
|
|
93
|
+
constant_name: str
|
|
94
|
+
row_name: str
|
|
95
|
+
insert_name: str
|
|
96
|
+
update_name: str
|
|
97
|
+
builder_name: str
|
|
98
|
+
complete_builder_name: str
|
|
99
|
+
update_builder_name: str
|
|
100
|
+
columns: tuple[GeneratedColumn, ...]
|
|
101
|
+
keys: tuple[GeneratedKey, ...]
|
|
102
|
+
relations: tuple[GeneratedRelation, ...]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def writable_columns(self) -> tuple[GeneratedColumn, ...]:
|
|
106
|
+
return tuple(column for column in self.columns if column.writable)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def required_columns(self) -> tuple[GeneratedColumn, ...]:
|
|
110
|
+
return tuple(column for column in self.columns if column.required)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class GeneratedEnum:
|
|
115
|
+
source: EnumType
|
|
116
|
+
schema_name: str | None
|
|
117
|
+
catalog_name: str | None
|
|
118
|
+
class_name: str
|
|
119
|
+
members: tuple[tuple[str, str], ...]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class GeneratedDomain:
|
|
124
|
+
"""A domain as a type of its own, distinct from the type it is written over.
|
|
125
|
+
|
|
126
|
+
Two domains over one base are two types, so each becomes a name a checker
|
|
127
|
+
can tell apart. The value is still the base value, so nothing is converted
|
|
128
|
+
on the way in or out.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
source: Domain
|
|
132
|
+
schema_name: str | None
|
|
133
|
+
catalog_name: str | None
|
|
134
|
+
type_name: str
|
|
135
|
+
base_annotation: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class GeneratedRoutineParameter:
|
|
140
|
+
source: RoutineParameter
|
|
141
|
+
field_name: str
|
|
142
|
+
annotation: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class GeneratedRoutine:
|
|
147
|
+
"""A routine as a call a caller can make, with the database's own signature.
|
|
148
|
+
|
|
149
|
+
The parameters and the result come from the catalog rather than from a
|
|
150
|
+
caller's assertion, so arity, order, and types are the database's facts.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
source: Routine
|
|
154
|
+
schema_name: str | None
|
|
155
|
+
catalog_name: str | None
|
|
156
|
+
function_name: str
|
|
157
|
+
parameters: tuple[GeneratedRoutineParameter, ...]
|
|
158
|
+
returns_annotation: str | None
|
|
159
|
+
dialect: DatabaseDialect
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def given_parameters(self) -> tuple[GeneratedRoutineParameter, ...]:
|
|
163
|
+
"""Values a caller supplies, excluding values the routine only writes."""
|
|
164
|
+
return tuple(
|
|
165
|
+
parameter
|
|
166
|
+
for parameter in self.parameters
|
|
167
|
+
if parameter.source.mode is not ParameterMode.OUT
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def sql_parameters(self) -> tuple[GeneratedRoutineParameter, ...]:
|
|
172
|
+
"""Parameters represented in SQL for this dialect and routine kind."""
|
|
173
|
+
if self.source.kind is RoutineKind.FUNCTION:
|
|
174
|
+
return self.given_parameters
|
|
175
|
+
return self.parameters
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def unsupported_reason(self) -> str | None:
|
|
179
|
+
"""Why the generated adapter cannot safely invoke this routine."""
|
|
180
|
+
has_outputs = any(
|
|
181
|
+
parameter.source.mode is not ParameterMode.IN
|
|
182
|
+
for parameter in self.parameters
|
|
183
|
+
)
|
|
184
|
+
if self.dialect is DatabaseDialect.MYSQL and has_outputs:
|
|
185
|
+
return "MySQL OUT and INOUT routines require an output adapter"
|
|
186
|
+
if self.source.kind is RoutineKind.PROCEDURE and _default_precedes_output(
|
|
187
|
+
self.parameters
|
|
188
|
+
):
|
|
189
|
+
return "Procedure defaults followed by OUT parameters are not supported"
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def outputs(self) -> tuple[GeneratedRoutineParameter, ...]:
|
|
194
|
+
"""The parameters the procedure answers through rather than reads."""
|
|
195
|
+
return tuple(
|
|
196
|
+
parameter
|
|
197
|
+
for parameter in self.parameters
|
|
198
|
+
if parameter.source.mode is not ParameterMode.IN
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class GeneratedOverload:
|
|
204
|
+
"""Routines a database lets share one name, called under that name.
|
|
205
|
+
|
|
206
|
+
Python resolves an overload from what a call gives it, and the only thing
|
|
207
|
+
it can tell two calls apart by at runtime is how many values they carry.
|
|
208
|
+
A group is written only where every routine in it takes a different
|
|
209
|
+
number, so the call that arrives names exactly one of them.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
function_name: str
|
|
213
|
+
routines: tuple[GeneratedRoutine, ...]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _default_precedes_output(
|
|
217
|
+
parameters: tuple[GeneratedRoutineParameter, ...],
|
|
218
|
+
) -> bool:
|
|
219
|
+
default_seen = False
|
|
220
|
+
for parameter in parameters:
|
|
221
|
+
default_seen = default_seen or parameter.source.has_default
|
|
222
|
+
if default_seen and parameter.source.mode is ParameterMode.OUT:
|
|
223
|
+
return True
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, slots=True)
|
|
228
|
+
class GeneratedPackage:
|
|
229
|
+
tables: tuple[GeneratedTable, ...]
|
|
230
|
+
enums: tuple[GeneratedEnum, ...]
|
|
231
|
+
domains: tuple[GeneratedDomain, ...] = ()
|
|
232
|
+
routines: tuple[GeneratedRoutine, ...] = ()
|
|
233
|
+
overloads: tuple[GeneratedOverload, ...] = ()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True, slots=True)
|
|
237
|
+
class _TableSource:
|
|
238
|
+
key: str
|
|
239
|
+
catalog: Catalog
|
|
240
|
+
schema: Schema
|
|
241
|
+
table: Table
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, slots=True)
|
|
245
|
+
class _RoutineSource:
|
|
246
|
+
key: str
|
|
247
|
+
catalog: Catalog
|
|
248
|
+
schema: Schema
|
|
249
|
+
routine: Routine
|
|
250
|
+
shared_name: bool = False
|
|
251
|
+
"""Whether another routine of this schema is written under the same name."""
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def group(self) -> tuple[str | None, str | None, str]:
|
|
255
|
+
"""What makes two routines overloads of each other rather than two names."""
|
|
256
|
+
return (
|
|
257
|
+
_identifier_value(self.catalog.name),
|
|
258
|
+
_identifier_value(self.schema.name),
|
|
259
|
+
self.routine.name.value,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True, slots=True)
|
|
264
|
+
class _DomainSource:
|
|
265
|
+
key: str
|
|
266
|
+
catalog: Catalog
|
|
267
|
+
schema: Schema
|
|
268
|
+
domain: Domain
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True, slots=True)
|
|
272
|
+
class _EnumSource:
|
|
273
|
+
key: str
|
|
274
|
+
catalog: Catalog
|
|
275
|
+
schema: Schema
|
|
276
|
+
enum: EnumType
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def build_codegen_package(
|
|
280
|
+
snapshot: SchemaSnapshot,
|
|
281
|
+
naming_policy: NamingPolicy | None = None,
|
|
282
|
+
) -> GeneratedPackage:
|
|
283
|
+
tables = _table_sources(snapshot)
|
|
284
|
+
enums = _enum_sources(snapshot)
|
|
285
|
+
domains = _domain_sources(snapshot)
|
|
286
|
+
routines = _routine_sources(snapshot)
|
|
287
|
+
graph = build_relation_graph(snapshot)
|
|
288
|
+
scopes = _naming_scopes(tables, enums, domains, routines, graph)
|
|
289
|
+
names = (naming_policy or NamingPolicy()).resolve(scopes)
|
|
290
|
+
generated_enums = _generated_enums(enums, names)
|
|
291
|
+
enum_names = _enum_name_map(generated_enums)
|
|
292
|
+
generated_domains = _generated_domains(domains, names, enum_names)
|
|
293
|
+
domain_names = _domain_name_map(generated_domains)
|
|
294
|
+
generated_routines = _generated_routines(
|
|
295
|
+
routines, names, enum_names, domain_names, snapshot.dialect
|
|
296
|
+
)
|
|
297
|
+
table_row_names = {
|
|
298
|
+
(
|
|
299
|
+
_identifier_value(table.catalog.name),
|
|
300
|
+
_identifier_value(table.schema.name),
|
|
301
|
+
table.table.name.value,
|
|
302
|
+
): f"{names.get('tables', table.key)}Row"
|
|
303
|
+
for table in tables
|
|
304
|
+
}
|
|
305
|
+
generated_tables = tuple(
|
|
306
|
+
_generated_table(table, names, enum_names, domain_names, table_row_names, graph)
|
|
307
|
+
for table in tables
|
|
308
|
+
)
|
|
309
|
+
return GeneratedPackage(
|
|
310
|
+
generated_tables,
|
|
311
|
+
generated_enums,
|
|
312
|
+
generated_domains,
|
|
313
|
+
generated_routines,
|
|
314
|
+
_generated_overloads(routines, generated_routines, names),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _table_sources(snapshot: SchemaSnapshot) -> tuple[_TableSource, ...]:
|
|
319
|
+
sources: list[_TableSource] = []
|
|
320
|
+
for catalog_index, catalog in enumerate(snapshot.catalogs):
|
|
321
|
+
for schema_index, schema in enumerate(catalog.schemas):
|
|
322
|
+
for table_index, table in enumerate(schema.tables):
|
|
323
|
+
key = _qualified_key(
|
|
324
|
+
catalog.name,
|
|
325
|
+
schema.name,
|
|
326
|
+
table.name,
|
|
327
|
+
(catalog_index, schema_index, table_index),
|
|
328
|
+
)
|
|
329
|
+
sources.append(_TableSource(key, catalog, schema, table))
|
|
330
|
+
return tuple(sorted(sources, key=lambda source: source.key))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _enum_sources(snapshot: SchemaSnapshot) -> tuple[_EnumSource, ...]:
|
|
334
|
+
sources: list[_EnumSource] = []
|
|
335
|
+
for catalog_index, catalog in enumerate(snapshot.catalogs):
|
|
336
|
+
for schema_index, schema in enumerate(catalog.schemas):
|
|
337
|
+
for enum_index, enum in enumerate(schema.enums):
|
|
338
|
+
key = _qualified_key(
|
|
339
|
+
catalog.name,
|
|
340
|
+
schema.name,
|
|
341
|
+
enum.name,
|
|
342
|
+
(catalog_index, schema_index, enum_index),
|
|
343
|
+
)
|
|
344
|
+
sources.append(_EnumSource(key, catalog, schema, enum))
|
|
345
|
+
return tuple(sorted(sources, key=lambda source: source.key))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _qualified_key(
|
|
349
|
+
catalog: Identifier | None,
|
|
350
|
+
schema: Identifier | None,
|
|
351
|
+
name: Identifier,
|
|
352
|
+
indexes: tuple[int, int, int],
|
|
353
|
+
) -> str:
|
|
354
|
+
qualified = tuple(
|
|
355
|
+
identifier.value if identifier is not None else ""
|
|
356
|
+
for identifier in (catalog, schema, name)
|
|
357
|
+
)
|
|
358
|
+
return "/".join((*qualified, *(str(index) for index in indexes)))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _naming_scopes(
|
|
362
|
+
tables: tuple[_TableSource, ...],
|
|
363
|
+
enums: tuple[_EnumSource, ...],
|
|
364
|
+
domains: tuple[_DomainSource, ...],
|
|
365
|
+
routines: tuple[_RoutineSource, ...],
|
|
366
|
+
graph: RelationGraph,
|
|
367
|
+
) -> tuple[NamingScope, ...]:
|
|
368
|
+
scopes: list[NamingScope] = [
|
|
369
|
+
NamingScope(
|
|
370
|
+
"tables",
|
|
371
|
+
PythonNameStyle.PASCAL_CASE,
|
|
372
|
+
tuple(NameRequest(table.key, table.table.name) for table in tables),
|
|
373
|
+
),
|
|
374
|
+
NamingScope(
|
|
375
|
+
"table-constants",
|
|
376
|
+
PythonNameStyle.UPPER_SNAKE_CASE,
|
|
377
|
+
tuple(NameRequest(table.key, table.table.name) for table in tables),
|
|
378
|
+
),
|
|
379
|
+
NamingScope(
|
|
380
|
+
"enums",
|
|
381
|
+
PythonNameStyle.PASCAL_CASE,
|
|
382
|
+
tuple(NameRequest(enum.key, enum.enum.name) for enum in enums),
|
|
383
|
+
),
|
|
384
|
+
NamingScope(
|
|
385
|
+
"domains",
|
|
386
|
+
PythonNameStyle.PASCAL_CASE,
|
|
387
|
+
tuple(NameRequest(domain.key, domain.domain.name) for domain in domains),
|
|
388
|
+
),
|
|
389
|
+
NamingScope(
|
|
390
|
+
"routines",
|
|
391
|
+
PythonNameStyle.SNAKE_CASE,
|
|
392
|
+
_routine_name_requests(routines),
|
|
393
|
+
),
|
|
394
|
+
*(
|
|
395
|
+
NamingScope(
|
|
396
|
+
f"routine-parameters:{routine.key}",
|
|
397
|
+
PythonNameStyle.SNAKE_CASE,
|
|
398
|
+
tuple(
|
|
399
|
+
NameRequest(str(index), parameter.name or Identifier(f"a{index}"))
|
|
400
|
+
for index, parameter in enumerate(routine.routine.parameters)
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
for routine in routines
|
|
404
|
+
),
|
|
405
|
+
]
|
|
406
|
+
for table in tables:
|
|
407
|
+
scopes.extend(_table_scopes(table, _table_relations(table, graph)))
|
|
408
|
+
for enum in enums:
|
|
409
|
+
scopes.append(
|
|
410
|
+
NamingScope(
|
|
411
|
+
f"enum-members:{enum.key}",
|
|
412
|
+
PythonNameStyle.UPPER_SNAKE_CASE,
|
|
413
|
+
tuple(
|
|
414
|
+
NameRequest(str(index), Identifier(value))
|
|
415
|
+
for index, value in enumerate(enum.enum.values)
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
return tuple(scopes)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _table_relations(
|
|
423
|
+
table: _TableSource,
|
|
424
|
+
graph: RelationGraph,
|
|
425
|
+
) -> tuple[TypedRelation, ...]:
|
|
426
|
+
"""Both directions of every key that touches this table."""
|
|
427
|
+
return graph.relations_from(
|
|
428
|
+
ObjectReference(table.table.name, table.schema.name, table.catalog.name)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _table_scopes(
|
|
433
|
+
table: _TableSource,
|
|
434
|
+
relations: tuple[TypedRelation, ...],
|
|
435
|
+
) -> tuple[NamingScope, ...]:
|
|
436
|
+
column_requests = tuple(
|
|
437
|
+
NameRequest(str(index), column.name)
|
|
438
|
+
for index, column in enumerate(table.table.columns)
|
|
439
|
+
)
|
|
440
|
+
key_requests = tuple(
|
|
441
|
+
NameRequest(str(index), _key_type_identifier(key))
|
|
442
|
+
for index, key in enumerate(table.table.keys)
|
|
443
|
+
)
|
|
444
|
+
key_constant_requests = tuple(
|
|
445
|
+
NameRequest(str(index), _key_constant_identifier(key, index))
|
|
446
|
+
for index, key in enumerate(table.table.keys)
|
|
447
|
+
)
|
|
448
|
+
relation_requests = tuple(
|
|
449
|
+
NameRequest(str(index), _relation_identifier(relation))
|
|
450
|
+
for index, relation in enumerate(relations)
|
|
451
|
+
)
|
|
452
|
+
return (
|
|
453
|
+
NamingScope(
|
|
454
|
+
f"fields:{table.key}",
|
|
455
|
+
PythonNameStyle.SNAKE_CASE,
|
|
456
|
+
column_requests,
|
|
457
|
+
_FIELD_RESERVED_NAMES,
|
|
458
|
+
),
|
|
459
|
+
NamingScope(
|
|
460
|
+
f"columns:{table.key}",
|
|
461
|
+
PythonNameStyle.UPPER_SNAKE_CASE,
|
|
462
|
+
column_requests,
|
|
463
|
+
),
|
|
464
|
+
NamingScope(
|
|
465
|
+
f"key-types:{table.key}",
|
|
466
|
+
PythonNameStyle.PASCAL_CASE,
|
|
467
|
+
key_requests,
|
|
468
|
+
),
|
|
469
|
+
NamingScope(
|
|
470
|
+
f"key-constants:{table.key}",
|
|
471
|
+
PythonNameStyle.UPPER_SNAKE_CASE,
|
|
472
|
+
key_constant_requests,
|
|
473
|
+
),
|
|
474
|
+
NamingScope(
|
|
475
|
+
f"relations:{table.key}",
|
|
476
|
+
PythonNameStyle.UPPER_SNAKE_CASE,
|
|
477
|
+
relation_requests,
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _key_constant_identifier(key: Key, index: int) -> Identifier:
|
|
483
|
+
if key.name is not None:
|
|
484
|
+
return key.name
|
|
485
|
+
columns = "_".join(column.value for column in key.columns)
|
|
486
|
+
prefix = "primary" if key.kind is KeyKind.PRIMARY else "unique"
|
|
487
|
+
return Identifier(f"{prefix}_{columns}_{index}")
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _key_type_identifier(key: Key) -> Identifier:
|
|
491
|
+
if key.kind is KeyKind.PRIMARY:
|
|
492
|
+
return Identifier("primary_key")
|
|
493
|
+
columns = "_".join(column.value for column in key.columns)
|
|
494
|
+
return Identifier(f"{columns}_unique_key")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _relation_identifier(relation: TypedRelation) -> Identifier:
|
|
498
|
+
"""Name a relation after the key it follows, and say which way.
|
|
499
|
+
|
|
500
|
+
Both directions of one key land on one table when the key points at that
|
|
501
|
+
table itself, so the direction is always part of the name rather than only
|
|
502
|
+
when it happens to be needed.
|
|
503
|
+
"""
|
|
504
|
+
base = _relation_base_identifier(relation)
|
|
505
|
+
if relation.direction is RelationDirection.FORWARD:
|
|
506
|
+
return base
|
|
507
|
+
return Identifier(f"{base.value}_reverse")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _relation_base_identifier(relation: TypedRelation) -> Identifier:
|
|
511
|
+
"""Name the key by the table across it and the columns that carry it.
|
|
512
|
+
|
|
513
|
+
Nothing here depends on where the relation sits in a list, so adding a key
|
|
514
|
+
to some other table cannot rename the ones already generated.
|
|
515
|
+
"""
|
|
516
|
+
if relation.constraint is not None:
|
|
517
|
+
return relation.constraint
|
|
518
|
+
columns = "_".join(column.value for column in _referencing_columns(relation))
|
|
519
|
+
return Identifier(f"{relation.target.table.name.value}_{columns}")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _referencing_columns(relation: TypedRelation) -> tuple[Identifier, ...]:
|
|
523
|
+
"""The columns that carry the key, which always sit on the child side."""
|
|
524
|
+
if relation.direction is RelationDirection.FORWARD:
|
|
525
|
+
return relation.source.columns
|
|
526
|
+
return relation.target.columns
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _generated_enums(
|
|
530
|
+
sources: tuple[_EnumSource, ...],
|
|
531
|
+
names: NamingResult,
|
|
532
|
+
) -> tuple[GeneratedEnum, ...]:
|
|
533
|
+
return tuple(
|
|
534
|
+
GeneratedEnum(
|
|
535
|
+
source=source.enum,
|
|
536
|
+
schema_name=_identifier_value(source.schema.name),
|
|
537
|
+
catalog_name=_identifier_value(source.catalog.name),
|
|
538
|
+
class_name=names.get("enums", source.key),
|
|
539
|
+
members=tuple(
|
|
540
|
+
(
|
|
541
|
+
names.get(f"enum-members:{source.key}", str(index)),
|
|
542
|
+
value,
|
|
543
|
+
)
|
|
544
|
+
for index, value in enumerate(source.enum.values)
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
for source in sources
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _domain_sources(snapshot: SchemaSnapshot) -> tuple[_DomainSource, ...]:
|
|
552
|
+
sources: list[_DomainSource] = []
|
|
553
|
+
for catalog_index, catalog in enumerate(snapshot.catalogs):
|
|
554
|
+
for schema_index, schema in enumerate(catalog.schemas):
|
|
555
|
+
for domain_index, domain in enumerate(schema.domains):
|
|
556
|
+
key = _qualified_key(
|
|
557
|
+
catalog.name,
|
|
558
|
+
schema.name,
|
|
559
|
+
domain.name,
|
|
560
|
+
(catalog_index, schema_index, domain_index),
|
|
561
|
+
)
|
|
562
|
+
sources.append(_DomainSource(key, catalog, schema, domain))
|
|
563
|
+
return tuple(sorted(sources, key=lambda source: source.key))
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _generated_domains(
|
|
567
|
+
sources: tuple[_DomainSource, ...],
|
|
568
|
+
names: NamingResult,
|
|
569
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
570
|
+
) -> tuple[GeneratedDomain, ...]:
|
|
571
|
+
return tuple(
|
|
572
|
+
GeneratedDomain(
|
|
573
|
+
source=source.domain,
|
|
574
|
+
schema_name=_identifier_value(source.schema.name),
|
|
575
|
+
catalog_name=_identifier_value(source.catalog.name),
|
|
576
|
+
type_name=names.get("domains", source.key),
|
|
577
|
+
base_annotation=_python_annotation(
|
|
578
|
+
source.domain.data_type,
|
|
579
|
+
enum_names,
|
|
580
|
+
default_catalog=_identifier_value(source.catalog.name),
|
|
581
|
+
default_schema=_identifier_value(source.schema.name),
|
|
582
|
+
),
|
|
583
|
+
)
|
|
584
|
+
for source in sources
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _routine_sources(snapshot: SchemaSnapshot) -> tuple[_RoutineSource, ...]:
|
|
589
|
+
placed: list[tuple[tuple[int, int, int], Catalog, Schema, Routine]] = [
|
|
590
|
+
((catalog_index, schema_index, routine_index), catalog, schema, routine)
|
|
591
|
+
for catalog_index, catalog in enumerate(snapshot.catalogs)
|
|
592
|
+
for schema_index, schema in enumerate(catalog.schemas)
|
|
593
|
+
for routine_index, routine in enumerate(schema.routines)
|
|
594
|
+
]
|
|
595
|
+
shared = _shared_routine_names(placed)
|
|
596
|
+
sources: list[_RoutineSource] = []
|
|
597
|
+
for position, catalog, schema, routine in placed:
|
|
598
|
+
group = (
|
|
599
|
+
_identifier_value(catalog.name),
|
|
600
|
+
_identifier_value(schema.name),
|
|
601
|
+
routine.name.value,
|
|
602
|
+
)
|
|
603
|
+
is_shared = group in shared
|
|
604
|
+
sources.append(
|
|
605
|
+
_RoutineSource(
|
|
606
|
+
_qualified_key(
|
|
607
|
+
catalog.name,
|
|
608
|
+
schema.name,
|
|
609
|
+
_overloaded(routine, is_shared),
|
|
610
|
+
position,
|
|
611
|
+
),
|
|
612
|
+
catalog,
|
|
613
|
+
schema,
|
|
614
|
+
routine,
|
|
615
|
+
is_shared,
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
return tuple(sorted(sources, key=lambda source: source.key))
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _routine_name_requests(
|
|
622
|
+
routines: tuple[_RoutineSource, ...],
|
|
623
|
+
) -> tuple[NameRequest, ...]:
|
|
624
|
+
"""Every routine, and the shared names the calls between them are written under.
|
|
625
|
+
|
|
626
|
+
They are asked for together so that no shared name can collide with a
|
|
627
|
+
routine that happens to be spelled the same way.
|
|
628
|
+
"""
|
|
629
|
+
requests = [
|
|
630
|
+
NameRequest(routine.key, _overloaded(routine.routine, routine.shared_name))
|
|
631
|
+
for routine in routines
|
|
632
|
+
]
|
|
633
|
+
shared = {
|
|
634
|
+
routine.group: routine.routine.name
|
|
635
|
+
for routine in routines
|
|
636
|
+
if routine.shared_name
|
|
637
|
+
}
|
|
638
|
+
requests.extend(
|
|
639
|
+
NameRequest(_overload_key(group), name) for group, name in shared.items()
|
|
640
|
+
)
|
|
641
|
+
return tuple(requests)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _generated_overloads(
|
|
645
|
+
sources: tuple[_RoutineSource, ...],
|
|
646
|
+
generated: tuple[GeneratedRoutine, ...],
|
|
647
|
+
names: NamingResult,
|
|
648
|
+
) -> tuple[GeneratedOverload, ...]:
|
|
649
|
+
"""One call per shared name, where the calls can be told apart.
|
|
650
|
+
|
|
651
|
+
Two routines of the same name and the same number of parameters differ
|
|
652
|
+
only by the types of them, and nothing a Python call carries at runtime
|
|
653
|
+
separates those reliably, so no call is written for that name and each
|
|
654
|
+
routine keeps the name of its own.
|
|
655
|
+
"""
|
|
656
|
+
grouped: dict[tuple[str | None, str | None, str], list[GeneratedRoutine]] = {}
|
|
657
|
+
for source, routine in zip(sources, generated, strict=True):
|
|
658
|
+
if source.shared_name:
|
|
659
|
+
grouped.setdefault(source.group, []).append(routine)
|
|
660
|
+
overloads: list[GeneratedOverload] = []
|
|
661
|
+
for group, members in grouped.items():
|
|
662
|
+
ordered = sorted(members, key=lambda item: len(item.given_parameters))
|
|
663
|
+
if not _tellable_apart(ordered):
|
|
664
|
+
continue
|
|
665
|
+
overloads.append(
|
|
666
|
+
GeneratedOverload(
|
|
667
|
+
names.get("routines", _overload_key(group)), tuple(ordered)
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
return tuple(sorted(overloads, key=lambda item: item.function_name))
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _tellable_apart(ordered: list[GeneratedRoutine]) -> bool:
|
|
674
|
+
"""Whether one call written under the shared name can reach each of them.
|
|
675
|
+
|
|
676
|
+
Each routine must take a different number of values, so the call names
|
|
677
|
+
one, and the routines must agree on the type of every position they
|
|
678
|
+
share, so the one signature that carries them out states each once.
|
|
679
|
+
"""
|
|
680
|
+
arities = [len(routine.given_parameters) for routine in ordered]
|
|
681
|
+
if len(set(arities)) != len(arities):
|
|
682
|
+
return False
|
|
683
|
+
widest = ordered[-1].given_parameters
|
|
684
|
+
return all(
|
|
685
|
+
parameter.annotation == widest[position].annotation
|
|
686
|
+
for routine in ordered
|
|
687
|
+
for position, parameter in enumerate(routine.given_parameters)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _overload_key(group: tuple[str | None, str | None, str]) -> str:
|
|
692
|
+
"""The naming key the shared name itself is written under."""
|
|
693
|
+
catalog, schema, name = group
|
|
694
|
+
return f"overload:{catalog}.{schema}.{name}"
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _shared_routine_names(
|
|
698
|
+
placed: list[tuple[tuple[int, int, int], Catalog, Schema, Routine]],
|
|
699
|
+
) -> frozenset[tuple[str | None, str | None, str]]:
|
|
700
|
+
"""Names more than one routine of a schema is written under."""
|
|
701
|
+
seen: dict[tuple[str | None, str | None, str], int] = {}
|
|
702
|
+
for _, catalog, schema, routine in placed:
|
|
703
|
+
group = (
|
|
704
|
+
_identifier_value(catalog.name),
|
|
705
|
+
_identifier_value(schema.name),
|
|
706
|
+
routine.name.value,
|
|
707
|
+
)
|
|
708
|
+
seen[group] = seen.get(group, 0) + 1
|
|
709
|
+
return frozenset(group for group, count in seen.items() if count > 1)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _overloaded(routine: Routine, shared: bool) -> Identifier:
|
|
713
|
+
"""A name that separates two routines the database lets share one.
|
|
714
|
+
|
|
715
|
+
A shared name is never taken by one of the routines that share it, so the
|
|
716
|
+
call that chooses between them can be written under the name itself.
|
|
717
|
+
"""
|
|
718
|
+
if routine.overload is not None:
|
|
719
|
+
return Identifier(f"{routine.name.value} {routine.overload}")
|
|
720
|
+
if shared:
|
|
721
|
+
return Identifier(f"{routine.name.value} 1")
|
|
722
|
+
return routine.name
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _generated_routines(
|
|
726
|
+
sources: tuple[_RoutineSource, ...],
|
|
727
|
+
names: NamingResult,
|
|
728
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
729
|
+
domain_names: dict[tuple[str | None, str | None, str], str],
|
|
730
|
+
dialect: DatabaseDialect,
|
|
731
|
+
) -> tuple[GeneratedRoutine, ...]:
|
|
732
|
+
return tuple(
|
|
733
|
+
GeneratedRoutine(
|
|
734
|
+
source=source.routine,
|
|
735
|
+
schema_name=_identifier_value(source.schema.name),
|
|
736
|
+
catalog_name=_identifier_value(source.catalog.name),
|
|
737
|
+
function_name=names.get("routines", source.key),
|
|
738
|
+
parameters=tuple(
|
|
739
|
+
_generated_parameter(
|
|
740
|
+
source, parameter, index, names, enum_names, domain_names
|
|
741
|
+
)
|
|
742
|
+
for index, parameter in enumerate(source.routine.parameters)
|
|
743
|
+
),
|
|
744
|
+
returns_annotation=(
|
|
745
|
+
None
|
|
746
|
+
if source.routine.returns is None
|
|
747
|
+
else _routine_annotation(
|
|
748
|
+
source, source.routine.returns, enum_names, domain_names
|
|
749
|
+
)
|
|
750
|
+
),
|
|
751
|
+
dialect=dialect,
|
|
752
|
+
)
|
|
753
|
+
for source in sources
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _generated_parameter(
|
|
758
|
+
source: _RoutineSource,
|
|
759
|
+
parameter: RoutineParameter,
|
|
760
|
+
index: int,
|
|
761
|
+
names: NamingResult,
|
|
762
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
763
|
+
domain_names: dict[tuple[str | None, str | None, str], str],
|
|
764
|
+
) -> GeneratedRoutineParameter:
|
|
765
|
+
written = _routine_annotation(source, parameter.data_type, enum_names, domain_names)
|
|
766
|
+
return GeneratedRoutineParameter(
|
|
767
|
+
source=parameter,
|
|
768
|
+
field_name=names.get(f"routine-parameters:{source.key}", str(index)),
|
|
769
|
+
annotation=f"{written} | None" if parameter.nullable else written,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _routine_annotation(
|
|
774
|
+
source: _RoutineSource,
|
|
775
|
+
data_type: SqlType,
|
|
776
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
777
|
+
domain_names: dict[tuple[str | None, str | None, str], str],
|
|
778
|
+
) -> str:
|
|
779
|
+
return _python_annotation(
|
|
780
|
+
data_type,
|
|
781
|
+
enum_names,
|
|
782
|
+
default_catalog=_identifier_value(source.catalog.name),
|
|
783
|
+
default_schema=_identifier_value(source.schema.name),
|
|
784
|
+
domain_names=domain_names,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _domain_name_map(
|
|
789
|
+
domains: tuple[GeneratedDomain, ...],
|
|
790
|
+
) -> dict[tuple[str | None, str | None, str], str]:
|
|
791
|
+
return {
|
|
792
|
+
(
|
|
793
|
+
domain.catalog_name,
|
|
794
|
+
domain.schema_name,
|
|
795
|
+
domain.source.name.value,
|
|
796
|
+
): domain.type_name
|
|
797
|
+
for domain in domains
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _enum_name_map(
|
|
802
|
+
enums: tuple[GeneratedEnum, ...],
|
|
803
|
+
) -> dict[tuple[str | None, str | None, str], str]:
|
|
804
|
+
return {
|
|
805
|
+
(enum.catalog_name, enum.schema_name, enum.source.name.value): enum.class_name
|
|
806
|
+
for enum in enums
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _generated_table(
|
|
811
|
+
source: _TableSource,
|
|
812
|
+
names: NamingResult,
|
|
813
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
814
|
+
domain_names: dict[tuple[str | None, str | None, str], str],
|
|
815
|
+
table_row_names: dict[tuple[str | None, str | None, str], str],
|
|
816
|
+
graph: RelationGraph,
|
|
817
|
+
) -> GeneratedTable:
|
|
818
|
+
class_name = names.get("tables", source.key)
|
|
819
|
+
columns = tuple(
|
|
820
|
+
_generated_column(source, column, index, names, enum_names, domain_names)
|
|
821
|
+
for index, column in enumerate(source.table.columns)
|
|
822
|
+
)
|
|
823
|
+
column_map = {column.source.name: column for column in columns}
|
|
824
|
+
keys = tuple(
|
|
825
|
+
_generated_key(source, key, index, names, column_map, class_name)
|
|
826
|
+
for index, key in enumerate(source.table.keys)
|
|
827
|
+
)
|
|
828
|
+
relations = tuple(
|
|
829
|
+
GeneratedRelation(
|
|
830
|
+
relation,
|
|
831
|
+
(
|
|
832
|
+
f"{names.get('table-constants', source.key)}_"
|
|
833
|
+
f"{names.get(f'relations:{source.key}', str(index))}"
|
|
834
|
+
),
|
|
835
|
+
_target_row_name(relation.target.table, table_row_names, source),
|
|
836
|
+
)
|
|
837
|
+
for index, relation in enumerate(_table_relations(source, graph))
|
|
838
|
+
)
|
|
839
|
+
return GeneratedTable(
|
|
840
|
+
source=source.table,
|
|
841
|
+
schema_name=_identifier_value(source.schema.name),
|
|
842
|
+
catalog_name=_identifier_value(source.catalog.name),
|
|
843
|
+
class_name=class_name,
|
|
844
|
+
constant_name=names.get("table-constants", source.key),
|
|
845
|
+
row_name=f"{class_name}Row",
|
|
846
|
+
insert_name=f"{class_name}Insert",
|
|
847
|
+
update_name=f"{class_name}Update",
|
|
848
|
+
builder_name=f"{class_name}Builder",
|
|
849
|
+
complete_builder_name=f"_{class_name}CompleteBuilder",
|
|
850
|
+
update_builder_name=f"{class_name}UpdateBuilder",
|
|
851
|
+
columns=columns,
|
|
852
|
+
keys=keys,
|
|
853
|
+
relations=relations,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _generated_column(
|
|
858
|
+
table: _TableSource,
|
|
859
|
+
column: Column,
|
|
860
|
+
index: int,
|
|
861
|
+
names: NamingResult,
|
|
862
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
863
|
+
domain_names: dict[tuple[str | None, str | None, str], str],
|
|
864
|
+
) -> GeneratedColumn:
|
|
865
|
+
return GeneratedColumn(
|
|
866
|
+
source=column,
|
|
867
|
+
field_name=names.get(f"fields:{table.key}", str(index)),
|
|
868
|
+
constant_name=names.get(f"columns:{table.key}", str(index)),
|
|
869
|
+
annotation=_python_annotation(
|
|
870
|
+
column.data_type,
|
|
871
|
+
enum_names,
|
|
872
|
+
default_catalog=_identifier_value(table.catalog.name),
|
|
873
|
+
default_schema=_identifier_value(table.schema.name),
|
|
874
|
+
domain_names=domain_names,
|
|
875
|
+
),
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _generated_key(
|
|
880
|
+
table: _TableSource,
|
|
881
|
+
key: Key,
|
|
882
|
+
index: int,
|
|
883
|
+
names: NamingResult,
|
|
884
|
+
columns: dict[Identifier, GeneratedColumn],
|
|
885
|
+
table_name: str,
|
|
886
|
+
) -> GeneratedKey:
|
|
887
|
+
suffix = names.get(f"key-types:{table.key}", str(index))
|
|
888
|
+
return GeneratedKey(
|
|
889
|
+
source=key,
|
|
890
|
+
type_name=f"{table_name}{suffix}",
|
|
891
|
+
constant_name=names.get(f"key-constants:{table.key}", str(index)),
|
|
892
|
+
columns=tuple(columns[column] for column in key.columns),
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _python_annotation(
|
|
897
|
+
sql_type: SqlType,
|
|
898
|
+
enum_names: dict[tuple[str | None, str | None, str], str],
|
|
899
|
+
*,
|
|
900
|
+
default_catalog: str | None,
|
|
901
|
+
default_schema: str | None,
|
|
902
|
+
domain_names: dict[tuple[str | None, str | None, str], str] | None = None,
|
|
903
|
+
) -> str:
|
|
904
|
+
scalar_annotations = {
|
|
905
|
+
TypeKind.BOOLEAN: "bool",
|
|
906
|
+
TypeKind.SMALL_INTEGER: "int",
|
|
907
|
+
TypeKind.INTEGER: "int",
|
|
908
|
+
TypeKind.BIG_INTEGER: "int",
|
|
909
|
+
TypeKind.DECIMAL: "Decimal",
|
|
910
|
+
TypeKind.REAL: "float",
|
|
911
|
+
TypeKind.DOUBLE: "float",
|
|
912
|
+
TypeKind.STRING: "str",
|
|
913
|
+
TypeKind.BINARY: "bytes",
|
|
914
|
+
TypeKind.DATE: "date",
|
|
915
|
+
TypeKind.TIME: "time",
|
|
916
|
+
TypeKind.DATETIME: "datetime",
|
|
917
|
+
TypeKind.INTERVAL: "timedelta",
|
|
918
|
+
TypeKind.UUID: "UUID",
|
|
919
|
+
TypeKind.JSON: "JsonValue",
|
|
920
|
+
TypeKind.OTHER: "object",
|
|
921
|
+
}
|
|
922
|
+
if sql_type.kind is TypeKind.DOMAIN and sql_type.element_type is not None:
|
|
923
|
+
named = _named_reference(
|
|
924
|
+
sql_type.named_type,
|
|
925
|
+
domain_names,
|
|
926
|
+
default_catalog=default_catalog,
|
|
927
|
+
default_schema=default_schema,
|
|
928
|
+
)
|
|
929
|
+
if named is not None:
|
|
930
|
+
return named
|
|
931
|
+
return _python_annotation(
|
|
932
|
+
sql_type.element_type,
|
|
933
|
+
enum_names,
|
|
934
|
+
default_catalog=default_catalog,
|
|
935
|
+
default_schema=default_schema,
|
|
936
|
+
)
|
|
937
|
+
if sql_type.kind is TypeKind.ARRAY and sql_type.element_type is not None:
|
|
938
|
+
element = _python_annotation(
|
|
939
|
+
sql_type.element_type,
|
|
940
|
+
enum_names,
|
|
941
|
+
default_catalog=default_catalog,
|
|
942
|
+
default_schema=default_schema,
|
|
943
|
+
)
|
|
944
|
+
return f"tuple[{element}, ...]"
|
|
945
|
+
if sql_type.kind is TypeKind.ENUM and sql_type.named_type is not None:
|
|
946
|
+
named = _named_reference(
|
|
947
|
+
sql_type.named_type,
|
|
948
|
+
enum_names,
|
|
949
|
+
default_catalog=default_catalog,
|
|
950
|
+
default_schema=default_schema,
|
|
951
|
+
)
|
|
952
|
+
return named if named is not None else "str"
|
|
953
|
+
return scalar_annotations[sql_type.kind]
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _named_reference(
|
|
957
|
+
reference: ObjectReference | None,
|
|
958
|
+
names: dict[tuple[str | None, str | None, str], str] | None,
|
|
959
|
+
*,
|
|
960
|
+
default_catalog: str | None,
|
|
961
|
+
default_schema: str | None,
|
|
962
|
+
) -> str | None:
|
|
963
|
+
"""What a generated artifact of this named type is called, if one exists."""
|
|
964
|
+
if reference is None or not names:
|
|
965
|
+
return None
|
|
966
|
+
key = (
|
|
967
|
+
_identifier_value(reference.catalog) or default_catalog,
|
|
968
|
+
_identifier_value(reference.schema) or default_schema,
|
|
969
|
+
reference.name.value,
|
|
970
|
+
)
|
|
971
|
+
return names.get(key)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _target_row_name(
|
|
975
|
+
reference: ObjectReference,
|
|
976
|
+
table_row_names: dict[tuple[str | None, str | None, str], str],
|
|
977
|
+
source: _TableSource,
|
|
978
|
+
) -> str:
|
|
979
|
+
catalog_name = reference.catalog or source.catalog.name
|
|
980
|
+
schema_name = reference.schema or source.schema.name
|
|
981
|
+
prefix = (
|
|
982
|
+
_identifier_value(catalog_name),
|
|
983
|
+
_identifier_value(schema_name),
|
|
984
|
+
reference.name.value,
|
|
985
|
+
)
|
|
986
|
+
return table_row_names.get(prefix, "object")
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _identifier_value(identifier: Identifier | None) -> str | None:
|
|
990
|
+
return identifier.value if identifier is not None else None
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
__all__ = (
|
|
994
|
+
"GeneratedColumn",
|
|
995
|
+
"GeneratedEnum",
|
|
996
|
+
"GeneratedKey",
|
|
997
|
+
"GeneratedPackage",
|
|
998
|
+
"GeneratedRelation",
|
|
999
|
+
"GeneratedTable",
|
|
1000
|
+
"build_codegen_package",
|
|
1001
|
+
)
|