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,420 @@
1
+ import { diagnostic } from "./diagnostics.js";
2
+ import { finalizeObject } from "./sql/facts.js";
3
+ import { shapeHash, stripLocations } from "./sql/object-hash.js";
4
+ import { buildDefaultPrivilegeObject, buildGrantObject, builtinPublicDefault, isBuiltinDefaultGrant, } from "./sql/privileges.js";
5
+ import { makeObject } from "./sql/statements.js";
6
+ import { canonicalizeRegclassLiterals } from "./sql/table-shape.js";
7
+ export async function normalizeSourceObjects(objects, diagnostics, options = {}) {
8
+ const afterDefaults = applyColumnDefaultAmendments(objects, diagnostics);
9
+ const afterOwnedBy = applySequenceOwnedByAmendments(afterDefaults, diagnostics);
10
+ const afterRls = await mergeRlsFacets(afterOwnedBy, options);
11
+ const merged = await mergeSplitPrivileges(afterRls, options);
12
+ return suppressDefaultAclImpliedGrants(suppressDefaultEqualPrivileges(merged));
13
+ }
14
+ const kindPhraseToDefaultObjectType = new Map([
15
+ ["DOMAIN", "TYPES"],
16
+ ["FUNCTION", "FUNCTIONS"],
17
+ ["PROCEDURE", "FUNCTIONS"],
18
+ ["SCHEMA", "SCHEMAS"],
19
+ ["SEQUENCE", "SEQUENCES"],
20
+ ["TABLE", "TABLES"],
21
+ ["TYPE", "TYPES"],
22
+ ]);
23
+ /**
24
+ * Postgres applies in-model ALTER DEFAULT PRIVILEGES to every later object,
25
+ * and the resulting ACL entry is indistinguishable from an explicit GRANT.
26
+ * A grant fully implied by an in-model default-privilege entry is therefore
27
+ * suppressed on BOTH lanes (catalog models route through this too): trees
28
+ * declare the default once, catalogs materialize it per object.
29
+ */
30
+ export function suppressDefaultAclImpliedGrants(objects) {
31
+ const defaults = new Map();
32
+ for (const object of objects) {
33
+ if (object.ref.kind !== "default-privilege" || object.metadata.verb !== "GRANT") {
34
+ continue;
35
+ }
36
+ const key = [
37
+ String(object.metadata.objectType ?? ""),
38
+ String(object.metadata.schema ?? ""),
39
+ String(object.metadata.grantee ?? ""),
40
+ ].join("|");
41
+ const privileges = defaults.get(key) ?? new Set();
42
+ for (const privilege of Array.isArray(object.metadata.privileges)
43
+ ? object.metadata.privileges
44
+ : []) {
45
+ privileges.add(privilege);
46
+ }
47
+ defaults.set(key, privileges);
48
+ }
49
+ if (defaults.size === 0) {
50
+ return objects;
51
+ }
52
+ return objects.filter((object) => {
53
+ if (object.ref.kind !== "grant" || object.metadata.verb !== "GRANT") {
54
+ return true;
55
+ }
56
+ const objectType = kindPhraseToDefaultObjectType.get(String(object.metadata.kindPhrase ?? ""));
57
+ if (!objectType) {
58
+ return true;
59
+ }
60
+ const implied = defaults.get([objectType, String(object.ref.schema ?? ""), String(object.metadata.grantee ?? "")].join("|"));
61
+ if (!implied) {
62
+ return true;
63
+ }
64
+ const privileges = Array.isArray(object.metadata.privileges)
65
+ ? object.metadata.privileges
66
+ : [];
67
+ return !privileges.every((privilege) => implied.has(privilege) || implied.has("ALL"));
68
+ });
69
+ }
70
+ /**
71
+ * acldefault delta on the source lane, mirroring the catalog lane: a GRANT
72
+ * that restates PostgreSQL's built-in default (PUBLIC EXECUTE on routines,
73
+ * PUBLIC USAGE on types) and a REVOKE aimed at a grantee that holds neither
74
+ * a built-in default nor a granted privilege in this model are semantic
75
+ * no-ops the catalog can never reproduce; keeping them would be permanent
76
+ * false drift.
77
+ */
78
+ function suppressDefaultEqualPrivileges(objects) {
79
+ const grantsByTarget = new Map();
80
+ for (const object of objects) {
81
+ if ((object.ref.kind === "grant" || object.ref.kind === "default-privilege") &&
82
+ object.metadata.verb === "GRANT") {
83
+ const key = privilegeTargetKey(object);
84
+ const group = grantsByTarget.get(key) ?? [];
85
+ group.push(object);
86
+ grantsByTarget.set(key, group);
87
+ }
88
+ }
89
+ // Statement order decides the net ACL: a revoke superseded by a later grant
90
+ // vanishes, and a trailing full-coverage revoke nets the pair to nothing.
91
+ // Two passes — netting decisions must complete before any object is kept.
92
+ const nettedAway = new Set();
93
+ for (const object of objects) {
94
+ if ((object.ref.kind !== "grant" && object.ref.kind !== "default-privilege") ||
95
+ object.metadata.verb !== "REVOKE") {
96
+ continue;
97
+ }
98
+ const meta = object.metadata;
99
+ const kindPhrase = typeof meta.kindPhrase === "string"
100
+ ? meta.kindPhrase
101
+ : typeof meta.objectType === "string"
102
+ ? meta.objectType
103
+ : "";
104
+ const grantee = typeof meta.grantee === "string" ? meta.grantee : "";
105
+ if (builtinPublicDefault(kindPhrase) !== undefined && grantee === "PUBLIC") {
106
+ continue;
107
+ }
108
+ const privileges = Array.isArray(meta.privileges) ? meta.privileges : [];
109
+ const counterparts = grantsByTarget.get(privilegeTargetKey(object)) ?? [];
110
+ if (counterparts.length === 0) {
111
+ nettedAway.add(object);
112
+ continue;
113
+ }
114
+ const latestGrant = Math.max(...counterparts.map((item) => item.ordinal));
115
+ if (object.ordinal < latestGrant) {
116
+ nettedAway.add(object);
117
+ continue;
118
+ }
119
+ if (privileges.includes("ALL") || coversAllGrants(privileges, counterparts)) {
120
+ nettedAway.add(object);
121
+ for (const counterpart of counterparts) {
122
+ nettedAway.add(counterpart);
123
+ }
124
+ }
125
+ }
126
+ return objects.filter((object) => {
127
+ if (nettedAway.has(object)) {
128
+ return false;
129
+ }
130
+ if (object.ref.kind !== "grant" && object.ref.kind !== "default-privilege") {
131
+ return true;
132
+ }
133
+ const meta = object.metadata;
134
+ const kindPhrase = typeof meta.kindPhrase === "string"
135
+ ? meta.kindPhrase
136
+ : typeof meta.objectType === "string"
137
+ ? meta.objectType
138
+ : "";
139
+ const grantee = typeof meta.grantee === "string" ? meta.grantee : "";
140
+ const privileges = Array.isArray(meta.privileges) ? meta.privileges : [];
141
+ if (meta.verb === "GRANT") {
142
+ return !isBuiltinDefaultGrant(kindPhrase, grantee, privileges);
143
+ }
144
+ return true;
145
+ });
146
+ }
147
+ function coversAllGrants(revoked, grants) {
148
+ const revokedSet = new Set(revoked);
149
+ return grants.every((grant) => {
150
+ const granted = Array.isArray(grant.metadata.privileges)
151
+ ? grant.metadata.privileges
152
+ : [];
153
+ return granted.every((privilege) => revokedSet.has(privilege) || privilege === "ALL");
154
+ });
155
+ }
156
+ function privilegeTargetKey(object) {
157
+ const meta = object.metadata;
158
+ return [
159
+ object.ref.kind,
160
+ typeof meta.kindPhrase === "string" ? meta.kindPhrase : String(meta.objectType ?? ""),
161
+ typeof meta.targetIdentity === "string" ? meta.targetIdentity : String(meta.schema ?? ""),
162
+ typeof meta.forRole === "string" ? meta.forRole : "",
163
+ typeof meta.grantee === "string" ? meta.grantee : "",
164
+ ].join("|");
165
+ }
166
+ function columnDefaultAmendment(object) {
167
+ const raw = object.metadata.columnDefaultAmendment;
168
+ if (typeof raw !== "object" || raw === null) {
169
+ return undefined;
170
+ }
171
+ const column = raw.column;
172
+ if (typeof column !== "string" || column.length === 0) {
173
+ return undefined;
174
+ }
175
+ return { column, expression: raw.expression ?? null };
176
+ }
177
+ function applyColumnDefaultAmendments(objects, diagnostics) {
178
+ const markers = objects.filter((object) => columnDefaultAmendment(object) !== undefined);
179
+ if (markers.length === 0) {
180
+ return objects;
181
+ }
182
+ const tablesByKey = new Map();
183
+ for (const object of objects) {
184
+ if (object.ref.kind === "table" && columnDefaultAmendment(object) === undefined) {
185
+ tablesByKey.set(object.key, object);
186
+ }
187
+ }
188
+ for (const marker of markers) {
189
+ const amendment = columnDefaultAmendment(marker);
190
+ const table = tablesByKey.get(marker.key);
191
+ const shape = table?.metadata.canonicalShape;
192
+ const column = shape?.columns?.find((item) => item.name === amendment?.column);
193
+ if (!(table && shape && column && amendment)) {
194
+ diagnostics.push(diagnostic("SUPA_EXTRACT_UNSUPPORTED", "error", "ALTER COLUMN DEFAULT targets a table or column not present in the source model", { file: marker.file, ref: marker.ref, statement: marker.sql }));
195
+ continue;
196
+ }
197
+ if (amendment.expression === null) {
198
+ delete column.default;
199
+ }
200
+ else {
201
+ column.default = canonicalizeRegclassLiterals(stripLocations(amendment.expression));
202
+ }
203
+ table.hash = shapeHash(shape, table.key, table.ref);
204
+ table.sql = `${table.sql};\n${marker.sql}`;
205
+ table.dependencies = mergedDependencies([table, marker]);
206
+ }
207
+ return objects.filter((object) => columnDefaultAmendment(object) === undefined);
208
+ }
209
+ function sequenceOwnedByAmendment(object) {
210
+ const raw = object.metadata.sequenceOwnedByAmendment;
211
+ if (typeof raw !== "object" || raw === null) {
212
+ return undefined;
213
+ }
214
+ const ownedBy = raw.ownedBy;
215
+ if (ownedBy !== null && typeof ownedBy !== "string") {
216
+ return undefined;
217
+ }
218
+ return { ownedBy };
219
+ }
220
+ function applySequenceOwnedByAmendments(objects, diagnostics) {
221
+ const markers = objects.filter((object) => sequenceOwnedByAmendment(object) !== undefined);
222
+ if (markers.length === 0) {
223
+ return objects;
224
+ }
225
+ const sequencesByKey = new Map();
226
+ for (const object of objects) {
227
+ if (object.ref.kind === "sequence" && sequenceOwnedByAmendment(object) === undefined) {
228
+ sequencesByKey.set(object.key, object);
229
+ }
230
+ }
231
+ for (const marker of markers) {
232
+ const amendment = sequenceOwnedByAmendment(marker);
233
+ const sequence = sequencesByKey.get(marker.key);
234
+ const shape = sequence?.metadata.canonicalShape;
235
+ if (!(sequence && shape && amendment)) {
236
+ diagnostics.push(diagnostic("SUPA_EXTRACT_UNSUPPORTED", "error", "ALTER SEQUENCE ... OWNED BY targets a sequence not present in the source model", { file: marker.file, ref: marker.ref, statement: marker.sql }));
237
+ continue;
238
+ }
239
+ if (amendment.ownedBy === null) {
240
+ delete shape.ownedBy;
241
+ }
242
+ else {
243
+ shape.ownedBy = amendment.ownedBy;
244
+ }
245
+ sequence.hash = shapeHash(shape, sequence.key, sequence.ref);
246
+ sequence.sql = `${sequence.sql};\n${marker.sql}`;
247
+ sequence.dependencies = mergedDependencies([sequence, marker]);
248
+ }
249
+ return objects.filter((object) => sequenceOwnedByAmendment(object) === undefined);
250
+ }
251
+ const rlsSubtypeOrder = new Map([
252
+ ["AT_EnableRowSecurity", 0],
253
+ ["AT_DisableRowSecurity", 1],
254
+ ["AT_ForceRowSecurity", 2],
255
+ ["AT_NoForceRowSecurity", 3],
256
+ ]);
257
+ /**
258
+ * ENABLE and FORCE ROW LEVEL SECURITY are facets of one table's RLS state
259
+ * sharing one identity; the catalog lane emits them as one multi-statement
260
+ * object, so split source statements merge the same way.
261
+ */
262
+ async function mergeRlsFacets(objects, options) {
263
+ const groups = new Map();
264
+ for (const object of objects) {
265
+ if (object.ref.kind !== "rls" || typeof object.metadata.rlsSubtype !== "string") {
266
+ continue;
267
+ }
268
+ const group = groups.get(object.key) ?? [];
269
+ group.push(object);
270
+ groups.set(object.key, group);
271
+ }
272
+ const replacements = new Map();
273
+ const removed = new Set();
274
+ for (const [key, group] of groups) {
275
+ if (group.length < 2) {
276
+ continue;
277
+ }
278
+ const ordered = [...group].sort((left, right) => (rlsSubtypeOrder.get(String(left.metadata.rlsSubtype)) ?? 9) -
279
+ (rlsSubtypeOrder.get(String(right.metadata.rlsSubtype)) ?? 9));
280
+ const first = ordered[0];
281
+ if (!first) {
282
+ continue;
283
+ }
284
+ const merged = makeObject(first.ref, ordered.map((member) => member.sql).join(";\n"), first.ordinal, first.file);
285
+ merged.dependencies = mergedDependencies(ordered);
286
+ await finalizeObject(merged, { normalize: options.normalize === true });
287
+ replacements.set(key, merged);
288
+ for (const member of group) {
289
+ removed.add(member);
290
+ }
291
+ }
292
+ return replaceMembers(objects, removed, replacements);
293
+ }
294
+ function replaceMembers(objects, removed, replacements) {
295
+ if (replacements.size === 0) {
296
+ return objects;
297
+ }
298
+ const result = [];
299
+ for (const object of objects) {
300
+ if (!removed.has(object)) {
301
+ result.push(object);
302
+ continue;
303
+ }
304
+ const replacement = replacements.get(object.key);
305
+ if (replacement) {
306
+ result.push(replacement);
307
+ replacements.delete(object.key);
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ async function mergeSplitPrivileges(objects, options) {
313
+ const groups = new Map();
314
+ for (const object of objects) {
315
+ if (object.ref.kind !== "grant" && object.ref.kind !== "default-privilege") {
316
+ continue;
317
+ }
318
+ const group = groups.get(object.key) ?? [];
319
+ group.push(object);
320
+ groups.set(object.key, group);
321
+ }
322
+ const replacements = new Map();
323
+ const removed = new Set();
324
+ for (const [key, group] of groups) {
325
+ if (group.length < 2) {
326
+ continue;
327
+ }
328
+ const merged = await mergePrivilegeGroup(group, options);
329
+ if (!merged) {
330
+ continue;
331
+ }
332
+ replacements.set(key, merged);
333
+ for (const member of group) {
334
+ removed.add(member);
335
+ }
336
+ }
337
+ return replaceMembers(objects, removed, replacements);
338
+ }
339
+ async function mergePrivilegeGroup(group, options) {
340
+ const first = group[0];
341
+ if (!first) {
342
+ return undefined;
343
+ }
344
+ const privileges = unionPrivileges(group);
345
+ if (!privileges) {
346
+ return undefined;
347
+ }
348
+ const meta = first.metadata;
349
+ let merged;
350
+ if (first.ref.kind === "grant") {
351
+ const grantOptions = new Set(group.map((item) => item.metadata.withGrantOption === true));
352
+ if (grantOptions.size > 1) {
353
+ return undefined;
354
+ }
355
+ if (typeof meta.grantee !== "string" ||
356
+ typeof meta.kindPhrase !== "string" ||
357
+ typeof meta.target !== "string" ||
358
+ typeof meta.targetIdentity !== "string" ||
359
+ (meta.verb !== "GRANT" && meta.verb !== "REVOKE")) {
360
+ return undefined;
361
+ }
362
+ merged = buildGrantObject({
363
+ file: first.file,
364
+ grantee: meta.grantee,
365
+ kindPhrase: meta.kindPhrase,
366
+ ordinal: first.ordinal,
367
+ privileges,
368
+ schema: first.ref.schema,
369
+ targetIdentity: meta.targetIdentity,
370
+ targetRendered: meta.target,
371
+ verb: meta.verb,
372
+ withGrantOption: meta.withGrantOption === true,
373
+ });
374
+ }
375
+ else {
376
+ if (typeof meta.grantee !== "string" ||
377
+ typeof meta.objectType !== "string" ||
378
+ (meta.verb !== "GRANT" && meta.verb !== "REVOKE")) {
379
+ return undefined;
380
+ }
381
+ merged = buildDefaultPrivilegeObject({
382
+ file: first.file,
383
+ forRole: typeof meta.forRole === "string" ? meta.forRole : undefined,
384
+ grantee: meta.grantee,
385
+ objectType: meta.objectType,
386
+ ordinal: first.ordinal,
387
+ privileges,
388
+ schema: typeof meta.schema === "string" ? meta.schema : undefined,
389
+ verb: meta.verb,
390
+ });
391
+ }
392
+ merged.dependencies = mergedDependencies(group);
393
+ await finalizeObject(merged, { normalize: options.normalize === true });
394
+ return merged;
395
+ }
396
+ function unionPrivileges(group) {
397
+ const union = new Set();
398
+ for (const member of group) {
399
+ const privileges = member.metadata.privileges;
400
+ if (!Array.isArray(privileges) || privileges.some((item) => typeof item !== "string")) {
401
+ return undefined;
402
+ }
403
+ for (const privilege of privileges) {
404
+ if (privilege === "ALL") {
405
+ return ["ALL"];
406
+ }
407
+ union.add(privilege);
408
+ }
409
+ }
410
+ return [...union].sort((left, right) => left.localeCompare(right));
411
+ }
412
+ function mergedDependencies(group) {
413
+ const union = new Set();
414
+ for (const member of group) {
415
+ for (const dependency of member.dependencies) {
416
+ union.add(dependency);
417
+ }
418
+ }
419
+ return [...union].sort((left, right) => left.localeCompare(right));
420
+ }
@@ -0,0 +1,4 @@
1
+ import type { ExtractOptions, SchemaModel } from "./core.js";
2
+ export declare function extractSourceModel(source: string, options?: ExtractOptions): Promise<SchemaModel>;
3
+ export declare function filterModelBySchemas(model: SchemaModel, schemas: Set<string>): SchemaModel;
4
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,cAAc,EACd,WAAW,EAGZ,MAAM,WAAW,CAAC;AAoBnB,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAItB;AA8CD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,WAAW,CAmB1F"}
package/dist/source.js ADDED
@@ -0,0 +1,233 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { extname, join, relative, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { extractCatalogModel } from "./catalog.js";
6
+ import { resolveConfig } from "./config.js";
7
+ import { diagnostic } from "./diagnostics.js";
8
+ import { fingerprintObjects, MODEL_FORMAT_VERSION } from "./hash.js";
9
+ import { normalizeSourceObjects } from "./source-normalize.js";
10
+ import { extractObjectsFromSql } from "./sql/extract.js";
11
+ const execFileAsync = promisify(execFile);
12
+ export async function extractSourceModel(source, options = {}) {
13
+ const cwd = options.cwd ?? process.cwd();
14
+ const config = resolveConfig(options.config);
15
+ return applyConfigModelFilters(await extractRawModel(source, cwd, config), config);
16
+ }
17
+ async function extractRawModel(source, cwd, config) {
18
+ if (source.startsWith("catalog:")) {
19
+ return readCatalogSource(source.slice("catalog:".length), cwd, source);
20
+ }
21
+ if (source.startsWith("database:")) {
22
+ const databaseUrl = resolveDatabaseUrl(source.slice("database:".length));
23
+ return extractCatalogModel({
24
+ databaseUrl,
25
+ normalize: config.normalize === "deparse",
26
+ source,
27
+ });
28
+ }
29
+ if (source.startsWith("dump:")) {
30
+ const target = source.slice("dump:".length);
31
+ if (target === "-") {
32
+ const sql = await readAllStdin();
33
+ return modelFromSqlFiles([{ path: "<stdin>", sql }], source, config);
34
+ }
35
+ const path = resolve(cwd, target);
36
+ const sql = await readFile(path, "utf8");
37
+ return modelFromSqlFiles([{ path, sql }], source, config);
38
+ }
39
+ if (source.startsWith("dir:")) {
40
+ const root = resolve(cwd, source.slice("dir:".length));
41
+ const files = await readSqlFiles(root);
42
+ return modelFromSqlFiles(files, source, config);
43
+ }
44
+ if (source.startsWith("git:")) {
45
+ const ref = source.slice("git:".length) || "HEAD";
46
+ const files = await readGitSqlFiles(ref, cwd, config.schemaPaths);
47
+ return modelFromSqlFiles(files, source, config);
48
+ }
49
+ throw new Error(`unsupported source "${source}"`);
50
+ }
51
+ const schemaScopedDiagnosticCodes = new Set([
52
+ "SUPA_EXTRACT_SIDE_EFFECT_UNSUPPORTED",
53
+ "SUPA_EXTRACT_UNSUPPORTED",
54
+ ]);
55
+ export function filterModelBySchemas(model, schemas) {
56
+ if (schemas.size === 0) {
57
+ return model;
58
+ }
59
+ // An include list defines the contract scope: extraction findings for
60
+ // statements that reference no in-scope schema (managed-schema bootstrap,
61
+ // out-of-contract partition wiring) must not block in-scope diffs.
62
+ const filtered = withObjects(model, model.objects.filter((object) => schemas.has(objectSchema(object))));
63
+ return {
64
+ ...filtered,
65
+ diagnostics: filtered.diagnostics.filter((item) => !schemaScopedDiagnosticCodes.has(item.code) ||
66
+ (item.schemas ?? []).some((schema) => schemas.has(schema))),
67
+ };
68
+ }
69
+ function applyConfigModelFilters(model, config) {
70
+ let current = model;
71
+ if (config.schemas.include.length > 0) {
72
+ current = filterModelBySchemas(current, new Set(config.schemas.include));
73
+ }
74
+ if (config.schemas.exclude.length > 0) {
75
+ const excluded = new Set(config.schemas.exclude);
76
+ current = withObjects(current, current.objects.filter((object) => !excluded.has(objectSchema(object))));
77
+ current = {
78
+ ...current,
79
+ diagnostics: current.diagnostics.filter((item) => !schemaScopedDiagnosticCodes.has(item.code) ||
80
+ (item.schemas ?? []).length === 0 ||
81
+ (item.schemas ?? []).some((schema) => !excluded.has(schema))),
82
+ };
83
+ }
84
+ if (config.excludedGrantRoles.length > 0) {
85
+ const roles = new Set(config.excludedGrantRoles);
86
+ current = withObjects(current, current.objects.filter((object) => !isExcludedGrant(object, roles)));
87
+ }
88
+ return current;
89
+ }
90
+ function withObjects(model, objects) {
91
+ if (objects.length === model.objects.length) {
92
+ return model;
93
+ }
94
+ return { ...model, fingerprint: fingerprintObjects(objects), objects };
95
+ }
96
+ function objectSchema(object) {
97
+ if (object.ref.kind === "schema") {
98
+ return object.ref.name;
99
+ }
100
+ if (object.ref.kind === "extension" && typeof object.metadata.schema === "string") {
101
+ return object.metadata.schema;
102
+ }
103
+ return object.ref.schema ?? "public";
104
+ }
105
+ function isExcludedGrant(object, roles) {
106
+ if (object.ref.kind !== "grant" && object.ref.kind !== "default-privilege") {
107
+ return false;
108
+ }
109
+ const grantee = typeof object.metadata.grantee === "string" ? object.metadata.grantee : undefined;
110
+ const forRole = typeof object.metadata.forRole === "string" ? object.metadata.forRole : undefined;
111
+ return ((grantee !== undefined && roles.has(grantee)) || (forRole !== undefined && roles.has(forRole)));
112
+ }
113
+ async function readCatalogSource(path, cwd, source) {
114
+ const fullPath = resolve(cwd, path);
115
+ const raw = JSON.parse(await readFile(fullPath, "utf8"));
116
+ const objects = Array.isArray(raw.objects) ? raw.objects : [];
117
+ const diagnostics = Array.isArray(raw.diagnostics) ? raw.diagnostics : [];
118
+ if (raw.formatVersion !== MODEL_FORMAT_VERSION) {
119
+ diagnostics.push(diagnostic("SUPA_CATALOG_SNAPSHOT_VERSION", "warning", `catalog snapshot model version ${raw.formatVersion ?? "unknown"} does not match this supaschema model version ${MODEL_FORMAT_VERSION}`, {
120
+ file: fullPath,
121
+ hint: "Object hashes are version-specific; regenerate the snapshot with `supaschema inspect` to avoid false replacements.",
122
+ }));
123
+ }
124
+ const model = {
125
+ diagnostics,
126
+ fingerprint: raw.fingerprint ?? fingerprintObjects(objects),
127
+ objects,
128
+ source,
129
+ };
130
+ if (raw.formatVersion !== undefined) {
131
+ model.formatVersion = raw.formatVersion;
132
+ }
133
+ return model;
134
+ }
135
+ async function modelFromSqlFiles(files, source, config) {
136
+ const extractedObjects = [];
137
+ const diagnostics = [];
138
+ let ordinal = 0;
139
+ for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
140
+ const extracted = await extractObjectsFromSql(file.sql, {
141
+ config,
142
+ file: file.path,
143
+ startOrdinal: ordinal,
144
+ });
145
+ extractedObjects.push(...extracted.objects);
146
+ diagnostics.push(...extracted.diagnostics);
147
+ ordinal = extracted.nextOrdinal;
148
+ }
149
+ const objects = await normalizeSourceObjects(extractedObjects, diagnostics, {
150
+ normalize: config.normalize === "deparse",
151
+ });
152
+ diagnostics.push(...duplicateKeyDiagnostics(objects));
153
+ return {
154
+ diagnostics,
155
+ fingerprint: fingerprintObjects(objects),
156
+ formatVersion: MODEL_FORMAT_VERSION,
157
+ objects: objects.sort((left, right) => left.ordinal - right.ordinal),
158
+ source,
159
+ };
160
+ }
161
+ async function readAllStdin() {
162
+ const chunks = [];
163
+ for await (const chunk of process.stdin) {
164
+ chunks.push(chunk);
165
+ }
166
+ return Buffer.concat(chunks).toString("utf8");
167
+ }
168
+ async function readSqlFiles(root) {
169
+ const files = [];
170
+ async function walk(directory) {
171
+ const entries = await readdir(directory, { withFileTypes: true });
172
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
173
+ const fullPath = join(directory, entry.name);
174
+ if (entry.isDirectory()) {
175
+ await walk(fullPath);
176
+ continue;
177
+ }
178
+ if (entry.isFile() && extname(entry.name) === ".sql") {
179
+ files.push({
180
+ path: relative(root, fullPath),
181
+ sql: await readFile(fullPath, "utf8"),
182
+ });
183
+ }
184
+ }
185
+ }
186
+ await walk(root);
187
+ return files;
188
+ }
189
+ async function readGitSqlFiles(ref, cwd, schemaPaths) {
190
+ const files = [];
191
+ for (const schemaPath of schemaPaths) {
192
+ const { stdout } = await execFileAsync("git", ["-C", cwd, "ls-tree", "-r", "--name-only", ref, "--", schemaPath], { maxBuffer: 1024 * 1024 * 10 });
193
+ const paths = stdout
194
+ .split("\n")
195
+ .map((line) => line.trim())
196
+ .filter((line) => line.endsWith(".sql"));
197
+ for (const path of paths) {
198
+ const { stdout: sql } = await execFileAsync("git", ["-C", cwd, "show", `${ref}:${path}`], {
199
+ maxBuffer: 1024 * 1024 * 20,
200
+ });
201
+ files.push({ path, sql });
202
+ }
203
+ }
204
+ return files;
205
+ }
206
+ function resolveDatabaseUrl(value) {
207
+ if (value.startsWith("$")) {
208
+ const envName = value.slice(1);
209
+ const resolved = process.env[envName];
210
+ if (!resolved) {
211
+ throw new Error(`environment variable ${envName} is not set`);
212
+ }
213
+ return resolved;
214
+ }
215
+ return value;
216
+ }
217
+ function duplicateKeyDiagnostics(objects) {
218
+ const diagnostics = [];
219
+ const seen = new Map();
220
+ for (const object of objects) {
221
+ const previous = seen.get(object.key);
222
+ if (previous) {
223
+ diagnostics.push(diagnostic("SUPA_EXTRACT_DUPLICATE_OBJECT", "error", "duplicate object identity", {
224
+ file: object.file,
225
+ hint: `first seen in ${previous.file ?? "unknown source"}`,
226
+ ref: object.ref,
227
+ }));
228
+ continue;
229
+ }
230
+ seen.set(object.key, object);
231
+ }
232
+ return diagnostics;
233
+ }