@supacloud/db 0.7.1 → 0.9.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.
package/dist/index.js CHANGED
@@ -1018,6 +1018,84 @@ ALTER TABLE supacloud_commands.execution_receipts
1018
1018
  DROP COLUMN IF EXISTS next_attempt_at,
1019
1019
  DROP COLUMN IF EXISTS recovery_attempts;
1020
1020
  `;
1021
+ // src/query.ts
1022
+ class QueryBoundaryError extends Error {
1023
+ code;
1024
+ constructor(code) {
1025
+ super(code);
1026
+ this.code = code;
1027
+ this.name = "QueryBoundaryError";
1028
+ }
1029
+ }
1030
+ function defineReadQuery(definition) {
1031
+ const contract = { ...definition };
1032
+ if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(contract.name) || contract.fingerprint !== undefined && !/^[a-f0-9]{16,64}$/.test(contract.fingerprint) || contract.slowMs !== undefined && (!Number.isFinite(contract.slowMs) || contract.slowMs < 0)) {
1033
+ throw new TypeError("Invalid query observation metadata");
1034
+ }
1035
+ const emit = (event) => {
1036
+ try {
1037
+ Promise.resolve(contract.observe?.(Object.freeze(event))).catch(() => {});
1038
+ } catch {}
1039
+ };
1040
+ const executeUnknown = async (identity, raw, signal) => {
1041
+ if (signal?.aborted)
1042
+ throw new QueryBoundaryError("QUERY_ABORTED");
1043
+ let input;
1044
+ try {
1045
+ input = contract.input(raw);
1046
+ } catch {
1047
+ throw new QueryBoundaryError("QUERY_INPUT_INVALID");
1048
+ }
1049
+ let decision;
1050
+ try {
1051
+ decision = await contract.authorize(identity, input);
1052
+ } catch {
1053
+ throw new QueryBoundaryError("QUERY_AUTHORIZATION_FAILED");
1054
+ }
1055
+ if (decision === "deny")
1056
+ throw new QueryBoundaryError("QUERY_REJECTED");
1057
+ if (decision !== "allow")
1058
+ throw new QueryBoundaryError("QUERY_AUTHORIZATION_FAILED");
1059
+ if (signal?.aborted)
1060
+ throw new QueryBoundaryError("QUERY_ABORTED");
1061
+ const started = performance.now();
1062
+ const metadata = { operation: contract.name, ...contract.fingerprint ? { fingerprint: contract.fingerprint } : {} };
1063
+ emit({ ...metadata, phase: "started" });
1064
+ try {
1065
+ const value = await contract.execute(input, identity, signal);
1066
+ if (signal?.aborted)
1067
+ throw new QueryBoundaryError("QUERY_ABORTED");
1068
+ let result;
1069
+ try {
1070
+ result = contract.result(value);
1071
+ } catch {
1072
+ throw new QueryBoundaryError("QUERY_RESULT_INVALID");
1073
+ }
1074
+ const durationMs = performance.now() - started;
1075
+ emit({ ...metadata, phase: "succeeded", durationMs, slow: durationMs >= (contract.slowMs ?? 500) });
1076
+ return result;
1077
+ } catch (error) {
1078
+ const durationMs = performance.now() - started;
1079
+ emit({ ...metadata, phase: "failed", durationMs, slow: durationMs >= (contract.slowMs ?? 500) });
1080
+ throw error;
1081
+ }
1082
+ };
1083
+ const execute = (identity, input, signal) => contract.policy ? contract.policy.execute((signal) => executeUnknown(identity, input, signal), signal) : executeUnknown(identity, input, signal);
1084
+ return Object.freeze({
1085
+ execute: (identity, input, signal) => execute(identity, input, signal),
1086
+ executeUnknown: execute
1087
+ });
1088
+ }
1089
+ async function decodePostgrestQuery(query, decode) {
1090
+ const response = await query;
1091
+ if (response.error !== null)
1092
+ throw new QueryBoundaryError("QUERY_TRANSPORT_FAILED");
1093
+ try {
1094
+ return decode(response.data);
1095
+ } catch {
1096
+ throw new QueryBoundaryError("QUERY_RESULT_INVALID");
1097
+ }
1098
+ }
1021
1099
  // src/command-adapter.ts
1022
1100
  import {
1023
1101
  CommandError,
@@ -1173,13 +1251,16 @@ export {
1173
1251
  COMMAND_PERSISTENCE_SQL,
1174
1252
  COMMAND_PERSISTENCE_UPGRADE_SQL,
1175
1253
  DatabaseAccessError,
1254
+ QueryBoundaryError,
1176
1255
  RpcContractError,
1177
1256
  applyModulePlan,
1178
1257
  buildDatabaseManifest,
1179
1258
  createDatabaseAccessBoundary,
1180
1259
  createPostgresCommandStore,
1181
1260
  createRpcClient,
1261
+ decodePostgrestQuery,
1182
1262
  defineDatabaseModule,
1263
+ defineReadQuery,
1183
1264
  defineRpcContract,
1184
1265
  explainObject,
1185
1266
  extractSearchPath,
@@ -40,9 +40,6 @@ export declare function renderMigrationBindings(options: {
40
40
  sql: string;
41
41
  }[];
42
42
  attestation: {
43
- environment: string;
44
- projectRef: string;
45
- schema: 'supacloud.migration-binding-attestation.v1';
46
43
  manifestSha256: string;
47
44
  files: {
48
45
  file: string;
@@ -53,5 +50,8 @@ export declare function renderMigrationBindings(options: {
53
50
  type: MigrationBindingType;
54
51
  }>;
55
52
  }[];
53
+ environment: string;
54
+ projectRef: string;
55
+ schema: "supacloud.migration-binding-attestation.v1";
56
56
  };
57
57
  };
@@ -0,0 +1,36 @@
1
+ export interface QueryEvent {
2
+ readonly operation: string;
3
+ readonly fingerprint?: string;
4
+ readonly phase: "started" | "succeeded" | "failed";
5
+ readonly durationMs?: number;
6
+ readonly slow?: boolean;
7
+ }
8
+ export type QueryObserver = (event: Readonly<QueryEvent>) => void | Promise<void>;
9
+ export interface ReadQueryDefinition<Input, Result, Identity> {
10
+ name: string;
11
+ /** Parser-derived SQL fingerprint or a generated operation hash; never raw SQL. */
12
+ fingerprint?: string;
13
+ input(value: unknown): Input;
14
+ result(value: unknown): Result;
15
+ authorize(identity: Identity, input: NoInfer<Input>): "allow" | "deny" | Promise<"allow" | "deny">;
16
+ execute(input: NoInfer<Input>, identity: Identity, signal?: AbortSignal): Promise<unknown>;
17
+ observe?: QueryObserver;
18
+ slowMs?: number;
19
+ policy?: {
20
+ execute<Value>(run: (signal: AbortSignal) => Promise<Value>, parent?: AbortSignal): Promise<Value>;
21
+ };
22
+ }
23
+ export declare class QueryBoundaryError extends Error {
24
+ readonly code: "QUERY_INPUT_INVALID" | "QUERY_REJECTED" | "QUERY_AUTHORIZATION_FAILED" | "QUERY_RESULT_INVALID" | "QUERY_ABORTED" | "QUERY_TRANSPORT_FAILED";
25
+ constructor(code: "QUERY_INPUT_INVALID" | "QUERY_REJECTED" | "QUERY_AUTHORIZATION_FAILED" | "QUERY_RESULT_INVALID" | "QUERY_ABORTED" | "QUERY_TRANSPORT_FAILED");
26
+ }
27
+ /** Explicit composition shared by SQL, pg_graphql and PostgREST. No DI, caching or SQL rewriting. */
28
+ export declare function defineReadQuery<Input, Result, Identity>(definition: ReadQueryDefinition<Input, Result, Identity>): Readonly<{
29
+ execute: (identity: Identity, input: Input, signal?: AbortSignal) => Promise<Result>;
30
+ executeUnknown: (identity: Identity, input: unknown, signal?: AbortSignal) => Promise<Result>;
31
+ }>;
32
+ /** Preserve the official client's selected/embedded result type; validate before returning it. */
33
+ export declare function decodePostgrestQuery<Query extends PromiseLike<{
34
+ data: unknown;
35
+ error: unknown;
36
+ }>>(query: Query, decode: (value: unknown) => NoInfer<Awaited<Query>["data"]>): Promise<Awaited<Query>["data"]>;
@@ -0,0 +1,47 @@
1
+ export interface SqlAnalysis {
2
+ fingerprint: string;
3
+ defines: string[];
4
+ touches: string[];
5
+ references: string[];
6
+ review: string[];
7
+ }
8
+ /** PostgreSQL 18 grammar, not regex-based SQL rewriting. Names group overloads conservatively. */
9
+ export declare function analyzeSql(sql: string): Promise<SqlAnalysis>;
10
+ /** Read-only admission check; PostgreSQL READ ONLY and least-privilege grants remain mandatory. */
11
+ export declare function assertReadSql(sql: string): Promise<string>;
12
+ export interface SqlMigrationSource {
13
+ id: string;
14
+ owner: string;
15
+ sql: string;
16
+ /** Object IDs from a reviewed catalog graph; never silently inferred for dynamic SQL. */
17
+ dependencies?: readonly string[];
18
+ }
19
+ export interface SqlDependencyEdge {
20
+ dependent: string;
21
+ dependency: string;
22
+ }
23
+ export interface SqlCatalogDependencies {
24
+ edges: SqlDependencyEdge[];
25
+ review: string[];
26
+ }
27
+ /** Collapse columns/overloads to their owning object for conservative impact propagation. */
28
+ export declare function readSqlDependencyGraph(executor: {
29
+ query(sql: string, parameters: unknown[]): Promise<unknown>;
30
+ }, schemas: readonly string[]): Promise<SqlCatalogDependencies>;
31
+ export interface SqlMigrationSnapshot {
32
+ id: string;
33
+ owner: string;
34
+ sha256: string;
35
+ analysis: SqlAnalysis;
36
+ }
37
+ export interface SqlImpactPlan {
38
+ migrations: SqlMigrationSnapshot[];
39
+ edges: SqlDependencyEdge[];
40
+ changed: string[];
41
+ affectedObjects: string[];
42
+ affectedMigrations: string[];
43
+ errors: string[];
44
+ review: string[];
45
+ }
46
+ /** Offline, append-only migration review. Does not execute SQL or approve a deployment. */
47
+ export declare function planSqlImpact(sources: readonly SqlMigrationSource[], baseline?: readonly Pick<SqlMigrationSnapshot, "id" | "sha256">[], catalogEdges?: readonly SqlDependencyEdge[]): Promise<SqlImpactPlan>;
@@ -0,0 +1,292 @@
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
+ function record(value) {
16
+ return value !== null && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ function walk(value, visit) {
19
+ if (Array.isArray(value)) {
20
+ for (const item of value)
21
+ walk(item, visit);
22
+ } else if (record(value)) {
23
+ visit(value);
24
+ for (const item of Object.values(value))
25
+ walk(item, visit);
26
+ }
27
+ }
28
+ function names(value) {
29
+ if (!Array.isArray(value))
30
+ return [];
31
+ return value.flatMap((part) => record(part) && record(part["String"]) && typeof part["String"]["sval"] === "string" ? [part["String"]["sval"]] : []);
32
+ }
33
+ async function analyzeSql(sql) {
34
+ const { fingerprint, parse } = await import("libpg-query");
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 { fingerprint, parse } = await import("libpg-query");
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
+ init_sql_analysis();
286
+
287
+ export {
288
+ analyzeSql,
289
+ assertReadSql,
290
+ planSqlImpact,
291
+ readSqlDependencyGraph
292
+ };
@@ -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
+ }>;