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,83 @@
1
+ import { diagnostic } from "./diagnostics.js";
2
+ import { asRecord, qualifiedName, readString } from "./sql/ast.js";
3
+ export function newEnumAdditionState() {
4
+ return new Map();
5
+ }
6
+ export function recordEnumAdditions(statement, state) {
7
+ if (statement.tag !== "AlterEnumStmt") {
8
+ return;
9
+ }
10
+ const node = asRecord(statement.node.AlterEnumStmt);
11
+ const name = qualifiedName(node?.typeName);
12
+ const newValue = readString(node?.newVal);
13
+ if (!name || newValue === undefined) {
14
+ return;
15
+ }
16
+ for (const key of [`${name.schema}.${name.name}`, name.name]) {
17
+ const values = state.get(key) ?? new Set();
18
+ values.add(newValue);
19
+ state.set(key, values);
20
+ }
21
+ }
22
+ export function enumValueUseDiagnostics(statement, state, config) {
23
+ if (state.size === 0 || statement.tag === "AlterEnumStmt") {
24
+ return [];
25
+ }
26
+ const uses = [];
27
+ collectEnumValueUses(statement.node, state, uses);
28
+ if (uses.length === 0) {
29
+ return [];
30
+ }
31
+ const severity = config.transactionMode === "per-migration" ? "error" : "warning";
32
+ return uses.map((use) => diagnostic("SUPA_CHECK_ENUM_VALUE_USE_SAME_TRANSACTION", severity, `enum value ${use} is added and used in the same migration; PostgreSQL cannot use a value added in the same transaction`, {
33
+ hint: "Move the usage to a follow-up migration, or run the runner without transaction wrapping and set transactionMode to per-statement.",
34
+ statement: statement.text,
35
+ }));
36
+ }
37
+ function collectEnumValueUses(value, state, uses) {
38
+ if (Array.isArray(value)) {
39
+ for (const item of value) {
40
+ collectEnumValueUses(item, state, uses);
41
+ }
42
+ return;
43
+ }
44
+ const record = asRecord(value);
45
+ if (!record) {
46
+ return;
47
+ }
48
+ const typeCast = asRecord(record.TypeCast);
49
+ if (typeCast) {
50
+ const typeName = asRecord(asRecord(typeCast.typeName)?.TypeName) ?? asRecord(typeCast.typeName);
51
+ const name = qualifiedName(typeName?.names);
52
+ const literal = castStringLiteral(typeCast.arg);
53
+ if (name && literal !== undefined) {
54
+ const added = state.get(`${name.schema}.${name.name}`) ??
55
+ (name.schema === "public" ? state.get(name.name) : undefined);
56
+ if (added?.has(literal)) {
57
+ uses.push(`'${literal}'::${name.schema}.${name.name}`);
58
+ }
59
+ }
60
+ }
61
+ for (const child of Object.values(record)) {
62
+ if (child && typeof child === "object") {
63
+ collectEnumValueUses(child, state, uses);
64
+ }
65
+ }
66
+ }
67
+ function castStringLiteral(arg) {
68
+ const constant = asRecord(asRecord(arg)?.A_Const);
69
+ return readString(asRecord(constant?.sval)?.sval);
70
+ }
71
+ export function escalateNontransactional(diagnostics, config) {
72
+ const escalate = config.adapter === "supabase-auto" || config.transactionMode === "per-migration";
73
+ if (!escalate) {
74
+ return diagnostics;
75
+ }
76
+ return diagnostics.map((item) => {
77
+ if (item.code === "SUPA_CHECK_NONTRANSACTIONAL_INDEX" ||
78
+ item.code === "SUPA_CHECK_NONTRANSACTIONAL_REFRESH") {
79
+ return { ...item, severity: "error" };
80
+ }
81
+ return item;
82
+ });
83
+ }
@@ -0,0 +1,8 @@
1
+ import type { Diagnostic } from "./core.js";
2
+ export type CheckReporter = "text" | "github" | "sarif" | "json";
3
+ export interface FileDiagnostics {
4
+ diagnostics: Diagnostic[];
5
+ file: string;
6
+ }
7
+ export declare function renderCheckReport(reporter: CheckReporter, files: FileDiagnostics[]): string;
8
+ //# sourceMappingURL=check-reporters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-reporters.d.ts","sourceRoot":"","sources":["../src/check-reporters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,CAe3F"}
@@ -0,0 +1,76 @@
1
+ import { formatDiagnostics } from "./diagnostics.js";
2
+ export function renderCheckReport(reporter, files) {
3
+ switch (reporter) {
4
+ case "github":
5
+ return renderGithub(files);
6
+ case "sarif":
7
+ return renderSarif(files);
8
+ case "json":
9
+ return `${JSON.stringify(files.map((entry) => ({ diagnostics: entry.diagnostics, file: entry.file })), null, 2)}\n`;
10
+ default:
11
+ return renderText(files);
12
+ }
13
+ }
14
+ function renderText(files) {
15
+ const lines = [];
16
+ for (const entry of files) {
17
+ if (entry.diagnostics.length === 0) {
18
+ continue;
19
+ }
20
+ if (files.length > 1) {
21
+ lines.push(`${entry.file}:`);
22
+ }
23
+ lines.push(formatDiagnostics(entry.diagnostics));
24
+ }
25
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
26
+ }
27
+ function renderGithub(files) {
28
+ const lines = [];
29
+ for (const entry of files) {
30
+ for (const item of entry.diagnostics) {
31
+ const level = item.severity === "error" ? "error" : "warning";
32
+ const message = escapeGithubData(`${item.code}: ${item.message}${item.hint ? ` (${item.hint})` : ""}`);
33
+ lines.push(`::${level} file=${escapeGithubProperty(entry.file)},title=${item.code}::${message}`);
34
+ }
35
+ }
36
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
37
+ }
38
+ function renderSarif(files) {
39
+ const results = files.flatMap((entry) => entry.diagnostics.map((item) => ({
40
+ level: item.severity === "error" ? "error" : "warning",
41
+ locations: [
42
+ {
43
+ physicalLocation: {
44
+ artifactLocation: { uri: entry.file },
45
+ },
46
+ },
47
+ ],
48
+ message: { text: `${item.message}${item.hint ? ` (${item.hint})` : ""}` },
49
+ ruleId: item.code,
50
+ })));
51
+ const sarif = {
52
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
53
+ runs: [
54
+ {
55
+ results,
56
+ tool: {
57
+ driver: {
58
+ informationUri: "https://github.com/jmclaughlin724/supaschema",
59
+ name: "supaschema",
60
+ rules: [...new Set(results.map((result) => result.ruleId))].map((code) => ({
61
+ id: code,
62
+ })),
63
+ },
64
+ },
65
+ },
66
+ ],
67
+ version: "2.1.0",
68
+ };
69
+ return `${JSON.stringify(sarif, null, 2)}\n`;
70
+ }
71
+ function escapeGithubData(value) {
72
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
73
+ }
74
+ function escapeGithubProperty(value) {
75
+ return escapeGithubData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
76
+ }
@@ -0,0 +1,3 @@
1
+ import type { CheckOptions, Diagnostic } from "./core.js";
2
+ export declare function checkMigrationSql(sql: string, options?: CheckOptions): Promise<Diagnostic[]>;
3
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAiC1D,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,UAAU,EAAE,CAAC,CA4BvB"}
package/dist/check.js ADDED
@@ -0,0 +1,229 @@
1
+ import { enumValueUseDiagnostics, escalateNontransactional, newEnumAdditionState, recordEnumAdditions, } from "./check-hazards.js";
2
+ import { resolveConfig } from "./config.js";
3
+ import { diagnostic } from "./diagnostics.js";
4
+ import { asRecord, astStatements, readArray, readBoolean, readString, stringList, } from "./sql/ast.js";
5
+ import { deparseFidelityDiagnostics } from "./sql/normalize-deparse.js";
6
+ import { parseSqlAst } from "./sql/parser.js";
7
+ import { runConfiguredValidators } from "./validators.js";
8
+ const guardedCreateChecks = [
9
+ { code: "SUPA_CHECK_CREATE_SCHEMA_GUARD", kind: "SCHEMA", tag: "CreateSchemaStmt" },
10
+ { code: "SUPA_CHECK_CREATE_EXTENSION_GUARD", kind: "EXTENSION", tag: "CreateExtensionStmt" },
11
+ { code: "SUPA_CHECK_CREATE_TABLE_GUARD", kind: "TABLE", tag: "CreateStmt" },
12
+ { code: "SUPA_CHECK_CREATE_SEQUENCE_GUARD", kind: "SEQUENCE", tag: "CreateSeqStmt" },
13
+ { code: "SUPA_CHECK_CREATE_INDEX_GUARD", kind: "INDEX", tag: "IndexStmt" },
14
+ ];
15
+ const volatileDefaultFunctions = new Set([
16
+ "clock_timestamp",
17
+ "gen_random_uuid",
18
+ "nextval",
19
+ "random",
20
+ "timeofday",
21
+ "uuid_generate_v1",
22
+ "uuid_generate_v4",
23
+ ]);
24
+ export async function checkMigrationSql(sql, options = {}) {
25
+ const config = resolveConfig(options.config);
26
+ const diagnostics = [];
27
+ if (options.parse ?? true) {
28
+ const parsed = await parseSqlAst(sql);
29
+ diagnostics.push(...parsed.diagnostics);
30
+ if (parsed.ast !== undefined) {
31
+ const statements = astStatements(parsed.ast, sql);
32
+ const enumAdditions = newEnumAdditionState();
33
+ for (const [index, statement] of statements.entries()) {
34
+ diagnostics.push(...escalateNontransactional(checkStatement(statement, statements[index - 1]), config), ...enumValueUseDiagnostics(statement, enumAdditions, config));
35
+ recordEnumAdditions(statement, enumAdditions);
36
+ }
37
+ diagnostics.push(...(await deparseFidelityDiagnostics(sql)));
38
+ }
39
+ }
40
+ const validatorOptions = {};
41
+ if (options.config !== undefined) {
42
+ validatorOptions.config = options.config;
43
+ }
44
+ if (options.cwd !== undefined) {
45
+ validatorOptions.cwd = options.cwd;
46
+ }
47
+ diagnostics.push(...(await runConfiguredValidators(sql, validatorOptions)));
48
+ return diagnostics;
49
+ }
50
+ function checkStatement(statement, previous) {
51
+ const diagnostics = [];
52
+ const node = asRecord(statement.node[statement.tag]);
53
+ if (!node) {
54
+ return diagnostics;
55
+ }
56
+ switch (statement.tag) {
57
+ case "DropStmt":
58
+ diagnostics.push(...checkDropStatement(node, statement.text));
59
+ break;
60
+ case "VariableSetStmt":
61
+ if (readString(node.name) === "search_path") {
62
+ diagnostics.push(diagnostic("SUPA_CHECK_SEARCH_PATH", "error", "migrations must not depend on session search_path", {
63
+ hint: "Use schema-qualified object references and function-level SET search_path where needed.",
64
+ statement: statement.text,
65
+ }));
66
+ }
67
+ break;
68
+ case "ViewStmt":
69
+ if (!readBoolean(node.replace)) {
70
+ diagnostics.push(diagnostic("SUPA_CHECK_CREATE_VIEW_REPLACE", "error", "VIEW creation must use OR REPLACE", {
71
+ statement: statement.text,
72
+ }));
73
+ }
74
+ break;
75
+ case "CreateFunctionStmt":
76
+ diagnostics.push(...checkFunctionStatement(node, statement.text));
77
+ break;
78
+ case "CreateEnumStmt":
79
+ case "CompositeTypeStmt":
80
+ case "CreateRangeStmt":
81
+ case "CreateDomainStmt":
82
+ diagnostics.push(diagnostic("SUPA_CHECK_CREATE_TYPE_GUARD", "error", "TYPE and DOMAIN creation must be wrapped in a catalog guard", { statement: statement.text }));
83
+ break;
84
+ case "CreateTableAsStmt":
85
+ if (readString(node.objtype) === "OBJECT_MATVIEW" &&
86
+ !readBoolean(node.if_not_exists) &&
87
+ !readBoolean(asRecord(node.into)?.if_not_exists)) {
88
+ diagnostics.push(diagnostic("SUPA_CHECK_CREATE_MATERIALIZED_VIEW_GUARD", "error", "MATERIALIZED VIEW creation must use IF NOT EXISTS or a catalog guard", { statement: statement.text }));
89
+ }
90
+ break;
91
+ case "CreatePolicyStmt":
92
+ if (!previousDrops(previous, "OBJECT_POLICY")) {
93
+ diagnostics.push(diagnostic("SUPA_CHECK_POLICY_REPLACEMENT", "error", "CREATE POLICY has no OR REPLACE form and must be preceded by DROP POLICY IF EXISTS", { statement: statement.text }));
94
+ }
95
+ break;
96
+ case "CreateTrigStmt":
97
+ if (!readBoolean(node.replace) && !previousDrops(previous, "OBJECT_TRIGGER")) {
98
+ diagnostics.push(diagnostic("SUPA_CHECK_CREATE_TRIGGER_REPLACEMENT", "error", "CREATE TRIGGER must be preceded by DROP TRIGGER IF EXISTS", { statement: statement.text }));
99
+ }
100
+ break;
101
+ case "IndexStmt":
102
+ if (readBoolean(node.concurrent)) {
103
+ diagnostics.push(diagnostic("SUPA_CHECK_NONTRANSACTIONAL_INDEX", "warning", "CREATE INDEX CONCURRENTLY cannot run inside a transaction block", {
104
+ hint: "Run this migration with transaction wrapping disabled.",
105
+ statement: statement.text,
106
+ }));
107
+ }
108
+ break;
109
+ case "RefreshMatViewStmt":
110
+ if (readBoolean(node.concurrent)) {
111
+ diagnostics.push(diagnostic("SUPA_CHECK_NONTRANSACTIONAL_REFRESH", "warning", "REFRESH MATERIALIZED VIEW CONCURRENTLY cannot run inside a transaction block", {
112
+ hint: "Run this migration with transaction wrapping disabled.",
113
+ statement: statement.text,
114
+ }));
115
+ }
116
+ break;
117
+ case "AlterTableStmt":
118
+ diagnostics.push(...checkAlterTableStatement(node, statement.text));
119
+ break;
120
+ case "InsertStmt":
121
+ if (asRecord(node.onConflictClause) === undefined) {
122
+ diagnostics.push(diagnostic("SUPA_CHECK_INSERT_ON_CONFLICT", "error", "INSERT statements in migrations must use ON CONFLICT for replay safety", { statement: statement.text }));
123
+ }
124
+ break;
125
+ case "UpdateStmt":
126
+ case "DeleteStmt":
127
+ diagnostics.push(diagnostic("SUPA_CHECK_DML_REVIEW", "warning", "data-modifying statements in migrations require explicit idempotency review", { statement: statement.text }));
128
+ break;
129
+ default:
130
+ break;
131
+ }
132
+ for (const guarded of guardedCreateChecks) {
133
+ if (statement.tag === guarded.tag && !readBoolean(node.if_not_exists)) {
134
+ diagnostics.push(diagnostic(guarded.code, "error", `${guarded.kind} creation must use IF NOT EXISTS or a catalog guard`, { statement: statement.text }));
135
+ }
136
+ }
137
+ return diagnostics;
138
+ }
139
+ function checkDropStatement(node, text) {
140
+ const diagnostics = [];
141
+ if (readString(node.behavior) === "DROP_CASCADE") {
142
+ diagnostics.push(diagnostic("SUPA_CHECK_CASCADE", "error", "implicit CASCADE is forbidden", {
143
+ hint: "Drop dependent objects explicitly in dependency order.",
144
+ statement: text,
145
+ }));
146
+ }
147
+ if (!readBoolean(node.missing_ok)) {
148
+ diagnostics.push(diagnostic("SUPA_CHECK_DROP_IF_EXISTS", "error", "DROP statements must use IF EXISTS", {
149
+ statement: text,
150
+ }));
151
+ }
152
+ return diagnostics;
153
+ }
154
+ function checkFunctionStatement(node, text) {
155
+ const diagnostics = [];
156
+ if (!readBoolean(node.replace)) {
157
+ diagnostics.push(diagnostic("SUPA_CHECK_CREATE_ROUTINE_REPLACE", "error", "FUNCTION and PROCEDURE creation must use OR REPLACE", { statement: text }));
158
+ }
159
+ let securityDefiner = false;
160
+ let setsSearchPath = false;
161
+ for (const item of readArray(node.options)) {
162
+ const defElem = asRecord(asRecord(item)?.DefElem);
163
+ const name = readString(defElem?.defname);
164
+ if (name === "security" && readBoolean(defElem?.arg)) {
165
+ securityDefiner = true;
166
+ }
167
+ if (name === "set") {
168
+ const setStmt = asRecord(asRecord(defElem?.arg)?.VariableSetStmt);
169
+ if (readString(setStmt?.name) === "search_path") {
170
+ setsSearchPath = true;
171
+ }
172
+ }
173
+ }
174
+ if (securityDefiner && !setsSearchPath) {
175
+ diagnostics.push(diagnostic("SUPA_CHECK_SECURITY_DEFINER_SEARCH_PATH", "warning", "SECURITY DEFINER functions should set a safe function-local search_path", { statement: text }));
176
+ }
177
+ return diagnostics;
178
+ }
179
+ function checkAlterTableStatement(node, text) {
180
+ const diagnostics = [];
181
+ for (const item of readArray(node.cmds)) {
182
+ const command = asRecord(asRecord(item)?.AlterTableCmd);
183
+ const subtype = readString(command?.subtype);
184
+ if (subtype === "AT_AddConstraint") {
185
+ diagnostics.push(diagnostic("SUPA_CHECK_ADD_CONSTRAINT_GUARD", "error", "ADD CONSTRAINT must be wrapped in a catalog guard", { statement: text }));
186
+ }
187
+ if (subtype === "AT_AlterColumnType") {
188
+ diagnostics.push(diagnostic("SUPA_CHECK_ALTER_COLUMN_TYPE_REWRITE", "warning", "ALTER COLUMN TYPE can rewrite the table under an ACCESS EXCLUSIVE lock", {
189
+ hint: "Verify the rewrite window is acceptable for populated tables.",
190
+ statement: text,
191
+ }));
192
+ }
193
+ if (subtype === "AT_SetNotNull") {
194
+ diagnostics.push(diagnostic("SUPA_CHECK_SET_NOT_NULL_SCAN", "warning", "SET NOT NULL scans the full table unless a validated CHECK constraint already proves it", { statement: text }));
195
+ }
196
+ if (subtype === "AT_AddColumn") {
197
+ const columnDef = asRecord(asRecord(command?.def)?.ColumnDef);
198
+ if (columnDef && hasVolatileDefault(columnDef)) {
199
+ diagnostics.push(diagnostic("SUPA_CHECK_VOLATILE_DEFAULT_REWRITE", "warning", "ADD COLUMN with a volatile default rewrites the whole table", {
200
+ hint: "Add the column without a default, backfill in batches, then set the default.",
201
+ statement: text,
202
+ }));
203
+ }
204
+ }
205
+ }
206
+ return diagnostics;
207
+ }
208
+ function hasVolatileDefault(columnDef) {
209
+ for (const item of readArray(columnDef.constraints)) {
210
+ const constraint = asRecord(asRecord(item)?.Constraint);
211
+ if (readString(constraint?.contype) !== "CONSTR_DEFAULT") {
212
+ continue;
213
+ }
214
+ const funcCall = asRecord(asRecord(constraint?.raw_expr)?.FuncCall);
215
+ const names = stringList(funcCall?.funcname);
216
+ const callee = names.at(-1);
217
+ if (callee && volatileDefaultFunctions.has(callee.toLowerCase())) {
218
+ return true;
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+ function previousDrops(previous, removeType) {
224
+ if (previous?.tag !== "DropStmt") {
225
+ return false;
226
+ }
227
+ const node = asRecord(previous.node.DropStmt);
228
+ return readString(node?.removeType) === removeType && readBoolean(node?.missing_ok);
229
+ }
@@ -0,0 +1,24 @@
1
+ import type { SupaschemaConfig } from "./config.js";
2
+ import type { MigrationPlan } from "./core.js";
3
+ export interface ResolvedSources {
4
+ from: string;
5
+ notice: string | undefined;
6
+ to: string;
7
+ }
8
+ export declare function defaultTreeSource(config: SupaschemaConfig): string;
9
+ export declare function resolveMigrationsDir(flagValue: string | undefined, config: SupaschemaConfig): string;
10
+ /**
11
+ * Zero-flag source resolution: --to defaults to the declarative tree from
12
+ * config.schemaPaths, --from defaults to the resolved database (the applied
13
+ * state) and falls back to git:HEAD when no database URL resolves. The
14
+ * notice names every defaulted lane so the chosen sources are never silent.
15
+ */
16
+ export declare function resolveSourceDefaults(options: {
17
+ from?: string;
18
+ to?: string;
19
+ }, config: SupaschemaConfig, resolveDbUrl: () => Promise<string | undefined>): Promise<ResolvedSources>;
20
+ export declare function defaultMigrationName(plan: MigrationPlan): string;
21
+ export declare function migrationNameSlug(value: string): string;
22
+ export declare function migrationFiles(directory: string): Promise<string[]>;
23
+ export declare function latestMigrationFile(directory: string): Promise<string | undefined>;
24
+ //# sourceMappingURL=cli-defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-defaults.d.ts","sourceRoot":"","sources":["../src/cli-defaults.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG/C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAElE;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GACvB,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EACvC,MAAM,EAAE,gBAAgB,EACxB,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAC9C,OAAO,CAAC,eAAe,CAAC,CAe1B;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAOhE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAczE;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGxF"}
@@ -0,0 +1,65 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { redactSecrets } from "./diagnostics.js";
4
+ export function defaultTreeSource(config) {
5
+ return `dir:${config.schemaPaths[0] ?? "supabase/schemas"}`;
6
+ }
7
+ export function resolveMigrationsDir(flagValue, config) {
8
+ return flagValue ?? config.migrationsDir;
9
+ }
10
+ /**
11
+ * Zero-flag source resolution: --to defaults to the declarative tree from
12
+ * config.schemaPaths, --from defaults to the resolved database (the applied
13
+ * state) and falls back to git:HEAD when no database URL resolves. The
14
+ * notice names every defaulted lane so the chosen sources are never silent.
15
+ */
16
+ export async function resolveSourceDefaults(options, config, resolveDbUrl) {
17
+ const defaulted = [];
18
+ const to = options.to ?? defaultTreeSource(config);
19
+ if (options.to === undefined) {
20
+ defaulted.push(`--to ${to}`);
21
+ }
22
+ let from = options.from;
23
+ if (from === undefined) {
24
+ const databaseUrl = await resolveDbUrl();
25
+ from = databaseUrl === undefined ? "git:HEAD" : `database:${databaseUrl}`;
26
+ defaulted.push(`--from ${redactSecrets(from)}`);
27
+ }
28
+ const notice = defaulted.length > 0 ? `defaults: ${defaulted.join(" · ")} (flags override)\n` : undefined;
29
+ return { from, notice, to };
30
+ }
31
+ export function defaultMigrationName(plan) {
32
+ const first = plan.operations[0];
33
+ if (plan.operations.length === 1 && first) {
34
+ const name = first.ref.name ?? first.key;
35
+ return migrationNameSlug(`${first.kind}_${first.ref.kind}_${name}`);
36
+ }
37
+ return "schema_diff";
38
+ }
39
+ export function migrationNameSlug(value) {
40
+ return value
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/gu, "_")
43
+ .replace(/^_+|_+$/gu, "")
44
+ .slice(0, 60);
45
+ }
46
+ export async function migrationFiles(directory) {
47
+ let entries;
48
+ try {
49
+ entries = await readdir(directory);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
53
+ return [];
54
+ }
55
+ throw error;
56
+ }
57
+ return entries
58
+ .filter((entry) => entry.endsWith(".sql"))
59
+ .sort((left, right) => left.localeCompare(right))
60
+ .map((entry) => join(directory, entry));
61
+ }
62
+ export async function latestMigrationFile(directory) {
63
+ const files = await migrationFiles(directory);
64
+ return files.at(-1);
65
+ }
@@ -0,0 +1,13 @@
1
+ import type { Command } from "commander";
2
+ import type { SupaschemaConfig } from "./config.js";
3
+ import type { Diagnostic, MigrationPlan, SchemaModel } from "./core.js";
4
+ export interface DiffCommandContext {
5
+ cliVersion: string;
6
+ loadCliConfig: () => Promise<SupaschemaConfig>;
7
+ printDiagnostics: (diagnostics: Diagnostic[]) => void;
8
+ resolveCliDatabaseUrl: (explicit?: string) => Promise<string | undefined>;
9
+ }
10
+ export declare function registerDiffCommands(program: Command, context: DiffCommandContext): void;
11
+ export declare function filterModel(model: SchemaModel, schemaFilter: string | undefined): SchemaModel;
12
+ export declare function renderPlanSummary(plan: MigrationPlan): string;
13
+ //# sourceMappingURL=cli-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-diff.d.ts","sourceRoot":"","sources":["../src/cli-diff.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuBxE,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/C,gBAAgB,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IACtD,qBAAqB,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA6DxF;AAmQD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAW7F;AAiDD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAiC7D"}