@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.
- package/CHANGELOG.md +56 -4
- package/COMPATIBILITY.md +41 -4
- package/README.md +8 -4
- package/dist/package.json +3 -1
- package/dist/src/cli/arguments.mjs +1 -0
- package/dist/src/cli/basic_command_handlers.mjs +4 -0
- package/dist/src/cli/editor_command_handlers.mjs +24 -0
- package/dist/src/project/configuration_contract.d.mts +1 -0
- package/dist/src/project/configuration_normalization.mjs +2 -1
- package/dist/src/project/configuration_rendering.mjs +3 -2
- package/dist/src/project/configuration_resolution.mjs +0 -2
- package/dist/src/project/project_detection.d.mts +2 -0
- package/dist/src/project/project_detection.mjs +1 -0
- package/dist/src/project/runtime_project_identity.d.mts +5 -0
- package/dist/src/project/runtime_project_identity.mjs +41 -0
- package/dist/src/project/setup.mjs +3 -1
- package/dist/src/runtime/migration_history.d.mts +5 -1
- package/dist/src/runtime/migration_history.mjs +24 -6
- package/dist/src/runtime/migration_impact.d.mts +125 -0
- package/dist/src/runtime/migration_impact.mjs +278 -0
- package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
- package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
- package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
- package/dist/src/runtime/migration_impact_plan.mjs +297 -0
- package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
- package/dist/src/runtime/migration_impact_queries.mjs +270 -0
- package/dist/src/runtime/migration_insights.d.mts +137 -0
- package/dist/src/runtime/migration_insights.mjs +143 -0
- package/dist/src/runtime/migration_insights_input.d.mts +13 -0
- package/dist/src/runtime/migration_insights_input.mjs +153 -0
- package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
- package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
- package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
- package/dist/src/runtime/migration_sql_scan.mjs +369 -0
- package/dist/src/runtime/plan.d.mts +2 -1
- package/dist/src/runtime/plan.mjs +4 -4
- package/dist/src/runtime/policy.d.mts +2 -0
- package/dist/src/runtime/policy.mjs +13 -1
- package/dist/src/runtime/privileges.d.mts +25 -0
- package/dist/src/runtime/privileges.mjs +198 -0
- package/dist/src/runtime/topology.d.mts +1 -1
- package/dist/src/runtime/topology.mjs +2 -2
- package/dist/src/shared/cancellation.mjs +1 -0
- package/dist/src/shared/held_path_ancestry.d.mts +7 -0
- package/dist/src/shared/held_path_ancestry.mjs +110 -0
- package/dist/src/shared/operation_guard.mjs +1 -0
- package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
- package/dist/src/targets/migration_impact_runtime.mjs +367 -0
- package/dist/src/targets/supabase.mjs +1 -0
- package/docs/README.md +20 -7
- package/docs/adapters.md +10 -0
- package/docs/architecture.md +75 -1
- package/docs/commands.md +169 -34
- package/docs/configuration.md +42 -9
- package/docs/getting-started.md +4 -4
- package/docs/index.md +1 -0
- package/docs/releasing.md +15 -11
- package/docs/roadmap.md +49 -9
- package/docs/runtime-lifecycle.md +15 -4
- package/docs/runtime-policies.md +145 -0
- package/docs/stability.md +8 -4
- package/docs/updating.md +73 -0
- package/docs/vscode.md +30 -19
- package/package.json +3 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/** Private non-starting transport for one explicitly selected owned local runtime. */
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { isLocalDockerEndpoint } from "../runtime/local_docker_context.mjs";
|
|
7
|
+
import { diagnoseMigrationFailure } from "../runtime/migration_failure.mjs";
|
|
8
|
+
import { holdPathAncestry } from "../shared/held_path_ancestry.mjs";
|
|
9
|
+
import { runOwnedProcess } from "../shared/owned_process.mjs";
|
|
10
|
+
import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
|
|
11
|
+
import { assertDockerResourceLabels } from "./docker_resource_ownership.mjs";
|
|
12
|
+
export class MigrationImpactRuntimeError extends Error {
|
|
13
|
+
reason;
|
|
14
|
+
constructor(reason) {
|
|
15
|
+
super("The selected local runtime could not complete bounded read-only inspection; private output withheld.");
|
|
16
|
+
this.reason = reason;
|
|
17
|
+
this.name = "MigrationImpactRuntimeError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const INSPECTION_BYTES = 65536;
|
|
21
|
+
const SQL_INPUT_BYTES = 262144;
|
|
22
|
+
const SQL_OUTPUT_BYTES = 131072;
|
|
23
|
+
function fail(reason) {
|
|
24
|
+
throw new MigrationImpactRuntimeError(reason);
|
|
25
|
+
}
|
|
26
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
const object = (value) => {
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
29
|
+
fail("runtime_unavailable");
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
const inside = (root, path) => {
|
|
33
|
+
const child = relative(root, path);
|
|
34
|
+
return (child !== "" &&
|
|
35
|
+
child !== ".." &&
|
|
36
|
+
!child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) &&
|
|
37
|
+
!isAbsolute(child));
|
|
38
|
+
};
|
|
39
|
+
const CONTAINER_FORMAT = '{"id":{{json .Id}},"name":{{json .Name}},"image":{{json .Config.Image}},"imageId":{{json .Image}},"labels":{{json .Config.Labels}},"running":{{json .State.Running}},"ports":{{json .NetworkSettings.Ports}}}';
|
|
40
|
+
/** No lifecycle, adapter, source-reader, URL or credential-file access is permitted here. */
|
|
41
|
+
export async function openMigrationImpactRuntime(options) {
|
|
42
|
+
const { target, baseline } = options;
|
|
43
|
+
const signal = options.signal ?? AbortSignal.timeout(30000);
|
|
44
|
+
const run = options.runProcessImplementation ?? runOwnedProcess;
|
|
45
|
+
const cancelled = () => {
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
fail("cancelled");
|
|
48
|
+
};
|
|
49
|
+
cancelled();
|
|
50
|
+
const provider = target.config.runtime.target;
|
|
51
|
+
const projectId = target.config.runtime.projectId;
|
|
52
|
+
const postgres = provider === "postgresql" ? target.config.postgresql : null;
|
|
53
|
+
const databasePort = target.config.runtime.ports.database;
|
|
54
|
+
if (!["postgresql", "supabase"].includes(provider) ||
|
|
55
|
+
(provider === "postgresql" && !postgres))
|
|
56
|
+
fail("ownership_mismatch");
|
|
57
|
+
const dataSha256 = baseline.files["sanitized-data.ndjson"]?.sha256;
|
|
58
|
+
if (!/^[a-f0-9]{64}$/u.test(dataSha256 ?? "") ||
|
|
59
|
+
!/^\d{8}T\d{6}Z-[a-f0-9]{12}$/u.test(baseline.generationId))
|
|
60
|
+
fail("stale_runtime");
|
|
61
|
+
let root, receiptDirectory;
|
|
62
|
+
try {
|
|
63
|
+
root = await realpath(resolve(options.projectRoot));
|
|
64
|
+
const lexicalRoot = resolve(options.projectRoot), configured = resolve(target.paths.runtimeWorkdir);
|
|
65
|
+
receiptDirectory = inside(lexicalRoot, configured)
|
|
66
|
+
? resolve(root, relative(lexicalRoot, configured))
|
|
67
|
+
: configured;
|
|
68
|
+
if (!inside(root, receiptDirectory) ||
|
|
69
|
+
(await realpath(receiptDirectory)) !== receiptDirectory)
|
|
70
|
+
fail("ownership_mismatch");
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
fail("ownership_mismatch");
|
|
74
|
+
}
|
|
75
|
+
const readReceipt = async () => {
|
|
76
|
+
cancelled();
|
|
77
|
+
let ancestry;
|
|
78
|
+
try {
|
|
79
|
+
ancestry = await holdPathAncestry(receiptDirectory);
|
|
80
|
+
await ancestry.assertUnchanged();
|
|
81
|
+
const path = join(ancestry.path, "baseline.json"), leaf = await lstat(path, { bigint: true });
|
|
82
|
+
if (!leaf.isFile() ||
|
|
83
|
+
leaf.isSymbolicLink() ||
|
|
84
|
+
leaf.size > BigInt(INSPECTION_BYTES) ||
|
|
85
|
+
(leaf.mode & 63n) !== 0n ||
|
|
86
|
+
(typeof process.getuid === "function" &&
|
|
87
|
+
leaf.uid !== BigInt(process.getuid())))
|
|
88
|
+
fail("stale_runtime");
|
|
89
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
90
|
+
try {
|
|
91
|
+
const before = await handle.stat({ bigint: true });
|
|
92
|
+
if (!before.isFile() ||
|
|
93
|
+
before.dev !== leaf.dev ||
|
|
94
|
+
before.ino !== leaf.ino ||
|
|
95
|
+
before.size !== leaf.size ||
|
|
96
|
+
before.mtimeNs !== leaf.mtimeNs ||
|
|
97
|
+
before.ctimeNs !== leaf.ctimeNs)
|
|
98
|
+
fail("stale_runtime");
|
|
99
|
+
const bytes = Buffer.alloc(Number(before.size) + 1);
|
|
100
|
+
let length = 0;
|
|
101
|
+
while (length < bytes.length) {
|
|
102
|
+
const next = await handle.read(bytes, length, bytes.length - length, length);
|
|
103
|
+
if (!next.bytesRead)
|
|
104
|
+
break;
|
|
105
|
+
length += next.bytesRead;
|
|
106
|
+
}
|
|
107
|
+
const after = await handle.stat({ bigint: true }), current = await lstat(path, { bigint: true });
|
|
108
|
+
if (BigInt(length) !== before.size ||
|
|
109
|
+
!current.isFile() ||
|
|
110
|
+
current.isSymbolicLink() ||
|
|
111
|
+
[after, current].some((stat) => stat.dev !== before.dev ||
|
|
112
|
+
stat.ino !== before.ino ||
|
|
113
|
+
stat.size !== before.size ||
|
|
114
|
+
stat.mtimeNs !== before.mtimeNs ||
|
|
115
|
+
stat.ctimeNs !== before.ctimeNs))
|
|
116
|
+
fail("stale_runtime");
|
|
117
|
+
await ancestry.assertUnchanged();
|
|
118
|
+
const contents = bytes.subarray(0, length);
|
|
119
|
+
const marker = object(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(contents)));
|
|
120
|
+
if (marker.target !== provider ||
|
|
121
|
+
marker.generationId !== baseline.generationId ||
|
|
122
|
+
marker.dataSha256 !== dataSha256 ||
|
|
123
|
+
(postgres
|
|
124
|
+
? marker.formatVersion !== 1 ||
|
|
125
|
+
marker.image !== postgres.image ||
|
|
126
|
+
typeof marker.databasePassword !== "string" ||
|
|
127
|
+
marker.databasePassword.length < 32
|
|
128
|
+
: marker.formatVersion !== baseline.formatVersion ||
|
|
129
|
+
marker.schemaSha256 !==
|
|
130
|
+
(baseline.files["production-schema.sql"]?.sha256 ?? null)))
|
|
131
|
+
fail("stale_runtime");
|
|
132
|
+
return {
|
|
133
|
+
sha256: digest(contents),
|
|
134
|
+
inode: `${before.dev}:${before.ino}:${before.ctimeNs}:${before.mtimeNs}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await handle.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof MigrationImpactRuntimeError)
|
|
143
|
+
throw error;
|
|
144
|
+
fail("stale_runtime");
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
try {
|
|
148
|
+
await ancestry?.close();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
cancelled();
|
|
152
|
+
fail("stale_runtime");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const initialReceipt = await readReceipt();
|
|
157
|
+
let endpoint;
|
|
158
|
+
const invoke = async (args, input) => {
|
|
159
|
+
cancelled();
|
|
160
|
+
const maximum = input === undefined ? INSPECTION_BYTES : SQL_OUTPUT_BYTES;
|
|
161
|
+
try {
|
|
162
|
+
const result = await run({
|
|
163
|
+
command: "docker",
|
|
164
|
+
args,
|
|
165
|
+
cwd: root,
|
|
166
|
+
env: createCleanProcessEnvironment({
|
|
167
|
+
overrides: { DOCKER_HOST: endpoint },
|
|
168
|
+
}),
|
|
169
|
+
signal,
|
|
170
|
+
stdin: "ignore",
|
|
171
|
+
...(input === undefined ? {} : { input }),
|
|
172
|
+
maxOutputBytes: maximum,
|
|
173
|
+
cancellationGraceMs: 1000,
|
|
174
|
+
});
|
|
175
|
+
cancelled();
|
|
176
|
+
if (typeof result.stdout !== "string" ||
|
|
177
|
+
typeof result.stderr !== "string" ||
|
|
178
|
+
!Number.isSafeInteger(result.stdoutByteCount) ||
|
|
179
|
+
!Number.isSafeInteger(result.stderrByteCount) ||
|
|
180
|
+
result.stdoutByteCount < Buffer.byteLength(result.stdout) ||
|
|
181
|
+
result.stderrByteCount < Buffer.byteLength(result.stderr) ||
|
|
182
|
+
result.stdoutByteCount > maximum ||
|
|
183
|
+
result.stderrByteCount > INSPECTION_BYTES ||
|
|
184
|
+
(input === undefined &&
|
|
185
|
+
result.stdoutByteCount + result.stderrByteCount > INSPECTION_BYTES))
|
|
186
|
+
fail(input === undefined ? "runtime_unavailable" : "query_failed");
|
|
187
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
188
|
+
if (input !== undefined) {
|
|
189
|
+
const code = diagnoseMigrationFailure({
|
|
190
|
+
provider: "postgresql",
|
|
191
|
+
stderr: result.stderr,
|
|
192
|
+
}).sqlState;
|
|
193
|
+
if (code === "55P03")
|
|
194
|
+
fail("lock_timeout");
|
|
195
|
+
if (code === "57014")
|
|
196
|
+
fail("statement_timeout");
|
|
197
|
+
if (code === "25P04")
|
|
198
|
+
fail("transaction_timeout");
|
|
199
|
+
}
|
|
200
|
+
fail(input === undefined ? "runtime_unavailable" : "query_failed");
|
|
201
|
+
}
|
|
202
|
+
return result.stdout;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
cancelled();
|
|
206
|
+
if (error instanceof MigrationImpactRuntimeError)
|
|
207
|
+
throw error;
|
|
208
|
+
fail(input === undefined ? "runtime_unavailable" : "query_failed");
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const parse = (text) => {
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(text);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
fail("runtime_unavailable");
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const context = (await invoke(["context", "show"])).trim();
|
|
220
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/u.test(context))
|
|
221
|
+
fail("ownership_mismatch");
|
|
222
|
+
const selected = parse(await invoke([
|
|
223
|
+
"context",
|
|
224
|
+
"inspect",
|
|
225
|
+
"--format",
|
|
226
|
+
"{{json .Endpoints.docker.Host}}",
|
|
227
|
+
context,
|
|
228
|
+
]));
|
|
229
|
+
if (!isLocalDockerEndpoint(selected))
|
|
230
|
+
fail("ownership_mismatch");
|
|
231
|
+
endpoint = selected;
|
|
232
|
+
const engineId = async () => {
|
|
233
|
+
const value = parse(await invoke(["info", "--format", "{{json .ID}}"]));
|
|
234
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9:-]{1,128}$/u.test(value))
|
|
235
|
+
fail("runtime_unavailable");
|
|
236
|
+
return value;
|
|
237
|
+
};
|
|
238
|
+
const engine = await engineId();
|
|
239
|
+
const expectedName = postgres
|
|
240
|
+
? `${projectId}-postgresql`
|
|
241
|
+
: `supabase_db_${projectId}`;
|
|
242
|
+
const inspect = async (id) => {
|
|
243
|
+
const value = object(parse(await invoke([
|
|
244
|
+
"container",
|
|
245
|
+
"inspect",
|
|
246
|
+
"--format",
|
|
247
|
+
CONTAINER_FORMAT,
|
|
248
|
+
id,
|
|
249
|
+
])));
|
|
250
|
+
if (typeof value.id !== "string" ||
|
|
251
|
+
!/^[a-f0-9]{64}$/u.test(value.id) ||
|
|
252
|
+
value.name !== `/${expectedName}` ||
|
|
253
|
+
value.running !== true ||
|
|
254
|
+
typeof value.imageId !== "string" ||
|
|
255
|
+
!/^sha256:[a-f0-9]{64}$/u.test(value.imageId))
|
|
256
|
+
fail("runtime_unavailable");
|
|
257
|
+
try {
|
|
258
|
+
assertDockerResourceLabels({
|
|
259
|
+
labels: value.labels,
|
|
260
|
+
expectedLabel: postgres
|
|
261
|
+
? "com.rehearsal-db.project"
|
|
262
|
+
: "com.supabase.cli.project",
|
|
263
|
+
projectId,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
fail("ownership_mismatch");
|
|
268
|
+
}
|
|
269
|
+
if (postgres) {
|
|
270
|
+
const ports = object(value.ports), mappings = ports["5432/tcp"];
|
|
271
|
+
if (value.image !== postgres.image ||
|
|
272
|
+
!Array.isArray(mappings) ||
|
|
273
|
+
mappings.length !== 1)
|
|
274
|
+
fail("ownership_mismatch");
|
|
275
|
+
const mapping = object(mappings[0]);
|
|
276
|
+
if (mapping.HostIp !== "127.0.0.1" ||
|
|
277
|
+
mapping.HostPort !== String(databasePort))
|
|
278
|
+
fail("ownership_mismatch");
|
|
279
|
+
}
|
|
280
|
+
return { id: value.id, imageId: value.imageId };
|
|
281
|
+
};
|
|
282
|
+
const container = await inspect(expectedName);
|
|
283
|
+
const assertCurrent = async () => {
|
|
284
|
+
cancelled();
|
|
285
|
+
const currentReceipt = await readReceipt();
|
|
286
|
+
if (currentReceipt.sha256 !== initialReceipt.sha256 ||
|
|
287
|
+
currentReceipt.inode !== initialReceipt.inode)
|
|
288
|
+
fail("stale_runtime");
|
|
289
|
+
if ((await engineId()) !== engine)
|
|
290
|
+
fail("stale_runtime");
|
|
291
|
+
const current = await inspect(container.id);
|
|
292
|
+
if (current.id !== container.id || current.imageId !== container.imageId)
|
|
293
|
+
fail("stale_runtime");
|
|
294
|
+
const finalReceipt = await readReceipt();
|
|
295
|
+
if (finalReceipt.sha256 !== initialReceipt.sha256 ||
|
|
296
|
+
finalReceipt.inode !== initialReceipt.inode)
|
|
297
|
+
fail("stale_runtime");
|
|
298
|
+
};
|
|
299
|
+
await assertCurrent();
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
identitySha256: digest(JSON.stringify({
|
|
302
|
+
endpoint,
|
|
303
|
+
engine,
|
|
304
|
+
container,
|
|
305
|
+
receipt: initialReceipt.sha256,
|
|
306
|
+
provider,
|
|
307
|
+
projectId,
|
|
308
|
+
generation: baseline.generationId,
|
|
309
|
+
})),
|
|
310
|
+
assertCurrent,
|
|
311
|
+
runSql: async (input) => {
|
|
312
|
+
// Compile the remaining server deadline only after the final potentially
|
|
313
|
+
// slow Docker/receipt preflight. Never rewrite a caller's SQL text.
|
|
314
|
+
if (typeof input === "function")
|
|
315
|
+
await assertCurrent();
|
|
316
|
+
const sql = typeof input === "function" ? input() : input;
|
|
317
|
+
if (typeof sql !== "string" ||
|
|
318
|
+
Buffer.byteLength(sql) > SQL_INPUT_BYTES ||
|
|
319
|
+
sql.length === 0 ||
|
|
320
|
+
sql.includes("\0"))
|
|
321
|
+
fail("query_failed");
|
|
322
|
+
if (typeof input !== "function")
|
|
323
|
+
await assertCurrent();
|
|
324
|
+
let result = "", failure;
|
|
325
|
+
try {
|
|
326
|
+
// Docker exec inherits container variables unless env -i clears them;
|
|
327
|
+
// force a local socket so PGHOSTADDR/service/options cannot redirect it.
|
|
328
|
+
result = await invoke([
|
|
329
|
+
"exec",
|
|
330
|
+
"--interactive",
|
|
331
|
+
container.id,
|
|
332
|
+
"/usr/bin/env",
|
|
333
|
+
"-i",
|
|
334
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
335
|
+
"PGSSLMODE=disable",
|
|
336
|
+
"PGPASSFILE=/dev/null",
|
|
337
|
+
"psql",
|
|
338
|
+
"--no-psqlrc",
|
|
339
|
+
"--no-password",
|
|
340
|
+
"--host=/var/run/postgresql",
|
|
341
|
+
"--port=5432",
|
|
342
|
+
"--quiet",
|
|
343
|
+
"--no-align",
|
|
344
|
+
"--tuples-only",
|
|
345
|
+
"--set",
|
|
346
|
+
"ON_ERROR_STOP=1",
|
|
347
|
+
"--set",
|
|
348
|
+
"VERBOSITY=sqlstate",
|
|
349
|
+
"--username",
|
|
350
|
+
// Supabase's application-facing postgres role is NOSUPERUSER.
|
|
351
|
+
// The fixed disk bound and namespace lock require its built-in
|
|
352
|
+
// local administrator; no role changes or credentials are needed.
|
|
353
|
+
postgres?.user ?? "supabase_admin",
|
|
354
|
+
"--dbname",
|
|
355
|
+
postgres?.database ?? "postgres",
|
|
356
|
+
], sql);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
failure = error;
|
|
360
|
+
}
|
|
361
|
+
await assertCurrent();
|
|
362
|
+
if (failure)
|
|
363
|
+
throw failure;
|
|
364
|
+
return result;
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
@@ -426,6 +426,7 @@ const resetRuntime = async () => {
|
|
|
426
426
|
path: runtimeMarkerPath,
|
|
427
427
|
marker: {
|
|
428
428
|
formatVersion: active.baseline.formatVersion,
|
|
429
|
+
target: "supabase",
|
|
429
430
|
generationId: active.baseline.generationId,
|
|
430
431
|
dataSha256: readBaselineDataSha256(active.baseline),
|
|
431
432
|
schemaSha256: active.baseline.files?.["production-schema.sql"]?.sha256 ?? null,
|
package/docs/README.md
CHANGED
|
@@ -10,17 +10,26 @@ applies only the exact migrations you approve, and runs your application's own p
|
|
|
10
10
|
You do not need to read this documentation from top to bottom. Choose what you want to
|
|
11
11
|
accomplish and follow that path.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
Install Rehearsal, rehearse a migration, understand the result and fix a failure.
|
|
14
16
|
|
|
15
17
|
| Your goal | Best next page |
|
|
16
18
|
| ----------------------------------- | ------------------------------------- |
|
|
17
19
|
| Add Rehearsal to a project | [Getting started](getting-started.md) |
|
|
18
20
|
| Use the visual database workbench | [VS Code extension](vscode.md) |
|
|
19
21
|
| Try it in a disposable project | [Hands-on tutorial](tutorial.md) |
|
|
22
|
+
| Update an existing installation | [Releases and updates](updating.md) |
|
|
20
23
|
| Fix a setup or runtime problem | [Troubleshooting](troubleshooting.md) |
|
|
21
24
|
| Browse the friendlier web-docs home | [Documentation home](index.md) |
|
|
22
25
|
|
|
23
|
-
##
|
|
26
|
+
## User reference
|
|
27
|
+
|
|
28
|
+
Look up only what you need for your next decision. Start with
|
|
29
|
+
[supported environments](https://github.com/Ddupasquier/rehearsal-db/blob/main/COMPATIBILITY.md),
|
|
30
|
+
[configuration](configuration.md) or [troubleshooting](troubleshooting.md).
|
|
31
|
+
|
|
32
|
+
### Data safety
|
|
24
33
|
|
|
25
34
|
- [Baselines](baselines.md) — the verified starting copy
|
|
26
35
|
- [Sanitization](sanitization.md) — what may enter that copy
|
|
@@ -28,7 +37,7 @@ accomplish and follow that path.
|
|
|
28
37
|
- [Security model](security-model.md) — the independent safety barriers
|
|
29
38
|
- [Standalone workflow](standalone-workflow.md) — the complete declarative contract
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
### Commands and settings
|
|
32
41
|
|
|
33
42
|
- [CLI commands](commands.md)
|
|
34
43
|
- [Configuration](configuration.md)
|
|
@@ -37,13 +46,17 @@ accomplish and follow that path.
|
|
|
37
46
|
- [Runtime and identity policies](runtime-policies.md)
|
|
38
47
|
- [Database adapters](adapters.md)
|
|
39
48
|
- [Glossary](glossary.md)
|
|
49
|
+
- [Roadmap](roadmap.md)
|
|
40
50
|
|
|
41
|
-
##
|
|
51
|
+
## Maintainers
|
|
52
|
+
|
|
53
|
+
These pages explain developing and publishing Rehearsal itself, not using it in
|
|
54
|
+
your application. Installing or updating does not require the publisher checklist.
|
|
42
55
|
|
|
43
|
-
- [Roadmap](roadmap.md)
|
|
44
|
-
- [Stability and the first stable release](stability.md)
|
|
45
56
|
- [Repository architecture](architecture.md)
|
|
46
|
-
- [
|
|
57
|
+
- [Building and testing](https://github.com/Ddupasquier/rehearsal-db/blob/main/CONTRIBUTING.md)
|
|
58
|
+
- [Release verification](stability.md)
|
|
59
|
+
- [Maintainers: publishing Rehearsal](releasing.md)
|
|
47
60
|
|
|
48
61
|
To preview the styled documentation locally, run `npm run docs:preview`, then open the
|
|
49
62
|
address it prints.
|
package/docs/adapters.md
CHANGED
|
@@ -35,3 +35,13 @@ adapter responsibility.
|
|
|
35
35
|
Keep adapters small, deterministic, tested, and visibly project-owned. If a behavior is
|
|
36
36
|
generic across unrelated projects, propose it for the engine instead of copying it into
|
|
37
37
|
every adapter.
|
|
38
|
+
|
|
39
|
+
## Migration-tool histories are a separate contract
|
|
40
|
+
|
|
41
|
+
Project runtime hooks do not add Prisma, Drizzle, Knex, Django or Alembic support.
|
|
42
|
+
The current timestamped-SQL inventory and Rehearsal/Supabase migration ledger must
|
|
43
|
+
not be presented as native compatibility with those tools. Do not flatten native
|
|
44
|
+
migration directories, rewrite an applied ledger or use a schema-push shortcut to
|
|
45
|
+
fit this adapter API. Versioned native history, exact review and owned-target
|
|
46
|
+
execution belong to the separate core integration tracked by
|
|
47
|
+
[issue #174](https://github.com/Ddupasquier/rehearsal-db/issues/174).
|
package/docs/architecture.md
CHANGED
|
@@ -39,6 +39,23 @@ rehearsal-db/
|
|
|
39
39
|
- Public consumers use only the paths declared in `package.json#exports`.
|
|
40
40
|
- Unit tests mirror their source domain. Cross-domain workflows belong in
|
|
41
41
|
`tests/integration/`; durable public promises belong in `tests/contracts/`.
|
|
42
|
+
- `tests/helpers/public_cli.mjs` bounds setup-test child output and projects only
|
|
43
|
+
fixed failure categories, exit/signal metadata and recognized setup refusal kinds.
|
|
44
|
+
It never retains raw child errors, argv, provider text or output in failure reports.
|
|
45
|
+
An unrecognized failure stays unclassified; a later green test does not establish
|
|
46
|
+
the cause of a historical failure. This test helper is not shipped product code.
|
|
47
|
+
|
|
48
|
+
## Documentation audiences
|
|
49
|
+
|
|
50
|
+
Site navigation separates Getting started, User reference and Maintainers. Installation,
|
|
51
|
+
rehearsal, results and failure recovery belong in the first journey; configuration,
|
|
52
|
+
supported environments, troubleshooting and data safety belong in reference. Keep
|
|
53
|
+
deletion, production-access and unsupported-behavior warnings visible to users.
|
|
54
|
+
|
|
55
|
+
`docs/updating.md` answers release changes, compatibility, updating, breaking changes
|
|
56
|
+
and origin verification with links to deeper owners. Build, test and publishing details
|
|
57
|
+
belong in maintainer guidance, not installation prerequisites. `docs/releasing.md` owns
|
|
58
|
+
current publishing controls; historical incidents stay in release and issue history.
|
|
42
59
|
|
|
43
60
|
## CLI modules
|
|
44
61
|
|
|
@@ -62,7 +79,8 @@ typed registry and produce the same result contract:
|
|
|
62
79
|
- `runtime_commands.mts` coordinates local runtimes and project proofs.
|
|
63
80
|
- `runtime_planning.mts` owns read-only stack plans and exact candidate summaries.
|
|
64
81
|
- `editor_command_handlers.mts` exposes bounded migration and configuration review
|
|
65
|
-
contracts plus digest-bound, non-destructive stale-environment stop review.
|
|
82
|
+
contracts plus digest-bound, non-destructive stale-environment stop review. It also
|
|
83
|
+
keeps impact preview/measurement dispatch thin; aggregate policy stays in runtime.
|
|
66
84
|
- `source_commands.mts` coordinates approved source access and refresh.
|
|
67
85
|
- `renderers.mts` formats human-readable results without changing state.
|
|
68
86
|
- `terminal.mts` owns prompts, colors, and terminal exit behavior.
|
|
@@ -98,6 +116,62 @@ handler-family module at 350 lines or fewer. A new command belongs in the narrow
|
|
|
98
116
|
existing family, or in a new focused family when it represents a genuinely separate
|
|
99
117
|
responsibility.
|
|
100
118
|
|
|
119
|
+
Static Migration Insights has a separate additive contract from migration-plan v1 and
|
|
120
|
+
post-failure SQLSTATE diagnosis. `runtime/migration_insights_input.mts` owns bounded
|
|
121
|
+
regular-file reads and private exact-input text; the existing inventory, baseline-prefix
|
|
122
|
+
and topology owners still calculate the candidate digest. Repeated shared-directory
|
|
123
|
+
inputs must keep the same raw hash; findings retrieve text only by its expected hash.
|
|
124
|
+
`shared/held_path_ancestry.mts` retains canonical directory identities and timestamps
|
|
125
|
+
to refuse detected concurrent ancestry changes. This is not atomic `openat` resolution
|
|
126
|
+
or a filesystem sandbox; leaf checks and existing workspace trust remain necessary.
|
|
127
|
+
`migration_sql_lexer.mts` and
|
|
128
|
+
`migration_sql_scan.mts` own bounded lexical positions and fixed review hints, not SQL
|
|
129
|
+
validation. `migration_insights.mts` alone projects redacted public findings and explicit
|
|
130
|
+
coverage/unknown states. The editor consumes that contract; it must not reimplement SQL
|
|
131
|
+
analysis or infer runtime measurements from static warnings.
|
|
132
|
+
|
|
133
|
+
Migration impact is a separate optional contract, not a new rehearsal gate.
|
|
134
|
+
`runtime/migration_impact_plan.mts` recognizes only bounded simple SQL forms and retains
|
|
135
|
+
private relation/column specifications; it does not execute migrations or claim SQL
|
|
136
|
+
validity. `migration_impact_queries.mts` validates those specifications and generates
|
|
137
|
+
fixed, identifier-quoted aggregate queries. `migration_impact_catalog.mts` owns the
|
|
138
|
+
closed native index, statistics and operator-family predicates. Metadata checks refuse unsupported
|
|
139
|
+
table, type and operator semantics; public output contains count strings or explicit
|
|
140
|
+
unknowns, never those private specifications, SQL, object names or row contents.
|
|
141
|
+
`migration_impact.mts` binds preview and measurement reports to the whole candidate
|
|
142
|
+
digest, selected target, immutable baseline and exact input bytes. It rechecks the
|
|
143
|
+
binding after measurement and never interprets current counts as sequential migration
|
|
144
|
+
simulation or a production prediction.
|
|
145
|
+
|
|
146
|
+
`targets/migration_impact_runtime.mts` owns access to an already-running, verified,
|
|
147
|
+
ownership-proven local sandbox and its unchanged identity. It does not start/repair
|
|
148
|
+
an engine, reset data or apply migrations. Measurement uses one read-only repeatable
|
|
149
|
+
read count transaction with bounded locks, statements, output and total duration.
|
|
150
|
+
The environment-scrubbed client connects only through the proven owned container's
|
|
151
|
+
fixed local socket. PostgreSQL uses its configured local owner; Supabase uses its
|
|
152
|
+
built-in `supabase_admin`, not the restricted application-facing `postgres` role.
|
|
153
|
+
The fixed temporary-file bound and namespace-catalog lock require that local
|
|
154
|
+
administrator. No grants, role promotion, password access, network connection or
|
|
155
|
+
caller-selected SQL is added; the same read-only compiler and metadata refusals apply.
|
|
156
|
+
A separate read-only catalog preflight retains relation OIDs; globally ordered
|
|
157
|
+
`SHARE ROW EXCLUSIVE` locks precede the first repeatable-read snapshot, preventing
|
|
158
|
+
index/statistics DDL from outrunning its metadata checks. Counts recheck the retained
|
|
159
|
+
OIDs and supported semantics after locking. A concurrent name-to-view swap can
|
|
160
|
+
transiently lock view dependencies within the selected sandbox, but cannot make the
|
|
161
|
+
compiler execute that view query or return its counts. This is not atomic name/OID
|
|
162
|
+
resolution or a sandbox for a compromised database server. The private transport
|
|
163
|
+
also takes a bounded database-local namespace-catalog `SHARE` lock before relation
|
|
164
|
+
name resolution, preventing schema rename/swap during checks and aggregates.
|
|
165
|
+
This can temporarily block schema-catalog changes anywhere in that selected
|
|
166
|
+
disposable database; it does not change another database or acquire a cluster-wide lock.
|
|
167
|
+
The transport invokes the script compiler after final identity preflight so a slow check cannot
|
|
168
|
+
leave an earlier, larger server deadline embedded in the submitted script. The CLI
|
|
169
|
+
classifies `measure impact` for the existing project-operation lock and cancellation
|
|
170
|
+
owner despite the SQL being read-only; operation metadata/history may be written.
|
|
171
|
+
`inspect impact` is an offline preview. Both require one explicit configured target;
|
|
172
|
+
measurement additionally requires the exact whole-plan confirmation. Neither command
|
|
173
|
+
is a prerequisite for ordinary `run` or `migrate`, and both remain free CLI features.
|
|
174
|
+
|
|
101
175
|
## Configuration pipeline
|
|
102
176
|
|
|
103
177
|
The stable `project/configuration.mts` facade keeps existing imports compatible while
|