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,396 @@
|
|
|
1
|
+
"""Generated stored routine call rendering."""
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
|
|
5
|
+
from pyoq.generation.contracts import RenderedFile
|
|
6
|
+
from pyoq.generation.model import (
|
|
7
|
+
GeneratedOverload,
|
|
8
|
+
GeneratedPackage,
|
|
9
|
+
GeneratedRoutine,
|
|
10
|
+
GeneratedRoutineParameter,
|
|
11
|
+
)
|
|
12
|
+
from pyoq.generation.rendering.imports import plan_annotation_imports
|
|
13
|
+
from pyoq.generation.rendering.source import (
|
|
14
|
+
PythonSource,
|
|
15
|
+
literal,
|
|
16
|
+
render_all,
|
|
17
|
+
render_from_import,
|
|
18
|
+
)
|
|
19
|
+
from pyoq.schema import ParameterMode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render_routines(package: GeneratedPackage) -> RenderedFile:
|
|
23
|
+
"""A routine as a call whose signature is the database's, not a caller's.
|
|
24
|
+
|
|
25
|
+
Arity, parameter order, and types come from the catalog, so a call that
|
|
26
|
+
no longer matches the routine stops type checking rather than failing at
|
|
27
|
+
the server.
|
|
28
|
+
"""
|
|
29
|
+
source = PythonSource()
|
|
30
|
+
source.line('"""Generated database routine calls."""')
|
|
31
|
+
if package.routines:
|
|
32
|
+
_render_imports(source, package)
|
|
33
|
+
for routine in package.routines:
|
|
34
|
+
if routine.returns_annotation is not None:
|
|
35
|
+
source.line()
|
|
36
|
+
_render_declaration(source, routine)
|
|
37
|
+
for routine in package.routines:
|
|
38
|
+
_render_routine(source, routine)
|
|
39
|
+
for overload in package.overloads:
|
|
40
|
+
_render_overload(source, overload)
|
|
41
|
+
source.line()
|
|
42
|
+
if package.routines:
|
|
43
|
+
source.line()
|
|
44
|
+
source.lines(
|
|
45
|
+
render_all(
|
|
46
|
+
tuple(routine.function_name for routine in package.routines)
|
|
47
|
+
+ tuple(item.function_name for item in package.overloads)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return RenderedFile(PurePosixPath("routines.py"), source.render())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _render_imports(source: PythonSource, package: GeneratedPackage) -> None:
|
|
54
|
+
annotations = tuple(
|
|
55
|
+
annotation
|
|
56
|
+
for routine in package.routines
|
|
57
|
+
for annotation in _annotations(routine)
|
|
58
|
+
)
|
|
59
|
+
imports = plan_annotation_imports(annotations, package)
|
|
60
|
+
source.line()
|
|
61
|
+
source.line("from __future__ import annotations")
|
|
62
|
+
source.line()
|
|
63
|
+
if package.overloads:
|
|
64
|
+
source.line("from typing import overload")
|
|
65
|
+
source.line()
|
|
66
|
+
errors = _error_import_names(package)
|
|
67
|
+
if errors:
|
|
68
|
+
source.lines(render_from_import("pyoq.errors", tuple(errors)))
|
|
69
|
+
if imports.standard:
|
|
70
|
+
source.lines(imports.standard)
|
|
71
|
+
source.line()
|
|
72
|
+
if imports.descriptor_names:
|
|
73
|
+
source.lines(render_from_import("pyoq.descriptors", imports.descriptor_names))
|
|
74
|
+
source.lines(render_from_import("pyoq.query", _query_import_names(package)))
|
|
75
|
+
if any(
|
|
76
|
+
item.sql_parameters
|
|
77
|
+
for item in package.routines
|
|
78
|
+
if item.unsupported_reason is None
|
|
79
|
+
):
|
|
80
|
+
source.lines(render_from_import("pyoq.query.routines", ("argument",)))
|
|
81
|
+
if package.overloads or any(_has_defaults(item) for item in package.routines):
|
|
82
|
+
source.lines(render_from_import("pyoq.unset", ("UNSET", "UnsetType")))
|
|
83
|
+
if imports.enum_names:
|
|
84
|
+
source.line()
|
|
85
|
+
source.lines(render_from_import(".enums", imports.enum_names))
|
|
86
|
+
if imports.domain_names:
|
|
87
|
+
source.line()
|
|
88
|
+
source.lines(render_from_import(".domains", imports.domain_names))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _error_import_names(package: GeneratedPackage) -> tuple[str, ...]:
|
|
92
|
+
names: list[str] = []
|
|
93
|
+
if any(routine.unsupported_reason is not None for routine in package.routines):
|
|
94
|
+
names.append("UnsupportedQueryError")
|
|
95
|
+
if any(_needs_default_gap_guard(routine) for routine in package.routines):
|
|
96
|
+
names.append("QueryValidationError")
|
|
97
|
+
return tuple(names)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _query_import_names(package: GeneratedPackage) -> tuple[str, ...]:
|
|
101
|
+
routines = package.routines
|
|
102
|
+
functions = tuple(item for item in routines if item.returns_annotation is not None)
|
|
103
|
+
procedures = tuple(item for item in routines if item.returns_annotation is None)
|
|
104
|
+
supported = tuple(item for item in procedures if item.unsupported_reason is None)
|
|
105
|
+
names: list[str] = []
|
|
106
|
+
if any(item.given_parameters for item in routines):
|
|
107
|
+
names.append("Expression")
|
|
108
|
+
if functions:
|
|
109
|
+
names.extend(("ComputedExpression", "vendor_function"))
|
|
110
|
+
if procedures:
|
|
111
|
+
names.append("Call")
|
|
112
|
+
if supported:
|
|
113
|
+
names.append("call")
|
|
114
|
+
if any(item.outputs for item in supported):
|
|
115
|
+
names.append("routine_output")
|
|
116
|
+
if any(_has_defaults(item) for item in routines):
|
|
117
|
+
names.append("NodeProvider")
|
|
118
|
+
return tuple(names)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _annotations(routine: GeneratedRoutine) -> tuple[str, ...]:
|
|
122
|
+
"""What the input signature and output descriptor write."""
|
|
123
|
+
given = tuple(parameter.annotation for parameter in routine.given_parameters)
|
|
124
|
+
outputs = tuple(parameter.annotation for parameter in routine.outputs)
|
|
125
|
+
if routine.returns_annotation is None:
|
|
126
|
+
return (*given, *outputs)
|
|
127
|
+
return (*given, *outputs, routine.returns_annotation)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _render_routine(source: PythonSource, routine: GeneratedRoutine) -> None:
|
|
131
|
+
source.line()
|
|
132
|
+
source.line()
|
|
133
|
+
given = routine.given_parameters
|
|
134
|
+
_render_signature(source, routine.function_name, given, _returns(routine))
|
|
135
|
+
if routine.unsupported_reason is not None:
|
|
136
|
+
source.line(f' """Refuse {routine.source.name.value} before SQL is sent."""')
|
|
137
|
+
source.line(" raise UnsupportedQueryError(")
|
|
138
|
+
source.line(f" {literal(routine.unsupported_reason)}")
|
|
139
|
+
source.line(" )")
|
|
140
|
+
return
|
|
141
|
+
if _has_defaults(routine):
|
|
142
|
+
_render_dynamic_call(source, routine)
|
|
143
|
+
return
|
|
144
|
+
arguments = ", ".join(
|
|
145
|
+
_sql_argument(parameter) for parameter in routine.sql_parameters
|
|
146
|
+
)
|
|
147
|
+
named = literal(routine.source.name.value)
|
|
148
|
+
if routine.returns_annotation is None:
|
|
149
|
+
source.line(f' """Run {routine.source.name.value}."""')
|
|
150
|
+
_render_run(source, routine, named, arguments, given)
|
|
151
|
+
return
|
|
152
|
+
source.line(f' """Ask {routine.source.name.value} for a value."""')
|
|
153
|
+
source.line(f" return _{routine.function_name}({arguments})")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _render_overload(source: PythonSource, overload: GeneratedOverload) -> None:
|
|
157
|
+
"""One name for routines a database lets share one, chosen by the call.
|
|
158
|
+
|
|
159
|
+
The declarations give a call site the exact signature it wrote, and the
|
|
160
|
+
one that carries them out reads the parameters no shorter call was given
|
|
161
|
+
as unset, so nothing is asserted and nothing is suppressed.
|
|
162
|
+
"""
|
|
163
|
+
widest = overload.routines[-1].given_parameters
|
|
164
|
+
for member in overload.routines:
|
|
165
|
+
source.line()
|
|
166
|
+
source.line()
|
|
167
|
+
source.line("@overload")
|
|
168
|
+
_render_signature(
|
|
169
|
+
source,
|
|
170
|
+
overload.function_name,
|
|
171
|
+
member.given_parameters,
|
|
172
|
+
_returns(member),
|
|
173
|
+
body="...",
|
|
174
|
+
)
|
|
175
|
+
source.line()
|
|
176
|
+
source.line()
|
|
177
|
+
_render_optional_signature(
|
|
178
|
+
source, overload, widest, len(overload.routines[0].given_parameters)
|
|
179
|
+
)
|
|
180
|
+
named = overload.routines[0].source.name.value
|
|
181
|
+
source.line(f' """Call {named}, whichever of them the arguments name."""')
|
|
182
|
+
for member in overload.routines[:-1]:
|
|
183
|
+
count = len(member.given_parameters)
|
|
184
|
+
source.line(f" if {widest[count].field_name} is UNSET:")
|
|
185
|
+
given = ", ".join(item.field_name for item in member.given_parameters)
|
|
186
|
+
source.line(f" return {member.function_name}({given})")
|
|
187
|
+
last = overload.routines[-1]
|
|
188
|
+
given = ", ".join(item.field_name for item in last.given_parameters)
|
|
189
|
+
source.line(f" return {last.function_name}({given})")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _render_optional_signature(
|
|
193
|
+
source: PythonSource,
|
|
194
|
+
overload: GeneratedOverload,
|
|
195
|
+
widest: tuple[GeneratedRoutineParameter, ...],
|
|
196
|
+
required: int,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Everything past the shortest call is unset until it is given."""
|
|
199
|
+
source.line(f"def {overload.function_name}(")
|
|
200
|
+
for position, parameter in enumerate(widest):
|
|
201
|
+
annotation = parameter.annotation
|
|
202
|
+
written = f"Expression[{annotation}] | {annotation}"
|
|
203
|
+
if position < required:
|
|
204
|
+
source.line(f" {parameter.field_name}: {written},")
|
|
205
|
+
else:
|
|
206
|
+
source.line(f" {parameter.field_name}: {written} | UnsetType = UNSET,")
|
|
207
|
+
source.line(f") -> {_shared_return(overload)}:")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _shared_return(overload: GeneratedOverload) -> str:
|
|
211
|
+
"""What the call answers with, which is any of what its routines answer with."""
|
|
212
|
+
written = sorted({_returns(member) for member in overload.routines})
|
|
213
|
+
return " | ".join(written)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _returns(routine: GeneratedRoutine) -> str:
|
|
217
|
+
"""What a call answers with: a value for a function, a row for a procedure."""
|
|
218
|
+
if routine.returns_annotation is not None:
|
|
219
|
+
return f"ComputedExpression[{routine.returns_annotation}]"
|
|
220
|
+
outputs = routine.outputs
|
|
221
|
+
if not outputs:
|
|
222
|
+
return "Call[object]"
|
|
223
|
+
written = ", ".join(parameter.annotation for parameter in outputs)
|
|
224
|
+
return f"Call[tuple[{written}]]"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _render_signature(
|
|
228
|
+
source: PythonSource,
|
|
229
|
+
function_name: str,
|
|
230
|
+
given: tuple[GeneratedRoutineParameter, ...],
|
|
231
|
+
returns: str,
|
|
232
|
+
*,
|
|
233
|
+
body: str | None = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""One parameter per line, because a signature has no width to spare."""
|
|
236
|
+
if not given:
|
|
237
|
+
source.line(f"def {function_name}() -> {returns}:{_body(body)}")
|
|
238
|
+
return
|
|
239
|
+
source.line(f"def {function_name}(")
|
|
240
|
+
for parameter in given:
|
|
241
|
+
annotation = parameter.annotation
|
|
242
|
+
written = f"Expression[{annotation}] | {annotation}"
|
|
243
|
+
if parameter.source.has_default:
|
|
244
|
+
written = f"{written} | UnsetType = UNSET"
|
|
245
|
+
source.line(f" {parameter.field_name}: {written},")
|
|
246
|
+
source.line(f") -> {returns}:{_body(body)}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _body(body: str | None) -> str:
|
|
250
|
+
return "" if body is None else f" {body}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _render_declaration(source: PythonSource, routine: GeneratedRoutine) -> None:
|
|
254
|
+
"""A function the catalog declared, named where the catalog keeps it.
|
|
255
|
+
|
|
256
|
+
Written a line at a time where it says a schema, because one line of it
|
|
257
|
+
would be longer than a generated file is allowed to hold.
|
|
258
|
+
"""
|
|
259
|
+
name = f"_{routine.function_name}"
|
|
260
|
+
named = literal(routine.source.name.value)
|
|
261
|
+
kept = _kept_in(routine)
|
|
262
|
+
if not kept:
|
|
263
|
+
source.line(f"{name} = vendor_function({routine.returns_annotation}, {named})")
|
|
264
|
+
return
|
|
265
|
+
source.line(f"{name} = vendor_function(")
|
|
266
|
+
source.line(f" {routine.returns_annotation},")
|
|
267
|
+
source.line(f" {named},")
|
|
268
|
+
for part in kept:
|
|
269
|
+
source.line(f" {part},")
|
|
270
|
+
source.line(")")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _render_run(
|
|
274
|
+
source: PythonSource,
|
|
275
|
+
routine: GeneratedRoutine,
|
|
276
|
+
named: str,
|
|
277
|
+
arguments: str,
|
|
278
|
+
given: tuple[GeneratedRoutineParameter, ...],
|
|
279
|
+
) -> None:
|
|
280
|
+
"""A procedure run where the catalog keeps it, one line at a time if long."""
|
|
281
|
+
kept = _kept_in(routine)
|
|
282
|
+
answered = _answered_with(routine)
|
|
283
|
+
if not kept and not answered:
|
|
284
|
+
source.line(f" return call({named}{_comma(given)}{arguments})")
|
|
285
|
+
return
|
|
286
|
+
source.line(" return call(")
|
|
287
|
+
source.line(f" {named},")
|
|
288
|
+
for parameter in routine.sql_parameters:
|
|
289
|
+
source.line(f" {_sql_argument(parameter)},")
|
|
290
|
+
for part in kept:
|
|
291
|
+
source.line(f" {part},")
|
|
292
|
+
source.line(f" ){answered}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _answered_with(routine: GeneratedRoutine) -> str:
|
|
296
|
+
"""What the procedure answers through, which is its OUT and INOUT values.
|
|
297
|
+
|
|
298
|
+
A procedure that reads every parameter answers with nothing, and one that
|
|
299
|
+
writes any of them answers with a row of those, named as the catalog names
|
|
300
|
+
them and read back as the types it declared.
|
|
301
|
+
"""
|
|
302
|
+
outputs = routine.outputs
|
|
303
|
+
if not outputs:
|
|
304
|
+
return ""
|
|
305
|
+
expressions = tuple(
|
|
306
|
+
f"routine_output({parameter.annotation.removesuffix(' | None')}, "
|
|
307
|
+
f"{literal(_output_name(parameter))}, nullable={parameter.source.nullable})"
|
|
308
|
+
for parameter in outputs
|
|
309
|
+
)
|
|
310
|
+
if len(expressions) == 1:
|
|
311
|
+
return f".returning({expressions[0]})"
|
|
312
|
+
written = "\n".join(f" {expression}," for expression in expressions)
|
|
313
|
+
return f".returning(\n{written}\n )"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _render_dynamic_call(source: PythonSource, routine: GeneratedRoutine) -> None:
|
|
317
|
+
"""Build only the trailing arguments whose defaults callers override."""
|
|
318
|
+
source.line(" arguments: list[NodeProvider] = []")
|
|
319
|
+
previous_default: GeneratedRoutineParameter | None = None
|
|
320
|
+
for parameter in routine.sql_parameters:
|
|
321
|
+
if parameter.source.mode is ParameterMode.OUT:
|
|
322
|
+
source.line(" arguments.append(argument(None))")
|
|
323
|
+
continue
|
|
324
|
+
if parameter.source.has_default:
|
|
325
|
+
_render_optional_argument(source, parameter, previous_default)
|
|
326
|
+
previous_default = parameter
|
|
327
|
+
continue
|
|
328
|
+
source.line(f" arguments.append(argument({parameter.field_name}))")
|
|
329
|
+
if routine.returns_annotation is not None:
|
|
330
|
+
source.line(f" return _{routine.function_name}(*arguments)")
|
|
331
|
+
return
|
|
332
|
+
named = literal(routine.source.name.value)
|
|
333
|
+
kept = _kept_in(routine)
|
|
334
|
+
keywords = "".join(f", {part}" for part in kept)
|
|
335
|
+
source.line(
|
|
336
|
+
f" return call({named}, *arguments{keywords}){_answered_with(routine)}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _render_optional_argument(
|
|
341
|
+
source: PythonSource,
|
|
342
|
+
parameter: GeneratedRoutineParameter,
|
|
343
|
+
previous: GeneratedRoutineParameter | None,
|
|
344
|
+
) -> None:
|
|
345
|
+
if previous is not None:
|
|
346
|
+
source.line(
|
|
347
|
+
f" if {previous.field_name} is UNSET and "
|
|
348
|
+
f"{parameter.field_name} is not UNSET:"
|
|
349
|
+
)
|
|
350
|
+
source.line(
|
|
351
|
+
' raise QueryValidationError("routine defaults cannot be skipped")'
|
|
352
|
+
)
|
|
353
|
+
source.line(f" if {parameter.field_name} is not UNSET:")
|
|
354
|
+
source.line(f" arguments.append(argument({parameter.field_name}))")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _has_defaults(routine: GeneratedRoutine) -> bool:
|
|
358
|
+
return any(parameter.source.has_default for parameter in routine.given_parameters)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _needs_default_gap_guard(routine: GeneratedRoutine) -> bool:
|
|
362
|
+
return (
|
|
363
|
+
sum(parameter.source.has_default for parameter in routine.given_parameters) > 1
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _output_name(parameter: GeneratedRoutineParameter) -> str:
|
|
368
|
+
"""What the answered column is called, which is the parameter's own name."""
|
|
369
|
+
named = parameter.source.name
|
|
370
|
+
return parameter.field_name if named is None else named.value
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _kept_in(routine: GeneratedRoutine) -> tuple[str, ...]:
|
|
374
|
+
"""Where the catalog says the routine is, written as the call's own words.
|
|
375
|
+
|
|
376
|
+
A routine outside the search path resolves to nothing, and one whose name
|
|
377
|
+
another schema shares resolves to the wrong one, so the schema the catalog
|
|
378
|
+
gave is carried into every generated call.
|
|
379
|
+
"""
|
|
380
|
+
if routine.schema_name is None:
|
|
381
|
+
return ()
|
|
382
|
+
return (f"schema={literal(routine.schema_name)}",)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _sql_argument(parameter: GeneratedRoutineParameter) -> str:
|
|
386
|
+
"""A supplied value, or the placeholder PostgreSQL requires for OUT."""
|
|
387
|
+
if parameter.source.mode is ParameterMode.OUT:
|
|
388
|
+
return "argument(None)"
|
|
389
|
+
return f"argument({parameter.field_name})"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _comma(parameters: tuple[object, ...]) -> str:
|
|
393
|
+
return ", " if parameters else ""
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
__all__ = ("render_routines",)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Generated immutable row and key source rendering."""
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
|
|
5
|
+
from pyoq.generation.contracts import RenderedFile
|
|
6
|
+
from pyoq.generation.model import GeneratedKey, GeneratedPackage, GeneratedTable
|
|
7
|
+
from pyoq.generation.rendering.imports import plan_annotation_imports
|
|
8
|
+
from pyoq.generation.rendering.source import (
|
|
9
|
+
PythonSource,
|
|
10
|
+
render_all,
|
|
11
|
+
render_from_import,
|
|
12
|
+
within_limit,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_rows(package: GeneratedPackage) -> RenderedFile:
|
|
17
|
+
source = PythonSource()
|
|
18
|
+
source.line('"""Generated immutable row and key value types."""')
|
|
19
|
+
source.line()
|
|
20
|
+
source.line("from __future__ import annotations")
|
|
21
|
+
if package.tables:
|
|
22
|
+
source.line()
|
|
23
|
+
source.line("from dataclasses import dataclass")
|
|
24
|
+
imports = plan_annotation_imports(_annotations(package), package)
|
|
25
|
+
source.lines(imports.standard)
|
|
26
|
+
if imports.descriptor_names:
|
|
27
|
+
source.line()
|
|
28
|
+
source.lines(
|
|
29
|
+
render_from_import("pyoq.descriptors", imports.descriptor_names)
|
|
30
|
+
)
|
|
31
|
+
if imports.enum_names:
|
|
32
|
+
source.line()
|
|
33
|
+
source.lines(render_from_import(".enums", imports.enum_names))
|
|
34
|
+
if imports.domain_names:
|
|
35
|
+
source.line()
|
|
36
|
+
source.lines(render_from_import(".domains", imports.domain_names))
|
|
37
|
+
for table in package.tables:
|
|
38
|
+
_render_row(source, table)
|
|
39
|
+
for key in table.keys:
|
|
40
|
+
_render_key(source, key)
|
|
41
|
+
source.line()
|
|
42
|
+
source.line()
|
|
43
|
+
source.lines(render_all(_exported_names(package)))
|
|
44
|
+
return RenderedFile(PurePosixPath("rows.py"), source.render())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _annotations(package: GeneratedPackage) -> tuple[str, ...]:
|
|
48
|
+
return tuple(
|
|
49
|
+
column.value_annotation for table in package.tables for column in table.columns
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _render_row(source: PythonSource, table: GeneratedTable) -> None:
|
|
54
|
+
source.line()
|
|
55
|
+
source.line()
|
|
56
|
+
source.line("@dataclass(frozen=True, slots=True)")
|
|
57
|
+
source.line(within_limit(f"class {table.row_name}:"))
|
|
58
|
+
for column in table.columns:
|
|
59
|
+
source.line(f" {column.field_name}: {column.value_annotation}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _render_key(source: PythonSource, key: GeneratedKey) -> None:
|
|
63
|
+
source.line()
|
|
64
|
+
source.line()
|
|
65
|
+
source.line("@dataclass(frozen=True, slots=True)")
|
|
66
|
+
source.line(within_limit(f"class {key.type_name}:"))
|
|
67
|
+
for column in key.columns:
|
|
68
|
+
source.line(f" {column.field_name}: {column.value_annotation}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _exported_names(package: GeneratedPackage) -> tuple[str, ...]:
|
|
72
|
+
names: list[str] = []
|
|
73
|
+
for table in package.tables:
|
|
74
|
+
names.append(table.row_name)
|
|
75
|
+
names.extend(key.type_name for key in table.keys)
|
|
76
|
+
return tuple(names)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ("render_rows",)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Deterministic Python source assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
LINE_LIMIT: Final = 88
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class PythonSource:
|
|
14
|
+
_lines: list[str] = field(default_factory=list[str])
|
|
15
|
+
|
|
16
|
+
def line(self, value: str = "") -> None:
|
|
17
|
+
self._lines.append(value.rstrip())
|
|
18
|
+
|
|
19
|
+
def lines(self, values: tuple[str, ...]) -> None:
|
|
20
|
+
for value in values:
|
|
21
|
+
self.line(value)
|
|
22
|
+
|
|
23
|
+
def render(self) -> str:
|
|
24
|
+
return "\n".join(self._lines).rstrip() + "\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def literal(value: str | None) -> str:
|
|
28
|
+
return "None" if value is None else json.dumps(value, ensure_ascii=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_string_tuple(values: tuple[str, ...]) -> str:
|
|
32
|
+
rendered = ", ".join(literal(value) for value in values)
|
|
33
|
+
trailing_comma = "," if len(values) == 1 else ""
|
|
34
|
+
return f"({rendered}{trailing_comma})"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_all(names: tuple[str, ...]) -> tuple[str, ...]:
|
|
38
|
+
if not names:
|
|
39
|
+
return ("__all__ = ()",)
|
|
40
|
+
if len(names) == 1:
|
|
41
|
+
return (f'__all__ = ("{names[0]}",)',)
|
|
42
|
+
lines = ["__all__ = ("]
|
|
43
|
+
lines.extend(f' "{name}",' for name in sorted(names, key=_export_name_key))
|
|
44
|
+
lines.append(")")
|
|
45
|
+
return tuple(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_from_import(module: str, names: tuple[str, ...]) -> tuple[str, ...]:
|
|
49
|
+
ordered_names = tuple(sorted(set(names), key=_import_name_key))
|
|
50
|
+
if not ordered_names:
|
|
51
|
+
return ()
|
|
52
|
+
inline = f"from {module} import {', '.join(ordered_names)}"
|
|
53
|
+
if len(inline) <= LINE_LIMIT:
|
|
54
|
+
return (inline,)
|
|
55
|
+
lines = [f"from {module} import ("]
|
|
56
|
+
lines.extend(f" {name}," for name in ordered_names)
|
|
57
|
+
lines.append(")")
|
|
58
|
+
return tuple(lines)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def within_limit(line: str) -> str:
|
|
62
|
+
"""Suppress the length rule on a line only a longer name could shorten.
|
|
63
|
+
|
|
64
|
+
A formatter breaks this line where it can and still leaves it over the
|
|
65
|
+
limit, because what overflows is an identifier the database chose. A line
|
|
66
|
+
that is one unbroken word is already exempt from the rule, so suppressing
|
|
67
|
+
it there would leave a directive that reports itself as unused.
|
|
68
|
+
"""
|
|
69
|
+
if len(line) <= LINE_LIMIT or " " not in line.strip():
|
|
70
|
+
return line
|
|
71
|
+
return f"{line} # noqa: E501"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render_subscript(
|
|
75
|
+
prefix: str,
|
|
76
|
+
name: str,
|
|
77
|
+
arguments: tuple[str, ...],
|
|
78
|
+
*,
|
|
79
|
+
argument_indent: str,
|
|
80
|
+
closing_indent: str,
|
|
81
|
+
suffix: str,
|
|
82
|
+
) -> tuple[str, ...]:
|
|
83
|
+
inline = f"{prefix}{name}[{', '.join(arguments)}]{suffix}"
|
|
84
|
+
if len(inline) <= LINE_LIMIT:
|
|
85
|
+
return (inline,)
|
|
86
|
+
lines = [within_limit(f"{prefix}{name}[")]
|
|
87
|
+
separator = "" if len(arguments) == 1 else ","
|
|
88
|
+
lines.extend(f"{argument_indent}{argument}{separator}" for argument in arguments)
|
|
89
|
+
lines.append(f"{closing_indent}]{suffix}")
|
|
90
|
+
return tuple(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _export_name_key(name: str) -> tuple[int, str]:
|
|
94
|
+
"""Order `__all__` the way a linter checking it expects to find it.
|
|
95
|
+
|
|
96
|
+
Same grouping as an import block, but comparing letters as written rather
|
|
97
|
+
than folded, because that is the order the check is written against.
|
|
98
|
+
"""
|
|
99
|
+
return _import_name_key(name)[0], name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _import_name_key(name: str) -> tuple[int, str]:
|
|
103
|
+
if name.isupper():
|
|
104
|
+
category = 0
|
|
105
|
+
elif name[0].isupper():
|
|
106
|
+
category = 1
|
|
107
|
+
else:
|
|
108
|
+
category = 2
|
|
109
|
+
return category, name.casefold()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = (
|
|
113
|
+
"LINE_LIMIT",
|
|
114
|
+
"PythonSource",
|
|
115
|
+
"literal",
|
|
116
|
+
"render_all",
|
|
117
|
+
"render_from_import",
|
|
118
|
+
"render_string_tuple",
|
|
119
|
+
"render_subscript",
|
|
120
|
+
"within_limit",
|
|
121
|
+
)
|