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
package/dist/render.js ADDED
@@ -0,0 +1,325 @@
1
+ import { resolveConfig } from "./config.js";
2
+ import { lineageLine } from "./lineage.js";
3
+ import { ensureSemicolon, qualifiedRef, qualifiedTableRef, quoteLiteral, renderConstraintGuard, renderDefaultPrivilegeDrop, renderFdwGuard, renderGrantDrop, renderRename, renderTypeGuard, } from "./render-guards.js";
4
+ import { quoteIdent } from "./sql/identifiers.js";
5
+ export function renderMigrationSplit(plan, options = {}) {
6
+ const concurrent = plan.operations.filter((operation) => !operation.blocked &&
7
+ operation.ref.kind === "index" &&
8
+ (operation.after ?? operation.before)?.metadata.concurrent === true);
9
+ if (concurrent.length === 0) {
10
+ return { sql: renderMigration(plan, options) };
11
+ }
12
+ const transactional = plan.operations.filter((operation) => !concurrent.includes(operation));
13
+ const sql = renderMigration({ ...plan, operations: transactional }, options);
14
+ const concurrentBody = concurrent.map((operation) => renderOperation(operation)).join("\n\n");
15
+ const concurrentSql = `-- supaschema: run each statement outside a transaction block.\n-- CREATE INDEX CONCURRENTLY cannot run inside a transaction.\n${concurrentBody}\n`;
16
+ return { concurrentSql, sql };
17
+ }
18
+ export function renderMigration(plan, options = {}) {
19
+ const config = resolveConfig(options.config);
20
+ const chunks = [];
21
+ if (options.includeHeader ?? true) {
22
+ chunks.push(renderHeader(plan, config, options.version));
23
+ }
24
+ for (const operation of plan.operations) {
25
+ if (operation.blocked) {
26
+ chunks.push(renderBlockedOperation(operation));
27
+ continue;
28
+ }
29
+ chunks.push(renderOperation(operation));
30
+ }
31
+ return `${chunks.filter(Boolean).join("\n\n").trim()}\n`;
32
+ }
33
+ function renderHeader(plan, config, version) {
34
+ const hasConcurrentIndex = plan.operations.some((operation) => operation.ref.kind === "index" &&
35
+ (operation.after ?? operation.before)?.metadata.concurrent === true);
36
+ const transaction = hasConcurrentIndex
37
+ ? "-- supaschema: transaction=false required for concurrent index statements\n"
38
+ : "";
39
+ const generator = version ? `supaschema ${version}` : "supaschema";
40
+ return `${transaction}-- Generated by ${generator}.
41
+ -- From: ${plan.from}
42
+ -- To: ${plan.to}
43
+ -- Operations: ${renderOperationSummary(plan)}
44
+ -- Fingerprint: ${plan.fingerprint}
45
+ ${lineageLine(plan)}
46
+ SET lock_timeout = '${config.lockTimeout}';
47
+ SET statement_timeout = '${config.statementTimeout}';`;
48
+ }
49
+ function renderOperationSummary(plan) {
50
+ if (plan.operations.length === 0) {
51
+ return "none";
52
+ }
53
+ const counts = new Map();
54
+ for (const operation of plan.operations) {
55
+ const label = `${operation.kind} ${operation.ref.kind}`;
56
+ counts.set(label, (counts.get(label) ?? 0) + 1);
57
+ }
58
+ return [...counts.entries()]
59
+ .sort(([left], [right]) => left.localeCompare(right))
60
+ .map(([label, count]) => `${count} ${label}`)
61
+ .join(", ");
62
+ }
63
+ function renderBlockedOperation(operation) {
64
+ const diagnostics = operation.diagnostics
65
+ .map((item) => `-- ${item.code}: ${item.message}`)
66
+ .join("\n");
67
+ return `-- BLOCKED ${operation.kind} ${operation.key}\n${diagnostics}`;
68
+ }
69
+ function renderOperation(operation) {
70
+ if (operation.kind === "alter") {
71
+ return renderAlter(operation);
72
+ }
73
+ if (operation.kind === "create") {
74
+ return renderCreate(requiredAfter(operation));
75
+ }
76
+ if (operation.kind === "drop") {
77
+ return renderDrop(requiredBefore(operation));
78
+ }
79
+ if (operation.kind === "rename") {
80
+ return renderRename(operation);
81
+ }
82
+ return renderReplace(operation);
83
+ }
84
+ function renderAlter(operation) {
85
+ const after = requiredAfter(operation);
86
+ if (after.ref.kind === "enum") {
87
+ const addEnumValues = operation.metadata.addEnumValues;
88
+ if (!Array.isArray(addEnumValues)) {
89
+ throw new Error(`alter operation ${operation.key} has no addEnumValues metadata`);
90
+ }
91
+ return addEnumValues
92
+ .map((value) => `ALTER TYPE ${qualifiedRef(after.ref)} ADD VALUE IF NOT EXISTS ${quoteLiteral(String(value))};`)
93
+ .join("\n");
94
+ }
95
+ if (after.ref.kind !== "table") {
96
+ throw new Error(`unsupported alter operation for ${operation.key}`);
97
+ }
98
+ const statements = [];
99
+ const addColumns = Array.isArray(operation.metadata.addColumns)
100
+ ? operation.metadata.addColumns
101
+ : [];
102
+ for (const column of addColumns) {
103
+ statements.push(renderAddColumn(after, columnFromMetadata(column)));
104
+ }
105
+ const alterColumns = Array.isArray(operation.metadata.alterColumns)
106
+ ? operation.metadata.alterColumns
107
+ : [];
108
+ for (const alteration of alterColumns) {
109
+ statements.push(...renderColumnAlteration(after, alteration));
110
+ }
111
+ const dropColumns = Array.isArray(operation.metadata.dropColumns)
112
+ ? operation.metadata.dropColumns
113
+ : [];
114
+ for (const column of dropColumns) {
115
+ statements.push(`ALTER TABLE ${qualifiedRef(after.ref)} DROP COLUMN IF EXISTS ${quoteIdent(String(column))};`);
116
+ }
117
+ if (statements.length === 0) {
118
+ throw new Error(`alter operation ${operation.key} has no column metadata`);
119
+ }
120
+ return statements.join("\n");
121
+ }
122
+ function renderColumnAlteration(table, alteration) {
123
+ if (!alteration || typeof alteration !== "object") {
124
+ throw new Error("invalid alter-column metadata");
125
+ }
126
+ const record = alteration;
127
+ const name = typeof record.name === "string" ? record.name : undefined;
128
+ if (!name) {
129
+ throw new Error("invalid alter-column metadata");
130
+ }
131
+ const prefix = `ALTER TABLE ${qualifiedRef(table.ref)} ALTER COLUMN ${quoteIdent(name)}`;
132
+ const statements = [];
133
+ if (typeof record.type === "string") {
134
+ statements.push(`${prefix} TYPE ${record.type} USING ${quoteIdent(name)}::${record.type};`);
135
+ }
136
+ if (record.dropDefault === true) {
137
+ statements.push(`${prefix} DROP DEFAULT;`);
138
+ }
139
+ if (typeof record.setDefault === "string") {
140
+ statements.push(`${prefix} SET DEFAULT ${record.setDefault};`);
141
+ }
142
+ if (record.setNotNull === true) {
143
+ statements.push(`${prefix} SET NOT NULL;`);
144
+ }
145
+ if (record.dropNotNull === true) {
146
+ statements.push(`${prefix} DROP NOT NULL;`);
147
+ }
148
+ return statements;
149
+ }
150
+ function renderReplace(operation) {
151
+ const before = requiredBefore(operation);
152
+ const after = requiredAfter(operation);
153
+ switch (after.ref.kind) {
154
+ case "function":
155
+ case "procedure":
156
+ case "view":
157
+ if (operation.metadata.routineDropRequired === true ||
158
+ operation.metadata.viewDropRequired === true) {
159
+ return `${renderDrop(before)}\n${renderCreate(after)}`;
160
+ }
161
+ return renderCreate(after);
162
+ case "policy":
163
+ case "trigger":
164
+ return renderCreate(after);
165
+ case "index":
166
+ case "constraint":
167
+ case "comment":
168
+ return `${renderDrop(before)}\n${renderCreate(after)}`;
169
+ case "materialized-view":
170
+ case "table":
171
+ case "foreign-data-wrapper":
172
+ case "foreign-server":
173
+ case "foreign-table":
174
+ case "enum":
175
+ case "type":
176
+ case "domain":
177
+ case "sequence":
178
+ case "rls":
179
+ return `${renderDrop(before)}\n${renderCreate(after)}`;
180
+ case "schema":
181
+ case "extension":
182
+ case "grant":
183
+ case "default-privilege":
184
+ return renderCreate(after);
185
+ }
186
+ throw new Error(`unsupported replace operation for ${after.ref.kind}`);
187
+ }
188
+ function renderAddColumn(table, column) {
189
+ return `ALTER TABLE ${qualifiedRef(table.ref)} ADD COLUMN IF NOT EXISTS ${quoteIdent(column.name)} ${column.definition};`;
190
+ }
191
+ function renderCreate(object) {
192
+ switch (object.ref.kind) {
193
+ case "schema":
194
+ case "extension":
195
+ case "sequence":
196
+ case "table":
197
+ case "foreign-server":
198
+ case "foreign-table":
199
+ case "index":
200
+ case "materialized-view":
201
+ case "function":
202
+ case "procedure":
203
+ case "view":
204
+ return spliceGuard(object);
205
+ case "enum":
206
+ case "type":
207
+ case "domain":
208
+ return renderTypeGuard(object);
209
+ case "foreign-data-wrapper":
210
+ return renderFdwGuard(object);
211
+ case "constraint":
212
+ return renderConstraintGuard(object);
213
+ case "policy":
214
+ case "trigger":
215
+ return `${renderDrop(object)}\n${ensureSemicolon(object.sql)}`;
216
+ case "rls":
217
+ case "grant":
218
+ case "default-privilege":
219
+ case "comment":
220
+ return ensureSemicolon(object.sql);
221
+ }
222
+ throw new Error(`unsupported create operation for ${object.ref.kind}`);
223
+ }
224
+ const guardInserts = {
225
+ ifNotExists: "IF NOT EXISTS ",
226
+ orReplace: "OR REPLACE ",
227
+ };
228
+ /**
229
+ * Splices the replay guard at the AST-located offset carried in
230
+ * metadata.render. No regex rewriting: classification and the splice point
231
+ * both come from extraction-time parse facts.
232
+ */
233
+ function spliceGuard(object) {
234
+ const facts = object.metadata.render;
235
+ if (!facts || typeof facts !== "object") {
236
+ throw new Error(`object ${object.key} has no render guard facts; re-extract the source model with this supaschema version`);
237
+ }
238
+ const record = facts;
239
+ const guard = record.guard;
240
+ if (guard !== "ifNotExists" && guard !== "orReplace") {
241
+ throw new Error(`object ${object.key} has unsupported render guard facts`);
242
+ }
243
+ if (record.present === true) {
244
+ return ensureSemicolon(object.sql);
245
+ }
246
+ const offset = record.offset;
247
+ if (typeof offset !== "number" || offset < 0 || offset > object.sql.length) {
248
+ throw new Error(`object ${object.key} has no render guard offset; re-extract the source model with this supaschema version`);
249
+ }
250
+ return ensureSemicolon(`${object.sql.slice(0, offset)}${guardInserts[guard]}${object.sql.slice(offset)}`);
251
+ }
252
+ function renderDrop(object) {
253
+ const ref = object.ref;
254
+ switch (ref.kind) {
255
+ case "schema":
256
+ return `DROP SCHEMA IF EXISTS ${quoteIdent(ref.name)};`;
257
+ case "extension":
258
+ return `DROP EXTENSION IF EXISTS ${quoteIdent(ref.name)};`;
259
+ case "table":
260
+ return `DROP TABLE IF EXISTS ${qualifiedRef(ref)};`;
261
+ case "foreign-data-wrapper":
262
+ return `DROP FOREIGN DATA WRAPPER IF EXISTS ${quoteIdent(ref.name)};`;
263
+ case "foreign-server":
264
+ return `DROP SERVER IF EXISTS ${quoteIdent(ref.name)};`;
265
+ case "foreign-table":
266
+ return `DROP FOREIGN TABLE IF EXISTS ${qualifiedRef(ref)};`;
267
+ case "sequence":
268
+ return `DROP SEQUENCE IF EXISTS ${qualifiedRef(ref)};`;
269
+ case "enum":
270
+ case "type":
271
+ return `DROP TYPE IF EXISTS ${qualifiedRef(ref)};`;
272
+ case "domain":
273
+ return `DROP DOMAIN IF EXISTS ${qualifiedRef(ref)};`;
274
+ case "view":
275
+ return `DROP VIEW IF EXISTS ${qualifiedRef(ref)};`;
276
+ case "materialized-view":
277
+ return `DROP MATERIALIZED VIEW IF EXISTS ${qualifiedRef(ref)};`;
278
+ case "index":
279
+ return `DROP INDEX IF EXISTS ${qualifiedRef(ref)};`;
280
+ case "function":
281
+ return `DROP FUNCTION IF EXISTS ${qualifiedRef(ref)}(${ref.signature ?? ""});`;
282
+ case "procedure":
283
+ return `DROP PROCEDURE IF EXISTS ${qualifiedRef(ref)}(${ref.signature ?? ""});`;
284
+ case "constraint":
285
+ return `ALTER TABLE ${qualifiedTableRef(ref)} DROP CONSTRAINT IF EXISTS ${quoteIdent(ref.name)};`;
286
+ case "trigger":
287
+ return `DROP TRIGGER IF EXISTS ${quoteIdent(ref.name)} ON ${qualifiedTableRef(ref)};`;
288
+ case "policy":
289
+ return `DROP POLICY IF EXISTS ${quoteIdent(ref.name)} ON ${qualifiedTableRef(ref)};`;
290
+ case "rls":
291
+ return `ALTER TABLE ${qualifiedTableRef(ref)} DISABLE ROW LEVEL SECURITY;`;
292
+ case "comment":
293
+ return typeof object.metadata.commentDropSql === "string"
294
+ ? ensureSemicolon(object.metadata.commentDropSql)
295
+ : `COMMENT ON ${String(object.metadata.descriptor ?? ref.name)} IS NULL;`;
296
+ case "grant":
297
+ return renderGrantDrop(object);
298
+ case "default-privilege":
299
+ return renderDefaultPrivilegeDrop(object);
300
+ }
301
+ throw new Error(`unsupported drop operation for ${ref.kind}`);
302
+ }
303
+ function columnFromMetadata(value) {
304
+ if (!value || typeof value !== "object") {
305
+ throw new Error("invalid add-column metadata");
306
+ }
307
+ const name = "name" in value && typeof value.name === "string" ? value.name : undefined;
308
+ const definition = "definition" in value && typeof value.definition === "string" ? value.definition : undefined;
309
+ if (!name || !definition) {
310
+ throw new Error("invalid add-column metadata");
311
+ }
312
+ return { definition, name };
313
+ }
314
+ function requiredBefore(operation) {
315
+ if (!operation.before) {
316
+ throw new Error(`operation ${operation.key} has no before object`);
317
+ }
318
+ return operation.before;
319
+ }
320
+ function requiredAfter(operation) {
321
+ if (!operation.after) {
322
+ throw new Error(`operation ${operation.key} has no after object`);
323
+ }
324
+ return operation.after;
325
+ }
@@ -0,0 +1,11 @@
1
+ import type { Diagnostic } from "./core.js";
2
+ export interface SelfCheckOptions {
3
+ databaseUrl: string;
4
+ }
5
+ export interface SelfCheckResult {
6
+ checkedObjects: number;
7
+ diagnostics: Diagnostic[];
8
+ mismatches: number;
9
+ }
10
+ export declare function selfCheckCatalog(options: SelfCheckOptions): Promise<SelfCheckResult>;
11
+ //# sourceMappingURL=selfcheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5C,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4D1F"}
@@ -0,0 +1,43 @@
1
+ import { extractCatalogModel } from "./catalog.js";
2
+ import { diagnostic } from "./diagnostics.js";
3
+ import { extractObjectsFromSql } from "./sql/extract.js";
4
+ export async function selfCheckCatalog(options) {
5
+ const model = await extractCatalogModel({
6
+ databaseUrl: options.databaseUrl,
7
+ source: "selfcheck",
8
+ });
9
+ const diagnostics = [...model.diagnostics];
10
+ const script = [...model.objects]
11
+ .sort((left, right) => left.ordinal - right.ordinal)
12
+ .map((object) => `${object.sql};`)
13
+ .join("\n\n");
14
+ // The reparse uses the plain postgres adapter: managed-schema policy is a
15
+ // posture concern, not an identity-parity concern.
16
+ const reparsed = await extractObjectsFromSql(script, {
17
+ config: { adapter: "postgres" },
18
+ file: "selfcheck:rendered",
19
+ });
20
+ diagnostics.push(...reparsed.diagnostics.filter((item) => item.severity === "error"));
21
+ const catalogByKey = new Map(model.objects.map((object) => [object.key, object]));
22
+ const reparsedByKey = new Map(reparsed.objects.map((object) => [object.key, object]));
23
+ let mismatches = 0;
24
+ for (const [key, object] of catalogByKey) {
25
+ const other = reparsedByKey.get(key);
26
+ if (!other) {
27
+ mismatches += 1;
28
+ diagnostics.push(diagnostic("SUPA_SELFCHECK_MISSING", "error", `catalog object ${key} disappeared when its rendered SQL was re-extracted`, { ref: object.ref, statement: object.sql }));
29
+ continue;
30
+ }
31
+ if (other.hash !== object.hash) {
32
+ mismatches += 1;
33
+ diagnostics.push(diagnostic("SUPA_SELFCHECK_HASH_MISMATCH", "error", `catalog object ${key} hashes differently after re-extraction; cross-lane identity would report a false change`, { ref: object.ref, statement: object.sql }));
34
+ }
35
+ }
36
+ for (const [key, object] of reparsedByKey) {
37
+ if (!catalogByKey.has(key)) {
38
+ mismatches += 1;
39
+ diagnostics.push(diagnostic("SUPA_SELFCHECK_UNEXPECTED", "error", `re-extraction produced ${key}, which the catalog model does not contain`, { ref: object.ref, statement: object.sql }));
40
+ }
41
+ }
42
+ return { checkedObjects: catalogByKey.size, diagnostics, mismatches };
43
+ }
@@ -0,0 +1,14 @@
1
+ import type { Diagnostic, SchemaObject } from "./core.js";
2
+ export interface SourceNormalizeOptions {
3
+ normalize?: boolean;
4
+ }
5
+ export declare function normalizeSourceObjects(objects: SchemaObject[], diagnostics: Diagnostic[], options?: SourceNormalizeOptions): Promise<SchemaObject[]>;
6
+ /**
7
+ * Postgres applies in-model ALTER DEFAULT PRIVILEGES to every later object,
8
+ * and the resulting ACL entry is indistinguishable from an explicit GRANT.
9
+ * A grant fully implied by an in-model default-privilege entry is therefore
10
+ * suppressed on BOTH lanes (catalog models route through this too): trees
11
+ * declare the default once, catalogs materialize it per object.
12
+ */
13
+ export declare function suppressDefaultAclImpliedGrants(objects: SchemaObject[]): SchemaObject[];
14
+ //# sourceMappingURL=source-normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-normalize.d.ts","sourceRoot":"","sources":["../src/source-normalize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAa1D,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,YAAY,EAAE,EACvB,WAAW,EAAE,UAAU,EAAE,EACzB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,YAAY,EAAE,CAAC,CAMzB;AAYD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CA2CvF"}