pyoq-sql 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
pyoq/schema/models.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""Immutable database schema model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Protocol, TypeAlias, TypeVar
|
|
9
|
+
|
|
10
|
+
from pyoq.config import DatabaseDialect
|
|
11
|
+
from pyoq.errors import SchemaValidationError
|
|
12
|
+
|
|
13
|
+
SCHEMA_FORMAT_VERSION = 1
|
|
14
|
+
UniqueValue = TypeVar("UniqueValue", bound=Hashable)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NamedSchemaItem(Protocol):
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> Identifier | None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
JsonScalar: TypeAlias = bool | int | float | str | None
|
|
23
|
+
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
|
24
|
+
"""What JSON can hold, said once for everything that needs to know.
|
|
25
|
+
|
|
26
|
+
A JSON number is not always whole, so a float is one of them.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TypeKind(StrEnum):
|
|
31
|
+
BOOLEAN = "boolean"
|
|
32
|
+
SMALL_INTEGER = "small-integer"
|
|
33
|
+
INTEGER = "integer"
|
|
34
|
+
BIG_INTEGER = "big-integer"
|
|
35
|
+
DECIMAL = "decimal"
|
|
36
|
+
REAL = "real"
|
|
37
|
+
DOUBLE = "double"
|
|
38
|
+
STRING = "string"
|
|
39
|
+
BINARY = "binary"
|
|
40
|
+
DATE = "date"
|
|
41
|
+
TIME = "time"
|
|
42
|
+
DATETIME = "datetime"
|
|
43
|
+
INTERVAL = "interval"
|
|
44
|
+
UUID = "uuid"
|
|
45
|
+
JSON = "json"
|
|
46
|
+
ENUM = "enum"
|
|
47
|
+
ARRAY = "array"
|
|
48
|
+
DOMAIN = "domain"
|
|
49
|
+
OTHER = "other"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GeneratedKind(StrEnum):
|
|
53
|
+
IDENTITY_ALWAYS = "identity-always"
|
|
54
|
+
IDENTITY_BY_DEFAULT = "identity-by-default"
|
|
55
|
+
COMPUTED_STORED = "computed-stored"
|
|
56
|
+
COMPUTED_VIRTUAL = "computed-virtual"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class KeyKind(StrEnum):
|
|
60
|
+
PRIMARY = "primary"
|
|
61
|
+
UNIQUE = "unique"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ReferentialAction(StrEnum):
|
|
65
|
+
NO_ACTION = "no-action"
|
|
66
|
+
RESTRICT = "restrict"
|
|
67
|
+
CASCADE = "cascade"
|
|
68
|
+
SET_NULL = "set-null"
|
|
69
|
+
SET_DEFAULT = "set-default"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Capability(StrEnum):
|
|
73
|
+
CATALOGS = "catalogs"
|
|
74
|
+
SCHEMAS = "schemas"
|
|
75
|
+
VIEWS = "views"
|
|
76
|
+
MATERIALIZED_VIEWS = "materialized-views"
|
|
77
|
+
ENUMS = "enums"
|
|
78
|
+
ARRAYS = "arrays"
|
|
79
|
+
GENERATED_COLUMNS = "generated-columns"
|
|
80
|
+
IDENTITY_COLUMNS = "identity-columns"
|
|
81
|
+
EXPRESSION_INDEXES = "expression-indexes"
|
|
82
|
+
PARTIAL_INDEXES = "partial-indexes"
|
|
83
|
+
CHECK_CONSTRAINTS = "check-constraints"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
87
|
+
class Identifier:
|
|
88
|
+
value: str
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
_require_text(self.value, "identifier")
|
|
92
|
+
if "\x00" in self.value:
|
|
93
|
+
message = "identifier cannot contain a null character"
|
|
94
|
+
raise SchemaValidationError(message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class ObjectReference:
|
|
99
|
+
name: Identifier
|
|
100
|
+
schema: Identifier | None = None
|
|
101
|
+
catalog: Identifier | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
105
|
+
class MetadataEntry:
|
|
106
|
+
key: str
|
|
107
|
+
value: str
|
|
108
|
+
|
|
109
|
+
def __post_init__(self) -> None:
|
|
110
|
+
_require_text(self.key, "metadata key")
|
|
111
|
+
_require_text(self.value, "metadata value")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class SqlExpression:
|
|
116
|
+
text: str
|
|
117
|
+
|
|
118
|
+
def __post_init__(self) -> None:
|
|
119
|
+
_require_text(self.text, "SQL expression")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class SqlType:
|
|
124
|
+
kind: TypeKind
|
|
125
|
+
source_name: str
|
|
126
|
+
length: int | None = None
|
|
127
|
+
precision: int | None = None
|
|
128
|
+
scale: int | None = None
|
|
129
|
+
element_type: SqlType | None = None
|
|
130
|
+
named_type: ObjectReference | None = None
|
|
131
|
+
metadata: tuple[MetadataEntry, ...] = ()
|
|
132
|
+
|
|
133
|
+
def __post_init__(self) -> None:
|
|
134
|
+
_require_text(self.source_name, "source type name")
|
|
135
|
+
_require_positive(self.length, "type length")
|
|
136
|
+
_require_positive(self.precision, "type precision")
|
|
137
|
+
_validate_scale(self.precision, self.scale)
|
|
138
|
+
_validate_nested_type(self.kind, self.element_type, self.named_type)
|
|
139
|
+
object.__setattr__(self, "metadata", _canonical_metadata(self.metadata))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class GeneratedValue:
|
|
144
|
+
kind: GeneratedKind
|
|
145
|
+
expression: SqlExpression | None = None
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
computed = self.kind in {
|
|
149
|
+
GeneratedKind.COMPUTED_STORED,
|
|
150
|
+
GeneratedKind.COMPUTED_VIRTUAL,
|
|
151
|
+
}
|
|
152
|
+
if computed != (self.expression is not None):
|
|
153
|
+
message = "computed generation requires an expression; identity does not"
|
|
154
|
+
raise SchemaValidationError(message)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True, slots=True)
|
|
158
|
+
class Column:
|
|
159
|
+
name: Identifier
|
|
160
|
+
data_type: SqlType
|
|
161
|
+
nullable: bool
|
|
162
|
+
default: SqlExpression | None = None
|
|
163
|
+
generated: GeneratedValue | None = None
|
|
164
|
+
comment: str | None = None
|
|
165
|
+
metadata: tuple[MetadataEntry, ...] = ()
|
|
166
|
+
|
|
167
|
+
def __post_init__(self) -> None:
|
|
168
|
+
_validate_optional_text(self.comment, "column comment")
|
|
169
|
+
object.__setattr__(self, "metadata", _canonical_metadata(self.metadata))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True, slots=True)
|
|
173
|
+
class Key:
|
|
174
|
+
kind: KeyKind
|
|
175
|
+
columns: tuple[Identifier, ...]
|
|
176
|
+
name: Identifier | None = None
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
_require_identifiers(self.columns, "key columns")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True, slots=True)
|
|
183
|
+
class Relation:
|
|
184
|
+
columns: tuple[Identifier, ...]
|
|
185
|
+
target: ObjectReference
|
|
186
|
+
target_columns: tuple[Identifier, ...]
|
|
187
|
+
name: Identifier | None = None
|
|
188
|
+
on_update: ReferentialAction = ReferentialAction.NO_ACTION
|
|
189
|
+
on_delete: ReferentialAction = ReferentialAction.NO_ACTION
|
|
190
|
+
|
|
191
|
+
def __post_init__(self) -> None:
|
|
192
|
+
_require_identifiers(self.columns, "relation columns")
|
|
193
|
+
_require_identifiers(self.target_columns, "relation target columns")
|
|
194
|
+
if len(self.columns) != len(self.target_columns):
|
|
195
|
+
message = "relation source and target column counts must match"
|
|
196
|
+
raise SchemaValidationError(message)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True, slots=True)
|
|
200
|
+
class IndexTerm:
|
|
201
|
+
column: Identifier | None = None
|
|
202
|
+
expression: SqlExpression | None = None
|
|
203
|
+
descending: bool = False
|
|
204
|
+
|
|
205
|
+
def __post_init__(self) -> None:
|
|
206
|
+
if (self.column is None) == (self.expression is None):
|
|
207
|
+
message = "index term requires exactly one column or expression"
|
|
208
|
+
raise SchemaValidationError(message)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True, slots=True)
|
|
212
|
+
class Index:
|
|
213
|
+
name: Identifier
|
|
214
|
+
terms: tuple[IndexTerm, ...]
|
|
215
|
+
unique: bool = False
|
|
216
|
+
predicate: SqlExpression | None = None
|
|
217
|
+
method: str | None = None
|
|
218
|
+
|
|
219
|
+
def __post_init__(self) -> None:
|
|
220
|
+
if not self.terms:
|
|
221
|
+
message = "index must contain at least one term"
|
|
222
|
+
raise SchemaValidationError(message)
|
|
223
|
+
_validate_optional_text(self.method, "index method")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True, slots=True)
|
|
227
|
+
class CheckConstraint:
|
|
228
|
+
expression: SqlExpression
|
|
229
|
+
name: Identifier | None = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True, slots=True)
|
|
233
|
+
class Table:
|
|
234
|
+
name: Identifier
|
|
235
|
+
columns: tuple[Column, ...]
|
|
236
|
+
keys: tuple[Key, ...] = ()
|
|
237
|
+
relations: tuple[Relation, ...] = ()
|
|
238
|
+
indexes: tuple[Index, ...] = ()
|
|
239
|
+
checks: tuple[CheckConstraint, ...] = ()
|
|
240
|
+
comment: str | None = None
|
|
241
|
+
metadata: tuple[MetadataEntry, ...] = ()
|
|
242
|
+
|
|
243
|
+
def __post_init__(self) -> None:
|
|
244
|
+
if not self.columns:
|
|
245
|
+
message = "table must contain at least one column"
|
|
246
|
+
raise SchemaValidationError(message)
|
|
247
|
+
column_names = tuple(column.name for column in self.columns)
|
|
248
|
+
_require_unique(column_names, "table column names")
|
|
249
|
+
_validate_table_keys(self.keys, column_names)
|
|
250
|
+
_validate_table_relations(self.relations, column_names)
|
|
251
|
+
_validate_table_indexes(self.indexes, column_names)
|
|
252
|
+
_validate_primary_key_count(self.keys)
|
|
253
|
+
_validate_named_items(self.keys, "key names")
|
|
254
|
+
_validate_named_items(self.relations, "relation names")
|
|
255
|
+
_validate_named_items(self.indexes, "index names")
|
|
256
|
+
_validate_named_items(self.checks, "check names")
|
|
257
|
+
_validate_optional_text(self.comment, "table comment")
|
|
258
|
+
object.__setattr__(self, "metadata", _canonical_metadata(self.metadata))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True, slots=True)
|
|
262
|
+
class View:
|
|
263
|
+
name: Identifier
|
|
264
|
+
columns: tuple[Column, ...]
|
|
265
|
+
definition: SqlExpression | None = None
|
|
266
|
+
materialized: bool = False
|
|
267
|
+
comment: str | None = None
|
|
268
|
+
metadata: tuple[MetadataEntry, ...] = ()
|
|
269
|
+
|
|
270
|
+
def __post_init__(self) -> None:
|
|
271
|
+
if not self.columns:
|
|
272
|
+
message = "view must contain at least one column"
|
|
273
|
+
raise SchemaValidationError(message)
|
|
274
|
+
_require_unique(tuple(column.name for column in self.columns), "view columns")
|
|
275
|
+
_validate_optional_text(self.comment, "view comment")
|
|
276
|
+
object.__setattr__(self, "metadata", _canonical_metadata(self.metadata))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True, slots=True)
|
|
280
|
+
class EnumType:
|
|
281
|
+
name: Identifier
|
|
282
|
+
values: tuple[str, ...]
|
|
283
|
+
comment: str | None = None
|
|
284
|
+
|
|
285
|
+
def __post_init__(self) -> None:
|
|
286
|
+
if not self.values:
|
|
287
|
+
message = "enum must contain at least one value"
|
|
288
|
+
raise SchemaValidationError(message)
|
|
289
|
+
for value in self.values:
|
|
290
|
+
_require_text(value, "enum value")
|
|
291
|
+
_require_unique(self.values, "enum values")
|
|
292
|
+
_validate_optional_text(self.comment, "enum comment")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataclass(frozen=True, slots=True)
|
|
296
|
+
class Domain:
|
|
297
|
+
"""A named type written over another, with rules of its own.
|
|
298
|
+
|
|
299
|
+
Two domains over the same base are different types, so the name is what
|
|
300
|
+
identifies one. Its checks and default belong to the domain rather than
|
|
301
|
+
to any column of it, which is why they are kept here.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
name: Identifier
|
|
305
|
+
data_type: SqlType
|
|
306
|
+
nullable: bool = True
|
|
307
|
+
default: SqlExpression | None = None
|
|
308
|
+
checks: tuple[CheckConstraint, ...] = ()
|
|
309
|
+
comment: str | None = None
|
|
310
|
+
|
|
311
|
+
def __post_init__(self) -> None:
|
|
312
|
+
if self.data_type.kind is TypeKind.DOMAIN:
|
|
313
|
+
message = "a domain is written over a type, not over another domain"
|
|
314
|
+
raise SchemaValidationError(message)
|
|
315
|
+
_require_unique(tuple(check.name for check in self.checks), "domain checks")
|
|
316
|
+
_validate_optional_text(self.comment, "domain comment")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class RoutineKind(StrEnum):
|
|
320
|
+
"""Whether a routine answers with a value or is run for what it does."""
|
|
321
|
+
|
|
322
|
+
FUNCTION = "function"
|
|
323
|
+
PROCEDURE = "procedure"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ParameterMode(StrEnum):
|
|
327
|
+
"""Which way a value travels through a routine parameter."""
|
|
328
|
+
|
|
329
|
+
IN = "in"
|
|
330
|
+
OUT = "out"
|
|
331
|
+
INOUT = "inout"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass(frozen=True, slots=True)
|
|
335
|
+
class RoutineParameter:
|
|
336
|
+
"""One parameter of a routine, as the catalog declares it.
|
|
337
|
+
|
|
338
|
+
No catalog states a parameter's nullability on the parameter. It comes
|
|
339
|
+
from the parameter's own type, which a domain can declare forbids null,
|
|
340
|
+
so that is the only way ``nullable`` is ever false.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
name: Identifier | None
|
|
344
|
+
data_type: SqlType
|
|
345
|
+
mode: ParameterMode = ParameterMode.IN
|
|
346
|
+
has_default: bool = False
|
|
347
|
+
nullable: bool = True
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass(frozen=True, slots=True)
|
|
351
|
+
class Routine:
|
|
352
|
+
"""A procedure or function the database keeps.
|
|
353
|
+
|
|
354
|
+
``overload`` separates routines that share a name, which several databases
|
|
355
|
+
allow. Without it a generated name could stand for more than one routine
|
|
356
|
+
and a call could reach the wrong one.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
name: Identifier
|
|
360
|
+
kind: RoutineKind
|
|
361
|
+
parameters: tuple[RoutineParameter, ...] = ()
|
|
362
|
+
returns: SqlType | None = None
|
|
363
|
+
overload: str | None = None
|
|
364
|
+
comment: str | None = None
|
|
365
|
+
|
|
366
|
+
def __post_init__(self) -> None:
|
|
367
|
+
if (self.kind is RoutineKind.FUNCTION) != (self.returns is not None):
|
|
368
|
+
message = "a function answers with a type and a procedure does not"
|
|
369
|
+
raise SchemaValidationError(message)
|
|
370
|
+
named = tuple(
|
|
371
|
+
parameter.name
|
|
372
|
+
for parameter in self.parameters
|
|
373
|
+
if parameter.name is not None
|
|
374
|
+
)
|
|
375
|
+
_require_unique(named, "routine parameter names")
|
|
376
|
+
_validate_optional_text(self.comment, "routine comment")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True, slots=True)
|
|
380
|
+
class Schema:
|
|
381
|
+
name: Identifier | None
|
|
382
|
+
tables: tuple[Table, ...] = ()
|
|
383
|
+
views: tuple[View, ...] = ()
|
|
384
|
+
enums: tuple[EnumType, ...] = ()
|
|
385
|
+
domains: tuple[Domain, ...] = ()
|
|
386
|
+
routines: tuple[Routine, ...] = ()
|
|
387
|
+
comment: str | None = None
|
|
388
|
+
|
|
389
|
+
def __post_init__(self) -> None:
|
|
390
|
+
table_names = tuple(table.name for table in self.tables)
|
|
391
|
+
view_names = tuple(view.name for view in self.views)
|
|
392
|
+
_require_unique(table_names + view_names, "schema relation names")
|
|
393
|
+
_require_unique(tuple(enum.name for enum in self.enums), "schema enum names")
|
|
394
|
+
_require_unique(
|
|
395
|
+
tuple(domain.name for domain in self.domains), "schema domain names"
|
|
396
|
+
)
|
|
397
|
+
_require_unique(
|
|
398
|
+
tuple((routine.name.value, routine.overload) for routine in self.routines),
|
|
399
|
+
"schema routine names",
|
|
400
|
+
)
|
|
401
|
+
_validate_optional_text(self.comment, "schema comment")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(frozen=True, slots=True)
|
|
405
|
+
class Catalog:
|
|
406
|
+
name: Identifier | None
|
|
407
|
+
schemas: tuple[Schema, ...]
|
|
408
|
+
comment: str | None = None
|
|
409
|
+
|
|
410
|
+
def __post_init__(self) -> None:
|
|
411
|
+
if not self.schemas:
|
|
412
|
+
message = "catalog must contain at least one schema"
|
|
413
|
+
raise SchemaValidationError(message)
|
|
414
|
+
_require_unique(tuple(schema.name for schema in self.schemas), "schema names")
|
|
415
|
+
_validate_optional_text(self.comment, "catalog comment")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass(frozen=True, slots=True)
|
|
419
|
+
class CapabilitySet:
|
|
420
|
+
values: frozenset[Capability] = frozenset()
|
|
421
|
+
|
|
422
|
+
def supports(self, capability: Capability) -> bool:
|
|
423
|
+
return capability in self.values
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@dataclass(frozen=True, slots=True)
|
|
427
|
+
class SchemaSnapshot:
|
|
428
|
+
dialect: DatabaseDialect
|
|
429
|
+
catalogs: tuple[Catalog, ...]
|
|
430
|
+
capabilities: CapabilitySet = CapabilitySet()
|
|
431
|
+
format_version: int = SCHEMA_FORMAT_VERSION
|
|
432
|
+
|
|
433
|
+
def __post_init__(self) -> None:
|
|
434
|
+
if self.format_version != SCHEMA_FORMAT_VERSION:
|
|
435
|
+
message = f"unsupported schema format version: {self.format_version}"
|
|
436
|
+
raise SchemaValidationError(message)
|
|
437
|
+
if not self.catalogs:
|
|
438
|
+
message = "schema snapshot must contain at least one catalog"
|
|
439
|
+
raise SchemaValidationError(message)
|
|
440
|
+
_require_unique(
|
|
441
|
+
tuple(catalog.name for catalog in self.catalogs),
|
|
442
|
+
"catalog names",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def to_json(self) -> str:
|
|
446
|
+
from pyoq.schema.codec import encode_snapshot
|
|
447
|
+
|
|
448
|
+
return encode_snapshot(self)
|
|
449
|
+
|
|
450
|
+
@staticmethod
|
|
451
|
+
def from_json(value: str) -> SchemaSnapshot:
|
|
452
|
+
from pyoq.schema.codec import decode_snapshot
|
|
453
|
+
|
|
454
|
+
return decode_snapshot(value)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _require_text(value: str, label: str) -> None:
|
|
458
|
+
if not value:
|
|
459
|
+
message = f"{label} cannot be empty"
|
|
460
|
+
raise SchemaValidationError(message)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _validate_optional_text(value: str | None, label: str) -> None:
|
|
464
|
+
if value is not None:
|
|
465
|
+
_require_text(value, label)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _require_positive(value: int | None, label: str) -> None:
|
|
469
|
+
if value is not None and value <= 0:
|
|
470
|
+
message = f"{label} must be positive"
|
|
471
|
+
raise SchemaValidationError(message)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _validate_scale(precision: int | None, scale: int | None) -> None:
|
|
475
|
+
if scale is None:
|
|
476
|
+
return
|
|
477
|
+
if scale < 0 or precision is None or scale > precision:
|
|
478
|
+
message = "type scale requires precision and must be within its range"
|
|
479
|
+
raise SchemaValidationError(message)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
_NESTING_KINDS = frozenset({TypeKind.ARRAY, TypeKind.DOMAIN})
|
|
483
|
+
"""The kinds that are written around another type."""
|
|
484
|
+
|
|
485
|
+
_NAMED_KINDS = frozenset({TypeKind.ENUM, TypeKind.DOMAIN})
|
|
486
|
+
"""The kinds a database knows by a name of their own."""
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _validate_nested_type(
|
|
490
|
+
kind: TypeKind,
|
|
491
|
+
element_type: SqlType | None,
|
|
492
|
+
named_type: ObjectReference | None,
|
|
493
|
+
) -> None:
|
|
494
|
+
if (kind in _NESTING_KINDS) != (element_type is not None):
|
|
495
|
+
message = (
|
|
496
|
+
"array and domain types require the type inside them, and other "
|
|
497
|
+
"types forbid one"
|
|
498
|
+
)
|
|
499
|
+
raise SchemaValidationError(message)
|
|
500
|
+
if (kind in _NAMED_KINDS) != (named_type is not None):
|
|
501
|
+
message = (
|
|
502
|
+
"enum and domain types require a named type and other types forbid one"
|
|
503
|
+
)
|
|
504
|
+
raise SchemaValidationError(message)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _canonical_metadata(
|
|
508
|
+
metadata: tuple[MetadataEntry, ...],
|
|
509
|
+
) -> tuple[MetadataEntry, ...]:
|
|
510
|
+
ordered = tuple(sorted(metadata))
|
|
511
|
+
_require_unique(tuple(entry.key for entry in ordered), "metadata keys")
|
|
512
|
+
return ordered
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _require_identifiers(values: tuple[Identifier, ...], label: str) -> None:
|
|
516
|
+
if not values:
|
|
517
|
+
message = f"{label} cannot be empty"
|
|
518
|
+
raise SchemaValidationError(message)
|
|
519
|
+
_require_unique(values, label)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _require_unique(values: tuple[UniqueValue, ...], label: str) -> None:
|
|
523
|
+
if len(set(values)) != len(values):
|
|
524
|
+
message = f"{label} must be unique"
|
|
525
|
+
raise SchemaValidationError(message)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _validate_primary_key_count(keys: tuple[Key, ...]) -> None:
|
|
529
|
+
count = sum(key.kind is KeyKind.PRIMARY for key in keys)
|
|
530
|
+
if count > 1:
|
|
531
|
+
message = "table cannot contain more than one primary key"
|
|
532
|
+
raise SchemaValidationError(message)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _validate_table_keys(
|
|
536
|
+
keys: tuple[Key, ...],
|
|
537
|
+
columns: tuple[Identifier, ...],
|
|
538
|
+
) -> None:
|
|
539
|
+
for key in keys:
|
|
540
|
+
_require_local_columns(key.columns, columns, "key")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _validate_table_relations(
|
|
544
|
+
relations: tuple[Relation, ...],
|
|
545
|
+
columns: tuple[Identifier, ...],
|
|
546
|
+
) -> None:
|
|
547
|
+
for relation in relations:
|
|
548
|
+
_require_local_columns(relation.columns, columns, "relation")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _validate_table_indexes(
|
|
552
|
+
indexes: tuple[Index, ...],
|
|
553
|
+
columns: tuple[Identifier, ...],
|
|
554
|
+
) -> None:
|
|
555
|
+
for index in indexes:
|
|
556
|
+
for term in index.terms:
|
|
557
|
+
if term.column is not None:
|
|
558
|
+
_require_local_columns((term.column,), columns, "index")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _require_local_columns(
|
|
562
|
+
selected: tuple[Identifier, ...],
|
|
563
|
+
available: tuple[Identifier, ...],
|
|
564
|
+
label: str,
|
|
565
|
+
) -> None:
|
|
566
|
+
missing = tuple(column for column in selected if column not in available)
|
|
567
|
+
if missing:
|
|
568
|
+
names = ", ".join(column.value for column in missing)
|
|
569
|
+
message = f"{label} references unknown local columns: {names}"
|
|
570
|
+
raise SchemaValidationError(message)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _validate_named_items(items: tuple[NamedSchemaItem, ...], label: str) -> None:
|
|
574
|
+
names = tuple(item.name for item in items if item.name is not None)
|
|
575
|
+
_require_unique(names, label)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
__all__ = (
|
|
579
|
+
"SCHEMA_FORMAT_VERSION",
|
|
580
|
+
"Capability",
|
|
581
|
+
"CapabilitySet",
|
|
582
|
+
"Catalog",
|
|
583
|
+
"CheckConstraint",
|
|
584
|
+
"Column",
|
|
585
|
+
"EnumType",
|
|
586
|
+
"GeneratedKind",
|
|
587
|
+
"GeneratedValue",
|
|
588
|
+
"Identifier",
|
|
589
|
+
"Index",
|
|
590
|
+
"IndexTerm",
|
|
591
|
+
"Key",
|
|
592
|
+
"KeyKind",
|
|
593
|
+
"MetadataEntry",
|
|
594
|
+
"ObjectReference",
|
|
595
|
+
"ReferentialAction",
|
|
596
|
+
"Relation",
|
|
597
|
+
"Schema",
|
|
598
|
+
"SchemaSnapshot",
|
|
599
|
+
"SqlExpression",
|
|
600
|
+
"SqlType",
|
|
601
|
+
"Table",
|
|
602
|
+
"TypeKind",
|
|
603
|
+
"View",
|
|
604
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""MySQL schema support."""
|
|
2
|
+
|
|
3
|
+
from pyoq.schema.mysql.connection import MySQLConnectionProvider
|
|
4
|
+
from pyoq.schema.mysql.dsn import MySQLConnectionSettings, parse_data_source_name
|
|
5
|
+
from pyoq.schema.mysql.reflection import MySQLSchemaInspector
|
|
6
|
+
from pyoq.schema.mysql.source import MySQLSchemaSource
|
|
7
|
+
from pyoq.schema.mysql.types import mysql_type
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"MySQLConnectionProvider",
|
|
11
|
+
"MySQLConnectionSettings",
|
|
12
|
+
"MySQLSchemaInspector",
|
|
13
|
+
"MySQLSchemaSource",
|
|
14
|
+
"mysql_type",
|
|
15
|
+
"parse_data_source_name",
|
|
16
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Read-only MySQL 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
|
+
from pyoq.schema.mysql.dsn import MySQLConnectionSettings, parse_data_source_name
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pymysql.connections import Connection
|
|
20
|
+
from pymysql.cursors import Cursor
|
|
21
|
+
|
|
22
|
+
_MISSING_DRIVER = "MySQL support requires the PyMySQL driver; install pyoq-sql[mysql]"
|
|
23
|
+
_READ_ONLY_SESSION = "SET SESSION TRANSACTION READ ONLY"
|
|
24
|
+
_CHARSET = "utf8mb4"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class MySQLConnectionProvider:
|
|
29
|
+
def open(
|
|
30
|
+
self,
|
|
31
|
+
configuration: Configuration,
|
|
32
|
+
) -> AbstractContextManager[Connection[Cursor]]:
|
|
33
|
+
return self._connect(parse_data_source_name(_data_source_name(configuration)))
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def _connect(
|
|
37
|
+
self,
|
|
38
|
+
settings: MySQLConnectionSettings,
|
|
39
|
+
) -> Generator[Connection[Cursor]]:
|
|
40
|
+
try:
|
|
41
|
+
import pymysql
|
|
42
|
+
except ImportError as error:
|
|
43
|
+
raise OperationUnavailableError(_MISSING_DRIVER) from error
|
|
44
|
+
try:
|
|
45
|
+
connection = pymysql.connect(
|
|
46
|
+
host=settings.host,
|
|
47
|
+
port=settings.port,
|
|
48
|
+
user=settings.user,
|
|
49
|
+
password=settings.password,
|
|
50
|
+
database=settings.database,
|
|
51
|
+
charset=_CHARSET,
|
|
52
|
+
autocommit=True,
|
|
53
|
+
init_command=_READ_ONLY_SESSION,
|
|
54
|
+
)
|
|
55
|
+
except pymysql.Error as error:
|
|
56
|
+
message = f"unable to open MySQL connection: {error}"
|
|
57
|
+
raise SchemaInspectionError(message) from error
|
|
58
|
+
try:
|
|
59
|
+
yield connection
|
|
60
|
+
finally:
|
|
61
|
+
connection.close()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _data_source_name(configuration: Configuration) -> str:
|
|
65
|
+
profile = configuration.active_profile
|
|
66
|
+
resolved = profile.resolve_dsn()
|
|
67
|
+
if resolved is None:
|
|
68
|
+
message = "MySQL profiles require a dsn setting"
|
|
69
|
+
raise ConfigurationValidationError(message)
|
|
70
|
+
return resolved.reveal()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ("MySQLConnectionProvider",)
|