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.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
pyoq/schema/codec.py ADDED
@@ -0,0 +1,711 @@
1
+ """Versioned schema snapshot JSON codec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Callable
7
+ from enum import StrEnum
8
+ from typing import TypeVar, cast
9
+
10
+ from pyoq.config import DatabaseDialect
11
+ from pyoq.errors import SchemaSerializationError, SchemaValidationError
12
+ from pyoq.schema import (
13
+ Capability,
14
+ CapabilitySet,
15
+ Catalog,
16
+ CheckConstraint,
17
+ Column,
18
+ Domain,
19
+ EnumType,
20
+ GeneratedKind,
21
+ GeneratedValue,
22
+ Identifier,
23
+ Index,
24
+ IndexTerm,
25
+ JsonValue,
26
+ Key,
27
+ KeyKind,
28
+ MetadataEntry,
29
+ ObjectReference,
30
+ ParameterMode,
31
+ ReferentialAction,
32
+ Relation,
33
+ Routine,
34
+ RoutineKind,
35
+ RoutineParameter,
36
+ Schema,
37
+ SchemaSnapshot,
38
+ SqlExpression,
39
+ SqlType,
40
+ Table,
41
+ TypeKind,
42
+ View,
43
+ )
44
+
45
+ JsonObject = dict[str, JsonValue]
46
+ DecodedValue = TypeVar("DecodedValue")
47
+ EnumValue = TypeVar("EnumValue", bound=StrEnum)
48
+
49
+
50
+ def encode_snapshot(snapshot: SchemaSnapshot) -> str:
51
+ capabilities: list[JsonValue] = [
52
+ capability.value
53
+ for capability in sorted(
54
+ snapshot.capabilities.values, key=lambda item: item.value
55
+ )
56
+ ]
57
+ payload: JsonObject = {
58
+ "capabilities": capabilities,
59
+ "catalogs": [_encode_catalog(catalog) for catalog in snapshot.catalogs],
60
+ "dialect": snapshot.dialect.value,
61
+ "format_version": snapshot.format_version,
62
+ }
63
+ return json.dumps(
64
+ payload,
65
+ ensure_ascii=False,
66
+ separators=(",", ":"),
67
+ sort_keys=True,
68
+ )
69
+
70
+
71
+ def decode_snapshot(value: str) -> SchemaSnapshot:
72
+ try:
73
+ decoded: object = json.loads(value)
74
+ return _decode_snapshot(_object(decoded, "snapshot"))
75
+ except SchemaSerializationError:
76
+ raise
77
+ except (json.JSONDecodeError, SchemaValidationError, ValueError) as error:
78
+ message = "serialized schema snapshot is invalid"
79
+ raise SchemaSerializationError(message) from error
80
+
81
+
82
+ def _encode_catalog(catalog: Catalog) -> JsonObject:
83
+ return {
84
+ "comment": catalog.comment,
85
+ "name": _encode_identifier(catalog.name),
86
+ "schemas": [_encode_schema(schema) for schema in catalog.schemas],
87
+ }
88
+
89
+
90
+ def _encode_schema(schema: Schema) -> JsonObject:
91
+ return {
92
+ "comment": schema.comment,
93
+ "domains": [_encode_domain(domain) for domain in schema.domains],
94
+ "enums": [_encode_enum(enum) for enum in schema.enums],
95
+ "name": _encode_identifier(schema.name),
96
+ "routines": [_encode_routine(routine) for routine in schema.routines],
97
+ "tables": [_encode_table(table) for table in schema.tables],
98
+ "views": [_encode_view(view) for view in schema.views],
99
+ }
100
+
101
+
102
+ def _encode_domain(domain: Domain) -> JsonObject:
103
+ return {
104
+ "checks": [_encode_check(check) for check in domain.checks],
105
+ "comment": domain.comment,
106
+ "data-type": _encode_sql_type(domain.data_type),
107
+ "default": None if domain.default is None else domain.default.text,
108
+ "name": domain.name.value,
109
+ "nullable": domain.nullable,
110
+ }
111
+
112
+
113
+ def _encode_routine(routine: Routine) -> JsonObject:
114
+ return {
115
+ "comment": routine.comment,
116
+ "kind": routine.kind.value,
117
+ "name": routine.name.value,
118
+ "overload": routine.overload,
119
+ "parameters": [
120
+ _encode_parameter(parameter) for parameter in routine.parameters
121
+ ],
122
+ "returns": (
123
+ None if routine.returns is None else _encode_sql_type(routine.returns)
124
+ ),
125
+ }
126
+
127
+
128
+ def _encode_parameter(parameter: RoutineParameter) -> JsonObject:
129
+ return {
130
+ "data-type": _encode_sql_type(parameter.data_type),
131
+ "has-default": parameter.has_default,
132
+ "mode": parameter.mode.value,
133
+ "name": _encode_identifier(parameter.name),
134
+ "nullable": parameter.nullable,
135
+ }
136
+
137
+
138
+ def _encode_table(table: Table) -> JsonObject:
139
+ return {
140
+ "checks": [_encode_check(check) for check in table.checks],
141
+ "columns": [_encode_column(column) for column in table.columns],
142
+ "comment": table.comment,
143
+ "indexes": [_encode_index(index) for index in table.indexes],
144
+ "keys": [_encode_key(key) for key in table.keys],
145
+ "metadata": [_encode_metadata(entry) for entry in table.metadata],
146
+ "name": table.name.value,
147
+ "relations": [_encode_relation(relation) for relation in table.relations],
148
+ }
149
+
150
+
151
+ def _encode_view(view: View) -> JsonObject:
152
+ return {
153
+ "columns": [_encode_column(column) for column in view.columns],
154
+ "comment": view.comment,
155
+ "definition": _encode_expression(view.definition),
156
+ "materialized": view.materialized,
157
+ "metadata": [_encode_metadata(entry) for entry in view.metadata],
158
+ "name": view.name.value,
159
+ }
160
+
161
+
162
+ def _encode_enum(enum: EnumType) -> JsonObject:
163
+ return {
164
+ "comment": enum.comment,
165
+ "name": enum.name.value,
166
+ "values": list(enum.values),
167
+ }
168
+
169
+
170
+ def _encode_column(column: Column) -> JsonObject:
171
+ return {
172
+ "comment": column.comment,
173
+ "data_type": _encode_sql_type(column.data_type),
174
+ "default": _encode_expression(column.default),
175
+ "generated": _encode_generated(column.generated),
176
+ "metadata": [_encode_metadata(entry) for entry in column.metadata],
177
+ "name": column.name.value,
178
+ "nullable": column.nullable,
179
+ }
180
+
181
+
182
+ def _encode_sql_type(data_type: SqlType) -> JsonObject:
183
+ return {
184
+ "element_type": (
185
+ None
186
+ if data_type.element_type is None
187
+ else _encode_sql_type(data_type.element_type)
188
+ ),
189
+ "kind": data_type.kind.value,
190
+ "length": data_type.length,
191
+ "metadata": [_encode_metadata(entry) for entry in data_type.metadata],
192
+ "named_type": _encode_reference(data_type.named_type),
193
+ "precision": data_type.precision,
194
+ "scale": data_type.scale,
195
+ "source_name": data_type.source_name,
196
+ }
197
+
198
+
199
+ def _encode_generated(value: GeneratedValue | None) -> JsonObject | None:
200
+ if value is None:
201
+ return None
202
+ return {
203
+ "expression": _encode_expression(value.expression),
204
+ "kind": value.kind.value,
205
+ }
206
+
207
+
208
+ def _encode_key(key: Key) -> JsonObject:
209
+ return {
210
+ "columns": [column.value for column in key.columns],
211
+ "kind": key.kind.value,
212
+ "name": _encode_identifier(key.name),
213
+ }
214
+
215
+
216
+ def _encode_relation(relation: Relation) -> JsonObject:
217
+ return {
218
+ "columns": [column.value for column in relation.columns],
219
+ "name": _encode_identifier(relation.name),
220
+ "on_delete": relation.on_delete.value,
221
+ "on_update": relation.on_update.value,
222
+ "target": _encode_reference(relation.target),
223
+ "target_columns": [column.value for column in relation.target_columns],
224
+ }
225
+
226
+
227
+ def _encode_index(index: Index) -> JsonObject:
228
+ return {
229
+ "method": index.method,
230
+ "name": index.name.value,
231
+ "predicate": _encode_expression(index.predicate),
232
+ "terms": [_encode_index_term(term) for term in index.terms],
233
+ "unique": index.unique,
234
+ }
235
+
236
+
237
+ def _encode_index_term(term: IndexTerm) -> JsonObject:
238
+ return {
239
+ "column": _encode_identifier(term.column),
240
+ "descending": term.descending,
241
+ "expression": _encode_expression(term.expression),
242
+ }
243
+
244
+
245
+ def _encode_check(check: CheckConstraint) -> JsonObject:
246
+ return {
247
+ "expression": check.expression.text,
248
+ "name": _encode_identifier(check.name),
249
+ }
250
+
251
+
252
+ def _encode_reference(reference: ObjectReference | None) -> JsonObject | None:
253
+ if reference is None:
254
+ return None
255
+ return {
256
+ "catalog": _encode_identifier(reference.catalog),
257
+ "name": reference.name.value,
258
+ "schema": _encode_identifier(reference.schema),
259
+ }
260
+
261
+
262
+ def _encode_metadata(entry: MetadataEntry) -> JsonObject:
263
+ return {"key": entry.key, "value": entry.value}
264
+
265
+
266
+ def _encode_identifier(identifier: Identifier | None) -> str | None:
267
+ return None if identifier is None else identifier.value
268
+
269
+
270
+ def _encode_expression(expression: SqlExpression | None) -> str | None:
271
+ return None if expression is None else expression.text
272
+
273
+
274
+ def _decode_snapshot(value: JsonObject) -> SchemaSnapshot:
275
+ location = "snapshot"
276
+ _require_fields(
277
+ value,
278
+ {"capabilities", "catalogs", "dialect", "format_version"},
279
+ location,
280
+ )
281
+ capabilities = frozenset(
282
+ _enum(Capability, item, f"{location}.capabilities")
283
+ for item in _array(value["capabilities"], f"{location}.capabilities")
284
+ )
285
+ catalogs = _decode_array(value["catalogs"], f"{location}.catalogs", _decode_catalog)
286
+ return SchemaSnapshot(
287
+ dialect=_enum(DatabaseDialect, value["dialect"], f"{location}.dialect"),
288
+ catalogs=catalogs,
289
+ capabilities=CapabilitySet(capabilities),
290
+ format_version=_integer(value["format_version"], f"{location}.format_version"),
291
+ )
292
+
293
+
294
+ def _decode_catalog(value: JsonValue, location: str) -> Catalog:
295
+ record = _record(value, {"comment", "name", "schemas"}, location)
296
+ return Catalog(
297
+ name=_optional_identifier(record["name"], f"{location}.name"),
298
+ schemas=_decode_array(record["schemas"], f"{location}.schemas", _decode_schema),
299
+ comment=_optional_string(record["comment"], f"{location}.comment"),
300
+ )
301
+
302
+
303
+ def _decode_schema(value: JsonValue, location: str) -> Schema:
304
+ record = _record(
305
+ value,
306
+ {"comment", "domains", "enums", "name", "routines", "tables", "views"},
307
+ location,
308
+ )
309
+ return Schema(
310
+ name=_optional_identifier(record["name"], f"{location}.name"),
311
+ tables=_decode_array(record["tables"], f"{location}.tables", _decode_table),
312
+ views=_decode_array(record["views"], f"{location}.views", _decode_view),
313
+ enums=_decode_array(record["enums"], f"{location}.enums", _decode_enum),
314
+ domains=_decode_array(record["domains"], f"{location}.domains", _decode_domain),
315
+ routines=_decode_array(
316
+ record["routines"], f"{location}.routines", _decode_routine
317
+ ),
318
+ comment=_optional_string(record["comment"], f"{location}.comment"),
319
+ )
320
+
321
+
322
+ def _decode_domain(value: JsonValue, location: str) -> Domain:
323
+ record = _record(
324
+ value,
325
+ {"checks", "comment", "data-type", "default", "name", "nullable"},
326
+ location,
327
+ )
328
+ default = _optional_string(record["default"], f"{location}.default")
329
+ return Domain(
330
+ name=_identifier(record["name"], f"{location}.name"),
331
+ data_type=_decode_sql_type(record["data-type"], f"{location}.data-type"),
332
+ nullable=_boolean(record["nullable"], f"{location}.nullable"),
333
+ default=None if default is None else SqlExpression(default),
334
+ checks=_decode_array(record["checks"], f"{location}.checks", _decode_check),
335
+ comment=_optional_string(record["comment"], f"{location}.comment"),
336
+ )
337
+
338
+
339
+ def _decode_routine(value: JsonValue, location: str) -> Routine:
340
+ record = _record(
341
+ value,
342
+ {"comment", "kind", "name", "overload", "parameters", "returns"},
343
+ location,
344
+ )
345
+ returns = record["returns"]
346
+ return Routine(
347
+ name=_identifier(record["name"], f"{location}.name"),
348
+ kind=_routine_kind(record["kind"], f"{location}.kind"),
349
+ parameters=_decode_array(
350
+ record["parameters"], f"{location}.parameters", _decode_parameter
351
+ ),
352
+ returns=(
353
+ None
354
+ if returns is None
355
+ else _decode_sql_type(returns, f"{location}.returns")
356
+ ),
357
+ overload=_optional_string(record["overload"], f"{location}.overload"),
358
+ comment=_optional_string(record["comment"], f"{location}.comment"),
359
+ )
360
+
361
+
362
+ def _decode_parameter(value: JsonValue, location: str) -> RoutineParameter:
363
+ record = _record(
364
+ value, {"data-type", "has-default", "mode", "name", "nullable"}, location
365
+ )
366
+ return RoutineParameter(
367
+ name=_optional_identifier(record["name"], f"{location}.name"),
368
+ data_type=_decode_sql_type(record["data-type"], f"{location}.data-type"),
369
+ mode=_parameter_mode(record["mode"], f"{location}.mode"),
370
+ has_default=_boolean(record["has-default"], f"{location}.has-default"),
371
+ nullable=_boolean(record["nullable"], f"{location}.nullable"),
372
+ )
373
+
374
+
375
+ def _routine_kind(value: JsonValue, location: str) -> RoutineKind:
376
+ return _enum(RoutineKind, value, location)
377
+
378
+
379
+ def _parameter_mode(value: JsonValue, location: str) -> ParameterMode:
380
+ return _enum(ParameterMode, value, location)
381
+
382
+
383
+ def _decode_table(value: JsonValue, location: str) -> Table:
384
+ record = _record(
385
+ value,
386
+ {
387
+ "checks",
388
+ "columns",
389
+ "comment",
390
+ "indexes",
391
+ "keys",
392
+ "metadata",
393
+ "name",
394
+ "relations",
395
+ },
396
+ location,
397
+ )
398
+ return Table(
399
+ name=_identifier(record["name"], f"{location}.name"),
400
+ columns=_decode_array(record["columns"], f"{location}.columns", _decode_column),
401
+ keys=_decode_array(record["keys"], f"{location}.keys", _decode_key),
402
+ relations=_decode_array(
403
+ record["relations"],
404
+ f"{location}.relations",
405
+ _decode_relation,
406
+ ),
407
+ indexes=_decode_array(record["indexes"], f"{location}.indexes", _decode_index),
408
+ checks=_decode_array(record["checks"], f"{location}.checks", _decode_check),
409
+ comment=_optional_string(record["comment"], f"{location}.comment"),
410
+ metadata=_decode_array(
411
+ record["metadata"],
412
+ f"{location}.metadata",
413
+ _decode_metadata,
414
+ ),
415
+ )
416
+
417
+
418
+ def _decode_view(value: JsonValue, location: str) -> View:
419
+ record = _record(
420
+ value,
421
+ {"columns", "comment", "definition", "materialized", "metadata", "name"},
422
+ location,
423
+ )
424
+ return View(
425
+ name=_identifier(record["name"], f"{location}.name"),
426
+ columns=_decode_array(record["columns"], f"{location}.columns", _decode_column),
427
+ definition=_optional_expression(record["definition"], f"{location}.definition"),
428
+ materialized=_boolean(record["materialized"], f"{location}.materialized"),
429
+ comment=_optional_string(record["comment"], f"{location}.comment"),
430
+ metadata=_decode_array(
431
+ record["metadata"],
432
+ f"{location}.metadata",
433
+ _decode_metadata,
434
+ ),
435
+ )
436
+
437
+
438
+ def _decode_enum(value: JsonValue, location: str) -> EnumType:
439
+ record = _record(value, {"comment", "name", "values"}, location)
440
+ values = tuple(
441
+ _string(item, f"{location}.values")
442
+ for item in _array(record["values"], f"{location}.values")
443
+ )
444
+ return EnumType(
445
+ name=_identifier(record["name"], f"{location}.name"),
446
+ values=values,
447
+ comment=_optional_string(record["comment"], f"{location}.comment"),
448
+ )
449
+
450
+
451
+ def _decode_column(value: JsonValue, location: str) -> Column:
452
+ record = _record(
453
+ value,
454
+ {
455
+ "comment",
456
+ "data_type",
457
+ "default",
458
+ "generated",
459
+ "metadata",
460
+ "name",
461
+ "nullable",
462
+ },
463
+ location,
464
+ )
465
+ return Column(
466
+ name=_identifier(record["name"], f"{location}.name"),
467
+ data_type=_decode_sql_type(record["data_type"], f"{location}.data_type"),
468
+ nullable=_boolean(record["nullable"], f"{location}.nullable"),
469
+ default=_optional_expression(record["default"], f"{location}.default"),
470
+ generated=_decode_generated(record["generated"], f"{location}.generated"),
471
+ comment=_optional_string(record["comment"], f"{location}.comment"),
472
+ metadata=_decode_array(
473
+ record["metadata"],
474
+ f"{location}.metadata",
475
+ _decode_metadata,
476
+ ),
477
+ )
478
+
479
+
480
+ def _decode_sql_type(value: JsonValue, location: str) -> SqlType:
481
+ record = _record(
482
+ value,
483
+ {
484
+ "element_type",
485
+ "kind",
486
+ "length",
487
+ "metadata",
488
+ "named_type",
489
+ "precision",
490
+ "scale",
491
+ "source_name",
492
+ },
493
+ location,
494
+ )
495
+ element = record["element_type"]
496
+ named = record["named_type"]
497
+ return SqlType(
498
+ kind=_enum(TypeKind, record["kind"], f"{location}.kind"),
499
+ source_name=_string(record["source_name"], f"{location}.source_name"),
500
+ length=_optional_integer(record["length"], f"{location}.length"),
501
+ precision=_optional_integer(record["precision"], f"{location}.precision"),
502
+ scale=_optional_integer(record["scale"], f"{location}.scale"),
503
+ element_type=(
504
+ None
505
+ if element is None
506
+ else _decode_sql_type(element, f"{location}.element_type")
507
+ ),
508
+ named_type=(
509
+ None
510
+ if named is None
511
+ else _decode_reference(named, f"{location}.named_type")
512
+ ),
513
+ metadata=_decode_array(
514
+ record["metadata"],
515
+ f"{location}.metadata",
516
+ _decode_metadata,
517
+ ),
518
+ )
519
+
520
+
521
+ def _decode_generated(value: JsonValue, location: str) -> GeneratedValue | None:
522
+ if value is None:
523
+ return None
524
+ record = _record(value, {"expression", "kind"}, location)
525
+ return GeneratedValue(
526
+ kind=_enum(GeneratedKind, record["kind"], f"{location}.kind"),
527
+ expression=_optional_expression(record["expression"], f"{location}.expression"),
528
+ )
529
+
530
+
531
+ def _decode_key(value: JsonValue, location: str) -> Key:
532
+ record = _record(value, {"columns", "kind", "name"}, location)
533
+ return Key(
534
+ kind=_enum(KeyKind, record["kind"], f"{location}.kind"),
535
+ columns=_decode_identifiers(record["columns"], f"{location}.columns"),
536
+ name=_optional_identifier(record["name"], f"{location}.name"),
537
+ )
538
+
539
+
540
+ def _decode_relation(value: JsonValue, location: str) -> Relation:
541
+ record = _record(
542
+ value,
543
+ {"columns", "name", "on_delete", "on_update", "target", "target_columns"},
544
+ location,
545
+ )
546
+ return Relation(
547
+ columns=_decode_identifiers(record["columns"], f"{location}.columns"),
548
+ target=_decode_reference(record["target"], f"{location}.target"),
549
+ target_columns=_decode_identifiers(
550
+ record["target_columns"],
551
+ f"{location}.target_columns",
552
+ ),
553
+ name=_optional_identifier(record["name"], f"{location}.name"),
554
+ on_update=_enum(
555
+ ReferentialAction,
556
+ record["on_update"],
557
+ f"{location}.on_update",
558
+ ),
559
+ on_delete=_enum(
560
+ ReferentialAction,
561
+ record["on_delete"],
562
+ f"{location}.on_delete",
563
+ ),
564
+ )
565
+
566
+
567
+ def _decode_index(value: JsonValue, location: str) -> Index:
568
+ record = _record(
569
+ value,
570
+ {"method", "name", "predicate", "terms", "unique"},
571
+ location,
572
+ )
573
+ return Index(
574
+ name=_identifier(record["name"], f"{location}.name"),
575
+ terms=_decode_array(record["terms"], f"{location}.terms", _decode_index_term),
576
+ unique=_boolean(record["unique"], f"{location}.unique"),
577
+ predicate=_optional_expression(record["predicate"], f"{location}.predicate"),
578
+ method=_optional_string(record["method"], f"{location}.method"),
579
+ )
580
+
581
+
582
+ def _decode_index_term(value: JsonValue, location: str) -> IndexTerm:
583
+ record = _record(value, {"column", "descending", "expression"}, location)
584
+ return IndexTerm(
585
+ column=_optional_identifier(record["column"], f"{location}.column"),
586
+ expression=_optional_expression(record["expression"], f"{location}.expression"),
587
+ descending=_boolean(record["descending"], f"{location}.descending"),
588
+ )
589
+
590
+
591
+ def _decode_check(value: JsonValue, location: str) -> CheckConstraint:
592
+ record = _record(value, {"expression", "name"}, location)
593
+ return CheckConstraint(
594
+ expression=SqlExpression(
595
+ _string(record["expression"], f"{location}.expression")
596
+ ),
597
+ name=_optional_identifier(record["name"], f"{location}.name"),
598
+ )
599
+
600
+
601
+ def _decode_reference(value: JsonValue, location: str) -> ObjectReference:
602
+ record = _record(value, {"catalog", "name", "schema"}, location)
603
+ return ObjectReference(
604
+ name=_identifier(record["name"], f"{location}.name"),
605
+ schema=_optional_identifier(record["schema"], f"{location}.schema"),
606
+ catalog=_optional_identifier(record["catalog"], f"{location}.catalog"),
607
+ )
608
+
609
+
610
+ def _decode_metadata(value: JsonValue, location: str) -> MetadataEntry:
611
+ record = _record(value, {"key", "value"}, location)
612
+ return MetadataEntry(
613
+ key=_string(record["key"], f"{location}.key"),
614
+ value=_string(record["value"], f"{location}.value"),
615
+ )
616
+
617
+
618
+ def _decode_identifiers(value: JsonValue, location: str) -> tuple[Identifier, ...]:
619
+ return tuple(_identifier(item, location) for item in _array(value, location))
620
+
621
+
622
+ def _decode_array(
623
+ value: JsonValue,
624
+ location: str,
625
+ decoder: Callable[[JsonValue, str], DecodedValue],
626
+ ) -> tuple[DecodedValue, ...]:
627
+ return tuple(
628
+ decoder(item, f"{location}[{index}]")
629
+ for index, item in enumerate(_array(value, location))
630
+ )
631
+
632
+
633
+ def _record(value: JsonValue, fields: set[str], location: str) -> JsonObject:
634
+ record = _object(value, location)
635
+ _require_fields(record, fields, location)
636
+ return record
637
+
638
+
639
+ def _object(value: object, location: str) -> JsonObject:
640
+ if not isinstance(value, dict):
641
+ message = f"{location} must be an object"
642
+ raise SchemaSerializationError(message)
643
+ return cast(JsonObject, value)
644
+
645
+
646
+ def _array(value: JsonValue, location: str) -> list[JsonValue]:
647
+ if not isinstance(value, list):
648
+ message = f"{location} must be an array"
649
+ raise SchemaSerializationError(message)
650
+ return value
651
+
652
+
653
+ def _require_fields(value: JsonObject, fields: set[str], location: str) -> None:
654
+ actual = set(value)
655
+ if actual != fields:
656
+ message = f"{location} fields do not match the schema format"
657
+ raise SchemaSerializationError(message)
658
+
659
+
660
+ def _string(value: JsonValue, location: str) -> str:
661
+ if not isinstance(value, str):
662
+ message = f"{location} must be a string"
663
+ raise SchemaSerializationError(message)
664
+ return value
665
+
666
+
667
+ def _optional_string(value: JsonValue, location: str) -> str | None:
668
+ return None if value is None else _string(value, location)
669
+
670
+
671
+ def _integer(value: JsonValue, location: str) -> int:
672
+ if type(value) is not int:
673
+ message = f"{location} must be an integer"
674
+ raise SchemaSerializationError(message)
675
+ return value
676
+
677
+
678
+ def _optional_integer(value: JsonValue, location: str) -> int | None:
679
+ return None if value is None else _integer(value, location)
680
+
681
+
682
+ def _boolean(value: JsonValue, location: str) -> bool:
683
+ if type(value) is not bool:
684
+ message = f"{location} must be a boolean"
685
+ raise SchemaSerializationError(message)
686
+ return value
687
+
688
+
689
+ def _identifier(value: JsonValue, location: str) -> Identifier:
690
+ return Identifier(_string(value, location))
691
+
692
+
693
+ def _optional_identifier(value: JsonValue, location: str) -> Identifier | None:
694
+ return None if value is None else _identifier(value, location)
695
+
696
+
697
+ def _optional_expression(value: JsonValue, location: str) -> SqlExpression | None:
698
+ return None if value is None else SqlExpression(_string(value, location))
699
+
700
+
701
+ def _enum(
702
+ enum_type: type[EnumValue],
703
+ value: JsonValue,
704
+ location: str,
705
+ ) -> EnumValue:
706
+ text = _string(value, location)
707
+ try:
708
+ return enum_type(text)
709
+ except ValueError as error:
710
+ message = f"{location} contains an unsupported value"
711
+ raise SchemaSerializationError(message) from error