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,466 @@
|
|
|
1
|
+
"""PostgreSQL metadata reflection into the immutable schema model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
|
+
|
|
10
|
+
from pyoq.config import DatabaseDialect
|
|
11
|
+
from pyoq.errors import SchemaInspectionError
|
|
12
|
+
from pyoq.schema.models import (
|
|
13
|
+
Capability,
|
|
14
|
+
CapabilitySet,
|
|
15
|
+
Catalog,
|
|
16
|
+
CheckConstraint,
|
|
17
|
+
Column,
|
|
18
|
+
Domain,
|
|
19
|
+
EnumType,
|
|
20
|
+
GeneratedValue,
|
|
21
|
+
Identifier,
|
|
22
|
+
Index,
|
|
23
|
+
IndexTerm,
|
|
24
|
+
Key,
|
|
25
|
+
KeyKind,
|
|
26
|
+
ObjectReference,
|
|
27
|
+
ParameterMode,
|
|
28
|
+
Relation,
|
|
29
|
+
Routine,
|
|
30
|
+
RoutineKind,
|
|
31
|
+
RoutineParameter,
|
|
32
|
+
Schema,
|
|
33
|
+
SchemaSnapshot,
|
|
34
|
+
SqlExpression,
|
|
35
|
+
SqlType,
|
|
36
|
+
Table,
|
|
37
|
+
TypeKind,
|
|
38
|
+
View,
|
|
39
|
+
)
|
|
40
|
+
from pyoq.schema.postgres import sql
|
|
41
|
+
from pyoq.schema.postgres.records import (
|
|
42
|
+
ColumnRecord,
|
|
43
|
+
ConstraintRecord,
|
|
44
|
+
DomainCheckRecord,
|
|
45
|
+
DomainRecord,
|
|
46
|
+
EnumRecord,
|
|
47
|
+
IndexRecord,
|
|
48
|
+
ObjectRecord,
|
|
49
|
+
RoutineRecord,
|
|
50
|
+
read_column,
|
|
51
|
+
read_constraint,
|
|
52
|
+
read_domain,
|
|
53
|
+
read_domain_check,
|
|
54
|
+
read_enum,
|
|
55
|
+
read_index,
|
|
56
|
+
read_object,
|
|
57
|
+
read_routine,
|
|
58
|
+
)
|
|
59
|
+
from pyoq.schema.postgres.types import postgres_type
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from psycopg import Connection
|
|
63
|
+
|
|
64
|
+
_ROUTINE_KINDS = {"f": RoutineKind.FUNCTION, "p": RoutineKind.PROCEDURE}
|
|
65
|
+
_PARAMETER_MODES = {
|
|
66
|
+
"i": ParameterMode.IN,
|
|
67
|
+
"o": ParameterMode.OUT,
|
|
68
|
+
"b": ParameterMode.INOUT,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_DOMAIN_TYPE = "d"
|
|
72
|
+
_ENUM_TYPE = "e"
|
|
73
|
+
|
|
74
|
+
Record = TypeVar("Record")
|
|
75
|
+
GroupKey = TypeVar("GroupKey")
|
|
76
|
+
|
|
77
|
+
_CAPABILITIES = CapabilitySet(
|
|
78
|
+
frozenset(
|
|
79
|
+
{
|
|
80
|
+
Capability.ARRAYS,
|
|
81
|
+
Capability.CHECK_CONSTRAINTS,
|
|
82
|
+
Capability.ENUMS,
|
|
83
|
+
Capability.EXPRESSION_INDEXES,
|
|
84
|
+
Capability.GENERATED_COLUMNS,
|
|
85
|
+
Capability.IDENTITY_COLUMNS,
|
|
86
|
+
Capability.MATERIALIZED_VIEWS,
|
|
87
|
+
Capability.PARTIAL_INDEXES,
|
|
88
|
+
Capability.SCHEMAS,
|
|
89
|
+
Capability.VIEWS,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class PostgresSchemaInspector:
|
|
97
|
+
def inspect(
|
|
98
|
+
self,
|
|
99
|
+
connection: Connection[tuple[object, ...]],
|
|
100
|
+
) -> SchemaSnapshot:
|
|
101
|
+
catalog = _read(connection)
|
|
102
|
+
schemas = tuple(_schema(name, catalog) for name in sorted(catalog.schema_names))
|
|
103
|
+
return SchemaSnapshot(
|
|
104
|
+
DatabaseDialect.POSTGRES,
|
|
105
|
+
(Catalog(None, schemas),),
|
|
106
|
+
_CAPABILITIES,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
111
|
+
class _Catalog:
|
|
112
|
+
"""Grouped catalog rows. An index, not a value, so it does not compare."""
|
|
113
|
+
|
|
114
|
+
objects: tuple[ObjectRecord, ...]
|
|
115
|
+
enums: dict[str, list[EnumRecord]]
|
|
116
|
+
domains: dict[str, list[DomainRecord]]
|
|
117
|
+
domain_checks: dict[tuple[str, str], list[DomainCheckRecord]]
|
|
118
|
+
routines: dict[str, list[RoutineRecord]]
|
|
119
|
+
columns: dict[tuple[str, str], list[ColumnRecord]]
|
|
120
|
+
constraints: dict[tuple[str, str], list[ConstraintRecord]]
|
|
121
|
+
indexes: dict[tuple[str, str], list[IndexRecord]]
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def schema_names(self) -> set[str]:
|
|
125
|
+
"""Every schema that holds anything, not only tables and views.
|
|
126
|
+
|
|
127
|
+
A schema of nothing but domains, enums, or routines is still a schema,
|
|
128
|
+
and leaving it out would lose everything it defines.
|
|
129
|
+
"""
|
|
130
|
+
return (
|
|
131
|
+
{record.schema for record in self.objects}
|
|
132
|
+
| set(self.enums)
|
|
133
|
+
| set(self.domains)
|
|
134
|
+
| set(self.routines)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _read(connection: Connection[tuple[object, ...]]) -> _Catalog:
|
|
139
|
+
try:
|
|
140
|
+
objects = tuple(
|
|
141
|
+
read_object(row) for row in connection.execute(sql.SCHEMA_OBJECTS)
|
|
142
|
+
)
|
|
143
|
+
columns = tuple(read_column(row) for row in connection.execute(sql.COLUMNS))
|
|
144
|
+
constraints = tuple(
|
|
145
|
+
read_constraint(row) for row in connection.execute(sql.CONSTRAINTS)
|
|
146
|
+
)
|
|
147
|
+
indexes = tuple(read_index(row) for row in connection.execute(sql.INDEXES))
|
|
148
|
+
enums = tuple(read_enum(row) for row in connection.execute(sql.ENUMS))
|
|
149
|
+
domains = tuple(read_domain(row) for row in connection.execute(sql.DOMAINS))
|
|
150
|
+
checks = tuple(
|
|
151
|
+
read_domain_check(row) for row in connection.execute(sql.DOMAIN_CHECKS)
|
|
152
|
+
)
|
|
153
|
+
routines = tuple(read_routine(row) for row in connection.execute(sql.ROUTINES))
|
|
154
|
+
except Exception as error:
|
|
155
|
+
message = f"unable to inspect PostgreSQL schema: {error}"
|
|
156
|
+
raise SchemaInspectionError(message) from error
|
|
157
|
+
return _Catalog(
|
|
158
|
+
objects,
|
|
159
|
+
_grouped(enums, lambda record: record.schema),
|
|
160
|
+
_grouped(domains, lambda record: record.schema),
|
|
161
|
+
_grouped(checks, lambda record: (record.schema, record.domain)),
|
|
162
|
+
_grouped(routines, lambda record: record.schema),
|
|
163
|
+
_grouped(columns, lambda record: (record.schema, record.table)),
|
|
164
|
+
_grouped(constraints, lambda record: (record.schema, record.table)),
|
|
165
|
+
_grouped(indexes, lambda record: (record.schema, record.table)),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _schema(name: str, catalog: _Catalog) -> Schema:
|
|
170
|
+
objects = tuple(record for record in catalog.objects if record.schema == name)
|
|
171
|
+
tables = tuple(_table(record, catalog) for record in objects if record.is_table)
|
|
172
|
+
views = tuple(_view(record, catalog) for record in objects if not record.is_table)
|
|
173
|
+
return Schema(
|
|
174
|
+
Identifier(name),
|
|
175
|
+
tables,
|
|
176
|
+
views,
|
|
177
|
+
_enums(catalog, name),
|
|
178
|
+
_domains(catalog, name),
|
|
179
|
+
_routines(catalog, name),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _domains(catalog: _Catalog, schema: str) -> tuple[Domain, ...]:
|
|
184
|
+
"""Every domain the schema defines, with the rules that belong to it."""
|
|
185
|
+
return tuple(
|
|
186
|
+
Domain(
|
|
187
|
+
Identifier(record.name),
|
|
188
|
+
postgres_type(record.base_type),
|
|
189
|
+
not record.not_null,
|
|
190
|
+
None if record.default is None else SqlExpression(record.default),
|
|
191
|
+
_domain_checks(catalog, schema, record.name),
|
|
192
|
+
record.comment,
|
|
193
|
+
)
|
|
194
|
+
for record in catalog.domains.get(schema, [])
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _domain_checks(
|
|
199
|
+
catalog: _Catalog,
|
|
200
|
+
schema: str,
|
|
201
|
+
domain: str,
|
|
202
|
+
) -> tuple[CheckConstraint, ...]:
|
|
203
|
+
return tuple(
|
|
204
|
+
CheckConstraint(SqlExpression(record.definition), Identifier(record.name))
|
|
205
|
+
for record in catalog.domain_checks.get((schema, domain), [])
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _routines(catalog: _Catalog, schema: str) -> tuple[Routine, ...]:
|
|
210
|
+
"""Every routine the schema keeps, one entry per routine rather than row."""
|
|
211
|
+
grouped: dict[int, list[RoutineRecord]] = defaultdict(list)
|
|
212
|
+
for record in catalog.routines.get(schema, []):
|
|
213
|
+
grouped[record.identifier].append(record)
|
|
214
|
+
named: dict[str, int] = defaultdict(int)
|
|
215
|
+
routines: list[Routine] = []
|
|
216
|
+
for records in grouped.values():
|
|
217
|
+
first = records[0]
|
|
218
|
+
named[first.name] += 1
|
|
219
|
+
routines.append(_routine(records, named[first.name]))
|
|
220
|
+
return tuple(routines)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _routine(records: list[RoutineRecord], seen: int) -> Routine:
|
|
224
|
+
first = records[0]
|
|
225
|
+
kind = _ROUTINE_KINDS[first.kind]
|
|
226
|
+
defaulted = _defaulted_parameter_positions(records, first.default_count)
|
|
227
|
+
return Routine(
|
|
228
|
+
Identifier(first.name),
|
|
229
|
+
kind,
|
|
230
|
+
tuple(
|
|
231
|
+
_parameter(record, record.position in defaulted)
|
|
232
|
+
for record in records
|
|
233
|
+
if record.position is not None
|
|
234
|
+
),
|
|
235
|
+
(
|
|
236
|
+
None
|
|
237
|
+
if first.returns is None
|
|
238
|
+
else _routine_type(
|
|
239
|
+
first.returns,
|
|
240
|
+
first.return_type_schema,
|
|
241
|
+
first.return_type_name,
|
|
242
|
+
first.return_type_kind,
|
|
243
|
+
first.return_base_type,
|
|
244
|
+
first.return_is_array,
|
|
245
|
+
)
|
|
246
|
+
),
|
|
247
|
+
None if seen == 1 else str(seen),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _defaulted_parameter_positions(
|
|
252
|
+
records: list[RoutineRecord], default_count: int
|
|
253
|
+
) -> frozenset[int]:
|
|
254
|
+
inputs = [
|
|
255
|
+
record.position
|
|
256
|
+
for record in records
|
|
257
|
+
if record.position is not None and record.mode in {"i", "b"}
|
|
258
|
+
]
|
|
259
|
+
return frozenset(inputs[-default_count:]) if default_count else frozenset()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _parameter(record: RoutineRecord, has_default: bool) -> RoutineParameter:
|
|
263
|
+
return RoutineParameter(
|
|
264
|
+
None if record.parameter_name is None else Identifier(record.parameter_name),
|
|
265
|
+
_routine_type(
|
|
266
|
+
record.parameter_type or "text",
|
|
267
|
+
record.parameter_type_schema,
|
|
268
|
+
record.parameter_type_name,
|
|
269
|
+
record.parameter_type_kind,
|
|
270
|
+
record.parameter_base_type,
|
|
271
|
+
record.parameter_is_array,
|
|
272
|
+
),
|
|
273
|
+
_PARAMETER_MODES.get(record.mode, ParameterMode.IN),
|
|
274
|
+
has_default,
|
|
275
|
+
nullable=not record.parameter_forbids_null,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _routine_type(
|
|
280
|
+
declared: str,
|
|
281
|
+
schema: str | None,
|
|
282
|
+
name: str | None,
|
|
283
|
+
kind: str | None,
|
|
284
|
+
base: str | None,
|
|
285
|
+
is_array: bool,
|
|
286
|
+
) -> SqlType:
|
|
287
|
+
"""A routine type, retaining named enum and domain identities."""
|
|
288
|
+
scalar = _routine_scalar_type(declared, schema, name, kind, base)
|
|
289
|
+
if not is_array:
|
|
290
|
+
return scalar
|
|
291
|
+
return SqlType(TypeKind.ARRAY, declared, element_type=scalar)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _routine_scalar_type(
|
|
295
|
+
declared: str,
|
|
296
|
+
schema: str | None,
|
|
297
|
+
name: str | None,
|
|
298
|
+
kind: str | None,
|
|
299
|
+
base: str | None,
|
|
300
|
+
) -> SqlType:
|
|
301
|
+
if kind not in {_ENUM_TYPE, _DOMAIN_TYPE} or schema is None or name is None:
|
|
302
|
+
return postgres_type(declared)
|
|
303
|
+
reference = ObjectReference(Identifier(name), Identifier(schema))
|
|
304
|
+
if kind == _ENUM_TYPE:
|
|
305
|
+
return SqlType(TypeKind.ENUM, declared, named_type=reference)
|
|
306
|
+
return SqlType(
|
|
307
|
+
TypeKind.DOMAIN,
|
|
308
|
+
declared,
|
|
309
|
+
element_type=postgres_type(base or "text"),
|
|
310
|
+
named_type=reference,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _enums(catalog: _Catalog, schema: str) -> tuple[EnumType, ...]:
|
|
315
|
+
"""Every enum the schema defines, each with its labels in the type's order."""
|
|
316
|
+
labels: dict[str, list[str]] = {}
|
|
317
|
+
comments: dict[str, str | None] = {}
|
|
318
|
+
for record in catalog.enums.get(schema, []):
|
|
319
|
+
labels.setdefault(record.name, []).append(record.label)
|
|
320
|
+
comments.setdefault(record.name, record.comment)
|
|
321
|
+
return tuple(
|
|
322
|
+
EnumType(Identifier(name), tuple(values), comments[name])
|
|
323
|
+
for name, values in sorted(labels.items())
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _table(record: ObjectRecord, catalog: _Catalog) -> Table:
|
|
328
|
+
location = (record.schema, record.name)
|
|
329
|
+
constraints = catalog.constraints.get(location, [])
|
|
330
|
+
return Table(
|
|
331
|
+
Identifier(record.name),
|
|
332
|
+
_columns(catalog, location),
|
|
333
|
+
tuple(
|
|
334
|
+
_key(item, kind)
|
|
335
|
+
for item in constraints
|
|
336
|
+
if (kind := item.key_kind) is not None
|
|
337
|
+
),
|
|
338
|
+
tuple(
|
|
339
|
+
_relation(item, target)
|
|
340
|
+
for item in constraints
|
|
341
|
+
if item.kind == "f" and (target := item.target_reference) is not None
|
|
342
|
+
),
|
|
343
|
+
tuple(_index_model(item) for item in catalog.indexes.get(location, [])),
|
|
344
|
+
tuple(_check(item) for item in constraints if item.kind == "c"),
|
|
345
|
+
record.comment,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _view(record: ObjectRecord, catalog: _Catalog) -> View:
|
|
350
|
+
definition = (
|
|
351
|
+
None if record.definition is None else SqlExpression(record.definition.strip())
|
|
352
|
+
)
|
|
353
|
+
return View(
|
|
354
|
+
Identifier(record.name),
|
|
355
|
+
_columns(catalog, (record.schema, record.name)),
|
|
356
|
+
definition,
|
|
357
|
+
record.is_materialized,
|
|
358
|
+
record.comment,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _columns(catalog: _Catalog, location: tuple[str, str]) -> tuple[Column, ...]:
|
|
363
|
+
return tuple(_column(record) for record in catalog.columns.get(location, []))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _column(record: ColumnRecord) -> Column:
|
|
367
|
+
kind = record.generated_kind
|
|
368
|
+
expression = None if record.default is None else SqlExpression(record.default)
|
|
369
|
+
generated = None if kind is None else GeneratedValue(kind, expression)
|
|
370
|
+
return Column(
|
|
371
|
+
Identifier(record.name),
|
|
372
|
+
_data_type(record),
|
|
373
|
+
record.nullable,
|
|
374
|
+
None if generated is not None else expression,
|
|
375
|
+
generated,
|
|
376
|
+
record.comment,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _data_type(record: ColumnRecord) -> SqlType:
|
|
381
|
+
"""What a column holds, as the type it was declared with rather than as text.
|
|
382
|
+
|
|
383
|
+
A domain is its base type with rules attached, so the base is kept as what
|
|
384
|
+
says which Python type reads it back. An enum is its own type with its own
|
|
385
|
+
values, so it is named rather than reduced to the string it is stored as.
|
|
386
|
+
Both keep their name: a schema that forgot it would no longer describe the
|
|
387
|
+
database it was read from.
|
|
388
|
+
"""
|
|
389
|
+
if record.type_kind == _ENUM_TYPE:
|
|
390
|
+
return SqlType(
|
|
391
|
+
TypeKind.ENUM, record.data_type, named_type=_type_reference(record)
|
|
392
|
+
)
|
|
393
|
+
if record.type_kind != _DOMAIN_TYPE or record.domain_base is None:
|
|
394
|
+
return postgres_type(record.data_type)
|
|
395
|
+
return SqlType(
|
|
396
|
+
TypeKind.DOMAIN,
|
|
397
|
+
record.data_type,
|
|
398
|
+
element_type=postgres_type(record.domain_base),
|
|
399
|
+
named_type=_type_reference(record),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _type_reference(record: ColumnRecord) -> ObjectReference:
|
|
404
|
+
return ObjectReference(Identifier(record.type_name), Identifier(record.type_schema))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _key(record: ConstraintRecord, kind: KeyKind) -> Key:
|
|
408
|
+
return Key(
|
|
409
|
+
kind,
|
|
410
|
+
tuple(Identifier(name) for name in record.columns),
|
|
411
|
+
Identifier(record.name),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _relation(record: ConstraintRecord, target: ObjectReference) -> Relation:
|
|
416
|
+
return Relation(
|
|
417
|
+
tuple(Identifier(name) for name in record.columns),
|
|
418
|
+
target,
|
|
419
|
+
tuple(Identifier(name) for name in record.target_columns),
|
|
420
|
+
Identifier(record.name),
|
|
421
|
+
record.update_action,
|
|
422
|
+
record.delete_action,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _check(record: ConstraintRecord) -> CheckConstraint:
|
|
427
|
+
return CheckConstraint(
|
|
428
|
+
SqlExpression(record.definition),
|
|
429
|
+
Identifier(record.name),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _index_model(record: IndexRecord) -> Index:
|
|
434
|
+
terms = tuple(
|
|
435
|
+
_index_term(record, position)
|
|
436
|
+
for position in range(len(record.attribute_numbers))
|
|
437
|
+
)
|
|
438
|
+
predicate = None if record.predicate is None else SqlExpression(record.predicate)
|
|
439
|
+
return Index(
|
|
440
|
+
Identifier(record.name),
|
|
441
|
+
terms,
|
|
442
|
+
record.unique,
|
|
443
|
+
predicate,
|
|
444
|
+
record.method,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _index_term(record: IndexRecord, position: int) -> IndexTerm:
|
|
449
|
+
descending = record.descending(position)
|
|
450
|
+
name = record.column_names[position]
|
|
451
|
+
if record.attribute_numbers[position] > 0 and name is not None:
|
|
452
|
+
return IndexTerm(Identifier(name), None, descending)
|
|
453
|
+
return IndexTerm(None, SqlExpression(record.definitions[position]), descending)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _grouped(
|
|
457
|
+
records: tuple[Record, ...],
|
|
458
|
+
key: Callable[[Record], GroupKey],
|
|
459
|
+
) -> dict[GroupKey, list[Record]]:
|
|
460
|
+
grouped: dict[GroupKey, list[Record]] = defaultdict(list)
|
|
461
|
+
for record in records:
|
|
462
|
+
grouped[key(record)].append(record)
|
|
463
|
+
return grouped
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
__all__ = ("PostgresSchemaInspector",)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuration-backed PostgreSQL schema source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from pyoq.config import Configuration, DatabaseDialect
|
|
8
|
+
from pyoq.errors import OperationUnavailableError
|
|
9
|
+
from pyoq.schema.models import SchemaSnapshot
|
|
10
|
+
from pyoq.schema.postgres.connection import PostgresConnectionProvider
|
|
11
|
+
from pyoq.schema.postgres.reflection import PostgresSchemaInspector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class PostgresSchemaSource:
|
|
16
|
+
connections: PostgresConnectionProvider = field(
|
|
17
|
+
default_factory=PostgresConnectionProvider
|
|
18
|
+
)
|
|
19
|
+
inspector: PostgresSchemaInspector = field(default_factory=PostgresSchemaInspector)
|
|
20
|
+
|
|
21
|
+
def load(self, configuration: Configuration) -> SchemaSnapshot:
|
|
22
|
+
dialect = configuration.active_profile.dialect
|
|
23
|
+
if dialect is not DatabaseDialect.POSTGRES:
|
|
24
|
+
message = f"PostgreSQL inspection is not available for {dialect.value}"
|
|
25
|
+
raise OperationUnavailableError(message)
|
|
26
|
+
with self.connections.open(configuration) as connection:
|
|
27
|
+
return self.inspector.inspect(connection)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ("PostgresSchemaSource",)
|