@rehearsal-db/core 1.1.0 → 1.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/COMPATIBILITY.md +31 -0
  3. package/README.md +7 -3
  4. package/dist/package.json +3 -1
  5. package/dist/src/cli/arguments.mjs +1 -0
  6. package/dist/src/cli/basic_command_handlers.mjs +4 -0
  7. package/dist/src/cli/editor_command_handlers.mjs +24 -0
  8. package/dist/src/runtime/migration_history.d.mts +5 -1
  9. package/dist/src/runtime/migration_history.mjs +24 -6
  10. package/dist/src/runtime/migration_impact.d.mts +125 -0
  11. package/dist/src/runtime/migration_impact.mjs +278 -0
  12. package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
  13. package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
  14. package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
  15. package/dist/src/runtime/migration_impact_plan.mjs +297 -0
  16. package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
  17. package/dist/src/runtime/migration_impact_queries.mjs +270 -0
  18. package/dist/src/runtime/migration_insights.d.mts +137 -0
  19. package/dist/src/runtime/migration_insights.mjs +143 -0
  20. package/dist/src/runtime/migration_insights_input.d.mts +13 -0
  21. package/dist/src/runtime/migration_insights_input.mjs +153 -0
  22. package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
  23. package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
  24. package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
  25. package/dist/src/runtime/migration_sql_scan.mjs +369 -0
  26. package/dist/src/runtime/plan.d.mts +2 -1
  27. package/dist/src/runtime/plan.mjs +4 -4
  28. package/dist/src/runtime/topology.d.mts +1 -1
  29. package/dist/src/runtime/topology.mjs +2 -2
  30. package/dist/src/shared/cancellation.mjs +1 -0
  31. package/dist/src/shared/held_path_ancestry.d.mts +7 -0
  32. package/dist/src/shared/held_path_ancestry.mjs +110 -0
  33. package/dist/src/shared/operation_guard.mjs +1 -0
  34. package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
  35. package/dist/src/targets/migration_impact_runtime.mjs +367 -0
  36. package/docs/README.md +20 -7
  37. package/docs/adapters.md +10 -0
  38. package/docs/architecture.md +75 -1
  39. package/docs/commands.md +169 -34
  40. package/docs/configuration.md +23 -5
  41. package/docs/index.md +1 -0
  42. package/docs/releasing.md +11 -9
  43. package/docs/roadmap.md +19 -0
  44. package/docs/runtime-lifecycle.md +10 -0
  45. package/docs/updating.md +73 -0
  46. package/docs/vscode.md +23 -17
  47. package/package.json +3 -1
@@ -0,0 +1,18 @@
1
+ /** Fixed catalog predicates. They inspect metadata, never evaluate stored SQL. */
2
+ type RelationAlias = "r" | "p";
3
+ /**
4
+ * Even COUNT planning opens indexes, including invalid ones, and may evaluate
5
+ * stored expressions. Permit only plain scalar bootstrap btree/hash metadata.
6
+ * Arrays/composites/domains are excluded because native support can delegate to
7
+ * their element/member operators. This is not protection against a compromised
8
+ * server or privileged replacement of PostgreSQL's bootstrap catalog contents.
9
+ */
10
+ export declare const nativeMigrationImpactIndexes: (alias: RelationAlias) => string;
11
+ /** PostgreSQL may const-fold expression statistics before consulting their data. */
12
+ export declare const nativeMigrationImpactStatistics: (alias: RelationAlias) => string;
13
+ /**
14
+ * Require one exact native default for each comparison AM. No binary-coercible
15
+ * fallback: varchar/bpchar are deliberately outside comparison recognition.
16
+ */
17
+ export declare const nativeMigrationImpactComparison: (alias: "t") => string;
18
+ export {};
@@ -0,0 +1,116 @@
1
+ const namespace = "(select oid from pg_catalog.pg_namespace where nspname = 'pg_catalog')";
2
+ const scalarTypes = "('bool', 'int2', 'int4', 'int8', 'numeric', 'float4', 'float8', 'text', 'varchar', 'bpchar', 'uuid', 'date', 'timestamp', 'timestamptz')";
3
+ const relationAlias = (value) => {
4
+ if (value !== "r" && value !== "p")
5
+ throw new Error("The fixed migration-impact catalog alias is unsupported.");
6
+ return value;
7
+ };
8
+ /** Only bootstrap functions; arbitrary objects merely named pg_catalog do not qualify. */
9
+ const nativeProcedure = (oid) => `exists (
10
+ select 1 from pg_catalog.pg_proc mi_proc
11
+ where mi_proc.oid = ${oid} and mi_proc.oid < 16384
12
+ and mi_proc.pronamespace = ${namespace}
13
+ and mi_proc.prokind = 'f' and mi_proc.prolang in (12, 13)
14
+ )`;
15
+ /** Fixed aliases mi_class/mi_am belong to the enclosing predicate's subquery. */
16
+ const nativeClass = () => `(
17
+ mi_class.oid < 16384 and mi_class.opcnamespace = ${namespace}
18
+ and mi_class.opcmethod = mi_am.oid
19
+ and mi_am.amtype = 'i'
20
+ and ((mi_am.oid = 403 and mi_am.amname = 'btree')
21
+ or (mi_am.oid = 405 and mi_am.amname = 'hash'))
22
+ and ${nativeProcedure("mi_am.amhandler")}
23
+ and exists (select 1 from pg_catalog.pg_opfamily mi_family
24
+ where mi_family.oid = mi_class.opcfamily and mi_family.oid < 16384
25
+ and mi_family.opfnamespace = ${namespace}
26
+ and mi_family.opfmethod = mi_am.oid
27
+ and not exists (select 1 from pg_catalog.pg_amop mi_member
28
+ where mi_member.amopfamily = mi_family.oid and not (
29
+ mi_member.oid < 16384 and mi_member.amopmethod = mi_am.oid
30
+ and exists (select 1 from pg_catalog.pg_operator mi_operator
31
+ where mi_operator.oid = mi_member.amopopr and mi_operator.oid < 16384
32
+ and mi_operator.oprnamespace = ${namespace}
33
+ and ${nativeProcedure("mi_operator.oprcode")}
34
+ and (mi_operator.oprrest = 0 or ${nativeProcedure("mi_operator.oprrest")})
35
+ and (mi_operator.oprjoin = 0 or ${nativeProcedure("mi_operator.oprjoin")}))
36
+ ))
37
+ and not exists (select 1 from pg_catalog.pg_amproc mi_support
38
+ where mi_support.amprocfamily = mi_family.oid and not (
39
+ mi_support.oid < 16384 and ${nativeProcedure("mi_support.amproc")}
40
+ ))
41
+ )
42
+ )`;
43
+ /**
44
+ * Even COUNT planning opens indexes, including invalid ones, and may evaluate
45
+ * stored expressions. Permit only plain scalar bootstrap btree/hash metadata.
46
+ * Arrays/composites/domains are excluded because native support can delegate to
47
+ * their element/member operators. This is not protection against a compromised
48
+ * server or privileged replacement of PostgreSQL's bootstrap catalog contents.
49
+ */
50
+ export const nativeMigrationImpactIndexes = (alias) => {
51
+ const owner = relationAlias(alias);
52
+ return `(not exists (
53
+ select 1 from pg_catalog.pg_index mi_index
54
+ where mi_index.indrelid = ${owner}.oid and (
55
+ mi_index.indexprs is null and mi_index.indpred is null
56
+ and mi_index.indnkeyatts > 0
57
+ and pg_catalog.array_length(mi_index.indkey::pg_catalog.int2[], 1) = mi_index.indnatts
58
+ and pg_catalog.array_length(mi_index.indclass::pg_catalog.oid[], 1) = mi_index.indnkeyatts
59
+ and pg_catalog.array_length(mi_index.indcollation::pg_catalog.oid[], 1) = mi_index.indnkeyatts
60
+ and exists (select 1 from pg_catalog.pg_class mi_index_relation
61
+ join pg_catalog.pg_am mi_am on mi_am.oid = mi_index_relation.relam
62
+ where mi_index_relation.oid = mi_index.indexrelid
63
+ and mi_index_relation.relkind = 'i' and not mi_index_relation.relispartition
64
+ and mi_am.amtype = 'i'
65
+ and ((mi_am.oid = 403 and mi_am.amname = 'btree')
66
+ or (mi_am.oid = 405 and mi_am.amname = 'hash'))
67
+ and ${nativeProcedure("mi_am.amhandler")}
68
+ and not exists (select 1 from pg_catalog.unnest(mi_index.indclass::pg_catalog.oid[]) mi_key(class_oid)
69
+ where not exists (select 1 from pg_catalog.pg_opclass mi_class
70
+ join pg_catalog.pg_type mi_input on mi_input.oid = mi_class.opcintype
71
+ where mi_class.oid = mi_key.class_oid
72
+ and mi_input.oid < 16384 and mi_input.typnamespace = ${namespace}
73
+ and mi_input.typbasetype = 0 and mi_input.typname in ${scalarTypes}
74
+ and ${nativeClass()}))
75
+ )
76
+ and not exists (select 1 from pg_catalog.unnest(mi_index.indkey::pg_catalog.int2[]) mi_key(attribute_number)
77
+ where not exists (select 1 from pg_catalog.pg_attribute mi_attribute
78
+ join pg_catalog.pg_type mi_type on mi_type.oid = mi_attribute.atttypid
79
+ where mi_attribute.attrelid = ${owner}.oid
80
+ and mi_attribute.attnum = mi_key.attribute_number and mi_attribute.attnum > 0
81
+ and not mi_attribute.attisdropped and mi_attribute.attgenerated in ('', 's')
82
+ and mi_type.oid < 16384 and mi_type.typnamespace = ${namespace}
83
+ and mi_type.typbasetype = 0 and mi_type.typname in ${scalarTypes}))
84
+ and not exists (select 1 from pg_catalog.unnest(mi_index.indcollation::pg_catalog.oid[]) mi_key(collation_oid)
85
+ where mi_key.collation_oid <> 0 and not exists (
86
+ select 1 from pg_catalog.pg_collation mi_collation
87
+ where mi_collation.oid = mi_key.collation_oid and mi_collation.oid < 16384
88
+ and mi_collation.collnamespace = ${namespace} and mi_collation.collisdeterministic))
89
+ ) is not true
90
+ ))`;
91
+ };
92
+ /** PostgreSQL may const-fold expression statistics before consulting their data. */
93
+ export const nativeMigrationImpactStatistics = (alias) => {
94
+ const owner = relationAlias(alias);
95
+ return `(not exists (select 1 from pg_catalog.pg_statistic_ext mi_statistics
96
+ where mi_statistics.stxrelid = ${owner}.oid and mi_statistics.stxexprs is not null))`;
97
+ };
98
+ /**
99
+ * Require one exact native default for each comparison AM. No binary-coercible
100
+ * fallback: varchar/bpchar are deliberately outside comparison recognition.
101
+ */
102
+ export const nativeMigrationImpactComparison = (alias) => {
103
+ if (alias !== "t")
104
+ throw new Error("The fixed migration-impact catalog alias is unsupported.");
105
+ return `(${[403, 405]
106
+ .map((method) => `(
107
+ (select pg_catalog.count(*) from pg_catalog.pg_opclass mi_default
108
+ where mi_default.opcintype = t.oid and mi_default.opcmethod = ${method}
109
+ and mi_default.opcdefault) = 1
110
+ and exists (select 1 from pg_catalog.pg_opclass mi_class
111
+ join pg_catalog.pg_am mi_am on mi_am.oid = mi_class.opcmethod
112
+ where mi_class.opcintype = t.oid and mi_class.opcdefault
113
+ and mi_am.oid = ${method} and ${nativeClass()})
114
+ )`)
115
+ .join(" and ")})`;
116
+ };
@@ -0,0 +1,47 @@
1
+ export interface MigrationImpactRelation {
2
+ readonly schema: string;
3
+ readonly name: string;
4
+ }
5
+ export type MigrationImpactUnknownReason = "unsupported_sql" | "malformed_sql" | "input_limit";
6
+ interface MigrationImpactTaskSource {
7
+ readonly relation: MigrationImpactRelation;
8
+ readonly line: number;
9
+ readonly column: number;
10
+ }
11
+ /** Private measurement inputs, never a public SQL/identifier payload. */
12
+ export type MigrationImpactTask = MigrationImpactTaskSource & ({
13
+ readonly metric: "rows";
14
+ readonly operation: "drop_table" | "truncate" | "broad_delete";
15
+ } | {
16
+ readonly metric: "populated";
17
+ readonly operation: "drop_column";
18
+ readonly columns: readonly [string];
19
+ } | {
20
+ readonly metric: "nulls";
21
+ readonly operation: "not_null";
22
+ readonly columns: readonly [string];
23
+ } | {
24
+ readonly metric: "duplicate_rows";
25
+ readonly operation: "new_constraint";
26
+ readonly columns: readonly string[];
27
+ } | {
28
+ readonly metric: "orphan_rows";
29
+ readonly operation: "new_constraint";
30
+ readonly columns: readonly string[];
31
+ readonly referencedRelation: MigrationImpactRelation;
32
+ readonly referencedColumns: readonly string[];
33
+ });
34
+ export interface MigrationImpactSqlPlan {
35
+ /** Coverage of the narrow recognizer, never SQL validity or a safe verdict. */
36
+ readonly complete: boolean;
37
+ readonly tasks: readonly MigrationImpactTask[];
38
+ readonly unknownReasons: readonly MigrationImpactUnknownReason[];
39
+ }
40
+ /**
41
+ * Plan narrow, current pre-migration snapshot measurements. No statement is
42
+ * executed; tasks do not predict earlier/later migration effects. The caller
43
+ * must validate the owned target, ordinary non-inherited/non-RLS tables and
44
+ * supported built-in column/operator semantics before using these private specs.
45
+ */
46
+ export declare const planMigrationImpactSql: (source: string) => MigrationImpactSqlPlan;
47
+ export {};
@@ -0,0 +1,297 @@
1
+ import { lexMigrationSql } from "./migration_sql_lexer.mjs";
2
+ const maximumTasks = 100;
3
+ // More columns are outside the supported grammar, not a whole-input limit.
4
+ const maximumColumns = 8;
5
+ const maximumIdentifierBytes = 63;
6
+ /**
7
+ * The shared lexer folds words to uppercase. Check the original ASCII span as
8
+ * well: Unicode uppercase expansions must not become supported identifiers or
9
+ * keywords. This walk is linear and uses the lexer's UTF-16/CRLF coordinates.
10
+ */
11
+ const asciiWords = (source, tokens) => {
12
+ const result = new Map();
13
+ let offset = 0;
14
+ let line = 1;
15
+ let column = 1;
16
+ const advance = () => {
17
+ const character = source[offset++];
18
+ if (character === "\r") {
19
+ if (source[offset] === "\n")
20
+ offset += 1;
21
+ line += 1;
22
+ column = 1;
23
+ }
24
+ else if (character === "\n") {
25
+ line += 1;
26
+ column = 1;
27
+ }
28
+ else
29
+ column += 1;
30
+ };
31
+ for (const token of tokens) {
32
+ while (offset < source.length &&
33
+ (line < token.line || (line === token.line && column < token.column)))
34
+ advance();
35
+ if (token.kind !== "word")
36
+ continue;
37
+ const start = offset;
38
+ if (!/[A-Za-z_]/u.test(source[offset] ?? ""))
39
+ continue;
40
+ while (/[A-Za-z0-9_$]/u.test(source[offset] ?? ""))
41
+ advance();
42
+ const raw = source.slice(start, offset);
43
+ if (raw.toUpperCase() === token.value)
44
+ result.set(token, raw.toLowerCase());
45
+ }
46
+ return result;
47
+ };
48
+ /** Exact token-shape matching, not a general PostgreSQL grammar/parser. */
49
+ class StatementReader {
50
+ tokens;
51
+ words;
52
+ index = 0;
53
+ constructor(tokens, words) {
54
+ this.tokens = tokens;
55
+ this.words = words;
56
+ }
57
+ get current() {
58
+ return this.tokens[this.index];
59
+ }
60
+ get done() {
61
+ return this.index === this.tokens.length;
62
+ }
63
+ word(value) {
64
+ const token = this.current;
65
+ if (!token || this.words.get(token) !== value.toLowerCase())
66
+ return false;
67
+ this.index += 1;
68
+ return true;
69
+ }
70
+ symbol(value) {
71
+ const token = this.current;
72
+ if (token?.kind !== "symbol" || token.value !== value)
73
+ return false;
74
+ this.index += 1;
75
+ return true;
76
+ }
77
+ identifier() {
78
+ const token = this.current;
79
+ const value = token && this.words.get(token);
80
+ if (!value || value.length > maximumIdentifierBytes)
81
+ return undefined;
82
+ this.index += 1;
83
+ return value;
84
+ }
85
+ relation() {
86
+ const schema = this.identifier();
87
+ if (!schema || !this.symbol("."))
88
+ return undefined;
89
+ const name = this.identifier();
90
+ return name ? { schema, name } : undefined;
91
+ }
92
+ columns() {
93
+ if (!this.symbol("("))
94
+ return undefined;
95
+ const columns = [];
96
+ do {
97
+ const column = this.identifier();
98
+ if (!column ||
99
+ columns.length >= maximumColumns ||
100
+ columns.includes(column))
101
+ return undefined;
102
+ columns.push(column);
103
+ } while (this.symbol(","));
104
+ return this.symbol(")") ? columns : undefined;
105
+ }
106
+ }
107
+ const recognizeStatement = (tokens, words) => {
108
+ const reader = new StatementReader(tokens, words);
109
+ const first = reader.current;
110
+ if (!first)
111
+ return undefined;
112
+ const position = { line: first.line, column: first.column };
113
+ if (reader.word("DROP")) {
114
+ if (!reader.word("TABLE"))
115
+ return undefined;
116
+ if (reader.word("IF") && !reader.word("EXISTS"))
117
+ return undefined;
118
+ const relation = reader.relation();
119
+ reader.word("RESTRICT");
120
+ return relation && reader.done
121
+ ? { ...position, relation, metric: "rows", operation: "drop_table" }
122
+ : undefined;
123
+ }
124
+ if (reader.word("TRUNCATE")) {
125
+ reader.word("TABLE");
126
+ reader.word("ONLY");
127
+ const relation = reader.relation();
128
+ if ((reader.word("RESTART") || reader.word("CONTINUE")) &&
129
+ !reader.word("IDENTITY"))
130
+ return undefined;
131
+ reader.word("RESTRICT");
132
+ return relation && reader.done
133
+ ? { ...position, relation, metric: "rows", operation: "truncate" }
134
+ : undefined;
135
+ }
136
+ if (reader.word("DELETE")) {
137
+ if (!reader.word("FROM"))
138
+ return undefined;
139
+ reader.word("ONLY");
140
+ const relation = reader.relation();
141
+ return relation && reader.done
142
+ ? { ...position, relation, metric: "rows", operation: "broad_delete" }
143
+ : undefined;
144
+ }
145
+ if (!reader.word("ALTER") || !reader.word("TABLE"))
146
+ return undefined;
147
+ reader.word("ONLY");
148
+ const relation = reader.relation();
149
+ if (!relation)
150
+ return undefined;
151
+ const action = reader.current;
152
+ if (!action)
153
+ return undefined;
154
+ if (reader.word("DROP")) {
155
+ reader.word("COLUMN");
156
+ if (reader.word("IF") && !reader.word("EXISTS"))
157
+ return undefined;
158
+ const column = reader.identifier();
159
+ reader.word("RESTRICT");
160
+ return column && reader.done
161
+ ? {
162
+ relation,
163
+ line: action.line,
164
+ column: action.column,
165
+ operation: "drop_column",
166
+ metric: "populated",
167
+ columns: [column],
168
+ }
169
+ : undefined;
170
+ }
171
+ if (reader.word("ALTER")) {
172
+ reader.word("COLUMN");
173
+ const column = reader.identifier();
174
+ if (!column || !reader.word("SET"))
175
+ return undefined;
176
+ const not = reader.current;
177
+ if (!not || !reader.word("NOT") || !reader.word("NULL") || !reader.done)
178
+ return undefined;
179
+ return {
180
+ relation,
181
+ line: not.line,
182
+ column: not.column,
183
+ operation: "not_null",
184
+ metric: "nulls",
185
+ columns: [column],
186
+ };
187
+ }
188
+ if (!reader.word("ADD"))
189
+ return undefined;
190
+ if (reader.word("CONSTRAINT") && !reader.identifier())
191
+ return undefined;
192
+ const constraintPosition = { line: action.line, column: action.column };
193
+ if (reader.word("UNIQUE")) {
194
+ const columns = reader.columns();
195
+ return columns && reader.done
196
+ ? {
197
+ ...constraintPosition,
198
+ relation,
199
+ operation: "new_constraint",
200
+ metric: "duplicate_rows",
201
+ columns,
202
+ }
203
+ : undefined;
204
+ }
205
+ if (!reader.word("FOREIGN") || !reader.word("KEY"))
206
+ return undefined;
207
+ const columns = reader.columns();
208
+ if (!columns || !reader.word("REFERENCES"))
209
+ return undefined;
210
+ const referencedRelation = reader.relation();
211
+ const referencedColumns = reader.columns();
212
+ if (reader.word("MATCH") && !reader.word("SIMPLE"))
213
+ return undefined;
214
+ return referencedRelation &&
215
+ referencedColumns &&
216
+ columns.length === referencedColumns.length &&
217
+ reader.done
218
+ ? {
219
+ ...constraintPosition,
220
+ relation,
221
+ operation: "new_constraint",
222
+ metric: "orphan_rows",
223
+ columns,
224
+ referencedRelation,
225
+ referencedColumns,
226
+ }
227
+ : undefined;
228
+ };
229
+ /**
230
+ * Plan narrow, current pre-migration snapshot measurements. No statement is
231
+ * executed; tasks do not predict earlier/later migration effects. The caller
232
+ * must validate the owned target, ordinary non-inherited/non-RLS tables and
233
+ * supported built-in column/operator semantics before using these private specs.
234
+ */
235
+ export const planMigrationImpactSql = (source) => {
236
+ const lexical = lexMigrationSql(source);
237
+ const reasons = new Set(lexical.unknownReasons);
238
+ const refusal = () => ({
239
+ complete: false,
240
+ tasks: [],
241
+ unknownReasons: [...reasons].sort(),
242
+ });
243
+ // Lexical ambiguity, malformed input and limits invalidate all boundaries.
244
+ if (reasons.size > 0 || lexical.ambiguousQuotes)
245
+ return refusal();
246
+ const words = asciiWords(source, lexical.tokens);
247
+ const statements = [];
248
+ let statement = [];
249
+ for (const token of lexical.tokens) {
250
+ if (token.kind === "symbol" && token.value === ";" && token.depth === 0) {
251
+ if (statement.length > 0)
252
+ statements.push(statement);
253
+ statement = [];
254
+ }
255
+ else
256
+ statement.push(token);
257
+ }
258
+ if (statement.length > 0)
259
+ statements.push(statement);
260
+ // SQL-standard routine bodies and explicit transactions need a real parser /
261
+ // transactional model. Never reinterpret their inner statements as top-level.
262
+ const transactionCommands = new Set([
263
+ "begin",
264
+ "start",
265
+ "commit",
266
+ "end",
267
+ "rollback",
268
+ "abort",
269
+ "savepoint",
270
+ "release",
271
+ "prepare",
272
+ ]);
273
+ if (lexical.tokens.some((token, index) => words.get(token) === "begin" &&
274
+ words.get(lexical.tokens[index + 1]) === "atomic") ||
275
+ statements.some((tokens) => transactionCommands.has(words.get(tokens[0]) ?? ""))) {
276
+ reasons.add("unsupported_sql");
277
+ return refusal();
278
+ }
279
+ const tasks = [];
280
+ for (const tokens of statements) {
281
+ const task = recognizeStatement(tokens, words);
282
+ if (!task)
283
+ reasons.add("unsupported_sql");
284
+ else {
285
+ if (tasks.length >= maximumTasks) {
286
+ reasons.add("input_limit");
287
+ return refusal();
288
+ }
289
+ tasks.push(task);
290
+ }
291
+ }
292
+ return {
293
+ complete: reasons.size === 0,
294
+ tasks,
295
+ unknownReasons: [...reasons].sort(),
296
+ };
297
+ };
@@ -0,0 +1,22 @@
1
+ /** Compile only fixed aggregate queries; migration SQL and identifiers stay private. */
2
+ import type { MigrationImpactTask } from "./migration_impact_plan.mjs";
3
+ export declare const MIGRATION_IMPACT_LIMITS: Readonly<{
4
+ maximumMeasurements: 100;
5
+ lockTimeoutMilliseconds: 1000;
6
+ statementTimeoutMilliseconds: 5000;
7
+ totalTimeoutMilliseconds: 30000;
8
+ maximumOutputBytes: 131072;
9
+ maximumScriptBytes: 262144;
10
+ }>;
11
+ export interface MigrationImpactCount {
12
+ readonly index: number;
13
+ readonly count: string | null;
14
+ readonly reason: "unsupported_runtime_metadata" | "unsupported_server_version" | "total_timeout" | null;
15
+ }
16
+ /** One consistent current-row snapshot. No pending migration statement is executed. */
17
+ export declare const buildMigrationImpactScript: (tasks: readonly MigrationImpactTask[], transactionTimeoutMilliseconds?: number) => string;
18
+ export declare const parseMigrationImpactBatchOutput: (output: string, expectedCount: number) => {
19
+ readonly snapshotAt: string | null;
20
+ readonly counts: readonly MigrationImpactCount[];
21
+ };
22
+ export declare const parseMigrationImpactOutput: (output: string, expectedCount: number) => readonly MigrationImpactCount[];