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,304 @@
1
+ import { asRecord, rangeVarName, readArray, readNumber, readString, stringList } from "./ast.js";
2
+ import { formatQualifiedName, quoteIdent } from "./identifiers.js";
3
+ import { elementText, findCharOutsideQuotes, findMatchingParen, fromByteString, tableElements, toByteString, } from "./statements.js";
4
+ export function tableConstraintSyntheses(createStmt, sql, byteOffset = 0) {
5
+ const relation = rangeVarName(createStmt.relation);
6
+ if (!relation) {
7
+ return [];
8
+ }
9
+ const qualified = formatQualifiedName(relation.schema, relation.name);
10
+ const bytes = toByteString(sql);
11
+ const syntheses = [];
12
+ for (const element of tableElements(createStmt, bytes, byteOffset)) {
13
+ if (element.isColumn) {
14
+ syntheses.push(...inlineConstraintSyntheses(element, bytes, byteOffset, relation.name, qualified));
15
+ continue;
16
+ }
17
+ const constraint = element.node;
18
+ const text = fromByteString(elementText(bytes, element));
19
+ const conname = readString(constraint.conname);
20
+ const name = conname ?? defaultConstraintName(relation.name, constraint, []);
21
+ if (!name) {
22
+ continue;
23
+ }
24
+ const fragment = conname ? text : `CONSTRAINT ${quoteIdent(name)} ${text}`;
25
+ syntheses.push({ name, sql: `ALTER TABLE ONLY ${qualified} ADD ${fragment}` });
26
+ }
27
+ return syntheses;
28
+ }
29
+ /**
30
+ * Rebuilds the CREATE TABLE statement without its declared constraints so the
31
+ * table object's SQL matches the columns-only shape the catalog lane emits.
32
+ * Raw-apply consumers (verify, parity tests) then apply the table once and
33
+ * each constraint once instead of creating hoisted constraints twice. Returns
34
+ * undefined when the statement declares no hoistable constraints.
35
+ */
36
+ export function stripDeclaredConstraints(createStmt, sql, byteOffset = 0) {
37
+ const relation = asRecord(createStmt.relation);
38
+ const bytes = toByteString(sql);
39
+ const elements = tableElements(createStmt, bytes, byteOffset);
40
+ if (elements.length === 0) {
41
+ return undefined;
42
+ }
43
+ let strippedAny = false;
44
+ const pieces = [];
45
+ const primaryColumns = primaryKeyColumns(elements);
46
+ for (const element of elements) {
47
+ if (!element.isColumn) {
48
+ strippedAny = true;
49
+ continue;
50
+ }
51
+ const { piece, stripped } = columnPieceWithoutHoisted(element, bytes, byteOffset);
52
+ if (stripped) {
53
+ strippedAny = true;
54
+ }
55
+ if (piece.length === 0) {
56
+ continue;
57
+ }
58
+ // A stripped PRIMARY KEY implied NOT NULL on its columns; spell it
59
+ // explicitly so the rebuilt statement keeps the same column facts. The
60
+ // AST is authoritative: the piece retains a NOT NULL span exactly when a
61
+ // CONSTR_NOTNULL constraint exists, because NOT NULL is never hoisted.
62
+ const columnName = readString(element.node.colname);
63
+ if (columnName && primaryColumns.has(columnName) && !hasExplicitNotNull(element.node)) {
64
+ pieces.push(`${piece} NOT NULL`);
65
+ continue;
66
+ }
67
+ pieces.push(piece);
68
+ }
69
+ if (!strippedAny || pieces.length === 0) {
70
+ return undefined;
71
+ }
72
+ const relationLocation = (readNumber(relation?.location) ?? 0) - byteOffset;
73
+ const open = findCharOutsideQuotes(bytes, "(", Math.max(relationLocation, 0));
74
+ if (open === -1) {
75
+ return undefined;
76
+ }
77
+ const close = findMatchingParen(bytes, open);
78
+ if (close === -1) {
79
+ return undefined;
80
+ }
81
+ const head = bytes.slice(0, open + 1);
82
+ const tail = bytes.slice(close);
83
+ return fromByteString(`${head}\n ${pieces.join(",\n ")}\n${tail}`);
84
+ }
85
+ function primaryKeyColumns(elements) {
86
+ const columns = new Set();
87
+ for (const element of elements) {
88
+ if (element.isColumn) {
89
+ const name = readString(element.node.colname);
90
+ const hasPrimary = readArray(element.node.constraints).some((item) => readString(asRecord(asRecord(item)?.Constraint)?.contype) === "CONSTR_PRIMARY");
91
+ if (name && hasPrimary) {
92
+ columns.add(name);
93
+ }
94
+ continue;
95
+ }
96
+ if (readString(element.node.contype) === "CONSTR_PRIMARY") {
97
+ for (const key of stringList(element.node.keys)) {
98
+ columns.add(key);
99
+ }
100
+ }
101
+ }
102
+ return columns;
103
+ }
104
+ function hasExplicitNotNull(columnDef) {
105
+ return readArray(columnDef.constraints).some((item) => readString(asRecord(asRecord(item)?.Constraint)?.contype) === "CONSTR_NOTNULL");
106
+ }
107
+ function columnPieceWithoutHoisted(element, bytes, byteOffset) {
108
+ const located = locatedInlineConstraints(element, byteOffset);
109
+ let piece = "";
110
+ let cursor = element.start;
111
+ let stripped = false;
112
+ for (const [index, item] of located.entries()) {
113
+ const end = located[index + 1]?.location ?? element.end;
114
+ const contype = readString(item.constraint.contype);
115
+ if (contype && inlineConstraintTypes.has(contype)) {
116
+ piece += bytes.slice(cursor, item.location);
117
+ cursor = end;
118
+ stripped = true;
119
+ }
120
+ }
121
+ piece += bytes.slice(cursor, element.end);
122
+ piece = piece.trim();
123
+ if (piece.endsWith(",")) {
124
+ piece = piece.slice(0, -1).trimEnd();
125
+ }
126
+ return { piece: fromByteString(piece), stripped };
127
+ }
128
+ function inlineConstraintSyntheses(element, bytes, byteOffset, table, qualified) {
129
+ const column = readString(element.node.colname);
130
+ if (!column) {
131
+ return [];
132
+ }
133
+ const located = locatedInlineConstraints(element, byteOffset);
134
+ const syntheses = [];
135
+ for (const [index, item] of located.entries()) {
136
+ const contype = readString(item.constraint.contype);
137
+ if (!contype || !inlineConstraintTypes.has(contype)) {
138
+ continue;
139
+ }
140
+ const end = located[index + 1]?.location ?? element.end;
141
+ let text = fromByteString(bytes.slice(item.location, end)).trim();
142
+ if (text.endsWith(",")) {
143
+ text = text.slice(0, -1).trimEnd();
144
+ }
145
+ const conname = readString(item.constraint.conname);
146
+ if (conname) {
147
+ // The AST already classified this as a named constraint; scanning only
148
+ // locates where the `CONSTRAINT <name>` prefix ends so the remainder
149
+ // fits the table-level template for its type.
150
+ text = skipConstraintNamePrefix(text);
151
+ }
152
+ const name = conname ?? defaultConstraintName(table, item.constraint, [column]);
153
+ if (!name) {
154
+ continue;
155
+ }
156
+ const body = inlineConstraintBody(contype, column, text);
157
+ if (!body) {
158
+ continue;
159
+ }
160
+ syntheses.push({
161
+ name,
162
+ sql: `ALTER TABLE ONLY ${qualified} ADD CONSTRAINT ${quoteIdent(name)} ${body}`,
163
+ });
164
+ }
165
+ return syntheses;
166
+ }
167
+ /**
168
+ * Skips a leading `CONSTRAINT <name>` token pair and returns the remainder.
169
+ * Character scanning only — whether the constraint is named comes from the
170
+ * AST (`conname`); this mirrors the render-guard keyword scanner in facts.ts.
171
+ */
172
+ function skipConstraintNamePrefix(text) {
173
+ let index = skipSqlWhitespace(text, 0);
174
+ const keywordEnd = index + "CONSTRAINT".length;
175
+ if (text.slice(index, keywordEnd).toUpperCase() !== "CONSTRAINT") {
176
+ return text;
177
+ }
178
+ index = skipSqlWhitespace(text, keywordEnd);
179
+ if (text[index] === '"') {
180
+ index += 1;
181
+ while (index < text.length) {
182
+ if (text[index] === '"' && text[index + 1] === '"') {
183
+ index += 2;
184
+ continue;
185
+ }
186
+ if (text[index] === '"') {
187
+ index += 1;
188
+ break;
189
+ }
190
+ index += 1;
191
+ }
192
+ }
193
+ else {
194
+ while (index < text.length && isIdentifierChar(text[index] ?? "")) {
195
+ index += 1;
196
+ }
197
+ }
198
+ return text.slice(skipSqlWhitespace(text, index));
199
+ }
200
+ function isIdentifierChar(char) {
201
+ return ((char >= "a" && char <= "z") ||
202
+ (char >= "A" && char <= "Z") ||
203
+ (char >= "0" && char <= "9") ||
204
+ char === "_" ||
205
+ char === "$");
206
+ }
207
+ function skipSqlWhitespace(text, start) {
208
+ let index = start;
209
+ while (index < text.length) {
210
+ const char = text[index];
211
+ if (char === " " || char === "\t" || char === "\n" || char === "\r") {
212
+ index += 1;
213
+ continue;
214
+ }
215
+ break;
216
+ }
217
+ return index;
218
+ }
219
+ function locatedInlineConstraints(element, byteOffset) {
220
+ return readArray(element.node.constraints)
221
+ .map((item) => asRecord(asRecord(item)?.Constraint))
222
+ .filter((item) => item !== undefined)
223
+ .map((constraint) => ({
224
+ constraint,
225
+ location: (readNumber(constraint.location) ?? -1) - byteOffset,
226
+ }))
227
+ .filter((item) => item.location >= 0)
228
+ .sort((left, right) => left.location - right.location);
229
+ }
230
+ const inlineConstraintTypes = new Set([
231
+ "CONSTR_CHECK",
232
+ "CONSTR_FOREIGN",
233
+ "CONSTR_PRIMARY",
234
+ "CONSTR_UNIQUE",
235
+ ]);
236
+ function inlineConstraintBody(contype, column, text) {
237
+ switch (contype) {
238
+ case "CONSTR_PRIMARY":
239
+ return `PRIMARY KEY (${quoteIdent(column)})`;
240
+ case "CONSTR_UNIQUE":
241
+ return `UNIQUE (${quoteIdent(column)})`;
242
+ case "CONSTR_CHECK":
243
+ return text;
244
+ case "CONSTR_FOREIGN":
245
+ return `FOREIGN KEY (${quoteIdent(column)}) ${text}`;
246
+ default:
247
+ return undefined;
248
+ }
249
+ }
250
+ function defaultConstraintName(table, constraint, impliedColumns) {
251
+ const contype = readString(constraint.contype);
252
+ const keys = stringList(constraint.keys);
253
+ const fkAttrs = stringList(constraint.fk_attrs);
254
+ const columns = fkAttrs.length > 0 ? fkAttrs : keys.length > 0 ? keys : impliedColumns;
255
+ const joined = columns.join("_");
256
+ switch (contype) {
257
+ case "CONSTR_PRIMARY":
258
+ return `${table}_pkey`;
259
+ case "CONSTR_UNIQUE":
260
+ return joined ? `${table}_${joined}_key` : undefined;
261
+ case "CONSTR_FOREIGN":
262
+ return joined ? `${table}_${joined}_fkey` : undefined;
263
+ case "CONSTR_CHECK": {
264
+ // PostgreSQL names a check after its column only when the expression
265
+ // references exactly one column; otherwise the bare `<table>_check`.
266
+ const referenced = columns.length > 0 ? columns : expressionColumns(constraint.raw_expr);
267
+ return referenced.length === 1 ? `${table}_${referenced[0]}_check` : `${table}_check`;
268
+ }
269
+ case "CONSTR_EXCLUSION":
270
+ return joined ? `${table}_${joined}_excl` : undefined;
271
+ default:
272
+ return undefined;
273
+ }
274
+ }
275
+ function expressionColumns(expression) {
276
+ const columns = new Set();
277
+ const visit = (value) => {
278
+ if (Array.isArray(value)) {
279
+ for (const item of value) {
280
+ visit(item);
281
+ }
282
+ return;
283
+ }
284
+ const node = asRecord(value);
285
+ if (!node) {
286
+ return;
287
+ }
288
+ const columnRef = asRecord(node.ColumnRef);
289
+ if (columnRef) {
290
+ const fields = stringList(columnRef.fields);
291
+ const name = fields.at(-1);
292
+ if (name) {
293
+ columns.add(name);
294
+ }
295
+ }
296
+ for (const child of Object.values(node)) {
297
+ if (child && typeof child === "object") {
298
+ visit(child);
299
+ }
300
+ }
301
+ };
302
+ visit(expression);
303
+ return [...columns];
304
+ }
@@ -0,0 +1,38 @@
1
+ import type { AstNode } from "./ast.js";
2
+ export declare function canonicalColumnType(typeName: unknown): string;
3
+ /**
4
+ * Canonical table identity for hashing. The raw parse tree differs between a
5
+ * declarative source ("id bigint PRIMARY KEY") and a catalog reconstruction
6
+ * ('"id" bigint NOT NULL' plus a separate ADD CONSTRAINT), so the shape
7
+ * carries columns only: every table constraint — inline, in-CREATE, or
8
+ * ALTER-declared — is identity-owned by its own `constraint:` object, making
9
+ * table identity independent of where constraints are declared. Inline and
10
+ * in-CREATE constraints still apply primary-key-implied NOT NULL to columns
11
+ * before exclusion.
12
+ */
13
+ export declare function canonicalTableShape(node: AstNode): Record<string, unknown>;
14
+ /**
15
+ * A regclass cast's string literal is an identifier reference, and
16
+ * pg_get_expr renders it unquoted (`ai.seq`) while declarative sources often
17
+ * quote it (`"ai"."seq"`). Both name the same object, so the canonical shape
18
+ * stores the unquoted spelling.
19
+ */
20
+ export declare function canonicalizeRegclassLiterals(node: unknown): unknown;
21
+ /**
22
+ * Canonical sequence identity: option DefElems normalize to a keyed object
23
+ * with PostgreSQL's ascending defaults dropped, so a declarative
24
+ * `START 100 INCREMENT 5` and the catalog reconstruction hash identically
25
+ * regardless of option order or explicitly-spelled defaults.
26
+ */
27
+ export declare function canonicalSequenceShape(node: AstNode): Record<string, unknown>;
28
+ /**
29
+ * Canonical constraint identity shared by every declaration site: an
30
+ * ALTER TABLE ADD CONSTRAINT statement, an in-CREATE table-level constraint,
31
+ * an inline column constraint, and a catalog pg_constraint row must all hash
32
+ * to the same shape for the same table.
33
+ */
34
+ export declare function canonicalConstraintShape(constraint: AstNode, table: {
35
+ name: string;
36
+ schema: string;
37
+ }, impliedColumns?: string[]): Record<string, unknown>;
38
+ //# sourceMappingURL=table-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-shape.d.ts","sourceRoot":"","sources":["../../src/sql/table-shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAaxC,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAmB7D;AAQD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuC1E;AAwDD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAyBnE;AAkCD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkD7E;AAuBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,OAAO,EACnB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACvC,cAAc,GAAE,MAAM,EAAO,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB"}
@@ -0,0 +1,287 @@
1
+ import { asRecord, readArray, readNumber, readString, stringList, typeNameToSql } from "./ast.js";
2
+ import { stripLocations } from "./object-hash.js";
3
+ export function canonicalColumnType(typeName) {
4
+ const base = typeNameToSql(typeName);
5
+ const node = asRecord(asRecord(typeName)?.TypeName) ?? asRecord(typeName);
6
+ const typmods = readArray(node?.typmods)
7
+ .map((item) => {
8
+ const constant = asRecord(asRecord(item)?.A_Const);
9
+ const integer = asRecord(constant?.ival);
10
+ const value = readNumber(integer?.ival);
11
+ return value === undefined ? undefined : String(value);
12
+ })
13
+ .filter((value) => value !== undefined);
14
+ if (typmods.length === 0) {
15
+ return base;
16
+ }
17
+ const arrayStart = base.indexOf("[]");
18
+ if (arrayStart === -1) {
19
+ return `${base}(${typmods.join(", ")})`;
20
+ }
21
+ return `${base.slice(0, arrayStart)}(${typmods.join(", ")})${base.slice(arrayStart)}`;
22
+ }
23
+ /**
24
+ * Canonical table identity for hashing. The raw parse tree differs between a
25
+ * declarative source ("id bigint PRIMARY KEY") and a catalog reconstruction
26
+ * ('"id" bigint NOT NULL' plus a separate ADD CONSTRAINT), so the shape
27
+ * carries columns only: every table constraint — inline, in-CREATE, or
28
+ * ALTER-declared — is identity-owned by its own `constraint:` object, making
29
+ * table identity independent of where constraints are declared. Inline and
30
+ * in-CREATE constraints still apply primary-key-implied NOT NULL to columns
31
+ * before exclusion.
32
+ */
33
+ export function canonicalTableShape(node) {
34
+ const relation = asRecord(node.relation);
35
+ const columns = [];
36
+ const constraints = [];
37
+ for (const item of readArray(node.tableElts)) {
38
+ const columnDef = asRecord(asRecord(item)?.ColumnDef);
39
+ if (columnDef) {
40
+ columns.push(canonicalColumn(columnDef, constraints));
41
+ continue;
42
+ }
43
+ const constraint = asRecord(asRecord(item)?.Constraint);
44
+ if (constraint) {
45
+ constraints.push(canonicalConstraint(constraint, []));
46
+ }
47
+ }
48
+ const primaryColumns = new Set(constraints
49
+ .filter((constraint) => constraint.type === "CONSTR_PRIMARY")
50
+ .flatMap((constraint) => constraint.columns));
51
+ for (const column of columns) {
52
+ if (primaryColumns.has(column.name)) {
53
+ column.notNull = true;
54
+ }
55
+ }
56
+ const shape = {
57
+ columns,
58
+ relation: {
59
+ name: readString(relation?.relname) ?? "",
60
+ persistence: readString(relation?.relpersistence) ?? "p",
61
+ schema: readString(relation?.schemaname) ?? "public",
62
+ },
63
+ };
64
+ for (const semanticKey of ["inhRelations", "oncommit", "options", "partspec", "tablespacename"]) {
65
+ if (node[semanticKey] !== undefined && node[semanticKey] !== null) {
66
+ shape[semanticKey] = stripLocations(node[semanticKey]);
67
+ }
68
+ }
69
+ return shape;
70
+ }
71
+ function canonicalColumn(columnDef, constraints) {
72
+ const name = readString(columnDef.colname) ?? "";
73
+ const column = {
74
+ name,
75
+ notNull: false,
76
+ type: canonicalColumnType(columnDef.typeName),
77
+ };
78
+ for (const item of readArray(columnDef.constraints)) {
79
+ const constraint = asRecord(asRecord(item)?.Constraint);
80
+ const contype = readString(constraint?.contype);
81
+ if (!constraint || !contype) {
82
+ continue;
83
+ }
84
+ switch (contype) {
85
+ case "CONSTR_NOTNULL":
86
+ column.notNull = true;
87
+ break;
88
+ case "CONSTR_NULL":
89
+ break;
90
+ case "CONSTR_DEFAULT":
91
+ column.default = canonicalizeRegclassLiterals(stripLocations(unwrapColumnTypeCast(constraint.raw_expr, columnDef.typeName)));
92
+ break;
93
+ case "CONSTR_IDENTITY":
94
+ column.identity = readString(constraint.generated_when) ?? "a";
95
+ break;
96
+ case "CONSTR_GENERATED":
97
+ column.generated = stripLocations(constraint.raw_expr);
98
+ break;
99
+ default:
100
+ constraints.push(canonicalConstraint(constraint, [name]));
101
+ break;
102
+ }
103
+ }
104
+ return column;
105
+ }
106
+ /**
107
+ * pg_get_expr renders a column default as `'x'::type` while declarative
108
+ * sources usually write the bare literal; a cast to the column's own base
109
+ * type is implied, so it is dropped from the canonical default.
110
+ */
111
+ function unwrapColumnTypeCast(expression, columnTypeName) {
112
+ const typeCast = asRecord(asRecord(expression)?.TypeCast);
113
+ if (!typeCast) {
114
+ return expression;
115
+ }
116
+ if (typeNameToSql(typeCast.typeName) !== typeNameToSql(columnTypeName)) {
117
+ return expression;
118
+ }
119
+ return typeCast.arg;
120
+ }
121
+ /**
122
+ * A regclass cast's string literal is an identifier reference, and
123
+ * pg_get_expr renders it unquoted (`ai.seq`) while declarative sources often
124
+ * quote it (`"ai"."seq"`). Both name the same object, so the canonical shape
125
+ * stores the unquoted spelling.
126
+ */
127
+ export function canonicalizeRegclassLiterals(node) {
128
+ if (Array.isArray(node)) {
129
+ return node.map((item) => canonicalizeRegclassLiterals(item));
130
+ }
131
+ if (typeof node !== "object" || node === null) {
132
+ return node;
133
+ }
134
+ const record = node;
135
+ const typeCast = asRecord(record.TypeCast);
136
+ if (typeCast && typeNameToSql(typeCast.typeName) === "regclass") {
137
+ const constant = asRecord(asRecord(typeCast.arg)?.A_Const);
138
+ const sval = asRecord(constant?.sval);
139
+ const literal = readString(sval?.sval);
140
+ if (literal !== undefined) {
141
+ // The cast itself is dropped: `'x'::regclass` and bare `'x'` name the
142
+ // same object inside a default expression, and trees commonly omit the
143
+ // cast that pg_get_expr always renders.
144
+ return { A_Const: { ...constant, sval: { sval: unquoteQualifiedName(literal) } } };
145
+ }
146
+ }
147
+ const result = {};
148
+ for (const [key, value] of Object.entries(record)) {
149
+ result[key] = canonicalizeRegclassLiterals(value);
150
+ }
151
+ return result;
152
+ }
153
+ function unquoteQualifiedName(literal) {
154
+ const parts = [];
155
+ let current = "";
156
+ let quoted = false;
157
+ for (let index = 0; index < literal.length; index += 1) {
158
+ const char = literal[index];
159
+ if (char === '"') {
160
+ if (quoted && literal[index + 1] === '"') {
161
+ current += '"';
162
+ index += 1;
163
+ continue;
164
+ }
165
+ quoted = !quoted;
166
+ continue;
167
+ }
168
+ if (char === "." && !quoted) {
169
+ parts.push(current);
170
+ current = "";
171
+ continue;
172
+ }
173
+ current += char;
174
+ }
175
+ parts.push(current);
176
+ return parts.join(".");
177
+ }
178
+ const sequenceTypeMax = new Map([
179
+ ["bigint", "9223372036854775807"],
180
+ ["integer", "2147483647"],
181
+ ["smallint", "32767"],
182
+ ]);
183
+ /**
184
+ * Canonical sequence identity: option DefElems normalize to a keyed object
185
+ * with PostgreSQL's ascending defaults dropped, so a declarative
186
+ * `START 100 INCREMENT 5` and the catalog reconstruction hash identically
187
+ * regardless of option order or explicitly-spelled defaults.
188
+ */
189
+ export function canonicalSequenceShape(node) {
190
+ const sequence = asRecord(node.sequence);
191
+ const shape = {
192
+ relation: {
193
+ name: readString(sequence?.relname) ?? "",
194
+ persistence: readString(sequence?.relpersistence) ?? "p",
195
+ schema: readString(sequence?.schemaname) ?? "public",
196
+ },
197
+ };
198
+ let dataType = "bigint";
199
+ const options = new Map();
200
+ for (const item of readArray(node.options)) {
201
+ const defElem = asRecord(asRecord(item)?.DefElem);
202
+ const name = readString(defElem?.defname);
203
+ if (!name) {
204
+ continue;
205
+ }
206
+ options.set(name, defElem?.arg);
207
+ }
208
+ const asType = options.get("as");
209
+ if (asType !== undefined) {
210
+ dataType = typeNameToSql(asType);
211
+ }
212
+ if (dataType !== "bigint") {
213
+ shape.as = dataType;
214
+ }
215
+ const defaults = new Map([
216
+ ["cache", "1"],
217
+ ["increment", "1"],
218
+ ["maxvalue", sequenceTypeMax.get(dataType) ?? ""],
219
+ ["minvalue", "1"],
220
+ ["start", "1"],
221
+ ]);
222
+ for (const [name, fallback] of defaults) {
223
+ const value = sequenceOptionValue(options.get(name));
224
+ if (value !== undefined && value !== fallback) {
225
+ shape[name] = value;
226
+ }
227
+ }
228
+ if (sequenceOptionValue(options.get("cycle")) === "true") {
229
+ shape.cycle = true;
230
+ }
231
+ const ownedBy = options.get("owned_by");
232
+ if (ownedBy !== undefined) {
233
+ const path = stringList(ownedBy);
234
+ if (path.length > 0 && path.at(-1) !== "none") {
235
+ shape.ownedBy = path.join(".");
236
+ }
237
+ }
238
+ return shape;
239
+ }
240
+ function sequenceOptionValue(arg) {
241
+ if (arg === undefined || arg === null) {
242
+ return undefined;
243
+ }
244
+ const integer = asRecord(asRecord(arg)?.Integer);
245
+ if (integer) {
246
+ return String(readNumber(integer.ival) ?? 0);
247
+ }
248
+ const float = asRecord(asRecord(arg)?.Float);
249
+ if (float) {
250
+ return readString(float.fval);
251
+ }
252
+ const boolean = asRecord(asRecord(arg)?.Boolean);
253
+ if (boolean) {
254
+ return boolean.boolval === true ? "true" : "false";
255
+ }
256
+ return readString(asRecord(asRecord(arg)?.String)?.sval);
257
+ }
258
+ const constraintIdentityKeys = new Set(["conname", "contype", "fk_attrs", "keys", "location"]);
259
+ /**
260
+ * Canonical constraint identity shared by every declaration site: an
261
+ * ALTER TABLE ADD CONSTRAINT statement, an in-CREATE table-level constraint,
262
+ * an inline column constraint, and a catalog pg_constraint row must all hash
263
+ * to the same shape for the same table.
264
+ */
265
+ export function canonicalConstraintShape(constraint, table, impliedColumns = []) {
266
+ return {
267
+ constraint: canonicalConstraint(constraint, impliedColumns),
268
+ table,
269
+ };
270
+ }
271
+ function canonicalConstraint(constraint, impliedColumns) {
272
+ const keys = stringList(constraint.keys);
273
+ const fkAttrs = stringList(constraint.fk_attrs);
274
+ const columns = fkAttrs.length > 0 ? fkAttrs : keys.length > 0 ? keys : impliedColumns;
275
+ const payload = {};
276
+ for (const [key, value] of Object.entries(constraint)) {
277
+ if (constraintIdentityKeys.has(key)) {
278
+ continue;
279
+ }
280
+ payload[key] = stripLocations(value);
281
+ }
282
+ return {
283
+ columns: [...columns],
284
+ payload,
285
+ type: readString(constraint.contype) ?? "",
286
+ };
287
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Diagnostic, SupaschemaConfig } from "./core.js";
2
+ export interface SyncOptions {
3
+ config?: Partial<SupaschemaConfig>;
4
+ databaseUrl?: string;
5
+ directory: string;
6
+ /** Apply pending migrations to the target via `supabase migration up`. */
7
+ local?: boolean;
8
+ /** Push pending migrations to the linked project via `supabase db push`. */
9
+ remote?: boolean;
10
+ }
11
+ export interface SyncResult {
12
+ applied: boolean;
13
+ diagnostics: Diagnostic[];
14
+ pending: string[];
15
+ report: string;
16
+ }
17
+ /**
18
+ * Auto-sync orchestration: supaschema gates, the Supabase CLI applies. Every
19
+ * pending migration must pass the static replay-safety check before any
20
+ * runner executes; ghost or out-of-order history refuses outright; and
21
+ * nothing touches a database unless `local`/`remote` was explicitly chosen —
22
+ * the default is a dry run that prints exactly what would execute. History
23
+ * stays runner-owned: supaschema never writes
24
+ * supabase_migrations.schema_migrations itself.
25
+ */
26
+ export declare function syncMigrations(options: SyncOptions): Promise<SyncResult>;
27
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAI9D,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAoE9E"}