supaschema 0.1.0-rc.1

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 (257) hide show
  1. package/.agents/skills/supaschema/SKILL.md +61 -0
  2. package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
  3. package/.claude/rules/supaschema.md +22 -0
  4. package/.claude/settings.json +16 -0
  5. package/.claude/skills/supaschema/SKILL.md +61 -0
  6. package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
  7. package/.codex/hooks.json +16 -0
  8. package/.codex/rules/supaschema.rules +22 -0
  9. package/AGENTS.md +40 -0
  10. package/LICENSE +661 -0
  11. package/LICENSE-COMMERCIAL.md +35 -0
  12. package/README.md +249 -0
  13. package/benchmarks/README.md +104 -0
  14. package/benchmarks/compare.js +489 -0
  15. package/benchmarks/fixtures/additive/from.sql +8 -0
  16. package/benchmarks/fixtures/additive/manifest.json +1 -0
  17. package/benchmarks/fixtures/additive/to.sql +9 -0
  18. package/benchmarks/fixtures/functions-policies/from.sql +24 -0
  19. package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
  20. package/benchmarks/fixtures/functions-policies/to.sql +24 -0
  21. package/benchmarks/plot-lib.js +234 -0
  22. package/benchmarks/plot-svg.js +339 -0
  23. package/benchmarks/plot.js +154 -0
  24. package/benchmarks/tools/bench-all.sh +49 -0
  25. package/benchmarks/tools/build-project-fixture.mjs +245 -0
  26. package/benchmarks/tools/compare-db.mjs +101 -0
  27. package/benchmarks/tools/compare-fixtures.mjs +84 -0
  28. package/benchmarks/tools/compare-report.mjs +90 -0
  29. package/benchmarks/tools/compare-supabase.mjs +67 -0
  30. package/benchmarks/tools/registry.js +266 -0
  31. package/benchmarks/tools/run-workflow.mjs +77 -0
  32. package/bin/postinstall.mjs +26 -0
  33. package/bin/supaschema +2 -0
  34. package/config-schema.json +208 -0
  35. package/corpus/supabase-style/corpus.json +6 -0
  36. package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
  37. package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
  38. package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
  39. package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
  40. package/corpus/supabase-style/roles.sql +13 -0
  41. package/corpus/supabase-style/tree/functions.sql +26 -0
  42. package/corpus/supabase-style/tree/policies.sql +4 -0
  43. package/corpus/supabase-style/tree/schema.sql +4 -0
  44. package/corpus/supabase-style/tree/tables.sql +20 -0
  45. package/corpus/supabase-style/tree/triggers.sql +3 -0
  46. package/corpus/supabase-style/tree/types.sql +2 -0
  47. package/corpus/supabase-style/tree/views.sql +6 -0
  48. package/dist/audit.d.ts +20 -0
  49. package/dist/audit.d.ts.map +1 -0
  50. package/dist/audit.js +68 -0
  51. package/dist/benchmark-db.d.ts +5 -0
  52. package/dist/benchmark-db.d.ts.map +1 -0
  53. package/dist/benchmark-db.js +71 -0
  54. package/dist/benchmark-fixtures.d.ts +10 -0
  55. package/dist/benchmark-fixtures.d.ts.map +1 -0
  56. package/dist/benchmark-fixtures.js +201 -0
  57. package/dist/benchmark.d.ts +2 -0
  58. package/dist/benchmark.d.ts.map +1 -0
  59. package/dist/benchmark.js +308 -0
  60. package/dist/catalog-comments.d.ts +9 -0
  61. package/dist/catalog-comments.d.ts.map +1 -0
  62. package/dist/catalog-comments.js +194 -0
  63. package/dist/catalog-extras.d.ts +12 -0
  64. package/dist/catalog-extras.d.ts.map +1 -0
  65. package/dist/catalog-extras.js +408 -0
  66. package/dist/catalog-foreign.d.ts +15 -0
  67. package/dist/catalog-foreign.d.ts.map +1 -0
  68. package/dist/catalog-foreign.js +114 -0
  69. package/dist/catalog-tables.d.ts +9 -0
  70. package/dist/catalog-tables.d.ts.map +1 -0
  71. package/dist/catalog-tables.js +114 -0
  72. package/dist/catalog.d.ts +8 -0
  73. package/dist/catalog.d.ts.map +1 -0
  74. package/dist/catalog.js +351 -0
  75. package/dist/check-hazards.d.ts +7 -0
  76. package/dist/check-hazards.d.ts.map +1 -0
  77. package/dist/check-hazards.js +83 -0
  78. package/dist/check-reporters.d.ts +8 -0
  79. package/dist/check-reporters.d.ts.map +1 -0
  80. package/dist/check-reporters.js +76 -0
  81. package/dist/check.d.ts +3 -0
  82. package/dist/check.d.ts.map +1 -0
  83. package/dist/check.js +229 -0
  84. package/dist/cli-defaults.d.ts +24 -0
  85. package/dist/cli-defaults.d.ts.map +1 -0
  86. package/dist/cli-defaults.js +65 -0
  87. package/dist/cli-diff.d.ts +13 -0
  88. package/dist/cli-diff.d.ts.map +1 -0
  89. package/dist/cli-diff.js +348 -0
  90. package/dist/cli-reports.d.ts +9 -0
  91. package/dist/cli-reports.d.ts.map +1 -0
  92. package/dist/cli-reports.js +90 -0
  93. package/dist/cli-tools.d.ts +17 -0
  94. package/dist/cli-tools.d.ts.map +1 -0
  95. package/dist/cli-tools.js +136 -0
  96. package/dist/cli.d.ts +2 -0
  97. package/dist/cli.d.ts.map +1 -0
  98. package/dist/cli.js +239 -0
  99. package/dist/config-schema-gen.d.ts +2 -0
  100. package/dist/config-schema-gen.d.ts.map +1 -0
  101. package/dist/config-schema-gen.js +11 -0
  102. package/dist/config.d.ts +58 -0
  103. package/dist/config.d.ts.map +1 -0
  104. package/dist/config.js +132 -0
  105. package/dist/core.d.ts +115 -0
  106. package/dist/core.d.ts.map +1 -0
  107. package/dist/core.js +1 -0
  108. package/dist/corpus.d.ts +26 -0
  109. package/dist/corpus.d.ts.map +1 -0
  110. package/dist/corpus.js +112 -0
  111. package/dist/database-url.d.ts +8 -0
  112. package/dist/database-url.d.ts.map +1 -0
  113. package/dist/database-url.js +74 -0
  114. package/dist/db-admin.d.ts +23 -0
  115. package/dist/db-admin.d.ts.map +1 -0
  116. package/dist/db-admin.js +147 -0
  117. package/dist/diagnostics.d.ts +16 -0
  118. package/dist/diagnostics.d.ts.map +1 -0
  119. package/dist/diagnostics.js +155 -0
  120. package/dist/diff-score.d.ts +12 -0
  121. package/dist/diff-score.d.ts.map +1 -0
  122. package/dist/diff-score.js +339 -0
  123. package/dist/doctor.d.ts +17 -0
  124. package/dist/doctor.d.ts.map +1 -0
  125. package/dist/doctor.js +110 -0
  126. package/dist/hash.d.ts +7 -0
  127. package/dist/hash.d.ts.map +1 -0
  128. package/dist/hash.js +34 -0
  129. package/dist/index.d.ts +24 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +17 -0
  132. package/dist/lineage.d.ts +23 -0
  133. package/dist/lineage.d.ts.map +1 -0
  134. package/dist/lineage.js +61 -0
  135. package/dist/migrations-status.d.ts +35 -0
  136. package/dist/migrations-status.d.ts.map +1 -0
  137. package/dist/migrations-status.js +131 -0
  138. package/dist/plan-order.d.ts +4 -0
  139. package/dist/plan-order.d.ts.map +1 -0
  140. package/dist/plan-order.js +178 -0
  141. package/dist/planner-replace.d.ts +4 -0
  142. package/dist/planner-replace.d.ts.map +1 -0
  143. package/dist/planner-replace.js +76 -0
  144. package/dist/planner-table.d.ts +3 -0
  145. package/dist/planner-table.d.ts.map +1 -0
  146. package/dist/planner-table.js +165 -0
  147. package/dist/planner.d.ts +5 -0
  148. package/dist/planner.d.ts.map +1 -0
  149. package/dist/planner.js +385 -0
  150. package/dist/render-guards.d.ts +12 -0
  151. package/dist/render-guards.d.ts.map +1 -0
  152. package/dist/render-guards.js +159 -0
  153. package/dist/render.d.ts +7 -0
  154. package/dist/render.d.ts.map +1 -0
  155. package/dist/render.js +325 -0
  156. package/dist/selfcheck.d.ts +11 -0
  157. package/dist/selfcheck.d.ts.map +1 -0
  158. package/dist/selfcheck.js +43 -0
  159. package/dist/source-normalize.d.ts +14 -0
  160. package/dist/source-normalize.d.ts.map +1 -0
  161. package/dist/source-normalize.js +420 -0
  162. package/dist/source.d.ts +4 -0
  163. package/dist/source.d.ts.map +1 -0
  164. package/dist/source.js +233 -0
  165. package/dist/sql/ast.d.ts +42 -0
  166. package/dist/sql/ast.d.ts.map +1 -0
  167. package/dist/sql/ast.js +241 -0
  168. package/dist/sql/canonical-nodes.d.ts +5 -0
  169. package/dist/sql/canonical-nodes.d.ts.map +1 -0
  170. package/dist/sql/canonical-nodes.js +101 -0
  171. package/dist/sql/extract-helpers.d.ts +18 -0
  172. package/dist/sql/extract-helpers.d.ts.map +1 -0
  173. package/dist/sql/extract-helpers.js +127 -0
  174. package/dist/sql/extract.d.ts +13 -0
  175. package/dist/sql/extract.d.ts.map +1 -0
  176. package/dist/sql/extract.js +323 -0
  177. package/dist/sql/facts.d.ts +34 -0
  178. package/dist/sql/facts.d.ts.map +1 -0
  179. package/dist/sql/facts.js +392 -0
  180. package/dist/sql/identifiers.d.ts +13 -0
  181. package/dist/sql/identifiers.d.ts.map +1 -0
  182. package/dist/sql/identifiers.js +83 -0
  183. package/dist/sql/normalize-deparse.d.ts +25 -0
  184. package/dist/sql/normalize-deparse.d.ts.map +1 -0
  185. package/dist/sql/normalize-deparse.js +96 -0
  186. package/dist/sql/object-hash.d.ts +5 -0
  187. package/dist/sql/object-hash.d.ts.map +1 -0
  188. package/dist/sql/object-hash.js +24 -0
  189. package/dist/sql/parser.d.ts +8 -0
  190. package/dist/sql/parser.d.ts.map +1 -0
  191. package/dist/sql/parser.js +89 -0
  192. package/dist/sql/privileges.d.ts +33 -0
  193. package/dist/sql/privileges.d.ts.map +1 -0
  194. package/dist/sql/privileges.js +379 -0
  195. package/dist/sql/split.d.ts +3 -0
  196. package/dist/sql/split.d.ts.map +1 -0
  197. package/dist/sql/split.js +182 -0
  198. package/dist/sql/statements.d.ts +17 -0
  199. package/dist/sql/statements.d.ts.map +1 -0
  200. package/dist/sql/statements.js +284 -0
  201. package/dist/sql/table-constraints.d.ts +15 -0
  202. package/dist/sql/table-constraints.d.ts.map +1 -0
  203. package/dist/sql/table-constraints.js +304 -0
  204. package/dist/sql/table-shape.d.ts +38 -0
  205. package/dist/sql/table-shape.d.ts.map +1 -0
  206. package/dist/sql/table-shape.js +287 -0
  207. package/dist/sync.d.ts +27 -0
  208. package/dist/sync.d.ts.map +1 -0
  209. package/dist/sync.js +86 -0
  210. package/dist/typegen-model.d.ts +78 -0
  211. package/dist/typegen-model.d.ts.map +1 -0
  212. package/dist/typegen-model.js +338 -0
  213. package/dist/typegen-views.d.ts +7 -0
  214. package/dist/typegen-views.d.ts.map +1 -0
  215. package/dist/typegen-views.js +92 -0
  216. package/dist/typegen-zod.d.ts +3 -0
  217. package/dist/typegen-zod.d.ts.map +1 -0
  218. package/dist/typegen-zod.js +149 -0
  219. package/dist/typegen.d.ts +4 -0
  220. package/dist/typegen.d.ts.map +1 -0
  221. package/dist/typegen.js +184 -0
  222. package/dist/validators.d.ts +3 -0
  223. package/dist/validators.d.ts.map +1 -0
  224. package/dist/validators.js +104 -0
  225. package/dist/verify-environment.d.ts +5 -0
  226. package/dist/verify-environment.d.ts.map +1 -0
  227. package/dist/verify-environment.js +92 -0
  228. package/dist/verify.d.ts +3 -0
  229. package/dist/verify.d.ts.map +1 -0
  230. package/dist/verify.js +261 -0
  231. package/docs/benchmarks/additive-correctness.svg +86 -0
  232. package/docs/benchmarks/additive-latency.svg +60 -0
  233. package/docs/benchmarks/functions-policies-correctness.svg +86 -0
  234. package/docs/benchmarks/functions-policies-latency.svg +60 -0
  235. package/docs/benchmarks/realistic-correctness.svg +86 -0
  236. package/docs/benchmarks/realistic-latency.svg +60 -0
  237. package/docs/benchmarks/scaling-latency.svg +106 -0
  238. package/docs/benchmarks/workflow-latency.svg +98 -0
  239. package/docs/benchmarks/xl-correctness.svg +86 -0
  240. package/docs/benchmarks/xl-latency.svg +60 -0
  241. package/docs/benchmarks/xxl-correctness.svg +86 -0
  242. package/docs/benchmarks/xxl-latency.svg +66 -0
  243. package/docs/case-study-anilize.md +51 -0
  244. package/docs/ci-gate.md +44 -0
  245. package/docs/ci.md +68 -0
  246. package/docs/commands.md +35 -0
  247. package/docs/config.md +72 -0
  248. package/docs/corpus.md +33 -0
  249. package/docs/diagnostics.md +77 -0
  250. package/docs/hints.md +92 -0
  251. package/docs/release.md +19 -0
  252. package/docs/support-matrix.md +57 -0
  253. package/examples/postgres/schemas/001_app.sql +17 -0
  254. package/examples/supabase/schemas/001_app.sql +13 -0
  255. package/examples/supabase/schemas-next/001_app.sql +21 -0
  256. package/examples/supabase/supaschema.config.json +15 -0
  257. package/package.json +99 -0
@@ -0,0 +1,194 @@
1
+ import { sha256 } from "./hash.js";
2
+ import { formatQualifiedName, quoteIdent } from "./sql/identifiers.js";
3
+ import { makeObject } from "./sql/statements.js";
4
+ const managedSchemaFilter = `
5
+ n.nspname !~ '^pg_'
6
+ and n.nspname <> 'information_schema'
7
+ `;
8
+ const relationCommentWords = new Map([
9
+ ["S", "sequence"],
10
+ ["i", "index"],
11
+ ["m", "materialized view"],
12
+ ["p", "table"],
13
+ ["r", "table"],
14
+ ["v", "view"],
15
+ ]);
16
+ export async function collectComments(pool) {
17
+ const sections = await Promise.all([
18
+ collectRelationComments(pool),
19
+ collectFunctionComments(pool),
20
+ collectSchemaComments(pool),
21
+ collectTypeComments(pool),
22
+ collectPolicyComments(pool),
23
+ collectTriggerComments(pool),
24
+ collectConstraintComments(pool),
25
+ collectExtensionComments(pool),
26
+ ]);
27
+ return sections.flat();
28
+ }
29
+ async function collectRelationComments(pool) {
30
+ const objects = [];
31
+ const rows = await pool.query(`
32
+ select n.nspname as schema, c.relname as name, c.relkind as relkind,
33
+ d.objsubid as column_number, a.attname as column_name, d.description as description
34
+ from pg_description d
35
+ join pg_class c on c.oid = d.objoid
36
+ join pg_namespace n on n.oid = c.relnamespace
37
+ left join pg_attribute a on a.attrelid = c.oid and a.attnum = d.objsubid and d.objsubid > 0
38
+ where d.classoid = 'pg_class'::regclass and ${managedSchemaFilter}
39
+ order by n.nspname, c.relname, d.objsubid
40
+ `);
41
+ for (const row of rows.rows) {
42
+ const schema = text(row.schema);
43
+ const name = text(row.name);
44
+ const word = relationCommentWords.get(text(row.relkind));
45
+ if (!word) {
46
+ continue;
47
+ }
48
+ const isColumn = typeof row.column_number === "number" && row.column_number > 0;
49
+ if (isColumn && !row.column_name) {
50
+ continue;
51
+ }
52
+ const descriptor = isColumn
53
+ ? `column ${schema}.${name}.${text(row.column_name)}`
54
+ : `${word} ${schema}.${name}`;
55
+ const targetSql = isColumn
56
+ ? `COLUMN ${formatQualifiedName(schema, name)}.${quoteIdent(text(row.column_name))}`
57
+ : `${word.toUpperCase()} ${formatQualifiedName(schema, name)}`;
58
+ objects.push(commentObject(descriptor, targetSql, schema, text(row.description)));
59
+ }
60
+ return objects;
61
+ }
62
+ async function collectFunctionComments(pool) {
63
+ const rows = await pool.query(`
64
+ select n.nspname as schema, p.proname as name,
65
+ oidvectortypes(p.proargtypes) as args, d.description as description
66
+ from pg_description d
67
+ join pg_proc p on p.oid = d.objoid
68
+ join pg_namespace n on n.oid = p.pronamespace
69
+ where d.classoid = 'pg_proc'::regclass and ${managedSchemaFilter}
70
+ order by n.nspname, p.proname
71
+ `);
72
+ return rows.rows.map((row) => {
73
+ const schema = text(row.schema);
74
+ const name = text(row.name);
75
+ const args = text(row.args);
76
+ return commentObject(`function ${schema}.${name}(${args})`, `FUNCTION ${formatQualifiedName(schema, name)}(${args})`, schema, text(row.description));
77
+ });
78
+ }
79
+ async function collectSchemaComments(pool) {
80
+ const rows = await pool.query(`
81
+ select n.nspname as name, d.description as description
82
+ from pg_description d
83
+ join pg_namespace n on n.oid = d.objoid
84
+ where d.classoid = 'pg_namespace'::regclass
85
+ and ${managedSchemaFilter}
86
+ and n.nspname <> 'public'
87
+ order by n.nspname
88
+ `);
89
+ return rows.rows.map((row) => {
90
+ const name = text(row.name);
91
+ return commentObject(`schema ${name}`, `SCHEMA ${name}`, undefined, text(row.description));
92
+ });
93
+ }
94
+ async function collectTypeComments(pool) {
95
+ const rows = await pool.query(`
96
+ select n.nspname as schema, t.typname as name, t.typtype as typtype,
97
+ d.description as description
98
+ from pg_description d
99
+ join pg_type t on t.oid = d.objoid
100
+ join pg_namespace n on n.oid = t.typnamespace
101
+ where d.classoid = 'pg_type'::regclass and ${managedSchemaFilter}
102
+ order by n.nspname, t.typname
103
+ `);
104
+ return rows.rows.map((row) => {
105
+ const schema = text(row.schema);
106
+ const name = text(row.name);
107
+ const word = text(row.typtype) === "d" ? "domain" : "type";
108
+ return commentObject(`${word} ${schema}.${name}`, `${word.toUpperCase()} ${formatQualifiedName(schema, name)}`, schema, text(row.description));
109
+ });
110
+ }
111
+ async function collectPolicyComments(pool) {
112
+ const rows = await pool.query(`
113
+ select n.nspname as schema, c.relname as table_name, p.polname as name,
114
+ d.description as description
115
+ from pg_description d
116
+ join pg_policy p on p.oid = d.objoid
117
+ join pg_class c on c.oid = p.polrelid
118
+ join pg_namespace n on n.oid = c.relnamespace
119
+ where d.classoid = 'pg_policy'::regclass and ${managedSchemaFilter}
120
+ order by n.nspname, c.relname, p.polname
121
+ `);
122
+ return rows.rows.map((row) => {
123
+ const schema = text(row.schema);
124
+ const table = text(row.table_name);
125
+ const name = text(row.name);
126
+ return commentObject(`policy ${schema}.${table}.${name}`, `POLICY ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
127
+ });
128
+ }
129
+ async function collectTriggerComments(pool) {
130
+ const rows = await pool.query(`
131
+ select n.nspname as schema, c.relname as table_name, t.tgname as name,
132
+ d.description as description
133
+ from pg_description d
134
+ join pg_trigger t on t.oid = d.objoid
135
+ join pg_class c on c.oid = t.tgrelid
136
+ join pg_namespace n on n.oid = c.relnamespace
137
+ where d.classoid = 'pg_trigger'::regclass
138
+ and not t.tgisinternal
139
+ and ${managedSchemaFilter}
140
+ order by n.nspname, c.relname, t.tgname
141
+ `);
142
+ return rows.rows.map((row) => {
143
+ const schema = text(row.schema);
144
+ const table = text(row.table_name);
145
+ const name = text(row.name);
146
+ return commentObject(`trigger ${schema}.${table}.${name}`, `TRIGGER ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
147
+ });
148
+ }
149
+ async function collectConstraintComments(pool) {
150
+ const rows = await pool.query(`
151
+ select n.nspname as schema, r.relname as table_name, c.conname as name,
152
+ d.description as description
153
+ from pg_description d
154
+ join pg_constraint c on c.oid = d.objoid
155
+ join pg_class r on r.oid = c.conrelid
156
+ join pg_namespace n on n.oid = r.relnamespace
157
+ where d.classoid = 'pg_constraint'::regclass and ${managedSchemaFilter}
158
+ order by n.nspname, r.relname, c.conname
159
+ `);
160
+ return rows.rows.map((row) => {
161
+ const schema = text(row.schema);
162
+ const table = text(row.table_name);
163
+ const name = text(row.name);
164
+ return commentObject(`constraint ${schema}.${table}.${name}`, `CONSTRAINT ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
165
+ });
166
+ }
167
+ async function collectExtensionComments(pool) {
168
+ const rows = await pool.query(`
169
+ select e.extname as name, d.description as description
170
+ from pg_description d
171
+ join pg_extension e on e.oid = d.objoid
172
+ where d.classoid = 'pg_extension'::regclass
173
+ and e.extname <> 'plpgsql'
174
+ order by e.extname
175
+ `);
176
+ return rows.rows.map((row) => {
177
+ const name = text(row.name);
178
+ return commentObject(`extension ${name}`, `EXTENSION ${quoteIdent(name)}`, undefined, text(row.description));
179
+ });
180
+ }
181
+ function commentObject(descriptor, targetSql, schema, description) {
182
+ const sql = `COMMENT ON ${targetSql} IS '${description.replaceAll("'", "''")}'`;
183
+ const ref = {
184
+ kind: "comment",
185
+ name: sha256(descriptor).slice(0, 16),
186
+ };
187
+ if (schema) {
188
+ ref.schema = schema;
189
+ }
190
+ return makeObject(ref, sql, 0, undefined, { descriptor });
191
+ }
192
+ function text(value) {
193
+ return typeof value === "string" ? value : String(value);
194
+ }
@@ -0,0 +1,12 @@
1
+ import type { SchemaObject } from "./core.js";
2
+ type CatalogQuery = {
3
+ query: <Row extends Record<string, unknown>>(text: string, values?: unknown[]) => Promise<{
4
+ rows: Row[];
5
+ }>;
6
+ };
7
+ export declare function collectTypes(pool: CatalogQuery): Promise<SchemaObject[]>;
8
+ export declare function collectSequences(pool: CatalogQuery): Promise<SchemaObject[]>;
9
+ export declare function collectGrants(pool: CatalogQuery): Promise<SchemaObject[]>;
10
+ export declare function collectDefaultPrivileges(pool: CatalogQuery): Promise<SchemaObject[]>;
11
+ export {};
12
+ //# sourceMappingURL=catalog-extras.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-extras.d.ts","sourceRoot":"","sources":["../src/catalog-extras.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAS9C,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CAC/B,CAAC;AAOF,wBAAsB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA+E9E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAsElF;AAQD,wBAAsB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA2L/E;AAUD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuE1F"}
@@ -0,0 +1,408 @@
1
+ import { formatQualifiedName, quoteIdent, stripOuterDoubleQuotes } from "./sql/identifiers.js";
2
+ import { buildDefaultPrivilegeObject, buildGrantObject, isBuiltinDefaultGrant, } from "./sql/privileges.js";
3
+ import { makeObject } from "./sql/statements.js";
4
+ const managedSchemaFilter = `
5
+ n.nspname !~ '^pg_'
6
+ and n.nspname <> 'information_schema'
7
+ `;
8
+ export async function collectTypes(pool) {
9
+ const objects = [];
10
+ const enums = await pool.query(`
11
+ select n.nspname as schema, t.typname as name,
12
+ array_agg(e.enumlabel order by e.enumsortorder) as values
13
+ from pg_type t
14
+ join pg_namespace n on n.oid = t.typnamespace
15
+ join pg_enum e on e.enumtypid = t.oid
16
+ where t.typtype = 'e' and ${managedSchemaFilter}
17
+ group by n.nspname, t.typname
18
+ order by n.nspname, t.typname
19
+ `);
20
+ for (const row of enums.rows) {
21
+ const schema = text(row.schema);
22
+ const name = text(row.name);
23
+ const values = textArray(row.values);
24
+ const rendered = values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ");
25
+ objects.push(makeObject({ kind: "enum", name, schema }, `CREATE TYPE ${formatQualifiedName(schema, name)} AS ENUM (${rendered})`, 0, undefined, { values }));
26
+ }
27
+ const domains = await pool.query(`
28
+ select n.nspname as schema, t.typname as name,
29
+ format_type(t.typbasetype, t.typtypmod) as base,
30
+ t.typnotnull as not_null,
31
+ coalesce(
32
+ (select string_agg(pg_get_constraintdef(c.oid, true), ' ' order by c.conname)
33
+ from pg_constraint c where c.contypid = t.oid and c.contype = 'c'),
34
+ ''
35
+ ) as constraints
36
+ from pg_type t
37
+ join pg_namespace n on n.oid = t.typnamespace
38
+ where t.typtype = 'd' and ${managedSchemaFilter}
39
+ order by n.nspname, t.typname
40
+ `);
41
+ for (const row of domains.rows) {
42
+ const schema = text(row.schema);
43
+ const name = text(row.name);
44
+ const parts = [`CREATE DOMAIN ${formatQualifiedName(schema, name)} AS ${text(row.base)}`];
45
+ if (row.not_null === true) {
46
+ parts.push("NOT NULL");
47
+ }
48
+ const constraints = text(row.constraints);
49
+ if (constraints.length > 0) {
50
+ parts.push(constraints);
51
+ }
52
+ objects.push(makeObject({ kind: "domain", name, schema }, parts.join(" "), 0));
53
+ }
54
+ const composites = await pool.query(`
55
+ select n.nspname as schema, c.relname as name,
56
+ string_agg(
57
+ quote_ident(a.attname) || ' ' || format_type(a.atttypid, a.atttypmod),
58
+ ', ' order by a.attnum
59
+ ) as columns
60
+ from pg_class c
61
+ join pg_namespace n on n.oid = c.relnamespace
62
+ join pg_attribute a on a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
63
+ where c.relkind = 'c' and ${managedSchemaFilter}
64
+ group by n.nspname, c.relname
65
+ order by n.nspname, c.relname
66
+ `);
67
+ for (const row of composites.rows) {
68
+ const schema = text(row.schema);
69
+ const name = text(row.name);
70
+ objects.push(makeObject({ kind: "type", name, schema }, `CREATE TYPE ${formatQualifiedName(schema, name)} AS (${text(row.columns)})`, 0));
71
+ }
72
+ return objects;
73
+ }
74
+ export async function collectSequences(pool) {
75
+ const objects = [];
76
+ const sequences = await pool.query(`
77
+ select n.nspname as schema, c.relname as name,
78
+ format_type(s.seqtypid, null) as data_type,
79
+ s.seqstart::text as start_value,
80
+ s.seqincrement::text as increment_by,
81
+ s.seqmin::text as min_value,
82
+ s.seqmax::text as max_value,
83
+ s.seqcache::text as cache_size,
84
+ s.seqcycle as cycle,
85
+ dn.nspname as owned_schema, dc.relname as owned_table, a.attname as owned_column
86
+ from pg_class c
87
+ join pg_namespace n on n.oid = c.relnamespace
88
+ join pg_sequence s on s.seqrelid = c.oid
89
+ left join pg_depend d
90
+ on d.objid = c.oid and d.classid = 'pg_class'::regclass and d.deptype = 'a'
91
+ left join pg_class dc on dc.oid = d.refobjid
92
+ left join pg_namespace dn on dn.oid = dc.relnamespace
93
+ left join pg_attribute a on a.attrelid = d.refobjid and a.attnum = d.refobjsubid
94
+ where c.relkind = 'S'
95
+ and ${managedSchemaFilter}
96
+ and not exists (
97
+ select 1 from pg_depend i
98
+ where i.objid = c.oid and i.classid = 'pg_class'::regclass and i.deptype = 'i'
99
+ )
100
+ order by n.nspname, c.relname
101
+ `);
102
+ for (const row of sequences.rows) {
103
+ const schema = text(row.schema);
104
+ const name = text(row.name);
105
+ const ownedBy = row.owned_table && row.owned_column
106
+ ? `${text(row.owned_schema)}.${text(row.owned_table)}.${text(row.owned_column)}`
107
+ : undefined;
108
+ const clauses = [`CREATE SEQUENCE ${formatQualifiedName(schema, name)}`];
109
+ const dataType = text(row.data_type);
110
+ if (dataType !== "bigint") {
111
+ clauses.push(`AS ${dataType}`);
112
+ }
113
+ for (const [keyword, value, fallback] of [
114
+ ["INCREMENT BY", text(row.increment_by), "1"],
115
+ ["MINVALUE", text(row.min_value), "1"],
116
+ ["MAXVALUE", text(row.max_value), sequenceTypeMax.get(dataType) ?? ""],
117
+ ["START WITH", text(row.start_value), "1"],
118
+ ["CACHE", text(row.cache_size), "1"],
119
+ ]) {
120
+ if (value !== fallback) {
121
+ clauses.push(`${keyword} ${value}`);
122
+ }
123
+ }
124
+ if (row.cycle === true) {
125
+ clauses.push("CYCLE");
126
+ }
127
+ if (ownedBy) {
128
+ clauses.push(`OWNED BY ${formatQualifiedName(text(row.owned_schema), text(row.owned_table))}.${quoteIdent(text(row.owned_column))}`);
129
+ }
130
+ objects.push(makeObject({ kind: "sequence", name, schema }, clauses.join(" "), 0, undefined, ownedBy ? { ownedBy } : {}));
131
+ }
132
+ return objects;
133
+ }
134
+ const sequenceTypeMax = new Map([
135
+ ["bigint", "9223372036854775807"],
136
+ ["integer", "2147483647"],
137
+ ["smallint", "32767"],
138
+ ]);
139
+ export async function collectGrants(pool) {
140
+ const objects = [];
141
+ const relationGrants = await pool.query(`
142
+ select n.nspname as schema, c.relname as name, c.relkind as relkind,
143
+ case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
144
+ array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
145
+ from pg_class c
146
+ join pg_namespace n on n.oid = c.relnamespace,
147
+ lateral aclexplode(c.relacl) as acl
148
+ where c.relacl is not null
149
+ and c.relkind in ('r', 'p', 'v', 'm', 'S')
150
+ and ${managedSchemaFilter}
151
+ and acl.grantee <> c.relowner
152
+ and not exists (
153
+ select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
154
+ where i.objoid = c.oid and i.classoid = 'pg_class'::regclass
155
+ and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
156
+ )
157
+ group by n.nspname, c.relname, c.relkind, acl.grantee
158
+ order by n.nspname, c.relname, grantee
159
+ `);
160
+ for (const row of relationGrants.rows) {
161
+ const schema = text(row.schema);
162
+ const name = text(row.name);
163
+ const kindPhrase = text(row.relkind) === "S" ? "SEQUENCE" : "TABLE";
164
+ objects.push(buildGrantObject({
165
+ grantee: text(row.grantee),
166
+ kindPhrase,
167
+ ordinal: 0,
168
+ privileges: textArray(row.privileges),
169
+ schema,
170
+ targetIdentity: `${schema}.${name}`,
171
+ targetRendered: formatQualifiedName(schema, name),
172
+ verb: "GRANT",
173
+ }));
174
+ }
175
+ // pg_init_privs records initdb/extension-time ACLs; like pg_dump, only the
176
+ // delta against them is declared state (initdb grants USAGE on schema
177
+ // public to PUBLIC in every database).
178
+ const schemaGrants = await pool.query(`
179
+ select n.nspname as name,
180
+ case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
181
+ array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
182
+ from pg_namespace n,
183
+ lateral aclexplode(n.nspacl) as acl
184
+ where n.nspacl is not null
185
+ and ${managedSchemaFilter}
186
+ and acl.grantee <> n.nspowner
187
+ and not exists (
188
+ select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
189
+ where i.objoid = n.oid and i.classoid = 'pg_namespace'::regclass
190
+ and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
191
+ )
192
+ group by n.nspname, acl.grantee
193
+ order by n.nspname, grantee
194
+ `);
195
+ for (const row of schemaGrants.rows) {
196
+ const name = text(row.name);
197
+ objects.push(buildGrantObject({
198
+ grantee: text(row.grantee),
199
+ kindPhrase: "SCHEMA",
200
+ ordinal: 0,
201
+ privileges: textArray(row.privileges),
202
+ schema: name,
203
+ targetIdentity: name,
204
+ targetRendered: `"${name}"`,
205
+ verb: "GRANT",
206
+ }));
207
+ }
208
+ const functionGrants = await pool.query(`
209
+ select n.nspname as schema, p.proname as name,
210
+ oidvectortypes(p.proargtypes) as args,
211
+ case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
212
+ array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
213
+ from pg_proc p
214
+ join pg_namespace n on n.oid = p.pronamespace,
215
+ lateral aclexplode(p.proacl) as acl
216
+ where p.proacl is not null
217
+ and ${managedSchemaFilter}
218
+ and acl.grantee <> p.proowner
219
+ and not exists (
220
+ select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
221
+ where i.objoid = p.oid and i.classoid = 'pg_proc'::regclass
222
+ and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
223
+ )
224
+ group by n.nspname, p.proname, args, acl.grantee
225
+ order by n.nspname, p.proname, grantee
226
+ `);
227
+ for (const row of functionGrants.rows) {
228
+ const schema = text(row.schema);
229
+ const name = text(row.name);
230
+ const args = text(row.args);
231
+ const grantee = text(row.grantee);
232
+ const privileges = textArray(row.privileges);
233
+ if (isBuiltinDefaultGrant("FUNCTION", grantee, privileges)) {
234
+ continue;
235
+ }
236
+ objects.push(buildGrantObject({
237
+ grantee,
238
+ kindPhrase: "FUNCTION",
239
+ ordinal: 0,
240
+ privileges,
241
+ schema,
242
+ targetIdentity: `${schema}.${name}(${args})`,
243
+ targetRendered: `${formatQualifiedName(schema, name)}(${args})`,
244
+ verb: "GRANT",
245
+ }));
246
+ }
247
+ // A non-null routine ACL lacking PUBLIC's built-in EXECUTE means it was
248
+ // explicitly revoked; emit that revoke so trees declaring it hash-match.
249
+ const revokedFunctionDefaults = await pool.query(`
250
+ select n.nspname as schema, p.proname as name, oidvectortypes(p.proargtypes) as args
251
+ from pg_proc p
252
+ join pg_namespace n on n.oid = p.pronamespace
253
+ where p.proacl is not null
254
+ and ${managedSchemaFilter}
255
+ and not exists (
256
+ select 1 from aclexplode(p.proacl) acl
257
+ where acl.grantee = 0 and acl.privilege_type = 'EXECUTE'
258
+ )
259
+ order by n.nspname, p.proname
260
+ `);
261
+ for (const row of revokedFunctionDefaults.rows) {
262
+ const schema = text(row.schema);
263
+ const name = text(row.name);
264
+ const args = text(row.args);
265
+ objects.push(buildGrantObject({
266
+ grantee: "PUBLIC",
267
+ kindPhrase: "FUNCTION",
268
+ ordinal: 0,
269
+ privileges: ["ALL"],
270
+ schema,
271
+ targetIdentity: `${schema}.${name}(${args})`,
272
+ targetRendered: `${formatQualifiedName(schema, name)}(${args})`,
273
+ verb: "REVOKE",
274
+ }));
275
+ }
276
+ const typeGrants = await pool.query(`
277
+ select n.nspname as schema, t.typname as name, t.typtype as typtype,
278
+ case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
279
+ array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
280
+ from pg_type t
281
+ join pg_namespace n on n.oid = t.typnamespace,
282
+ lateral aclexplode(t.typacl) as acl
283
+ where t.typacl is not null
284
+ and t.typtype in ('e', 'd', 'c', 'r')
285
+ and ${managedSchemaFilter}
286
+ and acl.grantee <> t.typowner
287
+ and not exists (
288
+ select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
289
+ where i.objoid = t.oid and i.classoid = 'pg_type'::regclass
290
+ and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
291
+ )
292
+ group by n.nspname, t.typname, t.typtype, acl.grantee
293
+ order by n.nspname, t.typname, grantee
294
+ `);
295
+ for (const row of typeGrants.rows) {
296
+ const schema = text(row.schema);
297
+ const name = text(row.name);
298
+ const grantee = text(row.grantee);
299
+ const privileges = textArray(row.privileges);
300
+ const kindPhrase = text(row.typtype) === "d" ? "DOMAIN" : "TYPE";
301
+ if (isBuiltinDefaultGrant(kindPhrase, grantee, privileges)) {
302
+ continue;
303
+ }
304
+ objects.push(buildGrantObject({
305
+ grantee,
306
+ kindPhrase,
307
+ ordinal: 0,
308
+ privileges,
309
+ schema,
310
+ targetIdentity: `${schema}.${name}`,
311
+ targetRendered: formatQualifiedName(schema, name),
312
+ verb: "GRANT",
313
+ }));
314
+ }
315
+ return objects;
316
+ }
317
+ const defaultPrivilegeObjectTypes = new Map([
318
+ ["S", "SEQUENCES"],
319
+ ["T", "TYPES"],
320
+ ["f", "FUNCTIONS"],
321
+ ["n", "SCHEMAS"],
322
+ ["r", "TABLES"],
323
+ ]);
324
+ export async function collectDefaultPrivileges(pool) {
325
+ const objects = [];
326
+ const rows = await pool.query(`
327
+ select pg_get_userbyid(d.defaclrole) as for_role,
328
+ case when d.defaclnamespace = 0 then null else n.nspname end as schema,
329
+ d.defaclobjtype as objtype,
330
+ case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
331
+ array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
332
+ from pg_default_acl d
333
+ left join pg_namespace n on n.oid = d.defaclnamespace,
334
+ lateral aclexplode(d.defaclacl) as acl
335
+ group by d.defaclrole, schema, d.defaclobjtype, acl.grantee
336
+ order by for_role, schema, d.defaclobjtype, grantee
337
+ `);
338
+ for (const row of rows.rows) {
339
+ const objectType = defaultPrivilegeObjectTypes.get(text(row.objtype));
340
+ if (!objectType) {
341
+ continue;
342
+ }
343
+ const forRole = text(row.for_role);
344
+ const grantee = text(row.grantee);
345
+ const privileges = textArray(row.privileges);
346
+ // The owner's self-entry and PUBLIC's built-in routine/type defaults are
347
+ // acldefault noise, not declared state.
348
+ if (grantee === forRole || isBuiltinDefaultGrant(objectType, grantee, privileges)) {
349
+ continue;
350
+ }
351
+ objects.push(buildDefaultPrivilegeObject({
352
+ forRole,
353
+ grantee,
354
+ objectType,
355
+ ordinal: 0,
356
+ privileges,
357
+ schema: row.schema === null ? undefined : text(row.schema),
358
+ verb: "GRANT",
359
+ }));
360
+ }
361
+ // A default-ACL row for routines whose entries lack PUBLIC EXECUTE records
362
+ // an ALTER DEFAULT PRIVILEGES ... REVOKE ... FROM PUBLIC.
363
+ const revokedDefaults = await pool.query(`
364
+ select pg_get_userbyid(d.defaclrole) as for_role,
365
+ case when d.defaclnamespace = 0 then null else n.nspname end as schema,
366
+ d.defaclobjtype as objtype
367
+ from pg_default_acl d
368
+ left join pg_namespace n on n.oid = d.defaclnamespace
369
+ where d.defaclobjtype in ('f', 'T')
370
+ and not exists (
371
+ select 1 from aclexplode(d.defaclacl) acl where acl.grantee = 0
372
+ )
373
+ order by for_role, schema, objtype
374
+ `);
375
+ for (const row of revokedDefaults.rows) {
376
+ const objectType = defaultPrivilegeObjectTypes.get(text(row.objtype));
377
+ if (!objectType) {
378
+ continue;
379
+ }
380
+ objects.push(buildDefaultPrivilegeObject({
381
+ forRole: text(row.for_role),
382
+ grantee: "PUBLIC",
383
+ objectType,
384
+ ordinal: 0,
385
+ privileges: ["ALL"],
386
+ schema: row.schema === null ? undefined : text(row.schema),
387
+ verb: "REVOKE",
388
+ }));
389
+ }
390
+ return objects;
391
+ }
392
+ function text(value) {
393
+ return typeof value === "string" ? value : String(value);
394
+ }
395
+ function textArray(value) {
396
+ if (Array.isArray(value)) {
397
+ return value.map(text);
398
+ }
399
+ const raw = text(value).trim();
400
+ if (raw.startsWith("{") && raw.endsWith("}")) {
401
+ return raw
402
+ .slice(1, -1)
403
+ .split(",")
404
+ .map((item) => stripOuterDoubleQuotes(item.trim()))
405
+ .filter(Boolean);
406
+ }
407
+ return raw.length > 0 ? [raw] : [];
408
+ }
@@ -0,0 +1,15 @@
1
+ import type { SchemaObject } from "./core.js";
2
+ type CatalogQuery = {
3
+ query: <Row extends Record<string, unknown>>(text: string, values?: unknown[]) => Promise<{
4
+ rows: Row[];
5
+ }>;
6
+ };
7
+ /**
8
+ * Whole-object FDW tier: servers and foreign tables are modeled by their
9
+ * complete definition (no column-level diffing). Server and column options
10
+ * are reconstructed from catalog option arrays; user mappings are excluded
11
+ * because they carry credentials.
12
+ */
13
+ export declare function collectForeignObjects(pool: CatalogQuery): Promise<SchemaObject[]>;
14
+ export {};
15
+ //# sourceMappingURL=catalog-foreign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-foreign.d.ts","sourceRoot":"","sources":["../src/catalog-foreign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAI9C,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuFvF"}