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,246 @@
1
+ """PostgreSQL system catalog queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ SCHEMA_OBJECTS = """
6
+ SELECT
7
+ n.nspname,
8
+ c.relname,
9
+ c.relkind,
10
+ obj_description(c.oid, 'pg_class'),
11
+ CASE
12
+ WHEN c.relkind IN ('v', 'm') THEN pg_get_viewdef(c.oid, true)
13
+ END
14
+ FROM pg_class AS c
15
+ JOIN pg_namespace AS n ON n.oid = c.relnamespace
16
+ WHERE c.relkind IN ('r', 'p', 'v', 'm')
17
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
18
+ AND n.nspname NOT LIKE 'pg\\_toast%'
19
+ AND n.nspname NOT LIKE 'pg\\_temp%'
20
+ ORDER BY n.nspname, c.relname
21
+ """
22
+
23
+ COLUMNS = """
24
+ SELECT
25
+ n.nspname,
26
+ c.relname,
27
+ a.attname,
28
+ format_type(a.atttypid, a.atttypmod),
29
+ NOT a.attnotnull AND NOT t.typnotnull,
30
+ t.typtype,
31
+ tn.nspname,
32
+ t.typname,
33
+ CASE
34
+ WHEN t.typtype = 'd'
35
+ THEN format_type(t.typbasetype, t.typtypmod)
36
+ END,
37
+ pg_get_expr(d.adbin, d.adrelid),
38
+ a.attidentity,
39
+ a.attgenerated,
40
+ col_description(c.oid, a.attnum)
41
+ FROM pg_attribute AS a
42
+ JOIN pg_class AS c ON c.oid = a.attrelid
43
+ JOIN pg_namespace AS n ON n.oid = c.relnamespace
44
+ JOIN pg_type AS t ON t.oid = a.atttypid
45
+ JOIN pg_namespace AS tn ON tn.oid = t.typnamespace
46
+ LEFT JOIN pg_attrdef AS d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
47
+ WHERE a.attnum > 0
48
+ AND NOT a.attisdropped
49
+ AND c.relkind IN ('r', 'p', 'v', 'm')
50
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
51
+ AND n.nspname NOT LIKE 'pg\\_toast%'
52
+ AND n.nspname NOT LIKE 'pg\\_temp%'
53
+ ORDER BY n.nspname, c.relname, a.attnum
54
+ """
55
+
56
+ CONSTRAINTS = """
57
+ SELECT
58
+ n.nspname,
59
+ c.relname,
60
+ con.conname,
61
+ con.contype,
62
+ pg_get_constraintdef(con.oid, true),
63
+ ARRAY(
64
+ SELECT a.attname
65
+ FROM unnest(con.conkey) WITH ORDINALITY AS k(attnum, position)
66
+ JOIN pg_attribute AS a
67
+ ON a.attrelid = con.conrelid AND a.attnum = k.attnum
68
+ ORDER BY k.position
69
+ ),
70
+ tn.nspname,
71
+ tc.relname,
72
+ ARRAY(
73
+ SELECT a.attname
74
+ FROM unnest(con.confkey) WITH ORDINALITY AS k(attnum, position)
75
+ JOIN pg_attribute AS a
76
+ ON a.attrelid = con.confrelid AND a.attnum = k.attnum
77
+ ORDER BY k.position
78
+ ),
79
+ con.confupdtype,
80
+ con.confdeltype
81
+ FROM pg_constraint AS con
82
+ JOIN pg_class AS c ON c.oid = con.conrelid
83
+ JOIN pg_namespace AS n ON n.oid = c.relnamespace
84
+ LEFT JOIN pg_class AS tc ON tc.oid = con.confrelid
85
+ LEFT JOIN pg_namespace AS tn ON tn.oid = tc.relnamespace
86
+ WHERE con.contype IN ('p', 'u', 'f', 'c')
87
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
88
+ AND n.nspname NOT LIKE 'pg\\_toast%'
89
+ AND n.nspname NOT LIKE 'pg\\_temp%'
90
+ ORDER BY n.nspname, c.relname, con.contype, con.conname
91
+ """
92
+
93
+ INDEXES = """
94
+ SELECT
95
+ n.nspname,
96
+ c.relname,
97
+ ic.relname,
98
+ i.indisunique,
99
+ am.amname,
100
+ pg_get_expr(i.indpred, i.indrelid, true),
101
+ ARRAY(
102
+ SELECT k.attnum
103
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, position)
104
+ ORDER BY k.position
105
+ ),
106
+ ARRAY(
107
+ SELECT a.attname
108
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, position)
109
+ LEFT JOIN pg_attribute AS a
110
+ ON a.attrelid = i.indrelid AND a.attnum = k.attnum
111
+ ORDER BY k.position
112
+ ),
113
+ ARRAY(
114
+ SELECT pg_get_indexdef(i.indexrelid, k.position::int, true)
115
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, position)
116
+ ORDER BY k.position
117
+ ),
118
+ ARRAY(
119
+ SELECT o.option
120
+ FROM unnest(i.indoption) WITH ORDINALITY AS o(option, position)
121
+ ORDER BY o.position
122
+ )
123
+ FROM pg_index AS i
124
+ JOIN pg_class AS c ON c.oid = i.indrelid
125
+ JOIN pg_class AS ic ON ic.oid = i.indexrelid
126
+ JOIN pg_namespace AS n ON n.oid = c.relnamespace
127
+ JOIN pg_am AS am ON am.oid = ic.relam
128
+ WHERE NOT i.indisprimary
129
+ AND c.relkind IN ('r', 'p')
130
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
131
+ AND n.nspname NOT LIKE 'pg\\_toast%'
132
+ AND n.nspname NOT LIKE 'pg\\_temp%'
133
+ ORDER BY n.nspname, c.relname, ic.relname
134
+ """
135
+
136
+ ENUMS = """
137
+ SELECT
138
+ n.nspname,
139
+ t.typname,
140
+ e.enumlabel,
141
+ obj_description(t.oid)
142
+ FROM pg_type AS t
143
+ JOIN pg_namespace AS n ON n.oid = t.typnamespace
144
+ JOIN pg_enum AS e ON e.enumtypid = t.oid
145
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
146
+ AND n.nspname NOT LIKE 'pg\\_toast%'
147
+ AND n.nspname NOT LIKE 'pg\\_temp%'
148
+ ORDER BY n.nspname, t.typname, e.enumsortorder
149
+ """
150
+ """Every enum type and its labels, in the order the database keeps them.
151
+
152
+ The order is the type's own, not alphabetical, because an enum compares and
153
+ sorts by it and a schema that reordered them would describe a different type.
154
+ """
155
+
156
+
157
+ DOMAINS = """
158
+ SELECT
159
+ n.nspname,
160
+ t.typname,
161
+ format_type(t.typbasetype, t.typtypmod),
162
+ t.typnotnull,
163
+ t.typdefault,
164
+ obj_description(t.oid)
165
+ FROM pg_type AS t
166
+ JOIN pg_namespace AS n ON n.oid = t.typnamespace
167
+ WHERE t.typtype = 'd'
168
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
169
+ AND n.nspname NOT LIKE 'pg\\_toast%'
170
+ AND n.nspname NOT LIKE 'pg\\_temp%'
171
+ ORDER BY n.nspname, t.typname
172
+ """
173
+
174
+ DOMAIN_CHECKS = """
175
+ SELECT
176
+ n.nspname,
177
+ t.typname,
178
+ c.conname,
179
+ pg_get_constraintdef(c.oid)
180
+ FROM pg_constraint AS c
181
+ JOIN pg_type AS t ON t.oid = c.contypid
182
+ JOIN pg_namespace AS n ON n.oid = t.typnamespace
183
+ WHERE c.contype = 'c'
184
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
185
+ ORDER BY n.nspname, t.typname, c.conname
186
+ """
187
+
188
+ ROUTINES = """
189
+ SELECT
190
+ n.nspname,
191
+ p.proname,
192
+ p.prokind,
193
+ p.oid,
194
+ CASE WHEN p.prokind = 'f' THEN format_type(p.prorettype, NULL) END,
195
+ a.ordinality,
196
+ p.proargnames[a.ordinality],
197
+ COALESCE(p.proargmodes[a.ordinality], 'i'),
198
+ format_type(a.argument_type, NULL),
199
+ rn.nspname,
200
+ rt.typname,
201
+ rt.typtype,
202
+ format_type(rt.typbasetype, NULL),
203
+ COALESCE(rd.typelem <> 0, FALSE),
204
+ an.nspname,
205
+ at.typname,
206
+ at.typtype,
207
+ format_type(at.typbasetype, NULL),
208
+ COALESCE(ad.typelem <> 0, FALSE),
209
+ COALESCE(at.typnotnull, FALSE),
210
+ p.pronargdefaults
211
+ FROM pg_proc AS p
212
+ JOIN pg_namespace AS n ON n.oid = p.pronamespace
213
+ LEFT JOIN pg_type AS rd ON rd.oid = p.prorettype
214
+ LEFT JOIN pg_type AS rt ON rt.oid = COALESCE(NULLIF(rd.typelem, 0), rd.oid)
215
+ LEFT JOIN pg_namespace AS rn ON rn.oid = rt.typnamespace
216
+ LEFT JOIN LATERAL unnest(
217
+ COALESCE(p.proallargtypes, p.proargtypes::oid[])
218
+ ) WITH ORDINALITY AS a(argument_type, ordinality) ON TRUE
219
+ LEFT JOIN pg_type AS ad ON ad.oid = a.argument_type
220
+ LEFT JOIN pg_type AS at ON at.oid = COALESCE(NULLIF(ad.typelem, 0), ad.oid)
221
+ LEFT JOIN pg_namespace AS an ON an.oid = at.typnamespace
222
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
223
+ AND p.prokind IN ('f', 'p')
224
+ ORDER BY n.nspname, p.proname, p.oid, a.ordinality
225
+ """
226
+ """One row per parameter, so a routine of no parameters is still one row.
227
+
228
+ The routine's own identifier separates two that share a name, which is what
229
+ keeps an overload from being mistaken for the other.
230
+
231
+ PostgreSQL states no nullability for a parameter itself. A parameter typed as
232
+ a domain that forbids null cannot be given one, and ``typnotnull`` is where
233
+ the catalog says so, so that is what is read here.
234
+ """
235
+
236
+
237
+ __all__ = (
238
+ "COLUMNS",
239
+ "CONSTRAINTS",
240
+ "DOMAINS",
241
+ "DOMAIN_CHECKS",
242
+ "ENUMS",
243
+ "INDEXES",
244
+ "ROUTINES",
245
+ "SCHEMA_OBJECTS",
246
+ )
@@ -0,0 +1,98 @@
1
+ """PostgreSQL declared-type normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from pyoq.schema.models import SqlType, TypeKind
8
+
9
+ _TYPE_ARGUMENTS = re.compile(r"^\s*([^()\[\]]*?)\s*(?:\(([^()]*)\))?\s*((?:\[\])*)\s*$")
10
+
11
+ _EXACT_KINDS = {
12
+ "BOOL": TypeKind.BOOLEAN,
13
+ "BOOLEAN": TypeKind.BOOLEAN,
14
+ "SMALLINT": TypeKind.SMALL_INTEGER,
15
+ "INT2": TypeKind.SMALL_INTEGER,
16
+ "SMALLSERIAL": TypeKind.SMALL_INTEGER,
17
+ "SERIAL2": TypeKind.SMALL_INTEGER,
18
+ "INTEGER": TypeKind.INTEGER,
19
+ "INT": TypeKind.INTEGER,
20
+ "INT4": TypeKind.INTEGER,
21
+ "SERIAL": TypeKind.INTEGER,
22
+ "SERIAL4": TypeKind.INTEGER,
23
+ "BIGINT": TypeKind.BIG_INTEGER,
24
+ "INT8": TypeKind.BIG_INTEGER,
25
+ "BIGSERIAL": TypeKind.BIG_INTEGER,
26
+ "SERIAL8": TypeKind.BIG_INTEGER,
27
+ "NUMERIC": TypeKind.DECIMAL,
28
+ "DECIMAL": TypeKind.DECIMAL,
29
+ "MONEY": TypeKind.DECIMAL,
30
+ "REAL": TypeKind.REAL,
31
+ "FLOAT4": TypeKind.REAL,
32
+ "DOUBLE PRECISION": TypeKind.DOUBLE,
33
+ "FLOAT8": TypeKind.DOUBLE,
34
+ "TEXT": TypeKind.STRING,
35
+ "VARCHAR": TypeKind.STRING,
36
+ "CHARACTER VARYING": TypeKind.STRING,
37
+ "CHAR": TypeKind.STRING,
38
+ "CHARACTER": TypeKind.STRING,
39
+ "BPCHAR": TypeKind.STRING,
40
+ "NAME": TypeKind.STRING,
41
+ "CITEXT": TypeKind.STRING,
42
+ "BYTEA": TypeKind.BINARY,
43
+ "DATE": TypeKind.DATE,
44
+ "TIME": TypeKind.TIME,
45
+ "TIME WITHOUT TIME ZONE": TypeKind.TIME,
46
+ "TIME WITH TIME ZONE": TypeKind.TIME,
47
+ "TIMETZ": TypeKind.TIME,
48
+ "TIMESTAMP": TypeKind.DATETIME,
49
+ "TIMESTAMP WITHOUT TIME ZONE": TypeKind.DATETIME,
50
+ "TIMESTAMP WITH TIME ZONE": TypeKind.DATETIME,
51
+ "TIMESTAMPTZ": TypeKind.DATETIME,
52
+ "INTERVAL": TypeKind.INTERVAL,
53
+ "UUID": TypeKind.UUID,
54
+ "JSON": TypeKind.JSON,
55
+ "JSONB": TypeKind.JSON,
56
+ }
57
+
58
+ _SIZED_KINDS = frozenset({TypeKind.STRING})
59
+ _PRECISION_KINDS = frozenset({TypeKind.DECIMAL})
60
+
61
+
62
+ def postgres_type(declared_type: str) -> SqlType:
63
+ source_name = declared_type.strip() or "text"
64
+ match = _TYPE_ARGUMENTS.fullmatch(source_name)
65
+ if match is None:
66
+ return SqlType(TypeKind.OTHER, source_name)
67
+ base_name, argument_text, array_suffix = match.groups()
68
+ if array_suffix:
69
+ element_source = source_name[: source_name.rindex("[]")].rstrip()
70
+ return SqlType(
71
+ TypeKind.ARRAY,
72
+ source_name,
73
+ element_type=postgres_type(element_source),
74
+ )
75
+ kind = _EXACT_KINDS.get(_normalized(base_name), TypeKind.OTHER)
76
+ arguments = _numeric_arguments(argument_text)
77
+ if kind in _SIZED_KINDS and len(arguments) == 1:
78
+ return SqlType(kind, source_name, length=arguments[0])
79
+ if kind in _PRECISION_KINDS and arguments:
80
+ scale = arguments[1] if len(arguments) == 2 else None
81
+ return SqlType(kind, source_name, precision=arguments[0], scale=scale)
82
+ return SqlType(kind, source_name)
83
+
84
+
85
+ def _normalized(value: str) -> str:
86
+ return " ".join(value.upper().split())
87
+
88
+
89
+ def _numeric_arguments(value: str | None) -> tuple[int, ...]:
90
+ if value is None:
91
+ return ()
92
+ parts = tuple(part.strip() for part in value.split(","))
93
+ if not 1 <= len(parts) <= 2 or any(not part.isdecimal() for part in parts):
94
+ return ()
95
+ return tuple(int(part) for part in parts)
96
+
97
+
98
+ __all__ = ("postgres_type",)
@@ -0,0 +1,45 @@
1
+ """Dialect to schema source resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
7
+ from typing import TypeAlias
8
+
9
+ from pyoq.config import Configuration, DatabaseDialect
10
+ from pyoq.schema.models import SchemaSnapshot
11
+ from pyoq.schema.mysql.source import MySQLSchemaSource
12
+ from pyoq.schema.postgres.source import PostgresSchemaSource
13
+ from pyoq.schema.source import SchemaSource
14
+ from pyoq.schema.sqlite.source import SQLiteSchemaSource
15
+
16
+ SchemaSourceFactory: TypeAlias = Callable[[], SchemaSource]
17
+
18
+ _SCHEMA_SOURCES: Mapping[DatabaseDialect, SchemaSourceFactory] = {
19
+ DatabaseDialect.MYSQL: MySQLSchemaSource,
20
+ DatabaseDialect.POSTGRES: PostgresSchemaSource,
21
+ DatabaseDialect.SQLITE: SQLiteSchemaSource,
22
+ }
23
+
24
+
25
+ def supported_dialects() -> tuple[DatabaseDialect, ...]:
26
+ return tuple(_SCHEMA_SOURCES)
27
+
28
+
29
+ def schema_source_for(dialect: DatabaseDialect, /) -> SchemaSource:
30
+ return _SCHEMA_SOURCES[dialect]()
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class DialectRoutingSchemaSource:
35
+ def load(self, configuration: Configuration) -> SchemaSnapshot:
36
+ dialect = configuration.active_profile.dialect
37
+ return schema_source_for(dialect).load(configuration)
38
+
39
+
40
+ __all__ = (
41
+ "DialectRoutingSchemaSource",
42
+ "SchemaSourceFactory",
43
+ "schema_source_for",
44
+ "supported_dialects",
45
+ )
pyoq/schema/source.py ADDED
@@ -0,0 +1,15 @@
1
+ """Dialect-neutral schema source contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from pyoq.config import Configuration
8
+ from pyoq.schema.models import SchemaSnapshot
9
+
10
+
11
+ class SchemaSource(Protocol):
12
+ def load(self, configuration: Configuration) -> SchemaSnapshot: ...
13
+
14
+
15
+ __all__ = ("SchemaSource",)
@@ -0,0 +1,6 @@
1
+ """SQLite schema inspection."""
2
+
3
+ from pyoq.schema.sqlite.reflection import SQLiteSchemaInspector
4
+ from pyoq.schema.sqlite.source import SQLiteSchemaSource
5
+
6
+ __all__ = ("SQLiteSchemaInspector", "SQLiteSchemaSource")
@@ -0,0 +1,54 @@
1
+ """Read-only SQLite connection ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from contextlib import AbstractContextManager, closing
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from pyoq.config import Configuration
11
+ from pyoq.errors import ConfigurationValidationError, SchemaInspectionError
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class SQLiteConnectionProvider:
16
+ def open(
17
+ self,
18
+ configuration: Configuration,
19
+ ) -> AbstractContextManager[sqlite3.Connection]:
20
+ path = _database_path(configuration)
21
+ _require_regular_database(path)
22
+ try:
23
+ connection = sqlite3.connect(
24
+ f"{path.as_uri()}?mode=ro",
25
+ uri=True,
26
+ check_same_thread=True,
27
+ )
28
+ except sqlite3.Error as error:
29
+ message = f"unable to open SQLite database: {error}"
30
+ raise SchemaInspectionError(message) from error
31
+ return closing(connection)
32
+
33
+
34
+ def _database_path(configuration: Configuration) -> Path:
35
+ configured = configuration.active_database_path
36
+ if configured is not None:
37
+ return configured
38
+ resolved = configuration.active_profile.resolve_dsn()
39
+ if resolved is None:
40
+ message = "SQLite profile requires database-path or a dsn setting"
41
+ raise ConfigurationValidationError(message)
42
+ return Path(resolved.reveal()).expanduser().resolve()
43
+
44
+
45
+ def _require_regular_database(path: Path) -> None:
46
+ if not path.exists():
47
+ message = f"SQLite database does not exist: {path}"
48
+ raise SchemaInspectionError(message)
49
+ if not path.is_file():
50
+ message = f"SQLite database is not a regular file: {path}"
51
+ raise SchemaInspectionError(message)
52
+
53
+
54
+ __all__ = ("SQLiteConnectionProvider",)
@@ -0,0 +1,167 @@
1
+ """Typed SQLite metadata records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TypeAlias, cast
7
+
8
+ from pyoq.errors import SchemaInspectionError
9
+
10
+ DatabaseRow: TypeAlias = tuple[object, ...]
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class SchemaObjectRecord:
15
+ kind: str
16
+ name: str
17
+ sql: str | None
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ColumnRecord:
22
+ position: int
23
+ name: str
24
+ declared_type: str
25
+ not_null: bool
26
+ default: str | None
27
+ primary_ordinal: int
28
+ hidden: int
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class IndexRecord:
33
+ position: int
34
+ name: str
35
+ unique: bool
36
+ origin: str
37
+ partial: bool
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class IndexTermRecord:
42
+ position: int
43
+ column_position: int
44
+ column_name: str | None
45
+ descending: bool
46
+ key_term: bool
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ForeignKeyRecord:
51
+ relation_id: int
52
+ position: int
53
+ target_table: str
54
+ source_column: str
55
+ target_column: str | None
56
+ on_update: str
57
+ on_delete: str
58
+
59
+
60
+ def schema_object(row: object) -> SchemaObjectRecord:
61
+ values = _row(row, 3, "schema object")
62
+ return SchemaObjectRecord(
63
+ _text(values[0], "schema object kind"),
64
+ _text(values[1], "schema object name"),
65
+ _optional_text(values[2], "schema object SQL"),
66
+ )
67
+
68
+
69
+ def column(row: object) -> ColumnRecord:
70
+ values = _row(row, 7, "column")
71
+ return ColumnRecord(
72
+ _integer(values[0], "column position"),
73
+ _text(values[1], "column name"),
74
+ _optional_text(values[2], "column type") or "ANY",
75
+ _flag(values[3], "column not-null flag"),
76
+ _optional_text(values[4], "column default"),
77
+ _integer(values[5], "column primary-key position"),
78
+ _integer(values[6], "column hidden flag"),
79
+ )
80
+
81
+
82
+ def index(row: object) -> IndexRecord:
83
+ values = _row(row, 5, "index")
84
+ return IndexRecord(
85
+ _integer(values[0], "index position"),
86
+ _text(values[1], "index name"),
87
+ _flag(values[2], "index unique flag"),
88
+ _text(values[3], "index origin"),
89
+ _flag(values[4], "index partial flag"),
90
+ )
91
+
92
+
93
+ def index_term(row: object) -> IndexTermRecord:
94
+ values = _row(row, 6, "index term")
95
+ return IndexTermRecord(
96
+ _integer(values[0], "index term position"),
97
+ _integer(values[1], "index term column position"),
98
+ _optional_text(values[2], "index term column name"),
99
+ _flag(values[3], "index descending flag"),
100
+ _flag(values[5], "index key-term flag"),
101
+ )
102
+
103
+
104
+ def foreign_key(row: object) -> ForeignKeyRecord:
105
+ values = _row(row, 8, "foreign key")
106
+ return ForeignKeyRecord(
107
+ _integer(values[0], "foreign-key id"),
108
+ _integer(values[1], "foreign-key position"),
109
+ _text(values[2], "foreign-key target table"),
110
+ _text(values[3], "foreign-key source column"),
111
+ _optional_text(values[4], "foreign-key target column"),
112
+ _text(values[5], "foreign-key update action"),
113
+ _text(values[6], "foreign-key delete action"),
114
+ )
115
+
116
+
117
+ def _row(value: object, size: int, label: str) -> DatabaseRow:
118
+ if not isinstance(value, tuple):
119
+ message = f"invalid SQLite {label} metadata"
120
+ raise SchemaInspectionError(message)
121
+ values = cast(DatabaseRow, value)
122
+ if len(values) != size:
123
+ message = f"invalid SQLite {label} metadata"
124
+ raise SchemaInspectionError(message)
125
+ return values
126
+
127
+
128
+ def _text(value: object, label: str) -> str:
129
+ if not isinstance(value, str) or not value:
130
+ message = f"invalid SQLite {label}"
131
+ raise SchemaInspectionError(message)
132
+ return value
133
+
134
+
135
+ def _optional_text(value: object, label: str) -> str | None:
136
+ if value is None:
137
+ return None
138
+ return _text(value, label)
139
+
140
+
141
+ def _integer(value: object, label: str) -> int:
142
+ if not isinstance(value, int) or isinstance(value, bool):
143
+ message = f"invalid SQLite {label}"
144
+ raise SchemaInspectionError(message)
145
+ return value
146
+
147
+
148
+ def _flag(value: object, label: str) -> bool:
149
+ flag = _integer(value, label)
150
+ if flag not in {0, 1}:
151
+ message = f"invalid SQLite {label}"
152
+ raise SchemaInspectionError(message)
153
+ return bool(flag)
154
+
155
+
156
+ __all__ = (
157
+ "ColumnRecord",
158
+ "ForeignKeyRecord",
159
+ "IndexRecord",
160
+ "IndexTermRecord",
161
+ "SchemaObjectRecord",
162
+ "column",
163
+ "foreign_key",
164
+ "index",
165
+ "index_term",
166
+ "schema_object",
167
+ )