@supacloud/db 0.6.0 → 0.8.0

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.
@@ -0,0 +1,291 @@
1
+ var __esm = (fn, res, err) => () => {
2
+ if (fn)
3
+ try {
4
+ res = fn(fn = 0);
5
+ } catch (e) {
6
+ err = [e];
7
+ }
8
+ if (err)
9
+ throw err[0];
10
+ return res;
11
+ };
12
+
13
+ // src/sql-analysis.ts
14
+ import { createHash } from "node:crypto";
15
+ import { fingerprint, parse } from "libpg-query";
16
+ function record(value) {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ function walk(value, visit) {
20
+ if (Array.isArray(value)) {
21
+ for (const item of value)
22
+ walk(item, visit);
23
+ } else if (record(value)) {
24
+ visit(value);
25
+ for (const item of Object.values(value))
26
+ walk(item, visit);
27
+ }
28
+ }
29
+ function names(value) {
30
+ if (!Array.isArray(value))
31
+ return [];
32
+ return value.flatMap((part) => record(part) && record(part["String"]) && typeof part["String"]["sval"] === "string" ? [part["String"]["sval"]] : []);
33
+ }
34
+ async function analyzeSql(sql) {
35
+ const result = await parse(sql);
36
+ const defines = new Set, touches = new Set, references = new Set, review = new Set;
37
+ const qualified = (kind, parts) => {
38
+ if (parts.length !== 2) {
39
+ review.add(`Resolve unqualified or unsupported ${kind} name: ${parts.join(".")}`);
40
+ return;
41
+ }
42
+ return objectId(kind, parts);
43
+ };
44
+ const relation = (value) => {
45
+ if (!record(value) || typeof value["relname"] !== "string")
46
+ return;
47
+ return qualified("relation", typeof value["schemaname"] === "string" ? [value["schemaname"], value["relname"]] : [value["relname"]]);
48
+ };
49
+ walk(result, (node) => {
50
+ if (record(node["RangeVar"])) {
51
+ const name = relation(node["RangeVar"]);
52
+ if (name)
53
+ references.add(name);
54
+ }
55
+ if (record(node["FuncCall"])) {
56
+ const name = qualified("function", names(node["FuncCall"]["funcname"]));
57
+ if (name)
58
+ references.add(name);
59
+ }
60
+ if (record(node["Constraint"])) {
61
+ const name = relation(node["Constraint"]["pktable"]);
62
+ if (name)
63
+ references.add(name);
64
+ }
65
+ });
66
+ const statements = record(result) && Array.isArray(result["stmts"]) ? result["stmts"] : [];
67
+ for (const raw of statements) {
68
+ if (!record(raw) || !record(raw["stmt"]))
69
+ throw new Error("Invalid PostgreSQL parse tree");
70
+ const [kind, node] = Object.entries(raw["stmt"])[0] ?? [];
71
+ if (!kind || !record(node))
72
+ throw new Error("Invalid SQL statement");
73
+ const target = relation(node["relation"] ?? node["view"] ?? node["table"]);
74
+ if (target) {
75
+ touches.add(target);
76
+ if (kind === "CreateStmt" || kind === "ViewStmt")
77
+ defines.add(target);
78
+ }
79
+ if (kind === "DropStmt") {
80
+ for (const item of Array.isArray(node["objects"]) ? node["objects"] : []) {
81
+ const name = record(item) && record(item["List"]) ? qualified(node["removeType"] === "OBJECT_TABLE" || node["removeType"] === "OBJECT_VIEW" ? "relation" : "object", names(item["List"]["items"])) : undefined;
82
+ if (name)
83
+ touches.add(name);
84
+ else
85
+ review.add("Review dropped object identity and dependencies");
86
+ }
87
+ review.add("Destructive DROP requires review");
88
+ } else if (kind === "CreateFunctionStmt") {
89
+ const name = qualified("function", names(node["funcname"]));
90
+ if (name) {
91
+ defines.add(name);
92
+ touches.add(name);
93
+ }
94
+ let language, body;
95
+ for (const item of Array.isArray(node["options"]) ? node["options"] : []) {
96
+ if (!record(item) || !record(item["DefElem"]))
97
+ continue;
98
+ const option = item["DefElem"];
99
+ if (option["defname"] === "language")
100
+ language = names([option["arg"]])[0];
101
+ if (option["defname"] === "as" && record(option["arg"]) && record(option["arg"]["List"]))
102
+ body = names(option["arg"]["List"]["items"])[0];
103
+ }
104
+ if (language === "sql" && body !== undefined) {
105
+ const nested = await analyzeSql(body);
106
+ for (const name of [...nested.references, ...nested.touches])
107
+ references.add(name);
108
+ for (const item of nested.review)
109
+ review.add(item);
110
+ } else {
111
+ review.add("Function body dependencies require catalog verification or explicit reviewed dependencies");
112
+ }
113
+ review.add("Review function security, grants and overloads");
114
+ } else if (![
115
+ "SelectStmt",
116
+ "CreateStmt",
117
+ "ViewStmt",
118
+ "AlterTableStmt",
119
+ "IndexStmt",
120
+ "InsertStmt",
121
+ "UpdateStmt",
122
+ "DeleteStmt",
123
+ "CreatePolicyStmt",
124
+ "AlterPolicyStmt",
125
+ "CreateTrigStmt"
126
+ ].includes(kind)) {
127
+ review.add(`Review unsupported dependency semantics: ${kind}`);
128
+ }
129
+ if (["AlterTableStmt", "CreatePolicyStmt", "AlterPolicyStmt", "CreateTrigStmt"].includes(kind)) {
130
+ review.add(`Review schema/access impact: ${kind}`);
131
+ }
132
+ }
133
+ return {
134
+ fingerprint: await fingerprint(sql),
135
+ defines: [...defines].sort(),
136
+ touches: [...touches].sort(),
137
+ references: [...references].sort(),
138
+ review: [...review].sort()
139
+ };
140
+ }
141
+ async function assertReadSql(sql) {
142
+ const tree = await parse(sql);
143
+ if (!record(tree) || !Array.isArray(tree["stmts"]) || tree["stmts"].length !== 1)
144
+ throw new Error("Expected one read statement");
145
+ const entry = tree["stmts"][0];
146
+ if (!record(entry) || !record(entry["stmt"]) || !record(entry["stmt"]["SelectStmt"]))
147
+ throw new Error("Expected SELECT");
148
+ walk(tree, (node) => {
149
+ if (["InsertStmt", "UpdateStmt", "DeleteStmt", "MergeStmt", "IntoClause", "intoClause", "LockingClause"].some((name) => (name in node))) {
150
+ throw new Error("Mutating or locking SQL is not a read query");
151
+ }
152
+ });
153
+ return fingerprint(sql);
154
+ }
155
+ async function readSqlDependencyGraph(executor, schemas) {
156
+ if (!schemas.length || schemas.some((schema) => !schema.trim()))
157
+ throw new TypeError("Select explicit application schemas");
158
+ const schemaParameters = schemas.map((_, index) => `$${index + 1}::text`).join(", ");
159
+ const rows = await executor.query(`
160
+ WITH objects AS (
161
+ SELECT 'pg_class'::regclass::oid AS classid, c.oid AS objid, n.nspname,
162
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text AS identity
163
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
164
+ UNION ALL
165
+ SELECT 'pg_proc'::regclass::oid, p.oid, n.nspname,
166
+ 'function:' || to_json(n.nspname)::text || '.' || to_json(p.proname)::text
167
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
168
+ UNION ALL
169
+ SELECT 'pg_rewrite'::regclass::oid, r.oid, n.nspname,
170
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
171
+ FROM pg_rewrite r JOIN pg_class c ON c.oid=r.ev_class JOIN pg_namespace n ON n.oid=c.relnamespace
172
+ UNION ALL
173
+ SELECT 'pg_policy'::regclass::oid, p.oid, n.nspname,
174
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
175
+ FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid JOIN pg_namespace n ON n.oid=c.relnamespace
176
+ UNION ALL
177
+ SELECT 'pg_trigger'::regclass::oid, t.oid, n.nspname,
178
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
179
+ FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace
180
+ )
181
+ SELECT DISTINCT a.identity AS dependent, b.identity AS dependency
182
+ FROM pg_depend d
183
+ JOIN objects a ON a.classid=d.classid AND a.objid=d.objid
184
+ JOIN objects b ON b.classid=d.refclassid AND b.objid=d.refobjid
185
+ WHERE a.nspname IN (${schemaParameters})
186
+ AND b.nspname IN (${schemaParameters}) AND a.identity<>b.identity
187
+ ORDER BY dependent, dependency`, [...schemas]);
188
+ if (!Array.isArray(rows))
189
+ throw new TypeError("Invalid PostgreSQL dependency rows");
190
+ const edges = rows.map((row) => {
191
+ if (!record(row) || typeof row["dependent"] !== "string" || typeof row["dependency"] !== "string")
192
+ throw new TypeError("Invalid PostgreSQL dependency edge");
193
+ return { dependent: row["dependent"], dependency: row["dependency"] };
194
+ });
195
+ const functions = await executor.query(`
196
+ SELECT DISTINCT n.nspname AS schema, p.proname AS name
197
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
198
+ JOIN pg_language l ON l.oid=p.prolang
199
+ WHERE n.nspname IN (${schemaParameters})
200
+ AND (l.lanname='plpgsql' OR (l.lanname='sql' AND p.prosqlbody IS NULL))
201
+ ORDER BY schema, name`, [...schemas]);
202
+ if (!Array.isArray(functions))
203
+ throw new TypeError("Invalid PostgreSQL function rows");
204
+ return {
205
+ edges,
206
+ review: functions.map((row) => {
207
+ if (!record(row) || typeof row["schema"] !== "string" || typeof row["name"] !== "string")
208
+ throw new TypeError("Invalid function identity");
209
+ return `Catalog cannot prove function-body dependencies: ${objectId("function", [row["schema"], row["name"]])}`;
210
+ })
211
+ };
212
+ }
213
+ async function planSqlImpact(sources, baseline = [], catalogEdges = []) {
214
+ const ids = new Set;
215
+ const migrations = [];
216
+ const edges = new Map;
217
+ const addEdge = (dependent, dependency) => {
218
+ if (dependent !== dependency)
219
+ edges.set(JSON.stringify([dependent, dependency]), { dependent, dependency });
220
+ };
221
+ for (const edge of catalogEdges)
222
+ addEdge(edge["dependent"], edge["dependency"]);
223
+ for (const source of sources) {
224
+ if (!source.id.trim() || !source.owner.trim() || ids.has(source.id))
225
+ throw new Error("Migration IDs must be unique and each migration must have an owner");
226
+ ids.add(source.id);
227
+ const analysis = await analyzeSql(source.sql);
228
+ const references = [...new Set([...analysis.references, ...source.dependencies ?? []])].sort();
229
+ for (const target of analysis.touches)
230
+ for (const reference of references)
231
+ addEdge(target, reference);
232
+ migrations.push({
233
+ id: source.id,
234
+ owner: source.owner,
235
+ sha256: createHash("sha256").update(source.sql).digest("hex"),
236
+ analysis: { ...analysis, references }
237
+ });
238
+ }
239
+ const current = new Map(migrations.map((entry) => [entry.id, entry]));
240
+ const previous = new Map(baseline.map((entry) => [entry.id, entry]));
241
+ const errors = [];
242
+ if (previous.size !== baseline.length)
243
+ errors.push("Duplicate migration in baseline");
244
+ for (const [index, old] of baseline.entries()) {
245
+ if (current.get(old.id)?.sha256 !== old.sha256 || migrations[index]?.id !== old.id) {
246
+ errors.push(`Applied migration removed, reordered or rewritten: ${old.id}`);
247
+ }
248
+ }
249
+ const changed = migrations.filter((entry) => previous.get(entry.id)?.sha256 !== entry.sha256);
250
+ const affected = new Set(changed.flatMap((entry) => entry.analysis.touches));
251
+ const reverse = new Map;
252
+ for (const edge of edges.values()) {
253
+ const values = reverse.get(edge["dependency"]) ?? [];
254
+ values.push(edge["dependent"]);
255
+ reverse.set(edge["dependency"], values);
256
+ }
257
+ for (const object of affected)
258
+ for (const dependent of reverse.get(object) ?? [])
259
+ affected.add(dependent);
260
+ const definitions = new Map;
261
+ migrations.forEach((entry, index) => {
262
+ for (const object of entry.analysis.defines)
263
+ if (!definitions.has(object))
264
+ definitions.set(object, index);
265
+ });
266
+ migrations.forEach((entry, index) => {
267
+ for (const reference of entry.analysis.references) {
268
+ if ((definitions.get(reference) ?? -1) > index)
269
+ errors.push(`${entry.id} references an object defined by a later migration: ${reference}`);
270
+ }
271
+ });
272
+ return {
273
+ migrations,
274
+ edges: [...edges.values()].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
275
+ changed: changed.map((entry) => entry.id),
276
+ affectedObjects: [...affected].sort(),
277
+ affectedMigrations: migrations.filter((entry) => [...entry.analysis.touches, ...entry.analysis.references].some((object) => affected.has(object))).map((entry) => entry.id),
278
+ errors,
279
+ review: changed.flatMap((entry) => entry.analysis.review.map((note) => `${entry.id}: ${note}`))
280
+ };
281
+ }
282
+ var objectId = (kind, parts) => `${kind}:${parts.map((part) => JSON.stringify(part)).join(".")}`;
283
+ var init_sql_analysis = () => {};
284
+ init_sql_analysis();
285
+
286
+ export {
287
+ analyzeSql,
288
+ assertReadSql,
289
+ planSqlImpact,
290
+ readSqlDependencyGraph
291
+ };
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { type SqlDependencyEdge } from "./sql-analysis";
3
+ export declare function runSqlImpactFile(path: string): Promise<{
4
+ digest: string;
5
+ approved: boolean;
6
+ ok: boolean;
7
+ migrations: import("./sql-analysis").SqlMigrationSnapshot[];
8
+ edges: SqlDependencyEdge[];
9
+ changed: string[];
10
+ affectedObjects: string[];
11
+ affectedMigrations: string[];
12
+ errors: string[];
13
+ review: string[];
14
+ }>;
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // src/sql-analysis.ts
15
+ import { createHash } from "node:crypto";
16
+ import { fingerprint, parse } from "libpg-query";
17
+ function record(value) {
18
+ return value !== null && typeof value === "object" && !Array.isArray(value);
19
+ }
20
+ function walk(value, visit) {
21
+ if (Array.isArray(value)) {
22
+ for (const item of value)
23
+ walk(item, visit);
24
+ } else if (record(value)) {
25
+ visit(value);
26
+ for (const item of Object.values(value))
27
+ walk(item, visit);
28
+ }
29
+ }
30
+ function names(value) {
31
+ if (!Array.isArray(value))
32
+ return [];
33
+ return value.flatMap((part) => record(part) && record(part["String"]) && typeof part["String"]["sval"] === "string" ? [part["String"]["sval"]] : []);
34
+ }
35
+ async function analyzeSql(sql) {
36
+ const result = await parse(sql);
37
+ const defines = new Set, touches = new Set, references = new Set, review = new Set;
38
+ const qualified = (kind, parts) => {
39
+ if (parts.length !== 2) {
40
+ review.add(`Resolve unqualified or unsupported ${kind} name: ${parts.join(".")}`);
41
+ return;
42
+ }
43
+ return objectId(kind, parts);
44
+ };
45
+ const relation = (value) => {
46
+ if (!record(value) || typeof value["relname"] !== "string")
47
+ return;
48
+ return qualified("relation", typeof value["schemaname"] === "string" ? [value["schemaname"], value["relname"]] : [value["relname"]]);
49
+ };
50
+ walk(result, (node) => {
51
+ if (record(node["RangeVar"])) {
52
+ const name = relation(node["RangeVar"]);
53
+ if (name)
54
+ references.add(name);
55
+ }
56
+ if (record(node["FuncCall"])) {
57
+ const name = qualified("function", names(node["FuncCall"]["funcname"]));
58
+ if (name)
59
+ references.add(name);
60
+ }
61
+ if (record(node["Constraint"])) {
62
+ const name = relation(node["Constraint"]["pktable"]);
63
+ if (name)
64
+ references.add(name);
65
+ }
66
+ });
67
+ const statements = record(result) && Array.isArray(result["stmts"]) ? result["stmts"] : [];
68
+ for (const raw of statements) {
69
+ if (!record(raw) || !record(raw["stmt"]))
70
+ throw new Error("Invalid PostgreSQL parse tree");
71
+ const [kind, node] = Object.entries(raw["stmt"])[0] ?? [];
72
+ if (!kind || !record(node))
73
+ throw new Error("Invalid SQL statement");
74
+ const target = relation(node["relation"] ?? node["view"] ?? node["table"]);
75
+ if (target) {
76
+ touches.add(target);
77
+ if (kind === "CreateStmt" || kind === "ViewStmt")
78
+ defines.add(target);
79
+ }
80
+ if (kind === "DropStmt") {
81
+ for (const item of Array.isArray(node["objects"]) ? node["objects"] : []) {
82
+ const name = record(item) && record(item["List"]) ? qualified(node["removeType"] === "OBJECT_TABLE" || node["removeType"] === "OBJECT_VIEW" ? "relation" : "object", names(item["List"]["items"])) : undefined;
83
+ if (name)
84
+ touches.add(name);
85
+ else
86
+ review.add("Review dropped object identity and dependencies");
87
+ }
88
+ review.add("Destructive DROP requires review");
89
+ } else if (kind === "CreateFunctionStmt") {
90
+ const name = qualified("function", names(node["funcname"]));
91
+ if (name) {
92
+ defines.add(name);
93
+ touches.add(name);
94
+ }
95
+ let language, body;
96
+ for (const item of Array.isArray(node["options"]) ? node["options"] : []) {
97
+ if (!record(item) || !record(item["DefElem"]))
98
+ continue;
99
+ const option = item["DefElem"];
100
+ if (option["defname"] === "language")
101
+ language = names([option["arg"]])[0];
102
+ if (option["defname"] === "as" && record(option["arg"]) && record(option["arg"]["List"]))
103
+ body = names(option["arg"]["List"]["items"])[0];
104
+ }
105
+ if (language === "sql" && body !== undefined) {
106
+ const nested = await analyzeSql(body);
107
+ for (const name of [...nested.references, ...nested.touches])
108
+ references.add(name);
109
+ for (const item of nested.review)
110
+ review.add(item);
111
+ } else {
112
+ review.add("Function body dependencies require catalog verification or explicit reviewed dependencies");
113
+ }
114
+ review.add("Review function security, grants and overloads");
115
+ } else if (![
116
+ "SelectStmt",
117
+ "CreateStmt",
118
+ "ViewStmt",
119
+ "AlterTableStmt",
120
+ "IndexStmt",
121
+ "InsertStmt",
122
+ "UpdateStmt",
123
+ "DeleteStmt",
124
+ "CreatePolicyStmt",
125
+ "AlterPolicyStmt",
126
+ "CreateTrigStmt"
127
+ ].includes(kind)) {
128
+ review.add(`Review unsupported dependency semantics: ${kind}`);
129
+ }
130
+ if (["AlterTableStmt", "CreatePolicyStmt", "AlterPolicyStmt", "CreateTrigStmt"].includes(kind)) {
131
+ review.add(`Review schema/access impact: ${kind}`);
132
+ }
133
+ }
134
+ return {
135
+ fingerprint: await fingerprint(sql),
136
+ defines: [...defines].sort(),
137
+ touches: [...touches].sort(),
138
+ references: [...references].sort(),
139
+ review: [...review].sort()
140
+ };
141
+ }
142
+ async function assertReadSql(sql) {
143
+ const tree = await parse(sql);
144
+ if (!record(tree) || !Array.isArray(tree["stmts"]) || tree["stmts"].length !== 1)
145
+ throw new Error("Expected one read statement");
146
+ const entry = tree["stmts"][0];
147
+ if (!record(entry) || !record(entry["stmt"]) || !record(entry["stmt"]["SelectStmt"]))
148
+ throw new Error("Expected SELECT");
149
+ walk(tree, (node) => {
150
+ if (["InsertStmt", "UpdateStmt", "DeleteStmt", "MergeStmt", "IntoClause", "intoClause", "LockingClause"].some((name) => (name in node))) {
151
+ throw new Error("Mutating or locking SQL is not a read query");
152
+ }
153
+ });
154
+ return fingerprint(sql);
155
+ }
156
+ async function readSqlDependencyGraph(executor, schemas) {
157
+ if (!schemas.length || schemas.some((schema) => !schema.trim()))
158
+ throw new TypeError("Select explicit application schemas");
159
+ const schemaParameters = schemas.map((_, index) => `$${index + 1}::text`).join(", ");
160
+ const rows = await executor.query(`
161
+ WITH objects AS (
162
+ SELECT 'pg_class'::regclass::oid AS classid, c.oid AS objid, n.nspname,
163
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text AS identity
164
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
165
+ UNION ALL
166
+ SELECT 'pg_proc'::regclass::oid, p.oid, n.nspname,
167
+ 'function:' || to_json(n.nspname)::text || '.' || to_json(p.proname)::text
168
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
169
+ UNION ALL
170
+ SELECT 'pg_rewrite'::regclass::oid, r.oid, n.nspname,
171
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
172
+ FROM pg_rewrite r JOIN pg_class c ON c.oid=r.ev_class JOIN pg_namespace n ON n.oid=c.relnamespace
173
+ UNION ALL
174
+ SELECT 'pg_policy'::regclass::oid, p.oid, n.nspname,
175
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
176
+ FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid JOIN pg_namespace n ON n.oid=c.relnamespace
177
+ UNION ALL
178
+ SELECT 'pg_trigger'::regclass::oid, t.oid, n.nspname,
179
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
180
+ FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace
181
+ )
182
+ SELECT DISTINCT a.identity AS dependent, b.identity AS dependency
183
+ FROM pg_depend d
184
+ JOIN objects a ON a.classid=d.classid AND a.objid=d.objid
185
+ JOIN objects b ON b.classid=d.refclassid AND b.objid=d.refobjid
186
+ WHERE a.nspname IN (${schemaParameters})
187
+ AND b.nspname IN (${schemaParameters}) AND a.identity<>b.identity
188
+ ORDER BY dependent, dependency`, [...schemas]);
189
+ if (!Array.isArray(rows))
190
+ throw new TypeError("Invalid PostgreSQL dependency rows");
191
+ const edges = rows.map((row) => {
192
+ if (!record(row) || typeof row["dependent"] !== "string" || typeof row["dependency"] !== "string")
193
+ throw new TypeError("Invalid PostgreSQL dependency edge");
194
+ return { dependent: row["dependent"], dependency: row["dependency"] };
195
+ });
196
+ const functions = await executor.query(`
197
+ SELECT DISTINCT n.nspname AS schema, p.proname AS name
198
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
199
+ JOIN pg_language l ON l.oid=p.prolang
200
+ WHERE n.nspname IN (${schemaParameters})
201
+ AND (l.lanname='plpgsql' OR (l.lanname='sql' AND p.prosqlbody IS NULL))
202
+ ORDER BY schema, name`, [...schemas]);
203
+ if (!Array.isArray(functions))
204
+ throw new TypeError("Invalid PostgreSQL function rows");
205
+ return {
206
+ edges,
207
+ review: functions.map((row) => {
208
+ if (!record(row) || typeof row["schema"] !== "string" || typeof row["name"] !== "string")
209
+ throw new TypeError("Invalid function identity");
210
+ return `Catalog cannot prove function-body dependencies: ${objectId("function", [row["schema"], row["name"]])}`;
211
+ })
212
+ };
213
+ }
214
+ async function planSqlImpact(sources, baseline = [], catalogEdges = []) {
215
+ const ids = new Set;
216
+ const migrations = [];
217
+ const edges = new Map;
218
+ const addEdge = (dependent, dependency) => {
219
+ if (dependent !== dependency)
220
+ edges.set(JSON.stringify([dependent, dependency]), { dependent, dependency });
221
+ };
222
+ for (const edge of catalogEdges)
223
+ addEdge(edge["dependent"], edge["dependency"]);
224
+ for (const source of sources) {
225
+ if (!source.id.trim() || !source.owner.trim() || ids.has(source.id))
226
+ throw new Error("Migration IDs must be unique and each migration must have an owner");
227
+ ids.add(source.id);
228
+ const analysis = await analyzeSql(source.sql);
229
+ const references = [...new Set([...analysis.references, ...source.dependencies ?? []])].sort();
230
+ for (const target of analysis.touches)
231
+ for (const reference of references)
232
+ addEdge(target, reference);
233
+ migrations.push({
234
+ id: source.id,
235
+ owner: source.owner,
236
+ sha256: createHash("sha256").update(source.sql).digest("hex"),
237
+ analysis: { ...analysis, references }
238
+ });
239
+ }
240
+ const current = new Map(migrations.map((entry) => [entry.id, entry]));
241
+ const previous = new Map(baseline.map((entry) => [entry.id, entry]));
242
+ const errors = [];
243
+ if (previous.size !== baseline.length)
244
+ errors.push("Duplicate migration in baseline");
245
+ for (const [index, old] of baseline.entries()) {
246
+ if (current.get(old.id)?.sha256 !== old.sha256 || migrations[index]?.id !== old.id) {
247
+ errors.push(`Applied migration removed, reordered or rewritten: ${old.id}`);
248
+ }
249
+ }
250
+ const changed = migrations.filter((entry) => previous.get(entry.id)?.sha256 !== entry.sha256);
251
+ const affected = new Set(changed.flatMap((entry) => entry.analysis.touches));
252
+ const reverse = new Map;
253
+ for (const edge of edges.values()) {
254
+ const values = reverse.get(edge["dependency"]) ?? [];
255
+ values.push(edge["dependent"]);
256
+ reverse.set(edge["dependency"], values);
257
+ }
258
+ for (const object of affected)
259
+ for (const dependent of reverse.get(object) ?? [])
260
+ affected.add(dependent);
261
+ const definitions = new Map;
262
+ migrations.forEach((entry, index) => {
263
+ for (const object of entry.analysis.defines)
264
+ if (!definitions.has(object))
265
+ definitions.set(object, index);
266
+ });
267
+ migrations.forEach((entry, index) => {
268
+ for (const reference of entry.analysis.references) {
269
+ if ((definitions.get(reference) ?? -1) > index)
270
+ errors.push(`${entry.id} references an object defined by a later migration: ${reference}`);
271
+ }
272
+ });
273
+ return {
274
+ migrations,
275
+ edges: [...edges.values()].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
276
+ changed: changed.map((entry) => entry.id),
277
+ affectedObjects: [...affected].sort(),
278
+ affectedMigrations: migrations.filter((entry) => [...entry.analysis.touches, ...entry.analysis.references].some((object) => affected.has(object))).map((entry) => entry.id),
279
+ errors,
280
+ review: changed.flatMap((entry) => entry.analysis.review.map((note) => `${entry.id}: ${note}`))
281
+ };
282
+ }
283
+ var objectId = (kind, parts) => `${kind}:${parts.map((part) => JSON.stringify(part)).join(".")}`;
284
+ var init_sql_analysis = () => {};
285
+
286
+ // src/sql-impact-cli.ts
287
+ init_sql_analysis();
288
+ import { createHash as createHash2 } from "node:crypto";
289
+ import { readFile } from "node:fs/promises";
290
+ import { dirname, resolve } from "node:path";
291
+ import { fileURLToPath } from "node:url";
292
+ function object(value) {
293
+ if (!isRecord(value))
294
+ throw new TypeError("Expected configuration object");
295
+ return value;
296
+ }
297
+ function isRecord(value) {
298
+ return value !== null && typeof value === "object" && !Array.isArray(value);
299
+ }
300
+ function text(value) {
301
+ if (typeof value !== "string" || !value.trim())
302
+ throw new TypeError("Expected nonempty text");
303
+ return value;
304
+ }
305
+ function strings(value) {
306
+ if (!Array.isArray(value))
307
+ throw new TypeError("Expected string list");
308
+ return value.map(text);
309
+ }
310
+ async function runSqlImpactFile(path) {
311
+ const absolute = resolve(path), directory = dirname(absolute);
312
+ const raw = JSON.parse(await readFile(absolute, "utf8"));
313
+ const config = object(raw);
314
+ if (!Array.isArray(config.migrations))
315
+ throw new TypeError("Expected ordered migrations");
316
+ const sources = [];
317
+ for (const value of config.migrations) {
318
+ const entry = object(value);
319
+ sources.push({
320
+ id: text(entry.id),
321
+ owner: text(entry.owner),
322
+ sql: await readFile(resolve(directory, text(entry.path)), "utf8"),
323
+ ...entry.dependencies === undefined ? {} : { dependencies: strings(entry.dependencies) }
324
+ });
325
+ }
326
+ const baseline = config.baseline === undefined ? [] : (() => {
327
+ if (!Array.isArray(config.baseline))
328
+ throw new TypeError("Invalid baseline");
329
+ return config.baseline.map((value) => {
330
+ const entry = object(value), sha256 = text(entry.sha256);
331
+ if (!/^[a-f0-9]{64}$/.test(sha256))
332
+ throw new TypeError("Invalid baseline hash");
333
+ return { id: text(entry.id), sha256 };
334
+ });
335
+ })();
336
+ const edges = config.catalogEdges === undefined ? [] : (() => {
337
+ if (!Array.isArray(config.catalogEdges))
338
+ throw new TypeError("Invalid catalogEdges");
339
+ return config.catalogEdges.map((value) => {
340
+ const entry = object(value);
341
+ return { dependent: text(entry.dependent), dependency: text(entry.dependency) };
342
+ });
343
+ })();
344
+ const plan = await planSqlImpact(sources, baseline, edges);
345
+ const catalogReview = config.catalogReview === undefined ? [] : strings(config.catalogReview);
346
+ plan.review.push(...catalogReview);
347
+ if (config.catalogEdges === undefined)
348
+ plan.review.push("No live catalog snapshot supplied; verify deployed dependencies before applying");
349
+ const digest = createHash2("sha256").update(JSON.stringify(plan)).digest("hex");
350
+ const approved = config.approvedDigest === digest;
351
+ return { ...plan, digest, approved, ok: plan.errors.length === 0 && (plan.review.length === 0 || approved) };
352
+ }
353
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
354
+ const args = process.argv.slice(2);
355
+ try {
356
+ const path = args[0];
357
+ if (!path || path.startsWith("-") || args.slice(1).some((arg) => arg !== "--check")) {
358
+ throw new Error("Usage: supacloud-sql-impact <config.json> [--check]");
359
+ }
360
+ const result = await runSqlImpactFile(path);
361
+ console.log(JSON.stringify(result, null, 2));
362
+ if (args.includes("--check") && !result.ok)
363
+ process.exitCode = 1;
364
+ } catch (error) {
365
+ console.error(error instanceof Error ? error.message : "SQL impact analysis failed");
366
+ process.exitCode = 1;
367
+ }
368
+ }
369
+ export {
370
+ runSqlImpactFile
371
+ };