@rehearsal-db/core 1.0.2 → 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 (64) hide show
  1. package/CHANGELOG.md +56 -4
  2. package/COMPATIBILITY.md +41 -4
  3. package/README.md +8 -4
  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/project/configuration_contract.d.mts +1 -0
  9. package/dist/src/project/configuration_normalization.mjs +2 -1
  10. package/dist/src/project/configuration_rendering.mjs +3 -2
  11. package/dist/src/project/configuration_resolution.mjs +0 -2
  12. package/dist/src/project/project_detection.d.mts +2 -0
  13. package/dist/src/project/project_detection.mjs +1 -0
  14. package/dist/src/project/runtime_project_identity.d.mts +5 -0
  15. package/dist/src/project/runtime_project_identity.mjs +41 -0
  16. package/dist/src/project/setup.mjs +3 -1
  17. package/dist/src/runtime/migration_history.d.mts +5 -1
  18. package/dist/src/runtime/migration_history.mjs +24 -6
  19. package/dist/src/runtime/migration_impact.d.mts +125 -0
  20. package/dist/src/runtime/migration_impact.mjs +278 -0
  21. package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
  22. package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
  23. package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
  24. package/dist/src/runtime/migration_impact_plan.mjs +297 -0
  25. package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
  26. package/dist/src/runtime/migration_impact_queries.mjs +270 -0
  27. package/dist/src/runtime/migration_insights.d.mts +137 -0
  28. package/dist/src/runtime/migration_insights.mjs +143 -0
  29. package/dist/src/runtime/migration_insights_input.d.mts +13 -0
  30. package/dist/src/runtime/migration_insights_input.mjs +153 -0
  31. package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
  32. package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
  33. package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
  34. package/dist/src/runtime/migration_sql_scan.mjs +369 -0
  35. package/dist/src/runtime/plan.d.mts +2 -1
  36. package/dist/src/runtime/plan.mjs +4 -4
  37. package/dist/src/runtime/policy.d.mts +2 -0
  38. package/dist/src/runtime/policy.mjs +13 -1
  39. package/dist/src/runtime/privileges.d.mts +25 -0
  40. package/dist/src/runtime/privileges.mjs +198 -0
  41. package/dist/src/runtime/topology.d.mts +1 -1
  42. package/dist/src/runtime/topology.mjs +2 -2
  43. package/dist/src/shared/cancellation.mjs +1 -0
  44. package/dist/src/shared/held_path_ancestry.d.mts +7 -0
  45. package/dist/src/shared/held_path_ancestry.mjs +110 -0
  46. package/dist/src/shared/operation_guard.mjs +1 -0
  47. package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
  48. package/dist/src/targets/migration_impact_runtime.mjs +367 -0
  49. package/dist/src/targets/supabase.mjs +1 -0
  50. package/docs/README.md +20 -7
  51. package/docs/adapters.md +10 -0
  52. package/docs/architecture.md +75 -1
  53. package/docs/commands.md +169 -34
  54. package/docs/configuration.md +42 -9
  55. package/docs/getting-started.md +4 -4
  56. package/docs/index.md +1 -0
  57. package/docs/releasing.md +15 -11
  58. package/docs/roadmap.md +49 -9
  59. package/docs/runtime-lifecycle.md +15 -4
  60. package/docs/runtime-policies.md +145 -0
  61. package/docs/stability.md +8 -4
  62. package/docs/updating.md +73 -0
  63. package/docs/vscode.md +30 -19
  64. package/package.json +3 -1
@@ -0,0 +1,278 @@
1
+ /** Bind aggregate-only sandbox measurements to exact reviewed pending inputs. */
2
+ import { createHash } from "node:crypto";
3
+ import { realpath } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+ import { verifyActiveBaseline } from "../baseline/artifact.mjs";
7
+ import { REHEARSAL_VERSION, RehearsalError } from "../shared/diagnostics.mjs";
8
+ import { getOperationCancellationSignal, throwIfOperationCancelled, } from "../shared/cancellation.mjs";
9
+ import { inspectRehearsalMigrations } from "./plan.mjs";
10
+ import { inspectRuntimeTopologyCandidates } from "./topology.mjs";
11
+ import { createMigrationInsightsInput, MIGRATION_INSIGHTS_LIMITS, } from "./migration_insights_input.mjs";
12
+ import { planMigrationImpactSql, } from "./migration_impact_plan.mjs";
13
+ import { buildMigrationImpactScript, parseMigrationImpactBatchOutput, MIGRATION_IMPACT_LIMITS, } from "./migration_impact_queries.mjs";
14
+ import { openMigrationImpactRuntime, MigrationImpactRuntimeError, } from "../targets/migration_impact_runtime.mjs";
15
+ const refusal = (message, unsafe = false) => new RehearsalError({
16
+ category: unsafe ? "unsafe_environment" : "configuration_invalid",
17
+ code: unsafe ? "UNSAFE_ENVIRONMENT" : "CONFIGURATION_INVALID",
18
+ message,
19
+ refused: "No migration was applied, no runtime was started or repaired, and no row contents are returned.",
20
+ suggestions: [
21
+ "Review a fresh impact plan and explicitly select its already-running disposable local target.",
22
+ ],
23
+ });
24
+ const prepare = async (options, targetName) => {
25
+ if (typeof targetName !== "string" || !targetName || targetName.length > 128)
26
+ throw refusal("Impact review requires one explicit configured --target name, including primary.");
27
+ const projectRoot = await realpath(resolve(options.projectRoot ?? process.cwd()));
28
+ const input = createMigrationInsightsInput(projectRoot);
29
+ const candidates = await inspectRuntimeTopologyCandidates({ ...options, projectRoot }, (selected) => inspectRehearsalMigrations(selected, input.readInventory));
30
+ if (candidates.candidateCount > MIGRATION_INSIGHTS_LIMITS.maximumCandidates)
31
+ throw refusal("Impact review exceeds its bounded pending-migration input.");
32
+ const target = candidates.targets.find((entry) => entry.name === targetName);
33
+ if (!target)
34
+ throw refusal("Impact review requires one target from this project's configuration.");
35
+ let remaining = MIGRATION_IMPACT_LIMITS.maximumMeasurements;
36
+ const entries = target.summary.candidates.map((candidate) => {
37
+ const source = input.sourceFor(join(target.paths.migrationDirectory, candidate.filename), candidate.sha256);
38
+ if (source === undefined)
39
+ throw refusal("The exact impact-review input could not be retained. Refresh the plan.");
40
+ const plan = planMigrationImpactSql(source);
41
+ const tasks = plan.tasks.slice(0, remaining);
42
+ remaining -= tasks.length;
43
+ const unknownReasons = [
44
+ ...new Set([
45
+ ...plan.unknownReasons,
46
+ ...(tasks.length < plan.tasks.length ? ["input_limit"] : []),
47
+ ]),
48
+ ];
49
+ return {
50
+ filename: candidate.filename,
51
+ sha256: candidate.sha256,
52
+ tasks,
53
+ unknownReasons,
54
+ };
55
+ });
56
+ const baseline = await verifyActiveBaseline({
57
+ artifactRoot: target.paths.artifactDirectory,
58
+ });
59
+ if (baseline.generationId !== target.summary.baselineGenerationId)
60
+ throw refusal("The active baseline changed during impact review. Refresh the plan.");
61
+ const binding = createHash("sha256")
62
+ .update(JSON.stringify({
63
+ candidateSha256: candidates.candidateSha256,
64
+ target: target.name,
65
+ config: target.config,
66
+ paths: target.paths,
67
+ baseline,
68
+ inputs: entries.map((entry) => ({
69
+ filename: entry.filename,
70
+ sha256: entry.sha256,
71
+ })),
72
+ }))
73
+ .digest("hex");
74
+ return {
75
+ projectRoot,
76
+ target,
77
+ baseline,
78
+ entries,
79
+ binding,
80
+ candidateSha256: candidates.candidateSha256,
81
+ candidateCount: candidates.candidateCount,
82
+ };
83
+ };
84
+ const reportFor = ({ prepared, measured = false, counts, reason, snapshotAt = null, runtimeIdentitySha256 = null, contactedDatabase = false, }) => {
85
+ let index = 0;
86
+ let measuredCount = 0;
87
+ let unknownMeasurementCount = 0;
88
+ let unknownEntryCount = 0;
89
+ const entries = prepared.entries.map((entry) => {
90
+ const measurements = entry.tasks.map((task) => {
91
+ const count = counts?.[index++];
92
+ const unknownReason = reason ?? count?.reason ?? null;
93
+ const evidence = !measured
94
+ ? "planned"
95
+ : count?.count !== null &&
96
+ count?.count !== undefined &&
97
+ unknownReason === null
98
+ ? "measured_current_snapshot"
99
+ : "unknown";
100
+ if (evidence === "measured_current_snapshot")
101
+ measuredCount++;
102
+ if (evidence === "unknown")
103
+ unknownMeasurementCount++;
104
+ return {
105
+ operation: task.operation,
106
+ metric: task.metric,
107
+ line: task.line,
108
+ column: task.column,
109
+ evidence,
110
+ count: evidence === "measured_current_snapshot" ? count.count : null,
111
+ unknownReason: evidence === "unknown" ? (unknownReason ?? "query_failed") : null,
112
+ };
113
+ });
114
+ if (entry.unknownReasons.length ||
115
+ measurements.some((entry) => entry.evidence === "unknown"))
116
+ unknownEntryCount++;
117
+ return {
118
+ filename: entry.filename,
119
+ sha256: entry.sha256,
120
+ coverage: entry.unknownReasons.length
121
+ ? "incomplete"
122
+ : "recognized_forms",
123
+ unknownReasons: entry.unknownReasons,
124
+ measurements,
125
+ };
126
+ });
127
+ const plannedCount = entries.reduce((total, entry) => total + entry.measurements.length, 0);
128
+ const measurementStatus = !measured
129
+ ? "planned"
130
+ : unknownEntryCount
131
+ ? measuredCount
132
+ ? "partial"
133
+ : "unknown"
134
+ : plannedCount
135
+ ? "measured"
136
+ : "not_required";
137
+ return {
138
+ schemaVersion: 1,
139
+ inspectorVersion: REHEARSAL_VERSION,
140
+ candidateSha256: prepared.candidateSha256,
141
+ candidateCount: prepared.candidateCount,
142
+ target: {
143
+ name: prepared.target.name,
144
+ provider: prepared.target.config.runtime.target,
145
+ baselineGenerationId: prepared.baseline.generationId,
146
+ candidateSha256: prepared.target.summary.candidateSha256,
147
+ },
148
+ mode: measured ? "measurement" : "preview",
149
+ measurementStatus,
150
+ snapshotAt,
151
+ runtimeIdentitySha256,
152
+ entries,
153
+ summary: {
154
+ plannedCount,
155
+ measuredCount,
156
+ unknownMeasurementCount,
157
+ unknownEntryCount,
158
+ },
159
+ limits: MIGRATION_IMPACT_LIMITS,
160
+ safety: {
161
+ appliesMigrations: false,
162
+ startsOrRepairsRuntime: false,
163
+ returnsRowContents: false,
164
+ returnsSqlOrObjectNames: false,
165
+ contactsDatabase: contactedDatabase,
166
+ transaction: measured && snapshotAt !== null
167
+ ? "repeatable_read_read_only"
168
+ : null,
169
+ },
170
+ limitation: "Counts describe one current disposable-sandbox snapshot before any pending migration. They do not simulate sequential migration effects, establish SQL validity or safety, measure cascade/trigger effects, or predict production locks and performance. Unsupported objects, expressions and incomplete evidence stay unknown.",
171
+ };
172
+ };
173
+ export const inspectMigrationImpact = async (options = {}, targetName) => reportFor({ prepared: await prepare(options, targetName) });
174
+ export const measureMigrationImpact = async ({ options = {}, targetName, confirmation, }, implementation = {}) => {
175
+ const controller = new AbortController();
176
+ const externalSignal = getOperationCancellationSignal();
177
+ const signal = externalSignal
178
+ ? AbortSignal.any([externalSignal, controller.signal])
179
+ : controller.signal;
180
+ const deadline = performance.now() + MIGRATION_IMPACT_LIMITS.totalTimeoutMilliseconds;
181
+ let timedOut = false;
182
+ const timer = setTimeout(() => {
183
+ timedOut = true;
184
+ controller.abort();
185
+ }, MIGRATION_IMPACT_LIMITS.totalTimeoutMilliseconds);
186
+ const checkpoint = () => {
187
+ throwIfOperationCancelled();
188
+ if (timedOut || performance.now() >= deadline) {
189
+ timedOut = true;
190
+ controller.abort();
191
+ throw new MigrationImpactRuntimeError("cancelled");
192
+ }
193
+ };
194
+ try {
195
+ const prepared = await prepare(options, targetName);
196
+ if (confirmation !== prepared.candidateSha256)
197
+ throw refusal("Measurement requires the exact whole-plan --confirm-candidates digest from a fresh impact review.");
198
+ const tasks = prepared.entries.flatMap((entry) => entry.tasks);
199
+ let contactedDatabase = false;
200
+ try {
201
+ checkpoint();
202
+ if (!tasks.length)
203
+ return reportFor({ prepared, measured: true });
204
+ const runtime = await (implementation.openRuntime ?? openMigrationImpactRuntime)({
205
+ projectRoot: prepared.projectRoot,
206
+ target: prepared.target,
207
+ baseline: prepared.baseline,
208
+ signal,
209
+ });
210
+ checkpoint();
211
+ try {
212
+ buildMigrationImpactScript(tasks);
213
+ }
214
+ catch {
215
+ return reportFor({ prepared, measured: true, reason: "input_limit" });
216
+ }
217
+ contactedDatabase = true;
218
+ const result = parseMigrationImpactBatchOutput(await runtime.runSql(() => {
219
+ checkpoint();
220
+ const script = buildMigrationImpactScript(tasks, Math.max(1, Math.floor(deadline - performance.now())));
221
+ checkpoint();
222
+ return script;
223
+ }), tasks.length);
224
+ checkpoint();
225
+ await runtime.assertCurrent();
226
+ checkpoint();
227
+ const current = await prepare(options, targetName);
228
+ checkpoint();
229
+ if (prepared.binding !== current.binding)
230
+ throw refusal("The reviewed configuration, baseline or migration inputs changed during measurement. No counts are returned.");
231
+ return reportFor({
232
+ prepared,
233
+ measured: true,
234
+ counts: result.counts,
235
+ snapshotAt: result.snapshotAt,
236
+ runtimeIdentitySha256: runtime.identitySha256,
237
+ contactedDatabase,
238
+ });
239
+ }
240
+ catch (error) {
241
+ throwIfOperationCancelled();
242
+ if (error instanceof RehearsalError)
243
+ throw error;
244
+ if (error instanceof MigrationImpactRuntimeError &&
245
+ ["runtime_unavailable", "ownership_mismatch", "stale_runtime"].includes(error.reason))
246
+ throw refusal("The selected sandbox is not a verified, unchanged running local runtime. No counts are returned.", true);
247
+ const reason = timedOut ||
248
+ (error instanceof MigrationImpactRuntimeError &&
249
+ error.reason === "transaction_timeout")
250
+ ? "total_timeout"
251
+ : error instanceof MigrationImpactRuntimeError &&
252
+ ["lock_timeout", "statement_timeout"].includes(error.reason)
253
+ ? error.reason
254
+ : "query_failed";
255
+ return reportFor({ prepared, measured: true, reason, contactedDatabase });
256
+ }
257
+ }
258
+ finally {
259
+ clearTimeout(timer);
260
+ controller.abort();
261
+ }
262
+ };
263
+ export const renderMigrationImpact = (report) => [
264
+ `MIGRATION DATA IMPACT — ${report.measurementStatus.toUpperCase()}`,
265
+ `${report.target.name}: ${report.summary.measuredCount} measured / ${report.summary.plannedCount} planned aggregates; ${report.summary.unknownEntryCount} incomplete files`,
266
+ `Exact whole-plan digest: ${report.candidateSha256}`,
267
+ ...(report.snapshotAt
268
+ ? [`Current-row snapshot: ${report.snapshotAt}`]
269
+ : []),
270
+ ...report.entries.flatMap((entry) => [
271
+ entry.filename,
272
+ ...entry.measurements.map((metric) => ` ${metric.line}:${metric.column} ${metric.metric}: ${metric.count ?? metric.evidence} ${metric.unknownReason ?? ""}`.trimEnd()),
273
+ ...(entry.unknownReasons.length
274
+ ? [" Unsupported or bounded SQL remains unknown."]
275
+ : []),
276
+ ]),
277
+ report.limitation,
278
+ ].join("\n");
@@ -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 {};