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,114 @@
1
+ import { formatQualifiedName, quoteIdent } from "./sql/identifiers.js";
2
+ import { makeObject } from "./sql/statements.js";
3
+ /**
4
+ * Whole-object FDW tier: servers and foreign tables are modeled by their
5
+ * complete definition (no column-level diffing). Server and column options
6
+ * are reconstructed from catalog option arrays; user mappings are excluded
7
+ * because they carry credentials.
8
+ */
9
+ export async function collectForeignObjects(pool) {
10
+ const objects = [];
11
+ const wrappers = await pool.query(`
12
+ select w.fdwname as name,
13
+ w.fdwhandler::regproc::text as handler,
14
+ w.fdwvalidator::regproc::text as validator,
15
+ w.fdwoptions as options
16
+ from pg_foreign_data_wrapper w
17
+ where not exists (
18
+ select 1 from pg_depend d where d.objid = w.oid and d.deptype = 'e'
19
+ )
20
+ order by w.fdwname
21
+ `);
22
+ for (const row of wrappers.rows) {
23
+ const clauses = [`CREATE FOREIGN DATA WRAPPER ${quoteIdent(text(row.name))}`];
24
+ if (row.handler && text(row.handler) !== "-") {
25
+ clauses.push(`HANDLER ${text(row.handler)}`);
26
+ }
27
+ if (row.validator && text(row.validator) !== "-") {
28
+ clauses.push(`VALIDATOR ${text(row.validator)}`);
29
+ }
30
+ const options = optionsClause(row.options);
31
+ if (options) {
32
+ clauses.push(options);
33
+ }
34
+ objects.push(makeObject({ kind: "foreign-data-wrapper", name: text(row.name) }, clauses.join(" "), 0));
35
+ }
36
+ const servers = await pool.query(`
37
+ select s.srvname as name, w.fdwname as wrapper, s.srvtype as server_type,
38
+ s.srvversion as server_version, s.srvoptions as options
39
+ from pg_foreign_server s
40
+ join pg_foreign_data_wrapper w on w.oid = s.srvfdw
41
+ order by s.srvname
42
+ `);
43
+ for (const row of servers.rows) {
44
+ const clauses = [`CREATE SERVER ${quoteIdent(text(row.name))}`];
45
+ if (row.server_type) {
46
+ clauses.push(`TYPE '${escapeLiteral(text(row.server_type))}'`);
47
+ }
48
+ if (row.server_version) {
49
+ clauses.push(`VERSION '${escapeLiteral(text(row.server_version))}'`);
50
+ }
51
+ clauses.push(`FOREIGN DATA WRAPPER ${quoteIdent(text(row.wrapper))}`);
52
+ const options = optionsClause(row.options);
53
+ if (options) {
54
+ clauses.push(options);
55
+ }
56
+ objects.push(makeObject({ kind: "foreign-server", name: text(row.name) }, clauses.join(" "), 0));
57
+ }
58
+ const tables = await pool.query(`
59
+ select n.nspname as schema, c.relname as name, s.srvname as server, ft.ftoptions as options,
60
+ array(
61
+ select format('%I %s', a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod))
62
+ from pg_attribute a
63
+ where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
64
+ order by a.attnum
65
+ ) as columns
66
+ from pg_foreign_table ft
67
+ join pg_class c on c.oid = ft.ftrelid
68
+ join pg_namespace n on n.oid = c.relnamespace
69
+ join pg_foreign_server s on s.oid = ft.ftserver
70
+ where n.nspname !~ '^pg_' and n.nspname <> 'information_schema'
71
+ order by n.nspname, c.relname
72
+ `);
73
+ for (const row of tables.rows) {
74
+ const schema = text(row.schema);
75
+ const name = text(row.name);
76
+ const columns = Array.isArray(row.columns) ? row.columns.map((item) => ` ${text(item)}`) : [];
77
+ const clauses = [
78
+ `CREATE FOREIGN TABLE ${formatQualifiedName(schema, name)} (\n${columns.join(",\n")}\n)`,
79
+ `SERVER ${quoteIdent(text(row.server))}`,
80
+ ];
81
+ const options = optionsClause(row.options);
82
+ if (options) {
83
+ clauses.push(options);
84
+ }
85
+ objects.push(makeObject({ kind: "foreign-table", name, schema }, clauses.join(" "), 0, undefined, {
86
+ server: text(row.server),
87
+ }));
88
+ }
89
+ return objects;
90
+ }
91
+ function optionsClause(value) {
92
+ if (!Array.isArray(value) || value.length === 0) {
93
+ return undefined;
94
+ }
95
+ const rendered = value
96
+ .map((item) => {
97
+ const raw = text(item);
98
+ const separator = raw.indexOf("=");
99
+ if (separator === -1) {
100
+ return undefined;
101
+ }
102
+ const key = raw.slice(0, separator);
103
+ const optionValue = raw.slice(separator + 1);
104
+ return `${quoteIdent(key)} '${escapeLiteral(optionValue)}'`;
105
+ })
106
+ .filter((item) => item !== undefined);
107
+ return rendered.length > 0 ? `OPTIONS (${rendered.join(", ")})` : undefined;
108
+ }
109
+ function escapeLiteral(value) {
110
+ return value.replaceAll("'", "''");
111
+ }
112
+ function text(value) {
113
+ return typeof value === "string" ? value : String(value);
114
+ }
@@ -0,0 +1,9 @@
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 collectTables(pool: CatalogQuery): Promise<SchemaObject[]>;
8
+ export {};
9
+ //# sourceMappingURL=catalog-tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-tables.d.ts","sourceRoot":"","sources":["../src/catalog-tables.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AAI3D,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,wBAAsB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA+E/E"}
@@ -0,0 +1,114 @@
1
+ import { formatQualifiedName, quoteIdent } from "./sql/identifiers.js";
2
+ import { makeObject } from "./sql/statements.js";
3
+ export async function collectTables(pool) {
4
+ const tables = await pool.query(`
5
+ select c.oid::text as oid, n.nspname as schema, c.relname as name
6
+ from pg_class c
7
+ join pg_namespace n on n.oid = c.relnamespace
8
+ where c.relkind in ('r', 'p')
9
+ and not c.relispartition
10
+ and n.nspname !~ '^pg_'
11
+ and n.nspname <> 'information_schema'
12
+ order by n.nspname, c.relname
13
+ `);
14
+ if (tables.rows.length === 0) {
15
+ return [];
16
+ }
17
+ const oids = tables.rows.map((row) => stringValue(row.oid));
18
+ const [columns, constraints] = await Promise.all([
19
+ pool.query(`
20
+ select
21
+ a.attrelid::text as oid,
22
+ a.attname as name,
23
+ format_type(a.atttypid, a.atttypmod) as type,
24
+ a.attnotnull as not_null,
25
+ a.attidentity as identity,
26
+ a.attgenerated as generated,
27
+ pg_get_expr(d.adbin, d.adrelid) as default_expression
28
+ from pg_attribute a
29
+ left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
30
+ where a.attrelid = any($1::oid[])
31
+ and a.attnum > 0
32
+ and not a.attisdropped
33
+ order by a.attrelid, a.attnum
34
+ `, [oids]),
35
+ pool.query(`
36
+ select conrelid::text as oid, conname as name, pg_get_constraintdef(oid, true) as definition
37
+ from pg_constraint
38
+ where conrelid = any($1::oid[])
39
+ and contype in ('p', 'u', 'f', 'c', 'x')
40
+ order by conrelid, conname
41
+ `, [oids]),
42
+ ]);
43
+ const columnsByOid = groupByOid(columns.rows);
44
+ const constraintsByOid = groupByOid(constraints.rows);
45
+ const objects = [];
46
+ for (const table of tables.rows) {
47
+ const oid = stringValue(table.oid);
48
+ const columnDefinitions = (columnsByOid.get(oid) ?? []).map(columnFromRow);
49
+ const lines = columnDefinitions.map((column) => ` ${quoteIdent(column.name)} ${column.definition}`);
50
+ const schema = stringValue(table.schema);
51
+ const name = stringValue(table.name);
52
+ const sql = `CREATE TABLE ${formatQualifiedName(schema, name)} (\n${lines.join(",\n")}\n)`;
53
+ objects.push(makeObject({ kind: "table", name, schema }, sql, 0, undefined, {
54
+ columns: columnDefinitions,
55
+ }));
56
+ // Constraints are separate identity owners on every lane, so a constraint
57
+ // declared inline in a source tree and the same constraint read from
58
+ // pg_constraint compare as the same object instead of changing the table.
59
+ for (const constraint of constraintsByOid.get(oid) ?? []) {
60
+ const constraintName = stringValue(constraint.name);
61
+ const constraintObject = makeObject({ kind: "constraint", name: constraintName, schema, table: name }, `ALTER TABLE ONLY ${formatQualifiedName(schema, name)} ADD CONSTRAINT ${quoteIdent(constraintName)} ${stringValue(constraint.definition)}`, 0, undefined);
62
+ constraintObject.dependencies.push(`${schema}.${name}`);
63
+ objects.push(constraintObject);
64
+ }
65
+ }
66
+ return objects;
67
+ }
68
+ function columnFromRow(column) {
69
+ const identity = stringValue(column.identity);
70
+ const generated = stringValue(column.generated);
71
+ const parts = [stringValue(column.type)];
72
+ if (generated === "s" && column.default_expression) {
73
+ parts.push(`GENERATED ALWAYS AS (${stringValue(column.default_expression)}) STORED`);
74
+ }
75
+ else if (identity === "a") {
76
+ parts.push("GENERATED ALWAYS AS IDENTITY");
77
+ }
78
+ else if (identity === "d") {
79
+ parts.push("GENERATED BY DEFAULT AS IDENTITY");
80
+ }
81
+ else if (column.default_expression) {
82
+ parts.push(`DEFAULT ${stringValue(column.default_expression)}`);
83
+ }
84
+ if (column.not_null) {
85
+ parts.push("NOT NULL");
86
+ }
87
+ const facts = {
88
+ definition: parts.join(" "),
89
+ generated: generated === "s",
90
+ hasDefault: Boolean(column.default_expression) && generated !== "s",
91
+ hasInlineConstraint: false,
92
+ identity: identity === "a" || identity === "d",
93
+ name: stringValue(column.name),
94
+ notNull: column.not_null === true,
95
+ type: stringValue(column.type),
96
+ };
97
+ if (facts.hasDefault && !facts.identity) {
98
+ facts.defaultExpression = stringValue(column.default_expression);
99
+ }
100
+ return facts;
101
+ }
102
+ function groupByOid(rows) {
103
+ const groups = new Map();
104
+ for (const row of rows) {
105
+ const oid = stringValue(row.oid);
106
+ const group = groups.get(oid) ?? [];
107
+ group.push(row);
108
+ groups.set(oid, group);
109
+ }
110
+ return groups;
111
+ }
112
+ function stringValue(value) {
113
+ return typeof value === "string" ? value : String(value);
114
+ }
@@ -0,0 +1,8 @@
1
+ import type { SchemaModel } from "./core.js";
2
+ export interface ExtractCatalogOptions {
3
+ databaseUrl: string;
4
+ source?: string;
5
+ normalize?: boolean;
6
+ }
7
+ export declare function extractCatalogModel(options: ExtractCatalogOptions): Promise<SchemaModel>;
8
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,WAAW,CAAC;AAQ3D,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAID,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwD9F"}
@@ -0,0 +1,351 @@
1
+ import { Pool } from "pg";
2
+ import { collectComments } from "./catalog-comments.js";
3
+ import { collectDefaultPrivileges, collectGrants, collectSequences, collectTypes, } from "./catalog-extras.js";
4
+ import { collectForeignObjects } from "./catalog-foreign.js";
5
+ import { collectTables } from "./catalog-tables.js";
6
+ import { diagnostic } from "./diagnostics.js";
7
+ import { fingerprintObjects, MODEL_FORMAT_VERSION } from "./hash.js";
8
+ import { suppressDefaultAclImpliedGrants } from "./source-normalize.js";
9
+ import { finalizeObjects } from "./sql/facts.js";
10
+ import { formatQualifiedName, quoteIdent, stripOuterDoubleQuotes } from "./sql/identifiers.js";
11
+ import { makeObject } from "./sql/statements.js";
12
+ export async function extractCatalogModel(options) {
13
+ // Empty search_path (pg_dump's convention) so pg_get_expr renders every
14
+ // reference schema-qualified; otherwise the session's search_path decides
15
+ // whether `auth.uid()` reconstructs as `uid()` and the cross-lane hash
16
+ // silently diverges from the declarative spelling.
17
+ const pool = new Pool({
18
+ connectionString: options.databaseUrl,
19
+ max: 4,
20
+ options: "-c search_path=",
21
+ });
22
+ pool.on("error", () => { });
23
+ try {
24
+ const sections = await Promise.all([
25
+ collectSection((objects) => appendSchemas(pool, objects, 0)),
26
+ collectSection((objects) => appendExtensions(pool, objects, 0)),
27
+ collectTypes(pool),
28
+ collectSequences(pool),
29
+ collectTables(pool),
30
+ collectForeignObjects(pool),
31
+ collectSection((objects) => appendFunctions(pool, objects, 0)),
32
+ collectSection((objects) => appendViews(pool, objects, 0)),
33
+ collectSection((objects) => appendIndexes(pool, objects, 0)),
34
+ collectSection((objects) => appendTriggers(pool, objects, 0)),
35
+ collectSection((objects) => appendPoliciesAndRls(pool, objects, 0)),
36
+ collectGrants(pool),
37
+ collectDefaultPrivileges(pool),
38
+ collectComments(pool),
39
+ ]);
40
+ const objects = suppressDefaultAclImpliedGrants(sections.flat());
41
+ objects.forEach((object, index) => {
42
+ object.ordinal = index;
43
+ });
44
+ const diagnostics = await finalizeObjects(objects, {
45
+ normalize: options.normalize === true,
46
+ });
47
+ return {
48
+ diagnostics,
49
+ fingerprint: fingerprintObjects(objects),
50
+ formatVersion: MODEL_FORMAT_VERSION,
51
+ objects,
52
+ source: options.source ?? "database",
53
+ };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ diagnostics: [
58
+ diagnostic("SUPA_CATALOG_EXTRACT_FAILED", "error", errorMessage(error), {
59
+ hint: "Confirm the database URL is reachable and the role can read pg_catalog.",
60
+ }),
61
+ ],
62
+ fingerprint: fingerprintObjects([]),
63
+ objects: [],
64
+ source: options.source ?? "database",
65
+ };
66
+ }
67
+ finally {
68
+ await pool.end();
69
+ }
70
+ }
71
+ async function collectSection(section) {
72
+ const objects = [];
73
+ await section(objects);
74
+ return objects;
75
+ }
76
+ async function appendSchemas(pool, objects, ordinal) {
77
+ // `public` is created by initdb in every database; modeling it as a
78
+ // droppable object would let a tree that never declares it render
79
+ // DROP SCHEMA public.
80
+ const result = await pool.query(`
81
+ select nspname as name
82
+ from pg_namespace
83
+ where nspname !~ '^pg_'
84
+ and nspname not in ('information_schema', 'public')
85
+ order by nspname
86
+ `);
87
+ for (const row of result.rows) {
88
+ const name = stringValue(row.name);
89
+ objects.push(makeObject({ kind: "schema", name }, `CREATE SCHEMA ${quoteIdent(name)}`, ordinal));
90
+ ordinal += 1;
91
+ }
92
+ return ordinal;
93
+ }
94
+ async function appendExtensions(pool, objects, ordinal) {
95
+ // plpgsql is installed by initdb in every database (same class as the
96
+ // public schema); modeling it would render DROP EXTENSION plpgsql for any
97
+ // tree that never declares it.
98
+ const result = await pool.query(`
99
+ select e.extname as name, n.nspname as schema
100
+ from pg_extension e
101
+ join pg_namespace n on n.oid = e.extnamespace
102
+ where e.extname <> 'plpgsql'
103
+ order by e.extname
104
+ `);
105
+ for (const row of result.rows) {
106
+ const name = stringValue(row.name);
107
+ const schema = stringValue(row.schema);
108
+ objects.push(makeObject({ kind: "extension", name }, `CREATE EXTENSION ${quoteIdent(name)} WITH SCHEMA ${quoteIdent(schema)}`, ordinal, undefined, { schema }));
109
+ ordinal += 1;
110
+ }
111
+ return ordinal;
112
+ }
113
+ async function appendFunctions(pool, objects, ordinal) {
114
+ const result = await pool.query(`
115
+ select
116
+ n.nspname as schema,
117
+ p.proname as name,
118
+ p.prokind as kind,
119
+ oidvectortypes(p.proargtypes) as args,
120
+ p.provariadic <> 0 as variadic,
121
+ pg_get_functiondef(p.oid) as definition
122
+ from pg_proc p
123
+ join pg_namespace n on n.oid = p.pronamespace
124
+ where n.nspname !~ '^pg_'
125
+ and n.nspname <> 'information_schema'
126
+ and p.prokind in ('f', 'p')
127
+ order by n.nspname, p.proname, oidvectortypes(p.proargtypes)
128
+ `);
129
+ for (const row of result.rows) {
130
+ const kind = stringValue(row.kind) === "p" ? "procedure" : "function";
131
+ objects.push(makeObject({
132
+ kind,
133
+ name: stringValue(row.name),
134
+ schema: stringValue(row.schema),
135
+ // Routine identity is input argument TYPES only (names are not part
136
+ // of PostgreSQL overload identity), matching the source lane.
137
+ signature: functionSignature(stringValue(row.args), row.variadic === true),
138
+ }, stringValue(row.definition), ordinal));
139
+ ordinal += 1;
140
+ }
141
+ return ordinal;
142
+ }
143
+ // oidvectortypes joins input argument types with ", "; type names contain no
144
+ // commas, so marking the trailing VARIADIC argument by splitting is safe.
145
+ function functionSignature(args, variadic) {
146
+ if (!variadic || args.length === 0) {
147
+ return args;
148
+ }
149
+ const types = args.split(", ");
150
+ types[types.length - 1] = `VARIADIC ${types.at(-1)}`;
151
+ return types.join(", ");
152
+ }
153
+ async function appendViews(pool, objects, ordinal) {
154
+ const result = await pool.query(`
155
+ select
156
+ n.nspname as schema,
157
+ c.relname as name,
158
+ c.relkind as relkind,
159
+ c.reloptions as reloptions,
160
+ pg_get_viewdef(c.oid, true) as definition
161
+ from pg_class c
162
+ join pg_namespace n on n.oid = c.relnamespace
163
+ where c.relkind in ('v', 'm')
164
+ and n.nspname !~ '^pg_'
165
+ and n.nspname <> 'information_schema'
166
+ order by n.nspname, c.relname
167
+ `);
168
+ for (const row of result.rows) {
169
+ const kind = stringValue(row.relkind) === "m" ? "materialized-view" : "view";
170
+ const prefix = kind === "materialized-view" ? "CREATE MATERIALIZED VIEW" : "CREATE VIEW";
171
+ const schema = stringValue(row.schema);
172
+ const name = stringValue(row.name);
173
+ const withClause = kind === "view" && reloptionEnabled(row.reloptions, "security_invoker")
174
+ ? " WITH (security_invoker = true)"
175
+ : "";
176
+ objects.push(makeObject({ kind, name, schema }, `${prefix} ${formatQualifiedName(schema, name)}${withClause} AS\n${stringValue(row.definition)}`, ordinal));
177
+ ordinal += 1;
178
+ }
179
+ return ordinal;
180
+ }
181
+ function reloptionEnabled(reloptions, option) {
182
+ if (!Array.isArray(reloptions)) {
183
+ return false;
184
+ }
185
+ for (const entry of reloptions) {
186
+ const text = String(entry);
187
+ const separator = text.indexOf("=");
188
+ const name = separator === -1 ? text : text.slice(0, separator);
189
+ if (name.trim().toLowerCase() !== option) {
190
+ continue;
191
+ }
192
+ const value = separator === -1
193
+ ? "true"
194
+ : text
195
+ .slice(separator + 1)
196
+ .trim()
197
+ .toLowerCase();
198
+ return value === "true" || value === "on" || value === "1" || value === "yes";
199
+ }
200
+ return false;
201
+ }
202
+ async function appendIndexes(pool, objects, ordinal) {
203
+ // Constraint-backed indexes (PK/UNIQUE/EXCLUDE) are owned by their
204
+ // constraint object; emitting them as index objects would double-own them
205
+ // and plan false index drops against trees that declare the constraint.
206
+ const result = await pool.query(`
207
+ select i.schemaname, i.tablename, i.indexname, i.indexdef
208
+ from pg_indexes i
209
+ where i.schemaname !~ '^pg_'
210
+ and i.schemaname <> 'information_schema'
211
+ and not exists (
212
+ select 1
213
+ from pg_constraint con
214
+ join pg_class ic on ic.oid = con.conindid
215
+ join pg_namespace icn on icn.oid = ic.relnamespace
216
+ where icn.nspname = i.schemaname and ic.relname = i.indexname
217
+ )
218
+ order by i.schemaname, i.indexname
219
+ `);
220
+ for (const row of result.rows) {
221
+ objects.push(makeObject({
222
+ kind: "index",
223
+ name: stringValue(row.indexname),
224
+ schema: stringValue(row.schemaname),
225
+ table: stringValue(row.tablename),
226
+ }, stringValue(row.indexdef), ordinal));
227
+ ordinal += 1;
228
+ }
229
+ return ordinal;
230
+ }
231
+ async function appendTriggers(pool, objects, ordinal) {
232
+ const result = await pool.query(`
233
+ select
234
+ n.nspname as schema,
235
+ c.relname as table_name,
236
+ t.tgname as name,
237
+ pg_get_triggerdef(t.oid, true) as definition
238
+ from pg_trigger t
239
+ join pg_class c on c.oid = t.tgrelid
240
+ join pg_namespace n on n.oid = c.relnamespace
241
+ where not t.tgisinternal
242
+ and n.nspname !~ '^pg_'
243
+ and n.nspname <> 'information_schema'
244
+ order by n.nspname, c.relname, t.tgname
245
+ `);
246
+ for (const row of result.rows) {
247
+ objects.push(makeObject({
248
+ kind: "trigger",
249
+ name: stringValue(row.name),
250
+ schema: stringValue(row.schema),
251
+ table: stringValue(row.table_name),
252
+ }, stringValue(row.definition), ordinal));
253
+ ordinal += 1;
254
+ }
255
+ return ordinal;
256
+ }
257
+ async function appendPoliciesAndRls(pool, objects, ordinal) {
258
+ const rls = await pool.query(`
259
+ select n.nspname as schema, c.relname as name, c.relrowsecurity as rls, c.relforcerowsecurity as force
260
+ from pg_class c
261
+ join pg_namespace n on n.oid = c.relnamespace
262
+ where c.relkind in ('r', 'p')
263
+ and (c.relrowsecurity or c.relforcerowsecurity)
264
+ and n.nspname !~ '^pg_'
265
+ and n.nspname <> 'information_schema'
266
+ order by n.nspname, c.relname
267
+ `);
268
+ for (const row of rls.rows) {
269
+ const schema = stringValue(row.schema);
270
+ const name = stringValue(row.name);
271
+ // ENABLE and FORCE are independent facets of one table's RLS state and
272
+ // share one rls identity; emit both statements so forced tables render
273
+ // correctly and hash-match a source tree that declares both.
274
+ const statements = [];
275
+ if (row.rls) {
276
+ statements.push(`ALTER TABLE ${formatQualifiedName(schema, name)} ENABLE ROW LEVEL SECURITY`);
277
+ }
278
+ if (row.force) {
279
+ statements.push(`ALTER TABLE ${formatQualifiedName(schema, name)} FORCE ROW LEVEL SECURITY`);
280
+ }
281
+ objects.push(makeObject({ kind: "rls", name, schema, table: name }, statements.join(";\n"), ordinal));
282
+ ordinal += 1;
283
+ }
284
+ const policies = await pool.query(`
285
+ select
286
+ schemaname as schema,
287
+ tablename as table_name,
288
+ policyname as name,
289
+ permissive,
290
+ roles,
291
+ cmd,
292
+ qual,
293
+ with_check
294
+ from pg_policies
295
+ order by schemaname, tablename, policyname
296
+ `);
297
+ for (const row of policies.rows) {
298
+ const schema = stringValue(row.schema);
299
+ const table = stringValue(row.table_name);
300
+ const name = stringValue(row.name);
301
+ const clauses = [
302
+ `CREATE POLICY ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`,
303
+ `AS ${stringValue(row.permissive)}`,
304
+ `FOR ${stringValue(row.cmd)}`,
305
+ // pg_policies reports PUBLIC as the role name "public"; quoting it
306
+ // would parse as a named role instead of the ROLESPEC_PUBLIC keyword
307
+ // and break cross-lane hash parity with `TO PUBLIC` trees.
308
+ `TO ${normalizePolicyRoles(row.roles)
309
+ .map((role) => (role === "public" ? "PUBLIC" : quoteIdent(role)))
310
+ .join(", ")}`,
311
+ ];
312
+ if (row.qual) {
313
+ clauses.push(`USING (${stringValue(row.qual)})`);
314
+ }
315
+ if (row.with_check) {
316
+ clauses.push(`WITH CHECK (${stringValue(row.with_check)})`);
317
+ }
318
+ objects.push(makeObject({ kind: "policy", name, schema, table }, clauses.join(" "), ordinal));
319
+ ordinal += 1;
320
+ }
321
+ return ordinal;
322
+ }
323
+ function normalizePolicyRoles(value) {
324
+ if (Array.isArray(value)) {
325
+ return value.map(String);
326
+ }
327
+ if (typeof value === "string") {
328
+ const trimmed = value.trim();
329
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
330
+ return trimmed
331
+ .slice(1, -1)
332
+ .split(",")
333
+ .map((role) => stripOuterDoubleQuotes(role.trim()))
334
+ .filter(Boolean);
335
+ }
336
+ return trimmed
337
+ .split(",")
338
+ .map((role) => role.trim())
339
+ .filter(Boolean);
340
+ }
341
+ return [String(value)];
342
+ }
343
+ function stringValue(value) {
344
+ return typeof value === "string" ? value : String(value);
345
+ }
346
+ function errorMessage(error) {
347
+ if (error instanceof Error) {
348
+ return error.message;
349
+ }
350
+ return String(error);
351
+ }
@@ -0,0 +1,7 @@
1
+ import type { Diagnostic, SupaschemaConfig } from "./core.js";
2
+ import type { AstStatement } from "./sql/ast.js";
3
+ export declare function newEnumAdditionState(): Map<string, Set<string>>;
4
+ export declare function recordEnumAdditions(statement: AstStatement, state: Map<string, Set<string>>): void;
5
+ export declare function enumValueUseDiagnostics(statement: AstStatement, state: Map<string, Set<string>>, config: SupaschemaConfig): Diagnostic[];
6
+ export declare function escalateNontransactional(diagnostics: Diagnostic[], config: SupaschemaConfig): Diagnostic[];
7
+ //# sourceMappingURL=check-hazards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-hazards.d.ts","sourceRoot":"","sources":["../src/check-hazards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,wBAAgB,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAE/D;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GAC9B,IAAI,CAeN;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EAC/B,MAAM,EAAE,gBAAgB,GACvB,UAAU,EAAE,CAqBd;AA2CD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,UAAU,EAAE,EACzB,MAAM,EAAE,gBAAgB,GACvB,UAAU,EAAE,CAcd"}