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