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,201 @@
1
+ import { objectKey } from "./sql/identifiers.js";
2
+ export function realisticFixtureManifest(tableCount) {
3
+ const entries = [
4
+ { change: "create", key: objectKey({ kind: "table", name: "audit_events", schema: "app" }) },
5
+ {
6
+ change: "create",
7
+ key: objectKey({
8
+ kind: "constraint",
9
+ name: "audit_events_pkey",
10
+ schema: "app",
11
+ table: "audit_events",
12
+ }),
13
+ },
14
+ {
15
+ change: "create",
16
+ key: objectKey({
17
+ kind: "constraint",
18
+ name: "audit_events_tenant_id_fkey",
19
+ schema: "app",
20
+ table: "audit_events",
21
+ }),
22
+ },
23
+ {
24
+ change: "create",
25
+ key: objectKey({
26
+ kind: "index",
27
+ name: "audit_events_tenant_id_idx",
28
+ schema: "app",
29
+ table: "audit_events",
30
+ }),
31
+ },
32
+ {
33
+ change: "create",
34
+ key: objectKey({
35
+ kind: "rls",
36
+ name: "audit_events",
37
+ schema: "app",
38
+ table: "audit_events",
39
+ }),
40
+ },
41
+ {
42
+ change: "create",
43
+ key: objectKey({
44
+ kind: "policy",
45
+ name: "audit_events_select",
46
+ schema: "app",
47
+ table: "audit_events",
48
+ }),
49
+ },
50
+ { change: "change", key: objectKey({ kind: "enum", name: "entity_status", schema: "app" }) },
51
+ {
52
+ change: "create",
53
+ key: objectKey({
54
+ kind: "index",
55
+ name: "entity_001_active_name_idx",
56
+ schema: "app",
57
+ table: "entity_001",
58
+ }),
59
+ },
60
+ {
61
+ change: "change",
62
+ key: objectKey({
63
+ kind: "policy",
64
+ name: "entity_002_select",
65
+ schema: "app",
66
+ table: "entity_002",
67
+ }),
68
+ },
69
+ { change: "change", key: objectKey({ kind: "view", name: "entity_005_names", schema: "app" }) },
70
+ {
71
+ change: "change",
72
+ key: objectKey({
73
+ kind: "function",
74
+ name: "entity_009_count",
75
+ schema: "app",
76
+ signature: "bigint",
77
+ }),
78
+ },
79
+ ];
80
+ for (let index = 1; index < tableCount; index += 1) {
81
+ if (index % 3 === 1) {
82
+ const table = `entity_${String(index).padStart(3, "0")}`;
83
+ entries.push({
84
+ change: "change",
85
+ key: objectKey({ kind: "table", name: table, schema: "app" }),
86
+ });
87
+ }
88
+ }
89
+ return entries;
90
+ }
91
+ export function makeRealisticSqlFixture(tableCount) {
92
+ if (tableCount < 12) {
93
+ throw new Error("realistic fixture needs at least 12 tables");
94
+ }
95
+ const preamble = (enumValues) => [
96
+ "CREATE SCHEMA app;",
97
+ "CREATE EXTENSION pgcrypto WITH SCHEMA public;",
98
+ "CREATE EXTENSION pg_trgm WITH SCHEMA public;",
99
+ `CREATE TYPE app.entity_status AS ENUM (${enumValues.map((value) => `'${value}'`).join(", ")});`,
100
+ "CREATE DOMAIN app.email_address AS text CHECK (VALUE ~ '@');",
101
+ `CREATE FUNCTION app.set_updated_at()
102
+ RETURNS trigger
103
+ LANGUAGE plpgsql
104
+ AS $$
105
+ BEGIN
106
+ NEW.updated_at := now();
107
+ RETURN NEW;
108
+ END
109
+ $$;`,
110
+ `CREATE TABLE app.tenants (
111
+ id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
112
+ name text NOT NULL,
113
+ status app.entity_status DEFAULT 'active'::app.entity_status NOT NULL,
114
+ contact_email app.email_address,
115
+ created_at timestamptz DEFAULT now() NOT NULL,
116
+ updated_at timestamptz DEFAULT now() NOT NULL
117
+ );`,
118
+ "CREATE TRIGGER tenants_updated_at BEFORE UPDATE ON app.tenants FOR EACH ROW EXECUTE FUNCTION app.set_updated_at();",
119
+ ];
120
+ const from = preamble(["active", "inactive"]);
121
+ const to = preamble(["active", "inactive", "archived"]);
122
+ for (let index = 1; index < tableCount; index += 1) {
123
+ from.push(...entityStatements(index, false));
124
+ to.push(...entityStatements(index, true));
125
+ }
126
+ to.push(`CREATE TABLE app.audit_events (
127
+ id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
128
+ tenant_id bigint NOT NULL,
129
+ action text NOT NULL,
130
+ recorded_at timestamptz DEFAULT now() NOT NULL
131
+ );`, "ALTER TABLE app.audit_events ADD CONSTRAINT audit_events_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES app.tenants (id);", "CREATE INDEX audit_events_tenant_id_idx ON app.audit_events (tenant_id);", "ALTER TABLE app.audit_events ENABLE ROW LEVEL SECURITY;", `CREATE POLICY audit_events_select ON app.audit_events FOR SELECT TO public USING (${tenantPredicate()});`);
132
+ return { from: from.join("\n\n"), to: to.join("\n\n") };
133
+ }
134
+ function entityStatements(index, mutated) {
135
+ const table = `entity_${String(index).padStart(3, "0")}`;
136
+ const qualified = `app.${table}`;
137
+ // Added columns go last so ALTER TABLE ADD COLUMN reproduces the same
138
+ // catalog column order as applying the target tree directly.
139
+ const externalRef = mutated && index % 3 === 1 ? ",\n external_ref text DEFAULT ''::text NOT NULL" : "";
140
+ const statements = [
141
+ `CREATE TABLE ${qualified} (
142
+ id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
143
+ tenant_id bigint NOT NULL,
144
+ name text NOT NULL,
145
+ status app.entity_status DEFAULT 'active'::app.entity_status NOT NULL,
146
+ payload jsonb DEFAULT '{}'::jsonb NOT NULL,
147
+ created_at timestamptz DEFAULT now() NOT NULL,
148
+ updated_at timestamptz DEFAULT now() NOT NULL${externalRef}
149
+ );`,
150
+ `ALTER TABLE ${qualified} ADD CONSTRAINT ${table}_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES app.tenants (id);`,
151
+ `CREATE INDEX ${table}_tenant_id_idx ON ${qualified} (tenant_id);`,
152
+ `ALTER TABLE ${qualified} ENABLE ROW LEVEL SECURITY;`,
153
+ `CREATE POLICY ${table}_select ON ${qualified} FOR SELECT TO public USING (${selectPredicate(index, mutated)});`,
154
+ `CREATE POLICY ${table}_insert ON ${qualified} FOR INSERT TO public WITH CHECK (${tenantPredicate()});`,
155
+ `CREATE TRIGGER ${table}_updated_at BEFORE UPDATE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION app.set_updated_at();`,
156
+ ];
157
+ if (mutated && index === 1) {
158
+ statements.push(`CREATE INDEX ${table}_active_name_idx ON ${qualified} (name) WHERE status = 'active'::app.entity_status;`);
159
+ }
160
+ if (index % 5 === 0) {
161
+ statements.push(`GRANT SELECT ON ${qualified} TO PUBLIC;`);
162
+ }
163
+ if (index % 10 === 5) {
164
+ const statusColumn = mutated && index === 5 ? ",\n e.status" : "";
165
+ statements.push(`CREATE VIEW app.${table}_names AS
166
+ SELECT e.id,
167
+ e.name,
168
+ t.name AS tenant_name${statusColumn}
169
+ FROM ${qualified} e
170
+ JOIN app.tenants t ON t.id = e.tenant_id;`);
171
+ }
172
+ if (index % 25 === 7) {
173
+ statements.push(`CREATE MATERIALIZED VIEW app.${table}_stats AS SELECT count(*) AS total FROM ${qualified};`);
174
+ }
175
+ if (index % 20 === 9) {
176
+ const body = mutated && index === 9
177
+ ? `SELECT count(id) FROM ${qualified} WHERE tenant_id = target_tenant`
178
+ : `SELECT count(*) FROM ${qualified} WHERE tenant_id = target_tenant`;
179
+ statements.push(`CREATE FUNCTION app.${table}_count(target_tenant bigint)
180
+ RETURNS bigint
181
+ LANGUAGE sql
182
+ STABLE
183
+ AS $$
184
+ ${body}
185
+ $$;`);
186
+ }
187
+ if (index % 15 === 3) {
188
+ const suffix = mutated && index === 3 ? " (audited)" : "";
189
+ statements.push(`COMMENT ON TABLE ${qualified} IS 'Entity ${table} records${suffix}';`);
190
+ }
191
+ return statements;
192
+ }
193
+ function selectPredicate(index, mutated) {
194
+ if (mutated && index === 2) {
195
+ return `${tenantPredicate()} AND status <> 'inactive'::app.entity_status`;
196
+ }
197
+ return tenantPredicate();
198
+ }
199
+ function tenantPredicate() {
200
+ return "tenant_id = (current_setting('app.tenant_id', true))::bigint";
201
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=benchmark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark.d.ts","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":""}
@@ -0,0 +1,308 @@
1
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { extname, join } from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { makeRealisticSqlFixture } from "./benchmark-fixtures.js";
6
+ import { resolveDatabaseUrl } from "./database-url.js";
7
+ import { applyMigrationSql, applySql, databasePair, withTemporaryDatabases } from "./db-admin.js";
8
+ import { formatDiagnostics } from "./diagnostics.js";
9
+ import { fingerprintObjects } from "./hash.js";
10
+ import { planSchemaDiff } from "./planner.js";
11
+ import { renderMigration } from "./render.js";
12
+ import { extractSourceModel } from "./source.js";
13
+ import { extractObjectsFromSql } from "./sql/extract.js";
14
+ import { verifyMigration } from "./verify.js";
15
+ const fastIterations = Number(process.env.SUPASCHEMA_BENCHMARK_ITERATIONS ?? "5");
16
+ const databaseIterations = Number(process.env.SUPASCHEMA_DATABASE_BENCHMARK_ITERATIONS ?? "3");
17
+ const xlTables = numberEnv("SUPASCHEMA_XL_TABLES", 1000);
18
+ const xxlTables = numberEnv("SUPASCHEMA_XXL_TABLES", 2500);
19
+ const thresholds = {
20
+ catalogSnapshotDiff: numberEnv("SUPASCHEMA_CATALOG_BENCHMARK_MS", 2000),
21
+ dumpDiff: numberEnv("SUPASCHEMA_DUMP_BENCHMARK_MS", 2000),
22
+ endToEndMigration: numberEnv("SUPASCHEMA_END_TO_END_BENCHMARK_MS", 10000),
23
+ endToEndMigrationLarge: numberEnv("SUPASCHEMA_END_TO_END_LARGE_BENCHMARK_MS", 60000),
24
+ endToEndMigrationXl: numberEnv("SUPASCHEMA_END_TO_END_XL_BENCHMARK_MS", 120000),
25
+ endToEndMigrationXxl: numberEnv("SUPASCHEMA_END_TO_END_XXL_BENCHMARK_MS", 300000),
26
+ largeInMemoryDiff: numberEnv("SUPASCHEMA_LARGE_BENCHMARK_MS", 10000),
27
+ liveCatalogDiff: numberEnv("SUPASCHEMA_LIVE_CATALOG_BENCHMARK_MS", 10000),
28
+ liveCatalogDiffXl: numberEnv("SUPASCHEMA_LIVE_CATALOG_XL_BENCHMARK_MS", 60000),
29
+ liveCatalogDiffXxl: numberEnv("SUPASCHEMA_LIVE_CATALOG_XXL_BENCHMARK_MS", 120000),
30
+ noDriftDiff: numberEnv("SUPASCHEMA_NO_DRIFT_BENCHMARK_MS", 10000),
31
+ realisticTreeDiff: numberEnv("SUPASCHEMA_REALISTIC_BENCHMARK_MS", 10000),
32
+ replayVerification: numberEnv("SUPASCHEMA_VERIFY_BENCHMARK_MS", 30000),
33
+ shadowRoundTripDiff: numberEnv("SUPASCHEMA_SHADOW_BENCHMARK_MS", 30000),
34
+ sourceTreeDiff: numberEnv("SUPASCHEMA_BENCHMARK_MS", 2000),
35
+ };
36
+ const tempRoot = await mkdtemp(join(tmpdir(), "supaschema-benchmark-"));
37
+ try {
38
+ const basicSources = {
39
+ from: "dir:tests/fixtures/basic/from",
40
+ to: "dir:tests/fixtures/basic/to",
41
+ };
42
+ const addColumnSources = {
43
+ from: "dir:tests/fixtures/add-column/from",
44
+ to: "dir:tests/fixtures/add-column/to",
45
+ };
46
+ const basicSql = {
47
+ from: await readSqlDirectory("tests/fixtures/basic/from"),
48
+ to: await readSqlDirectory("tests/fixtures/basic/to"),
49
+ };
50
+ const addColumnSql = {
51
+ from: await readSqlDirectory("tests/fixtures/add-column/from"),
52
+ to: await readSqlDirectory("tests/fixtures/add-column/to"),
53
+ };
54
+ const dumpSources = await writeDumpSources(basicSql);
55
+ const catalogSources = await writeCatalogSources(basicSources);
56
+ const realisticSql = makeRealisticSqlFixture(60);
57
+ const largeSql = makeRealisticSqlFixture(250);
58
+ const xlSql = makeRealisticSqlFixture(xlTables);
59
+ const xxlSql = makeRealisticSqlFixture(xxlTables);
60
+ const realisticTreeSources = await writeTreeSources("realistic", realisticSql);
61
+ const largeTreeSources = await writeTreeSources("large", largeSql);
62
+ const xlTreeSources = await writeTreeSources("xl", xlSql);
63
+ const xxlTreeSources = await writeTreeSources("xxl", xxlSql);
64
+ const databaseUrl = process.env.SUPASCHEMA_BENCHMARK_DATABASE_URL ?? resolveDatabaseUrl();
65
+ const results = {
66
+ catalogSnapshotDiff: await benchmarkDiffSources("catalogSnapshotDiff", thresholds.catalogSnapshotDiff, fastIterations, catalogSources),
67
+ database: {
68
+ endToEndMigration: await benchmarkEndToEndMigration("endToEndMigration", thresholds.endToEndMigration, databaseUrl, addColumnSources, addColumnSql),
69
+ endToEndMigrationLarge: await benchmarkEndToEndMigration("endToEndMigrationLarge", thresholds.endToEndMigrationLarge, databaseUrl, largeTreeSources, largeSql),
70
+ endToEndMigrationXl: await benchmarkEndToEndMigration("endToEndMigrationXl", thresholds.endToEndMigrationXl, databaseUrl, xlTreeSources, xlSql),
71
+ endToEndMigrationXxl: await benchmarkEndToEndMigration("endToEndMigrationXxl", thresholds.endToEndMigrationXxl, databaseUrl, xxlTreeSources, xxlSql),
72
+ liveCatalogDiff: await benchmarkLiveCatalogDiff("liveCatalogDiff", thresholds.liveCatalogDiff, databaseUrl, realisticSql),
73
+ liveCatalogDiffXl: await benchmarkLiveCatalogDiff("liveCatalogDiffXl", thresholds.liveCatalogDiffXl, databaseUrl, xlSql),
74
+ liveCatalogDiffXxl: await benchmarkLiveCatalogDiff("liveCatalogDiffXxl", thresholds.liveCatalogDiffXxl, databaseUrl, xxlSql),
75
+ replayVerification: await benchmarkReplayVerification(databaseUrl, addColumnSources),
76
+ shadowRoundTripDiff: await benchmarkShadowRoundTripDiff(databaseUrl, realisticSql),
77
+ },
78
+ dumpDiff: await benchmarkDiffSources("dumpDiff", thresholds.dumpDiff, fastIterations, dumpSources),
79
+ largeInMemoryDiff: await runBenchmark("largeInMemoryDiff", thresholds.largeInMemoryDiff, fastIterations, async () => {
80
+ const from = await modelFromSql("large:from", largeSql.from);
81
+ const to = await modelFromSql("large:to", largeSql.to);
82
+ diffModels("largeInMemoryDiff", from, to);
83
+ }),
84
+ noDriftDiff: await runBenchmark("noDriftDiff", thresholds.noDriftDiff, fastIterations, async () => {
85
+ const from = await extractSourceModel(realisticTreeSources.from);
86
+ const to = await extractSourceModel(realisticTreeSources.from);
87
+ const plan = diffModels("noDriftDiff", from, to);
88
+ if (plan.operations.length > 0) {
89
+ throw new Error(`noDriftDiff expected an empty plan for identical sources, found ${plan.operations.length} operations`);
90
+ }
91
+ }),
92
+ realisticTreeDiff: await benchmarkDiffSources("realisticTreeDiff", thresholds.realisticTreeDiff, fastIterations, realisticTreeSources),
93
+ sourceTreeDiff: await benchmarkDiffSources("sourceTreeDiff", thresholds.sourceTreeDiff, fastIterations, basicSources),
94
+ };
95
+ process.stdout.write(JSON.stringify(results, null, 2));
96
+ process.stdout.write("\n");
97
+ }
98
+ finally {
99
+ await rm(tempRoot, { force: true, recursive: true });
100
+ }
101
+ async function benchmarkDiffSources(name, thresholdMs, iterations, sources) {
102
+ return runBenchmark(name, thresholdMs, iterations, async () => {
103
+ const from = await extractSourceModel(sources.from);
104
+ const to = await extractSourceModel(sources.to);
105
+ diffModels(name, from, to);
106
+ });
107
+ }
108
+ async function benchmarkLiveCatalogDiff(name, thresholdMs, databaseUrl, sql) {
109
+ if (!databaseUrl) {
110
+ return skippedDatabaseBenchmark(thresholdMs);
111
+ }
112
+ return withTemporaryDatabases(databaseUrl, 2, async (databaseUrls) => {
113
+ const [fromUrl, toUrl] = databasePair(databaseUrls);
114
+ await applySql(fromUrl, sql.from);
115
+ await applySql(toUrl, sql.to);
116
+ return benchmarkDiffSources(name, thresholdMs, databaseIterations, {
117
+ from: `database:${fromUrl}`,
118
+ to: `database:${toUrl}`,
119
+ });
120
+ });
121
+ }
122
+ async function benchmarkReplayVerification(databaseUrl, sources) {
123
+ if (!databaseUrl) {
124
+ return skippedDatabaseBenchmark(thresholds.replayVerification);
125
+ }
126
+ const migrationPath = join(tempRoot, "add-column.sql");
127
+ const from = await extractSourceModel(sources.from);
128
+ const to = await extractSourceModel(sources.to);
129
+ const plan = diffModels("replayVerification:plan", from, to);
130
+ await writeFile(migrationPath, renderMigration(plan), "utf8");
131
+ return runBenchmark("replayVerification", thresholds.replayVerification, databaseIterations, async () => {
132
+ const diagnostics = await verifyMigration({
133
+ databaseUrl,
134
+ from: sources.from,
135
+ migrationPath,
136
+ to: sources.to,
137
+ });
138
+ assertNoErrors("replayVerification", diagnostics);
139
+ });
140
+ }
141
+ async function benchmarkEndToEndMigration(name, thresholdMs, databaseUrl, sources, sql) {
142
+ if (!databaseUrl) {
143
+ return skippedDatabaseBenchmark(thresholdMs);
144
+ }
145
+ return withTemporaryDatabases(databaseUrl, 1, async (targetUrls) => {
146
+ const [targetUrl] = targetUrls;
147
+ if (!targetUrl) {
148
+ throw new Error("expected a temporary target database");
149
+ }
150
+ await applySql(targetUrl, sql.to);
151
+ const target = await extractSourceModel(`database:${targetUrl}`);
152
+ assertNoErrors(`${name}:target`, target.diagnostics);
153
+ return runMeasuredBenchmark(name, thresholdMs, databaseIterations, async () => withTemporaryDatabases(databaseUrl, 1, async (migrationUrls) => {
154
+ const [migrationUrl] = migrationUrls;
155
+ if (!migrationUrl) {
156
+ throw new Error("expected a temporary migration database");
157
+ }
158
+ await applySql(migrationUrl, sql.from);
159
+ const started = performance.now();
160
+ const from = await extractSourceModel(sources.from);
161
+ const to = await extractSourceModel(sources.to);
162
+ assertNoErrors(`${name}:from`, from.diagnostics);
163
+ assertNoErrors(`${name}:to`, to.diagnostics);
164
+ const plan = planSchemaDiff(from, to);
165
+ assertNoErrors(`${name}:plan`, plan.diagnostics);
166
+ await applyMigrationSql(migrationUrl, renderMigration(plan));
167
+ const elapsedMs = performance.now() - started;
168
+ const applied = await extractSourceModel(`database:${migrationUrl}`);
169
+ assertNoErrors(`${name}:applied`, applied.diagnostics);
170
+ if (applied.fingerprint !== target.fingerprint) {
171
+ throw new Error(`${name} migrated catalog fingerprint does not match target: migration=${applied.fingerprint} target=${target.fingerprint}`);
172
+ }
173
+ return elapsedMs;
174
+ }));
175
+ });
176
+ }
177
+ async function benchmarkShadowRoundTripDiff(databaseUrl, sql) {
178
+ if (!databaseUrl) {
179
+ return skippedDatabaseBenchmark(thresholds.shadowRoundTripDiff);
180
+ }
181
+ return runBenchmark("shadowRoundTripDiff", thresholds.shadowRoundTripDiff, databaseIterations, async () => {
182
+ await withTemporaryDatabases(databaseUrl, 2, async (databaseUrls) => {
183
+ const [fromUrl, toUrl] = databasePair(databaseUrls);
184
+ await applySql(fromUrl, sql.from);
185
+ await applySql(toUrl, sql.to);
186
+ const from = await extractSourceModel(`database:${fromUrl}`);
187
+ const to = await extractSourceModel(`database:${toUrl}`);
188
+ diffModels("shadowRoundTripDiff", from, to);
189
+ });
190
+ });
191
+ }
192
+ async function runBenchmark(name, thresholdMs, iterations, action) {
193
+ return runMeasuredBenchmark(name, thresholdMs, iterations, async () => {
194
+ const start = performance.now();
195
+ await action();
196
+ return performance.now() - start;
197
+ });
198
+ }
199
+ async function runMeasuredBenchmark(name, thresholdMs, iterations, measure) {
200
+ const timings = [];
201
+ for (let index = 0; index < iterations; index += 1) {
202
+ timings.push(await measure());
203
+ }
204
+ const warm = timings.slice(1);
205
+ const maxWarm = Math.max(...(warm.length > 0 ? warm : timings));
206
+ if (maxWarm > thresholdMs) {
207
+ throw new Error(`${name} warm benchmark exceeded ${thresholdMs}ms`);
208
+ }
209
+ return {
210
+ maxWarmMs: Math.round(maxWarm),
211
+ status: "passed",
212
+ thresholdMs,
213
+ timingsMs: timings.map((value) => Math.round(value)),
214
+ };
215
+ }
216
+ function diffModels(name, from, to) {
217
+ assertNoErrors(`${name}:from`, from.diagnostics);
218
+ assertNoErrors(`${name}:to`, to.diagnostics);
219
+ const plan = planSchemaDiff(from, to);
220
+ assertNoErrors(`${name}:plan`, plan.diagnostics);
221
+ renderMigration(plan);
222
+ return plan;
223
+ }
224
+ async function modelFromSql(source, sql) {
225
+ const extracted = await extractObjectsFromSql(sql, {
226
+ config: { adapter: "postgres" },
227
+ file: `${source}.sql`,
228
+ });
229
+ return {
230
+ diagnostics: extracted.diagnostics,
231
+ fingerprint: fingerprintObjects(extracted.objects),
232
+ objects: extracted.objects,
233
+ source,
234
+ };
235
+ }
236
+ async function writeDumpSources(sql) {
237
+ const fromPath = join(tempRoot, "from.dump.sql");
238
+ const toPath = join(tempRoot, "to.dump.sql");
239
+ await writeFile(fromPath, sql.from, "utf8");
240
+ await writeFile(toPath, sql.to, "utf8");
241
+ return {
242
+ from: `dump:${fromPath}`,
243
+ to: `dump:${toPath}`,
244
+ };
245
+ }
246
+ async function writeTreeSources(label, sql) {
247
+ const fromDirectory = join(tempRoot, `${label}-from`);
248
+ const toDirectory = join(tempRoot, `${label}-to`);
249
+ await mkdir(fromDirectory, { recursive: true });
250
+ await mkdir(toDirectory, { recursive: true });
251
+ await writeFile(join(fromDirectory, "001_app.sql"), sql.from, "utf8");
252
+ await writeFile(join(toDirectory, "001_app.sql"), sql.to, "utf8");
253
+ return {
254
+ from: `dir:${fromDirectory}`,
255
+ to: `dir:${toDirectory}`,
256
+ };
257
+ }
258
+ async function writeCatalogSources(sources) {
259
+ const from = await extractSourceModel(sources.from);
260
+ const to = await extractSourceModel(sources.to);
261
+ const fromPath = join(tempRoot, "from.catalog.json");
262
+ const toPath = join(tempRoot, "to.catalog.json");
263
+ await writeFile(fromPath, `${JSON.stringify(from, null, 2)}\n`, "utf8");
264
+ await writeFile(toPath, `${JSON.stringify(to, null, 2)}\n`, "utf8");
265
+ return {
266
+ from: `catalog:${fromPath}`,
267
+ to: `catalog:${toPath}`,
268
+ };
269
+ }
270
+ async function readSqlDirectory(root) {
271
+ const files = [];
272
+ async function walk(directory) {
273
+ const entries = await readdir(directory, { withFileTypes: true });
274
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
275
+ const path = join(directory, entry.name);
276
+ if (entry.isDirectory()) {
277
+ await walk(path);
278
+ continue;
279
+ }
280
+ if (entry.isFile() && extname(entry.name) === ".sql") {
281
+ files.push(path);
282
+ }
283
+ }
284
+ }
285
+ await walk(root);
286
+ const chunks = [];
287
+ for (const file of files.sort()) {
288
+ chunks.push(await readFile(file, "utf8"));
289
+ }
290
+ return chunks.join("\n\n");
291
+ }
292
+ function assertNoErrors(name, diagnostics) {
293
+ const errors = diagnostics.filter((item) => item.severity === "error");
294
+ if (errors.length > 0) {
295
+ throw new Error(`${name} produced diagnostics:\n${formatDiagnostics(errors)}`);
296
+ }
297
+ }
298
+ function skippedDatabaseBenchmark(thresholdMs) {
299
+ return {
300
+ reason: "set SUPASCHEMA_BENCHMARK_DATABASE_URL to a disposable PostgreSQL admin URL",
301
+ status: "skipped",
302
+ thresholdMs,
303
+ };
304
+ }
305
+ function numberEnv(name, fallback) {
306
+ const raw = process.env[name];
307
+ return raw === undefined ? fallback : Number(raw);
308
+ }
@@ -0,0 +1,9 @@
1
+ import type { SchemaObject } from "./core.js";
2
+ type CatalogQuery = {
3
+ query: <Row extends Record<string, unknown>>(text: string, values?: unknown[]) => Promise<{
4
+ rows: Row[];
5
+ }>;
6
+ };
7
+ export declare function collectComments(pool: CatalogQuery): Promise<SchemaObject[]>;
8
+ export {};
9
+ //# sourceMappingURL=catalog-comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-comments.d.ts","sourceRoot":"","sources":["../src/catalog-comments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAK9C,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CAC/B,CAAC;AAgBF,wBAAsB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAYjF"}