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,309 @@
|
|
|
1
|
+
"""MySQL 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, Protocol, TypeVar, cast
|
|
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
|
+
EnumType,
|
|
19
|
+
Identifier,
|
|
20
|
+
Index,
|
|
21
|
+
Key,
|
|
22
|
+
KeyKind,
|
|
23
|
+
ObjectReference,
|
|
24
|
+
ParameterMode,
|
|
25
|
+
Relation,
|
|
26
|
+
Routine,
|
|
27
|
+
RoutineKind,
|
|
28
|
+
RoutineParameter,
|
|
29
|
+
Schema,
|
|
30
|
+
SchemaSnapshot,
|
|
31
|
+
SqlExpression,
|
|
32
|
+
Table,
|
|
33
|
+
View,
|
|
34
|
+
)
|
|
35
|
+
from pyoq.schema.mysql import sql
|
|
36
|
+
from pyoq.schema.mysql.records import (
|
|
37
|
+
CheckRecord,
|
|
38
|
+
ColumnRecord,
|
|
39
|
+
ConstraintRecord,
|
|
40
|
+
IndexRecord,
|
|
41
|
+
ObjectRecord,
|
|
42
|
+
RoutineRecord,
|
|
43
|
+
read_check,
|
|
44
|
+
read_column,
|
|
45
|
+
read_constraint,
|
|
46
|
+
read_index,
|
|
47
|
+
read_object,
|
|
48
|
+
read_routine,
|
|
49
|
+
)
|
|
50
|
+
from pyoq.schema.mysql.types import mysql_type
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from pymysql.connections import Connection
|
|
54
|
+
from pymysql.cursors import Cursor
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NamedRecord(Protocol):
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Record = TypeVar("Record")
|
|
63
|
+
Named = TypeVar("Named", bound=NamedRecord)
|
|
64
|
+
|
|
65
|
+
_CAPABILITIES = CapabilitySet(
|
|
66
|
+
frozenset(
|
|
67
|
+
{
|
|
68
|
+
Capability.CHECK_CONSTRAINTS,
|
|
69
|
+
Capability.EXPRESSION_INDEXES,
|
|
70
|
+
Capability.GENERATED_COLUMNS,
|
|
71
|
+
Capability.IDENTITY_COLUMNS,
|
|
72
|
+
Capability.SCHEMAS,
|
|
73
|
+
Capability.VIEWS,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class MySQLSchemaInspector:
|
|
81
|
+
def inspect(self, connection: Connection[Cursor]) -> SchemaSnapshot:
|
|
82
|
+
catalog = _read(connection)
|
|
83
|
+
return SchemaSnapshot(
|
|
84
|
+
DatabaseDialect.MYSQL,
|
|
85
|
+
(Catalog(None, (_schema(catalog),)),),
|
|
86
|
+
_CAPABILITIES,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
91
|
+
class _Catalog:
|
|
92
|
+
"""Grouped catalog rows. An index, not a value, so it does not compare."""
|
|
93
|
+
|
|
94
|
+
database: str
|
|
95
|
+
objects: tuple[ObjectRecord, ...]
|
|
96
|
+
columns: dict[str, list[ColumnRecord]]
|
|
97
|
+
constraints: dict[str, list[ConstraintRecord]]
|
|
98
|
+
checks: dict[str, list[CheckRecord]]
|
|
99
|
+
indexes: dict[str, list[IndexRecord]]
|
|
100
|
+
routines: dict[str, list[RoutineRecord]]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _read(connection: Connection[Cursor]) -> _Catalog:
|
|
104
|
+
try:
|
|
105
|
+
database = _database_name(connection)
|
|
106
|
+
objects = tuple(map(read_object, _rows(connection, sql.SCHEMA_OBJECTS)))
|
|
107
|
+
columns = tuple(map(read_column, _rows(connection, sql.COLUMNS)))
|
|
108
|
+
constraints = tuple(map(read_constraint, _rows(connection, sql.CONSTRAINTS)))
|
|
109
|
+
checks = tuple(map(read_check, _rows(connection, sql.CHECKS)))
|
|
110
|
+
indexes = tuple(map(read_index, _rows(connection, sql.INDEXES)))
|
|
111
|
+
routines = tuple(map(read_routine, _rows(connection, sql.ROUTINES)))
|
|
112
|
+
except SchemaInspectionError:
|
|
113
|
+
raise
|
|
114
|
+
except Exception as error:
|
|
115
|
+
message = f"unable to inspect MySQL schema: {error}"
|
|
116
|
+
raise SchemaInspectionError(message) from error
|
|
117
|
+
return _Catalog(
|
|
118
|
+
database,
|
|
119
|
+
objects,
|
|
120
|
+
_grouped(columns, lambda record: record.table),
|
|
121
|
+
_grouped(constraints, lambda record: record.table),
|
|
122
|
+
_grouped(checks, lambda record: record.table),
|
|
123
|
+
_grouped(indexes, lambda record: record.table),
|
|
124
|
+
_grouped(routines, lambda record: record.name),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _database_name(connection: Connection[Cursor]) -> str:
|
|
129
|
+
rows = _rows(connection, sql.DATABASE_NAME)
|
|
130
|
+
name = rows[0][0] if rows and rows[0] else None
|
|
131
|
+
if not isinstance(name, str) or name == "":
|
|
132
|
+
message = "MySQL inspection requires a connection with a selected database"
|
|
133
|
+
raise SchemaInspectionError(message)
|
|
134
|
+
return name
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _rows(
|
|
138
|
+
connection: Connection[Cursor],
|
|
139
|
+
query: str,
|
|
140
|
+
) -> tuple[tuple[object, ...], ...]:
|
|
141
|
+
with connection.cursor() as cursor:
|
|
142
|
+
cursor.execute(query)
|
|
143
|
+
return cast("tuple[tuple[object, ...], ...]", cursor.fetchall())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _schema(catalog: _Catalog) -> Schema:
|
|
147
|
+
objects = catalog.objects
|
|
148
|
+
tables = tuple(_table(item, catalog) for item in objects if item.is_table)
|
|
149
|
+
views = tuple(_view(item, catalog) for item in objects if not item.is_table)
|
|
150
|
+
return Schema(
|
|
151
|
+
Identifier(catalog.database),
|
|
152
|
+
tables,
|
|
153
|
+
views,
|
|
154
|
+
_enums(catalog),
|
|
155
|
+
routines=_routines(catalog),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_PARAMETER_MODES = {
|
|
160
|
+
"IN": ParameterMode.IN,
|
|
161
|
+
"OUT": ParameterMode.OUT,
|
|
162
|
+
"INOUT": ParameterMode.INOUT,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _routines(catalog: _Catalog) -> tuple[Routine, ...]:
|
|
167
|
+
"""Every procedure and function this database keeps, with its parameters.
|
|
168
|
+
|
|
169
|
+
MySQL refuses a second routine of one name, so a name identifies a routine
|
|
170
|
+
on its own and nothing here separates overloads.
|
|
171
|
+
"""
|
|
172
|
+
return tuple(
|
|
173
|
+
_routine(records) for _, records in sorted(catalog.routines.items()) if records
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _routine(records: list[RoutineRecord]) -> Routine:
|
|
178
|
+
first = records[0]
|
|
179
|
+
function = first.kind.upper() == "FUNCTION"
|
|
180
|
+
kind = RoutineKind.FUNCTION if function else RoutineKind.PROCEDURE
|
|
181
|
+
returns = mysql_type(first.returns) if function and first.returns else None
|
|
182
|
+
return Routine(
|
|
183
|
+
Identifier(first.name),
|
|
184
|
+
kind,
|
|
185
|
+
tuple(
|
|
186
|
+
_routine_parameter(record)
|
|
187
|
+
for record in records
|
|
188
|
+
if record.position is not None
|
|
189
|
+
),
|
|
190
|
+
returns,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _routine_parameter(record: RoutineRecord) -> RoutineParameter:
|
|
195
|
+
"""MySQL states no nullability for a parameter, so one may hold nothing."""
|
|
196
|
+
return RoutineParameter(
|
|
197
|
+
None if record.parameter_name is None else Identifier(record.parameter_name),
|
|
198
|
+
mysql_type(record.parameter_type or "text"),
|
|
199
|
+
_PARAMETER_MODES.get((record.mode or "IN").upper(), ParameterMode.IN),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _enums(catalog: _Catalog) -> tuple[EnumType, ...]:
|
|
204
|
+
"""Every column-level enum, named for the column that carries it."""
|
|
205
|
+
enums = [
|
|
206
|
+
EnumType(record.enum_name, record.enum_values, record.comment)
|
|
207
|
+
for records in catalog.columns.values()
|
|
208
|
+
for record in records
|
|
209
|
+
if record.enum_values
|
|
210
|
+
]
|
|
211
|
+
return tuple(sorted(enums, key=lambda enum: enum.name.value))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _table(record: ObjectRecord, catalog: _Catalog) -> Table:
|
|
215
|
+
constraints = _by_name(catalog.constraints.get(record.name, []))
|
|
216
|
+
indexes = _by_name(catalog.indexes.get(record.name, []))
|
|
217
|
+
return Table(
|
|
218
|
+
Identifier(record.name),
|
|
219
|
+
_columns(catalog, record.name),
|
|
220
|
+
tuple(
|
|
221
|
+
_key(rows, kind)
|
|
222
|
+
for rows in constraints.values()
|
|
223
|
+
if (kind := rows[0].key_kind) is not None
|
|
224
|
+
),
|
|
225
|
+
tuple(
|
|
226
|
+
_relation(rows, target)
|
|
227
|
+
for rows in constraints.values()
|
|
228
|
+
if rows[0].is_foreign_key
|
|
229
|
+
and (target := rows[0].target_reference) is not None
|
|
230
|
+
),
|
|
231
|
+
tuple(_index(rows) for rows in indexes.values()),
|
|
232
|
+
tuple(_check(item) for item in catalog.checks.get(record.name, [])),
|
|
233
|
+
record.comment,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _view(record: ObjectRecord, catalog: _Catalog) -> View:
|
|
238
|
+
return View(
|
|
239
|
+
Identifier(record.name),
|
|
240
|
+
_columns(catalog, record.name),
|
|
241
|
+
record.definition,
|
|
242
|
+
materialized=False,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _columns(catalog: _Catalog, table: str) -> tuple[Column, ...]:
|
|
247
|
+
return tuple(_column(record) for record in catalog.columns.get(table, []))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _column(record: ColumnRecord) -> Column:
|
|
251
|
+
return Column(
|
|
252
|
+
Identifier(record.name),
|
|
253
|
+
record.data_type,
|
|
254
|
+
record.nullable,
|
|
255
|
+
record.default,
|
|
256
|
+
record.generated,
|
|
257
|
+
record.comment,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _key(rows: list[ConstraintRecord], kind: KeyKind) -> Key:
|
|
262
|
+
return Key(
|
|
263
|
+
kind,
|
|
264
|
+
tuple(Identifier(row.column) for row in rows),
|
|
265
|
+
Identifier(rows[0].name),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _relation(rows: list[ConstraintRecord], target: ObjectReference) -> Relation:
|
|
270
|
+
return Relation(
|
|
271
|
+
tuple(Identifier(row.column) for row in rows),
|
|
272
|
+
target,
|
|
273
|
+
tuple(row.target_identifier for row in rows),
|
|
274
|
+
Identifier(rows[0].name),
|
|
275
|
+
rows[0].update_action,
|
|
276
|
+
rows[0].delete_action,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _check(record: CheckRecord) -> CheckConstraint:
|
|
281
|
+
return CheckConstraint(SqlExpression(record.clause), Identifier(record.name))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _index(rows: list[IndexRecord]) -> Index:
|
|
285
|
+
first = rows[0]
|
|
286
|
+
return Index(
|
|
287
|
+
Identifier(first.name),
|
|
288
|
+
tuple(row.term for row in rows),
|
|
289
|
+
first.unique,
|
|
290
|
+
None,
|
|
291
|
+
first.method,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _by_name(records: list[Named]) -> dict[str, list[Named]]:
|
|
296
|
+
return _grouped(tuple(records), lambda record: record.name)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _grouped(
|
|
300
|
+
records: tuple[Record, ...],
|
|
301
|
+
key: Callable[[Record], str],
|
|
302
|
+
) -> dict[str, list[Record]]:
|
|
303
|
+
grouped: dict[str, list[Record]] = defaultdict(list)
|
|
304
|
+
for record in records:
|
|
305
|
+
grouped[key(record)].append(record)
|
|
306
|
+
return grouped
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
__all__ = ("MySQLSchemaInspector",)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuration-backed MySQL 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.mysql.connection import MySQLConnectionProvider
|
|
11
|
+
from pyoq.schema.mysql.reflection import MySQLSchemaInspector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class MySQLSchemaSource:
|
|
16
|
+
connections: MySQLConnectionProvider = field(
|
|
17
|
+
default_factory=MySQLConnectionProvider
|
|
18
|
+
)
|
|
19
|
+
inspector: MySQLSchemaInspector = field(default_factory=MySQLSchemaInspector)
|
|
20
|
+
|
|
21
|
+
def load(self, configuration: Configuration) -> SchemaSnapshot:
|
|
22
|
+
dialect = configuration.active_profile.dialect
|
|
23
|
+
if dialect is not DatabaseDialect.MYSQL:
|
|
24
|
+
message = f"MySQL 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__ = ("MySQLSchemaSource",)
|
pyoq/schema/mysql/sql.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""MySQL information schema queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DATABASE_NAME = "SELECT DATABASE()"
|
|
6
|
+
|
|
7
|
+
SCHEMA_OBJECTS = """
|
|
8
|
+
SELECT
|
|
9
|
+
t.TABLE_NAME,
|
|
10
|
+
t.TABLE_TYPE,
|
|
11
|
+
t.TABLE_COMMENT,
|
|
12
|
+
v.VIEW_DEFINITION
|
|
13
|
+
FROM information_schema.TABLES AS t
|
|
14
|
+
LEFT JOIN information_schema.VIEWS AS v
|
|
15
|
+
ON v.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
16
|
+
AND v.TABLE_NAME = t.TABLE_NAME
|
|
17
|
+
WHERE t.TABLE_SCHEMA = DATABASE()
|
|
18
|
+
AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
19
|
+
ORDER BY t.TABLE_NAME
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
COLUMNS = """
|
|
23
|
+
SELECT
|
|
24
|
+
TABLE_NAME,
|
|
25
|
+
COLUMN_NAME,
|
|
26
|
+
COLUMN_TYPE,
|
|
27
|
+
IS_NULLABLE,
|
|
28
|
+
COLUMN_DEFAULT,
|
|
29
|
+
EXTRA,
|
|
30
|
+
GENERATION_EXPRESSION,
|
|
31
|
+
COLUMN_COMMENT
|
|
32
|
+
FROM information_schema.COLUMNS
|
|
33
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
34
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
CONSTRAINTS = """
|
|
38
|
+
SELECT
|
|
39
|
+
tc.TABLE_NAME,
|
|
40
|
+
tc.CONSTRAINT_NAME,
|
|
41
|
+
tc.CONSTRAINT_TYPE,
|
|
42
|
+
k.COLUMN_NAME,
|
|
43
|
+
k.REFERENCED_TABLE_SCHEMA,
|
|
44
|
+
k.REFERENCED_TABLE_NAME,
|
|
45
|
+
k.REFERENCED_COLUMN_NAME,
|
|
46
|
+
rc.UPDATE_RULE,
|
|
47
|
+
rc.DELETE_RULE
|
|
48
|
+
FROM information_schema.TABLE_CONSTRAINTS AS tc
|
|
49
|
+
JOIN information_schema.KEY_COLUMN_USAGE AS k
|
|
50
|
+
ON k.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
|
51
|
+
AND k.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
|
52
|
+
AND k.TABLE_NAME = tc.TABLE_NAME
|
|
53
|
+
LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
|
|
54
|
+
ON rc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
|
55
|
+
AND rc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
|
56
|
+
AND rc.TABLE_NAME = tc.TABLE_NAME
|
|
57
|
+
WHERE tc.TABLE_SCHEMA = DATABASE()
|
|
58
|
+
AND tc.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')
|
|
59
|
+
ORDER BY
|
|
60
|
+
tc.TABLE_NAME,
|
|
61
|
+
tc.CONSTRAINT_TYPE,
|
|
62
|
+
tc.CONSTRAINT_NAME,
|
|
63
|
+
k.ORDINAL_POSITION
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
CHECKS = """
|
|
67
|
+
SELECT
|
|
68
|
+
tc.TABLE_NAME,
|
|
69
|
+
cc.CONSTRAINT_NAME,
|
|
70
|
+
cc.CHECK_CLAUSE
|
|
71
|
+
FROM information_schema.CHECK_CONSTRAINTS AS cc
|
|
72
|
+
JOIN information_schema.TABLE_CONSTRAINTS AS tc
|
|
73
|
+
ON tc.CONSTRAINT_SCHEMA = cc.CONSTRAINT_SCHEMA
|
|
74
|
+
AND tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
|
|
75
|
+
WHERE tc.TABLE_SCHEMA = DATABASE()
|
|
76
|
+
AND tc.CONSTRAINT_TYPE = 'CHECK'
|
|
77
|
+
ORDER BY tc.TABLE_NAME, cc.CONSTRAINT_NAME
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
INDEXES = """
|
|
81
|
+
SELECT
|
|
82
|
+
TABLE_NAME,
|
|
83
|
+
INDEX_NAME,
|
|
84
|
+
NON_UNIQUE,
|
|
85
|
+
COLUMN_NAME,
|
|
86
|
+
COLLATION,
|
|
87
|
+
EXPRESSION,
|
|
88
|
+
INDEX_TYPE
|
|
89
|
+
FROM information_schema.STATISTICS
|
|
90
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
91
|
+
AND INDEX_NAME <> 'PRIMARY'
|
|
92
|
+
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
__all__ = (
|
|
96
|
+
"CHECKS",
|
|
97
|
+
"COLUMNS",
|
|
98
|
+
"CONSTRAINTS",
|
|
99
|
+
"DATABASE_NAME",
|
|
100
|
+
"INDEXES",
|
|
101
|
+
"SCHEMA_OBJECTS",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
ROUTINES = """
|
|
106
|
+
SELECT
|
|
107
|
+
r.SPECIFIC_NAME,
|
|
108
|
+
r.ROUTINE_TYPE,
|
|
109
|
+
r.DTD_IDENTIFIER,
|
|
110
|
+
p.ORDINAL_POSITION,
|
|
111
|
+
p.PARAMETER_NAME,
|
|
112
|
+
p.PARAMETER_MODE,
|
|
113
|
+
p.DTD_IDENTIFIER
|
|
114
|
+
FROM information_schema.ROUTINES AS r
|
|
115
|
+
LEFT JOIN information_schema.PARAMETERS AS p
|
|
116
|
+
ON p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
|
|
117
|
+
AND p.SPECIFIC_NAME = r.SPECIFIC_NAME
|
|
118
|
+
AND p.ROUTINE_TYPE = r.ROUTINE_TYPE
|
|
119
|
+
AND p.ORDINAL_POSITION > 0
|
|
120
|
+
WHERE r.ROUTINE_SCHEMA = DATABASE()
|
|
121
|
+
ORDER BY r.SPECIFIC_NAME, p.ORDINAL_POSITION
|
|
122
|
+
"""
|
|
123
|
+
"""One row per parameter, so a routine of no parameters is still one row.
|
|
124
|
+
|
|
125
|
+
A function's own result is ordinal zero in this catalog, and it is left out
|
|
126
|
+
here because the routine already carries it. MySQL refuses two routines of one
|
|
127
|
+
name, so a name is its own identity and no overload marker is needed.
|
|
128
|
+
"""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""MySQL declared-type normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pyoq.schema.models import SqlType, TypeKind
|
|
8
|
+
|
|
9
|
+
_TYPE_ARGUMENTS = re.compile(
|
|
10
|
+
r"^\s*([A-Za-z][A-Za-z ]*?)\s*(?:\(([^()]*)\))?"
|
|
11
|
+
r"((?:\s+(?:unsigned|signed|zerofill))*)\s*$",
|
|
12
|
+
re.IGNORECASE,
|
|
13
|
+
)
|
|
14
|
+
_MODIFIERS = frozenset({"UNSIGNED", "SIGNED", "ZEROFILL"})
|
|
15
|
+
|
|
16
|
+
_EXACT_KINDS = {
|
|
17
|
+
"BOOL": TypeKind.BOOLEAN,
|
|
18
|
+
"BOOLEAN": TypeKind.BOOLEAN,
|
|
19
|
+
"TINYINT": TypeKind.SMALL_INTEGER,
|
|
20
|
+
"SMALLINT": TypeKind.SMALL_INTEGER,
|
|
21
|
+
"MEDIUMINT": TypeKind.INTEGER,
|
|
22
|
+
"INT": TypeKind.INTEGER,
|
|
23
|
+
"INTEGER": TypeKind.INTEGER,
|
|
24
|
+
"BIGINT": TypeKind.BIG_INTEGER,
|
|
25
|
+
"DECIMAL": TypeKind.DECIMAL,
|
|
26
|
+
"NUMERIC": TypeKind.DECIMAL,
|
|
27
|
+
"FLOAT": TypeKind.REAL,
|
|
28
|
+
"DOUBLE": TypeKind.DOUBLE,
|
|
29
|
+
"DOUBLE PRECISION": TypeKind.DOUBLE,
|
|
30
|
+
"REAL": TypeKind.DOUBLE,
|
|
31
|
+
"CHAR": TypeKind.STRING,
|
|
32
|
+
"VARCHAR": TypeKind.STRING,
|
|
33
|
+
"TINYTEXT": TypeKind.STRING,
|
|
34
|
+
"TEXT": TypeKind.STRING,
|
|
35
|
+
"MEDIUMTEXT": TypeKind.STRING,
|
|
36
|
+
"LONGTEXT": TypeKind.STRING,
|
|
37
|
+
"BIT": TypeKind.BINARY,
|
|
38
|
+
"BINARY": TypeKind.BINARY,
|
|
39
|
+
"VARBINARY": TypeKind.BINARY,
|
|
40
|
+
"TINYBLOB": TypeKind.BINARY,
|
|
41
|
+
"BLOB": TypeKind.BINARY,
|
|
42
|
+
"MEDIUMBLOB": TypeKind.BINARY,
|
|
43
|
+
"LONGBLOB": TypeKind.BINARY,
|
|
44
|
+
"DATE": TypeKind.DATE,
|
|
45
|
+
"TIME": TypeKind.TIME,
|
|
46
|
+
"DATETIME": TypeKind.DATETIME,
|
|
47
|
+
"TIMESTAMP": TypeKind.DATETIME,
|
|
48
|
+
"YEAR": TypeKind.SMALL_INTEGER,
|
|
49
|
+
"JSON": TypeKind.JSON,
|
|
50
|
+
"SET": TypeKind.STRING,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_ENUM_VALUES = re.compile(r"(?i)^enum\((.*)\)$", re.S)
|
|
54
|
+
_ENUM_LABEL = re.compile(r"'((?:[^']|'')*)'")
|
|
55
|
+
|
|
56
|
+
_SIZED_KINDS = frozenset({TypeKind.STRING, TypeKind.BINARY})
|
|
57
|
+
_PRECISION_KINDS = frozenset({TypeKind.DECIMAL})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def mysql_enum_values(declared_type: str) -> tuple[str, ...]:
|
|
61
|
+
"""The labels a column-level enum lists, in the order it lists them.
|
|
62
|
+
|
|
63
|
+
MySQL keeps an enum on the column rather than as a type of its own, and
|
|
64
|
+
reports it as the whole declaration. The labels are its own order, which
|
|
65
|
+
is what it sorts and compares by.
|
|
66
|
+
"""
|
|
67
|
+
match = _ENUM_VALUES.fullmatch(declared_type.strip())
|
|
68
|
+
if match is None:
|
|
69
|
+
return ()
|
|
70
|
+
return tuple(
|
|
71
|
+
label.replace("''", "'") for label in _ENUM_LABEL.findall(match.group(1))
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mysql_type(declared_type: str) -> SqlType:
|
|
76
|
+
source_name = declared_type.strip() or "text"
|
|
77
|
+
match = _TYPE_ARGUMENTS.fullmatch(source_name)
|
|
78
|
+
if match is None:
|
|
79
|
+
return SqlType(TypeKind.OTHER, source_name)
|
|
80
|
+
base_name, argument_text, trailing = match.groups()
|
|
81
|
+
kind = _EXACT_KINDS.get(_base_name(base_name, trailing), TypeKind.OTHER)
|
|
82
|
+
arguments = _numeric_arguments(argument_text)
|
|
83
|
+
if kind in _SIZED_KINDS and len(arguments) == 1:
|
|
84
|
+
return SqlType(kind, source_name, length=arguments[0])
|
|
85
|
+
if kind in _PRECISION_KINDS and arguments:
|
|
86
|
+
scale = arguments[1] if len(arguments) == 2 else None
|
|
87
|
+
return SqlType(kind, source_name, precision=arguments[0], scale=scale)
|
|
88
|
+
return SqlType(kind, source_name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _base_name(base: str, trailing: str) -> str:
|
|
92
|
+
words = f"{base} {trailing}".upper().split()
|
|
93
|
+
return " ".join(word for word in words if word not in _MODIFIERS)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _numeric_arguments(value: str | None) -> tuple[int, ...]:
|
|
97
|
+
if value is None:
|
|
98
|
+
return ()
|
|
99
|
+
parts = tuple(part.strip() for part in value.split(","))
|
|
100
|
+
if not 1 <= len(parts) <= 2 or any(not part.isdecimal() for part in parts):
|
|
101
|
+
return ()
|
|
102
|
+
return tuple(int(part) for part in parts)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ("mysql_type",)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""PostgreSQL schema support."""
|
|
2
|
+
|
|
3
|
+
from pyoq.schema.postgres.connection import PostgresConnectionProvider
|
|
4
|
+
from pyoq.schema.postgres.reflection import PostgresSchemaInspector
|
|
5
|
+
from pyoq.schema.postgres.source import PostgresSchemaSource
|
|
6
|
+
from pyoq.schema.postgres.types import postgres_type
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"PostgresConnectionProvider",
|
|
10
|
+
"PostgresSchemaInspector",
|
|
11
|
+
"PostgresSchemaSource",
|
|
12
|
+
"postgres_type",
|
|
13
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Read-only PostgreSQL connection ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from pyoq.config import Configuration
|
|
11
|
+
from pyoq.errors import (
|
|
12
|
+
ConfigurationValidationError,
|
|
13
|
+
OperationUnavailableError,
|
|
14
|
+
SchemaInspectionError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from psycopg import Connection
|
|
19
|
+
|
|
20
|
+
_MISSING_DRIVER = (
|
|
21
|
+
"PostgreSQL support requires the psycopg driver; install pyoq-sql[postgres]"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class PostgresConnectionProvider:
|
|
27
|
+
def open(
|
|
28
|
+
self,
|
|
29
|
+
configuration: Configuration,
|
|
30
|
+
) -> AbstractContextManager[Connection[tuple[object, ...]]]:
|
|
31
|
+
return self._connect(_data_source_name(configuration))
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def _connect(
|
|
35
|
+
self,
|
|
36
|
+
dsn: str,
|
|
37
|
+
) -> Generator[Connection[tuple[object, ...]]]:
|
|
38
|
+
try:
|
|
39
|
+
import psycopg
|
|
40
|
+
except ImportError as error:
|
|
41
|
+
raise OperationUnavailableError(_MISSING_DRIVER) from error
|
|
42
|
+
try:
|
|
43
|
+
connection = psycopg.connect(dsn, autocommit=True)
|
|
44
|
+
except psycopg.Error as error:
|
|
45
|
+
message = f"unable to open PostgreSQL connection: {error}"
|
|
46
|
+
raise SchemaInspectionError(message) from error
|
|
47
|
+
try:
|
|
48
|
+
connection.execute("SET default_transaction_read_only = on")
|
|
49
|
+
yield connection
|
|
50
|
+
finally:
|
|
51
|
+
connection.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _data_source_name(configuration: Configuration) -> str:
|
|
55
|
+
profile = configuration.active_profile
|
|
56
|
+
resolved = profile.resolve_dsn()
|
|
57
|
+
if resolved is None:
|
|
58
|
+
message = "PostgreSQL profiles require a dsn setting"
|
|
59
|
+
raise ConfigurationValidationError(message)
|
|
60
|
+
return resolved.reveal()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ("PostgresConnectionProvider",)
|