@youtyan/code-viewer 0.6.0 → 0.6.2
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/README.md +21 -15
- package/dist/code-viewer.js +468 -71
- package/package.json +1 -1
- package/skills/code-viewer-snapshot/SKILL.md +6 -0
- package/web/app.js +3127 -808
- package/web/index.html +46 -15
- package/web/style.css +943 -50
package/dist/code-viewer.js
CHANGED
|
@@ -1438,7 +1438,19 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
|
|
|
1438
1438
|
return "internal";
|
|
1439
1439
|
return omitDirNames.has(name) ? "heavy" : undefined;
|
|
1440
1440
|
}
|
|
1441
|
-
function
|
|
1441
|
+
function worktreeSubmodulePaths(cwd) {
|
|
1442
|
+
if (!existsSync(join2(cwd, ".gitmodules")))
|
|
1443
|
+
return new Set;
|
|
1444
|
+
const res = run(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
|
|
1445
|
+
if (res.code !== 0)
|
|
1446
|
+
return new Set;
|
|
1447
|
+
return new Set(res.stdout.split(`
|
|
1448
|
+
`).map((line) => {
|
|
1449
|
+
const split = line.indexOf(" ");
|
|
1450
|
+
return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
|
|
1451
|
+
}).filter(Boolean));
|
|
1452
|
+
}
|
|
1453
|
+
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
|
|
1442
1454
|
if (excludeNames.has(name.toLowerCase()))
|
|
1443
1455
|
return {
|
|
1444
1456
|
name,
|
|
@@ -1448,23 +1460,24 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
|
|
|
1448
1460
|
const entryPath = base ? `${base}/${name}` : name;
|
|
1449
1461
|
const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
|
|
1450
1462
|
const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
|
|
1463
|
+
const submodule = type === "commit" && submodulePaths.has(entryPath) ? true : undefined;
|
|
1464
|
+
const baseEntry = submodule ? { name, path: entryPath, type, submodule } : { name, path: entryPath, type };
|
|
1451
1465
|
return omittedReason ? {
|
|
1452
|
-
|
|
1453
|
-
path: entryPath,
|
|
1454
|
-
type,
|
|
1466
|
+
...baseEntry,
|
|
1455
1467
|
children_omitted: true,
|
|
1456
1468
|
children_omitted_reason: omittedReason
|
|
1457
|
-
} :
|
|
1469
|
+
} : baseEntry;
|
|
1458
1470
|
}
|
|
1459
1471
|
function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
|
|
1460
1472
|
const base = normalizeTreePath(path);
|
|
1461
1473
|
const root = join2(cwd, base);
|
|
1462
1474
|
const omitDirNameSet = new Set(omitDirNames);
|
|
1463
1475
|
const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
1476
|
+
const submodulePaths = worktreeSubmodulePaths(cwd);
|
|
1464
1477
|
let directEntries;
|
|
1465
1478
|
try {
|
|
1466
1479
|
const dirents = readdirSync(root, { withFileTypes: true });
|
|
1467
|
-
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
|
|
1480
|
+
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
|
|
1468
1481
|
} catch {
|
|
1469
1482
|
return [];
|
|
1470
1483
|
}
|
|
@@ -3765,6 +3778,93 @@ Default (non --json) output:
|
|
|
3765
3778
|
]);
|
|
3766
3779
|
});
|
|
3767
3780
|
|
|
3781
|
+
// web-src/core/routes.ts
|
|
3782
|
+
function assertNever(value) {
|
|
3783
|
+
throw new Error(`unhandled route: ${JSON.stringify(value)}`);
|
|
3784
|
+
}
|
|
3785
|
+
function formatLineTarget(line) {
|
|
3786
|
+
return typeof line === "number" ? String(line) : `${line.start}-${line.end}`;
|
|
3787
|
+
}
|
|
3788
|
+
function buildRoute(route) {
|
|
3789
|
+
switch (route.screen) {
|
|
3790
|
+
case "repo": {
|
|
3791
|
+
const params = new URLSearchParams;
|
|
3792
|
+
if (route.ref && route.ref !== "worktree")
|
|
3793
|
+
params.set("ref", route.ref);
|
|
3794
|
+
if (route.path)
|
|
3795
|
+
params.set("path", route.path);
|
|
3796
|
+
const qs = params.toString();
|
|
3797
|
+
return `/${qs ? `?${qs}` : ""}`;
|
|
3798
|
+
}
|
|
3799
|
+
case "file":
|
|
3800
|
+
if (route.view === "blob") {
|
|
3801
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
|
|
3802
|
+
}
|
|
3803
|
+
if (route.view === "blame") {
|
|
3804
|
+
const ref = route.ref || "worktree";
|
|
3805
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
|
|
3806
|
+
}
|
|
3807
|
+
if (route.view === "history") {
|
|
3808
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
|
|
3809
|
+
}
|
|
3810
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
|
|
3811
|
+
case "diff":
|
|
3812
|
+
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? `&path=${encodeURIComponent(route.path)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
|
|
3813
|
+
case "help": {
|
|
3814
|
+
const params = new URLSearchParams;
|
|
3815
|
+
if (route.lang && route.lang !== "en")
|
|
3816
|
+
params.set("lang", route.lang);
|
|
3817
|
+
if (route.section && route.section !== "overview")
|
|
3818
|
+
params.set("section", route.section);
|
|
3819
|
+
const qs = params.toString();
|
|
3820
|
+
return `/help${qs ? `?${qs}` : ""}`;
|
|
3821
|
+
}
|
|
3822
|
+
case "history": {
|
|
3823
|
+
const params = new URLSearchParams;
|
|
3824
|
+
if (route.ref && route.ref !== "HEAD")
|
|
3825
|
+
params.set("ref", route.ref);
|
|
3826
|
+
if (route.commit)
|
|
3827
|
+
params.set("commit", route.commit);
|
|
3828
|
+
const qs = params.toString();
|
|
3829
|
+
return `/history${qs ? `?${qs}` : ""}`;
|
|
3830
|
+
}
|
|
3831
|
+
case "database": {
|
|
3832
|
+
const params = new URLSearchParams;
|
|
3833
|
+
if (route.db)
|
|
3834
|
+
params.set("db", route.db);
|
|
3835
|
+
if (route.schema)
|
|
3836
|
+
params.set("schema", route.schema);
|
|
3837
|
+
if (route.table)
|
|
3838
|
+
params.set("table", route.table);
|
|
3839
|
+
if (route.tab)
|
|
3840
|
+
params.set("tab", route.tab);
|
|
3841
|
+
if (route.diffBefore)
|
|
3842
|
+
params.set("diffBefore", route.diffBefore);
|
|
3843
|
+
if (route.diffAfter)
|
|
3844
|
+
params.set("diffAfter", route.diffAfter);
|
|
3845
|
+
const qs = params.toString();
|
|
3846
|
+
return `/database${qs ? `?${qs}` : ""}`;
|
|
3847
|
+
}
|
|
3848
|
+
case "unknown":
|
|
3849
|
+
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
3850
|
+
default:
|
|
3851
|
+
return assertNever(route);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
3855
|
+
var init_routes = __esm(() => {
|
|
3856
|
+
SPA_PATHS = [
|
|
3857
|
+
"/todif",
|
|
3858
|
+
"/todiff",
|
|
3859
|
+
"/file",
|
|
3860
|
+
"/help",
|
|
3861
|
+
"/history",
|
|
3862
|
+
"/database",
|
|
3863
|
+
"/doctor"
|
|
3864
|
+
];
|
|
3865
|
+
APP_ENTRY_PATHS = ["/", "/index.html"];
|
|
3866
|
+
});
|
|
3867
|
+
|
|
3768
3868
|
// web-src/server/query-cli.ts
|
|
3769
3869
|
var exports_query_cli = {};
|
|
3770
3870
|
__export(exports_query_cli, {
|
|
@@ -5149,6 +5249,18 @@ function buildSnapshotPollCommand(serverUrl, db, schema) {
|
|
|
5149
5249
|
const schemaArg = schema ? ` --schema ${shellSingleQuote(schema)}` : "";
|
|
5150
5250
|
return `${cli} snapshot list --db ${shellSingleQuote(db)}${schemaArg} --json`;
|
|
5151
5251
|
}
|
|
5252
|
+
function buildSnapshotDiffUrl(serverUrl, dbId, schema, beforeId, afterId) {
|
|
5253
|
+
const path = buildRoute({
|
|
5254
|
+
screen: "database",
|
|
5255
|
+
db: dbId,
|
|
5256
|
+
schema,
|
|
5257
|
+
tab: "snapshot",
|
|
5258
|
+
diffBefore: beforeId,
|
|
5259
|
+
diffAfter: afterId,
|
|
5260
|
+
range: { from: "", to: "" }
|
|
5261
|
+
});
|
|
5262
|
+
return new URL(path, serverUrl).toString();
|
|
5263
|
+
}
|
|
5152
5264
|
function buildDiffRowsCommand(serverUrl, before, after, table) {
|
|
5153
5265
|
const cli = `code-viewer query --server ${shellSingleQuote(serverUrl)}`;
|
|
5154
5266
|
return `${cli} diff rows --before ${shellSingleQuote(before)} ` + `--after ${shellSingleQuote(after)} --table ${shellSingleQuote(table)} --json`;
|
|
@@ -5301,6 +5413,7 @@ async function runDiffTables(serverUrl, command) {
|
|
|
5301
5413
|
const body = await requestJson(serverUrl, `/_db/snapshot/diff/tables${qs}`, "GET", undefined, "diff tables");
|
|
5302
5414
|
const enriched = {
|
|
5303
5415
|
...body,
|
|
5416
|
+
diffUrl: buildSnapshotDiffUrl(serverUrl, body.dbId, body.schema, body.beforeId, body.afterId),
|
|
5304
5417
|
tables: body.tables.map((t) => ({
|
|
5305
5418
|
...t,
|
|
5306
5419
|
diffRowsCommand: buildDiffRowsCommand(serverUrl, body.beforeId, body.afterId, t.tableName)
|
|
@@ -5312,8 +5425,10 @@ async function runDiffTables(serverUrl, command) {
|
|
|
5312
5425
|
}
|
|
5313
5426
|
if (!enriched.tables.length) {
|
|
5314
5427
|
console.log("no tables in diff");
|
|
5428
|
+
console.log(`# view in browser: ${enriched.diffUrl}`);
|
|
5315
5429
|
return;
|
|
5316
5430
|
}
|
|
5431
|
+
console.log(`# view in browser: ${enriched.diffUrl}`);
|
|
5317
5432
|
for (const t of enriched.tables) {
|
|
5318
5433
|
const cov = t.coverage === "both" ? "" : ` (${t.coverage})`;
|
|
5319
5434
|
console.log(`${t.tableName} +${t.insertedCount} ~${t.updatedCount} -${t.deletedCount} =${t.unchangedCount}${cov}`);
|
|
@@ -5570,10 +5685,13 @@ no separate stored diff entity, so you always pass both snapshot ids.
|
|
|
5570
5685
|
code-viewer query snapshot list --db app.db --json
|
|
5571
5686
|
|
|
5572
5687
|
5. View the diff (per-table summary, then per-row detail). diff tables
|
|
5573
|
-
prints
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
the
|
|
5688
|
+
prints a "# view in browser: <url>" hint up top (opens the same diff in
|
|
5689
|
+
the human's browser, Database > Snapshot tab), then each per-table
|
|
5690
|
+
summary line plus a paste-safe "# diff rows: ..." hint right below it.
|
|
5691
|
+
--json adds the same browser link as a diffUrl field and a
|
|
5692
|
+
diffRowsCommand field on each tables[] element, so you can hand the
|
|
5693
|
+
human a direct link or drill into row detail without rebuilding the
|
|
5694
|
+
command yourself:
|
|
5577
5695
|
code-viewer query diff tables --before snap-abc123 --after snap-def456 --json
|
|
5578
5696
|
code-viewer query diff rows --before snap-abc123 --after snap-def456 \\
|
|
5579
5697
|
--table users --json
|
|
@@ -5760,10 +5878,13 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
|
|
|
5760
5878
|
- diff tables: human-readable lines (default) plus a paste-safe
|
|
5761
5879
|
"# diff rows: code-viewer query --server '<url>' diff rows --before '<id>'
|
|
5762
5880
|
--after '<id>' --table '<table>' --json" comment line right below each table,
|
|
5763
|
-
so AI/human can drill into row detail without rebuilding the command.
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5881
|
+
so AI/human can drill into row detail without rebuilding the command. A
|
|
5882
|
+
"# view in browser: <url>" hint is printed once up top — opens the same
|
|
5883
|
+
before/after comparison in the human's browser (Database > Snapshot tab).
|
|
5884
|
+
With --json the full /_db/snapshot/diff/tables payload is emitted, the
|
|
5885
|
+
top-level diffUrl field carries the same browser link, and each tables[]
|
|
5886
|
+
element gains an additive diffRowsCommand field with the same literal.
|
|
5887
|
+
server URL / snapshot ids / table names are POSIX single-quoted.
|
|
5767
5888
|
- snapshot create: prints "snapshot started" immediately with the snapshotId.
|
|
5768
5889
|
The no-wait output also includes a paste-safe poll command that pins
|
|
5769
5890
|
--server '<url>' and single-quotes db/schema so AI/human paste does not
|
|
@@ -5843,6 +5964,7 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
|
|
|
5843
5964
|
server only scans text-like columns (faster, cheaper).
|
|
5844
5965
|
`, VALUE_FLAGS2, BOOL_FLAGS2, DEFAULT_SEARCH_TIMEOUT_SEC = 60, DEFAULT_SNAPSHOT_WAIT_TIMEOUT_SEC = 120, REDIS_ACTION_ALLOWLIST, NON_REDIS_SUBCOMMAND_ALLOWLIST, ES_ACTION_ALLOWLIST, S3_ACTION_ALLOWLIST;
|
|
5845
5966
|
var init_query_cli = __esm(() => {
|
|
5967
|
+
init_routes();
|
|
5846
5968
|
init_cli_helpers();
|
|
5847
5969
|
init_cli_helpers();
|
|
5848
5970
|
VALUE_FLAGS2 = new Set([
|
|
@@ -10039,6 +10161,11 @@ var init_source_meta = __esm(() => {
|
|
|
10039
10161
|
"conf",
|
|
10040
10162
|
"env",
|
|
10041
10163
|
"properties",
|
|
10164
|
+
"rules",
|
|
10165
|
+
"rule",
|
|
10166
|
+
"prompt",
|
|
10167
|
+
"prompts",
|
|
10168
|
+
"instructions",
|
|
10042
10169
|
"gitignore",
|
|
10043
10170
|
"dockerignore",
|
|
10044
10171
|
"editorconfig",
|
|
@@ -11775,6 +11902,15 @@ function parseDockerDbId(dbId) {
|
|
|
11775
11902
|
return null;
|
|
11776
11903
|
return { serviceName: rest, relDir: "", database };
|
|
11777
11904
|
}
|
|
11905
|
+
function canonicalizeDockerDbId(dbId) {
|
|
11906
|
+
const parsed = parseDockerDbId(dbId);
|
|
11907
|
+
if (!parsed)
|
|
11908
|
+
return null;
|
|
11909
|
+
const database = parsed.database ? `:${parsed.database}` : "";
|
|
11910
|
+
if (!parsed.relDir)
|
|
11911
|
+
return `docker:${parsed.serviceName}${database}`;
|
|
11912
|
+
return `docker:${parsed.serviceName}@${encodeURIComponent(parsed.relDir)}${database}`;
|
|
11913
|
+
}
|
|
11778
11914
|
async function findDockerServiceByDbIdAsync(cwd, dbId, kind, omitDirNames, signal) {
|
|
11779
11915
|
const parsed = parseDockerDbId(dbId);
|
|
11780
11916
|
if (!parsed)
|
|
@@ -12194,6 +12330,9 @@ function sanitizeSettings(raw) {
|
|
|
12194
12330
|
const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
|
|
12195
12331
|
if (annotationPanelOpen !== undefined)
|
|
12196
12332
|
out.annotationPanelOpen = annotationPanelOpen;
|
|
12333
|
+
const annotationPanelWidth = optionalNumber(raw.annotationPanelWidth, 260, 720);
|
|
12334
|
+
if (annotationPanelWidth !== undefined)
|
|
12335
|
+
out.annotationPanelWidth = annotationPanelWidth;
|
|
12197
12336
|
const annotationFollow = optionalBoolean(raw.annotationFollow);
|
|
12198
12337
|
if (annotationFollow !== undefined)
|
|
12199
12338
|
out.annotationFollow = annotationFollow;
|
|
@@ -14175,6 +14314,9 @@ async function getStoreDb(cwd) {
|
|
|
14175
14314
|
try {
|
|
14176
14315
|
storeDb.exec("ALTER TABLE snapshots ADD COLUMN schema_name TEXT");
|
|
14177
14316
|
} catch {}
|
|
14317
|
+
try {
|
|
14318
|
+
storeDb.exec("ALTER TABLE snapshot_tables ADD COLUMN revision_id TEXT");
|
|
14319
|
+
} catch {}
|
|
14178
14320
|
return storeDb;
|
|
14179
14321
|
}
|
|
14180
14322
|
function makeId2(prefix) {
|
|
@@ -14183,32 +14325,163 @@ function makeId2(prefix) {
|
|
|
14183
14325
|
function hashPayload(payloadJson) {
|
|
14184
14326
|
return createHash5("sha256").update(payloadJson).digest("hex");
|
|
14185
14327
|
}
|
|
14328
|
+
function hashLengthPrefixed(hasher, value) {
|
|
14329
|
+
hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
|
|
14330
|
+
hasher.update(value);
|
|
14331
|
+
hasher.update(`
|
|
14332
|
+
`);
|
|
14333
|
+
}
|
|
14334
|
+
function deleteOrphanPayloads(db) {
|
|
14335
|
+
db.prepare(`DELETE FROM snapshot_payloads
|
|
14336
|
+
WHERE NOT EXISTS (
|
|
14337
|
+
SELECT 1 FROM snapshot_rows
|
|
14338
|
+
WHERE snapshot_rows.payload_hash = snapshot_payloads.payload_hash
|
|
14339
|
+
)
|
|
14340
|
+
AND NOT EXISTS (
|
|
14341
|
+
SELECT 1 FROM snapshot_table_revision_rows
|
|
14342
|
+
WHERE snapshot_table_revision_rows.payload_hash = snapshot_payloads.payload_hash
|
|
14343
|
+
)`).run();
|
|
14344
|
+
}
|
|
14345
|
+
function dockerDbIdFilterValues(dbId) {
|
|
14346
|
+
const values = [dbId];
|
|
14347
|
+
const canonical = canonicalizeDockerDbId(dbId);
|
|
14348
|
+
if (canonical)
|
|
14349
|
+
values.push(canonical);
|
|
14350
|
+
const parsed = parseDockerDbId(dbId);
|
|
14351
|
+
if (parsed?.relDir) {
|
|
14352
|
+
const database = parsed.database ? `:${parsed.database}` : "";
|
|
14353
|
+
values.push(`docker:${parsed.serviceName}@${parsed.relDir}${database}`);
|
|
14354
|
+
}
|
|
14355
|
+
return [...new Set(values)];
|
|
14356
|
+
}
|
|
14357
|
+
function getSnapshotScopeRow(db, snapshotId) {
|
|
14358
|
+
const row = db.prepare("SELECT db_id, COALESCE(schema_name, 'public') AS schema_name FROM snapshots WHERE id = ?").get(snapshotId);
|
|
14359
|
+
if (!row)
|
|
14360
|
+
throw new Error(`snapshot not found: ${snapshotId}`);
|
|
14361
|
+
return { dbId: row.db_id, schema: row.schema_name };
|
|
14362
|
+
}
|
|
14186
14363
|
async function createSnapshot(cwd, dbId, kind, tables, note, schema) {
|
|
14187
14364
|
const db = await getStoreDb(cwd);
|
|
14188
14365
|
const id = makeId2("snap");
|
|
14189
|
-
|
|
14366
|
+
const storedDbId = canonicalizeDockerDbId(dbId) ?? dbId;
|
|
14367
|
+
db.prepare("INSERT INTO snapshots (id, db_id, schema_name, kind, note, created_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, storedDbId, schema ?? null, kind, note, new Date().toISOString(), "running");
|
|
14190
14368
|
for (const t of tables) {
|
|
14191
14369
|
db.prepare("INSERT INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(id, t);
|
|
14192
14370
|
}
|
|
14193
14371
|
return id;
|
|
14194
14372
|
}
|
|
14195
|
-
async function
|
|
14373
|
+
async function beginSnapshotTableRevision(cwd, snapshotId, tableName, pkColumns) {
|
|
14374
|
+
const db = await getStoreDb(cwd);
|
|
14375
|
+
const id = makeId2("rev");
|
|
14376
|
+
const scope = getSnapshotScopeRow(db, snapshotId);
|
|
14377
|
+
db.exec("BEGIN");
|
|
14378
|
+
try {
|
|
14379
|
+
db.prepare("INSERT OR IGNORE INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(snapshotId, tableName);
|
|
14380
|
+
db.prepare(`INSERT INTO snapshot_table_revisions
|
|
14381
|
+
(id, source_snapshot_id, db_id, schema_name, table_name, row_count, table_hash, hash_version, pk_columns_json, status, created_at)
|
|
14382
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, snapshotId, scope.dbId, scope.schema, tableName, 0, "", SNAPSHOT_TABLE_HASH_VERSION, JSON.stringify(pkColumns), "building", new Date().toISOString());
|
|
14383
|
+
db.exec("COMMIT");
|
|
14384
|
+
} catch (err) {
|
|
14385
|
+
try {
|
|
14386
|
+
db.exec("ROLLBACK");
|
|
14387
|
+
} catch {}
|
|
14388
|
+
throw err;
|
|
14389
|
+
}
|
|
14390
|
+
return id;
|
|
14391
|
+
}
|
|
14392
|
+
async function addSnapshotTableRows(cwd, revisionId, rows) {
|
|
14393
|
+
if (rows.length === 0)
|
|
14394
|
+
return;
|
|
14196
14395
|
const db = await getStoreDb(cwd);
|
|
14197
|
-
const
|
|
14198
|
-
const insertRow = db.prepare("INSERT OR IGNORE INTO snapshot_rows (snapshot_id, table_name, row_key_hash, row_key_json, row_hash, payload_hash) VALUES (?, ?, ?, ?, ?, ?)");
|
|
14396
|
+
const insertRow = db.prepare("INSERT OR IGNORE INTO snapshot_table_revision_rows (revision_id, row_key_hash, row_key_json, row_hash, payload_hash) VALUES (?, ?, ?, ?, ?)");
|
|
14199
14397
|
const insertPayload = db.prepare("INSERT OR IGNORE INTO snapshot_payloads (payload_hash, payload_json) VALUES (?, ?)");
|
|
14200
|
-
const updateTable = db.prepare("UPDATE snapshot_tables SET row_count = ?, table_hash = ?, pk_columns_json = ? WHERE snapshot_id = ? AND table_name = ?");
|
|
14201
14398
|
db.exec("BEGIN");
|
|
14202
14399
|
try {
|
|
14203
14400
|
for (const row of rows) {
|
|
14204
14401
|
const rowKeyHash = createHash5("sha256").update(row.rowKeyJson).digest("hex");
|
|
14205
14402
|
const payloadHash = hashPayload(row.payloadJson);
|
|
14206
|
-
|
|
14207
|
-
insertRow.run(snapshotId, tableName, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
|
|
14403
|
+
insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
|
|
14208
14404
|
insertPayload.run(payloadHash, row.payloadJson);
|
|
14209
14405
|
}
|
|
14210
|
-
|
|
14211
|
-
|
|
14406
|
+
db.exec("COMMIT");
|
|
14407
|
+
} catch (err) {
|
|
14408
|
+
try {
|
|
14409
|
+
db.exec("ROLLBACK");
|
|
14410
|
+
} catch {}
|
|
14411
|
+
throw err;
|
|
14412
|
+
}
|
|
14413
|
+
}
|
|
14414
|
+
function computeRevisionTableHash(db, revisionId) {
|
|
14415
|
+
const hasher = createHash5("sha256");
|
|
14416
|
+
hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
|
|
14417
|
+
let rowCount = 0;
|
|
14418
|
+
let last;
|
|
14419
|
+
for (;; ) {
|
|
14420
|
+
const rows = last ? db.prepare(`SELECT row_key_hash, row_key_json, row_hash, payload_hash
|
|
14421
|
+
FROM snapshot_table_revision_rows
|
|
14422
|
+
WHERE revision_id = ?
|
|
14423
|
+
AND (row_key_hash > ? OR (row_key_hash = ? AND row_key_json > ?))
|
|
14424
|
+
ORDER BY row_key_hash, row_key_json
|
|
14425
|
+
LIMIT ?`).all(revisionId, last.row_key_hash, last.row_key_hash, last.row_key_json, SNAPSHOT_REVISION_HASH_BATCH_SIZE) : db.prepare(`SELECT row_key_hash, row_key_json, row_hash, payload_hash
|
|
14426
|
+
FROM snapshot_table_revision_rows
|
|
14427
|
+
WHERE revision_id = ?
|
|
14428
|
+
ORDER BY row_key_hash, row_key_json
|
|
14429
|
+
LIMIT ?`).all(revisionId, SNAPSHOT_REVISION_HASH_BATCH_SIZE);
|
|
14430
|
+
if (rows.length === 0)
|
|
14431
|
+
break;
|
|
14432
|
+
for (const row of rows) {
|
|
14433
|
+
hashLengthPrefixed(hasher, row.row_key_hash);
|
|
14434
|
+
hashLengthPrefixed(hasher, row.row_key_json);
|
|
14435
|
+
hashLengthPrefixed(hasher, row.row_hash);
|
|
14436
|
+
hashLengthPrefixed(hasher, row.payload_hash);
|
|
14437
|
+
rowCount++;
|
|
14438
|
+
last = {
|
|
14439
|
+
row_key_hash: row.row_key_hash,
|
|
14440
|
+
row_key_json: row.row_key_json
|
|
14441
|
+
};
|
|
14442
|
+
}
|
|
14443
|
+
if (rows.length < SNAPSHOT_REVISION_HASH_BATCH_SIZE)
|
|
14444
|
+
break;
|
|
14445
|
+
}
|
|
14446
|
+
return {
|
|
14447
|
+
rowCount,
|
|
14448
|
+
tableHash: `${SNAPSHOT_TABLE_HASH_PREFIX}${hasher.digest("hex")}`
|
|
14449
|
+
};
|
|
14450
|
+
}
|
|
14451
|
+
async function finalizeSnapshotTableRevision(cwd, snapshotId, tableName, revisionId) {
|
|
14452
|
+
const db = await getStoreDb(cwd);
|
|
14453
|
+
const revision = db.prepare(`SELECT db_id, schema_name, table_name, pk_columns_json
|
|
14454
|
+
FROM snapshot_table_revisions
|
|
14455
|
+
WHERE id = ?`).get(revisionId);
|
|
14456
|
+
if (!revision)
|
|
14457
|
+
throw new Error(`snapshot table revision not found: ${revisionId}`);
|
|
14458
|
+
const { rowCount, tableHash } = computeRevisionTableHash(db, revisionId);
|
|
14459
|
+
const existing = db.prepare(`SELECT id
|
|
14460
|
+
FROM snapshot_table_revisions
|
|
14461
|
+
WHERE id != ?
|
|
14462
|
+
AND db_id = ?
|
|
14463
|
+
AND schema_name = ?
|
|
14464
|
+
AND table_name = ?
|
|
14465
|
+
AND hash_version = ?
|
|
14466
|
+
AND table_hash = ?
|
|
14467
|
+
AND row_count = ?
|
|
14468
|
+
AND pk_columns_json = ?
|
|
14469
|
+
AND status = 'done'
|
|
14470
|
+
ORDER BY created_at ASC
|
|
14471
|
+
LIMIT 1`).get(revisionId, revision.db_id, revision.schema_name, revision.table_name, SNAPSHOT_TABLE_HASH_VERSION, tableHash, rowCount, revision.pk_columns_json);
|
|
14472
|
+
const adoptedRevisionId = existing?.id ?? revisionId;
|
|
14473
|
+
db.exec("BEGIN");
|
|
14474
|
+
try {
|
|
14475
|
+
if (existing) {
|
|
14476
|
+
db.prepare("DELETE FROM snapshot_table_revisions WHERE id = ?").run(revisionId);
|
|
14477
|
+
} else {
|
|
14478
|
+
db.prepare(`UPDATE snapshot_table_revisions
|
|
14479
|
+
SET row_count = ?, table_hash = ?, status = 'done', source_snapshot_id = NULL
|
|
14480
|
+
WHERE id = ?`).run(rowCount, tableHash, revisionId);
|
|
14481
|
+
}
|
|
14482
|
+
db.prepare(`UPDATE snapshot_tables
|
|
14483
|
+
SET row_count = ?, table_hash = ?, pk_columns_json = ?, revision_id = ?
|
|
14484
|
+
WHERE snapshot_id = ? AND table_name = ?`).run(rowCount, tableHash, revision.pk_columns_json, adoptedRevisionId, snapshotId, tableName);
|
|
14212
14485
|
db.exec("COMMIT");
|
|
14213
14486
|
} catch (err) {
|
|
14214
14487
|
try {
|
|
@@ -14220,7 +14493,18 @@ async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows)
|
|
|
14220
14493
|
async function finalizeSnapshot(cwd, snapshotId, error) {
|
|
14221
14494
|
const db = await getStoreDb(cwd);
|
|
14222
14495
|
if (error) {
|
|
14223
|
-
db.
|
|
14496
|
+
db.exec("BEGIN");
|
|
14497
|
+
try {
|
|
14498
|
+
db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
|
|
14499
|
+
deleteOrphanPayloads(db);
|
|
14500
|
+
db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
|
|
14501
|
+
db.exec("COMMIT");
|
|
14502
|
+
} catch (err) {
|
|
14503
|
+
try {
|
|
14504
|
+
db.exec("ROLLBACK");
|
|
14505
|
+
} catch {}
|
|
14506
|
+
throw err;
|
|
14507
|
+
}
|
|
14224
14508
|
} else {
|
|
14225
14509
|
db.prepare("UPDATE snapshots SET status = 'done' WHERE id = ?").run(snapshotId);
|
|
14226
14510
|
}
|
|
@@ -14230,8 +14514,9 @@ async function listSnapshots(cwd, dbId, schema) {
|
|
|
14230
14514
|
const conditions = [];
|
|
14231
14515
|
const params = [];
|
|
14232
14516
|
if (dbId) {
|
|
14233
|
-
|
|
14234
|
-
|
|
14517
|
+
const dbIdValues = dockerDbIdFilterValues(dbId);
|
|
14518
|
+
conditions.push(dbIdValues.length === 1 ? "db_id = ?" : `db_id IN (${dbIdValues.map(() => "?").join(", ")})`);
|
|
14519
|
+
params.push(...dbIdValues);
|
|
14235
14520
|
}
|
|
14236
14521
|
if (schema !== undefined) {
|
|
14237
14522
|
conditions.push("COALESCE(schema_name, 'public') = ?");
|
|
@@ -14264,11 +14549,14 @@ async function deleteSnapshot(cwd, snapshotId) {
|
|
|
14264
14549
|
db.exec("BEGIN");
|
|
14265
14550
|
try {
|
|
14266
14551
|
db.prepare("DELETE FROM snapshots WHERE id = ?").run(snapshotId);
|
|
14267
|
-
db.prepare(`DELETE FROM
|
|
14268
|
-
WHERE
|
|
14269
|
-
|
|
14270
|
-
|
|
14271
|
-
|
|
14552
|
+
db.prepare(`DELETE FROM snapshot_table_revisions
|
|
14553
|
+
WHERE status = 'done'
|
|
14554
|
+
AND NOT EXISTS (
|
|
14555
|
+
SELECT 1 FROM snapshot_tables
|
|
14556
|
+
WHERE snapshot_tables.revision_id = snapshot_table_revisions.id
|
|
14557
|
+
)`).run();
|
|
14558
|
+
db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
|
|
14559
|
+
deleteOrphanPayloads(db);
|
|
14272
14560
|
db.exec("COMMIT");
|
|
14273
14561
|
} catch (err) {
|
|
14274
14562
|
try {
|
|
@@ -14278,15 +14566,22 @@ async function deleteSnapshot(cwd, snapshotId) {
|
|
|
14278
14566
|
}
|
|
14279
14567
|
}
|
|
14280
14568
|
function getSnapshotScope(db, snapshotId) {
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14569
|
+
return getSnapshotScopeRow(db, snapshotId);
|
|
14570
|
+
}
|
|
14571
|
+
async function getSnapshotScopeById(cwd, snapshotId) {
|
|
14572
|
+
const db = await getStoreDb(cwd);
|
|
14573
|
+
const scope = getSnapshotScope(db, snapshotId);
|
|
14574
|
+
return {
|
|
14575
|
+
dbId: canonicalizeDockerDbId(scope.dbId) ?? scope.dbId,
|
|
14576
|
+
schema: scope.schema
|
|
14577
|
+
};
|
|
14285
14578
|
}
|
|
14286
14579
|
function assertSameSnapshotScope(db, beforeId, afterId) {
|
|
14287
14580
|
const before = getSnapshotScope(db, beforeId);
|
|
14288
14581
|
const after = getSnapshotScope(db, afterId);
|
|
14289
|
-
|
|
14582
|
+
const beforeDbId = canonicalizeDockerDbId(before.dbId) ?? before.dbId;
|
|
14583
|
+
const afterDbId = canonicalizeDockerDbId(after.dbId) ?? after.dbId;
|
|
14584
|
+
if (beforeDbId !== afterDbId || before.schema !== after.schema) {
|
|
14290
14585
|
throw new Error(`cannot compare snapshots from different database/schema (${before.dbId}:${before.schema} vs ${after.dbId}:${after.schema})`);
|
|
14291
14586
|
}
|
|
14292
14587
|
}
|
|
@@ -14338,16 +14633,16 @@ async function computeDiffTables(cwd, beforeId, afterId) {
|
|
|
14338
14633
|
continue;
|
|
14339
14634
|
}
|
|
14340
14635
|
const insertedCount = db.prepare(`SELECT COUNT(*) AS cnt
|
|
14341
|
-
FROM
|
|
14342
|
-
LEFT JOIN
|
|
14636
|
+
FROM snapshot_rows_resolved a
|
|
14637
|
+
LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
|
|
14343
14638
|
WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL`).get(beforeId, table, afterId, table).cnt;
|
|
14344
14639
|
const deletedCount = db.prepare(`SELECT COUNT(*) AS cnt
|
|
14345
|
-
FROM
|
|
14346
|
-
LEFT JOIN
|
|
14640
|
+
FROM snapshot_rows_resolved b
|
|
14641
|
+
LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
|
|
14347
14642
|
WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL`).get(afterId, table, beforeId, table).cnt;
|
|
14348
14643
|
const updatedCount = db.prepare(`SELECT COUNT(*) AS cnt
|
|
14349
|
-
FROM
|
|
14350
|
-
INNER JOIN
|
|
14644
|
+
FROM snapshot_rows_resolved b
|
|
14645
|
+
INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
|
|
14351
14646
|
WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash`).get(afterId, table, beforeId, table).cnt;
|
|
14352
14647
|
const unchangedCount = b.row_count - deletedCount - updatedCount;
|
|
14353
14648
|
results.push({
|
|
@@ -14373,8 +14668,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
14373
14668
|
assertSameSnapshotScope(db, beforeId, afterId);
|
|
14374
14669
|
const allDiffRows = [];
|
|
14375
14670
|
const inserted = db.prepare(`SELECT a.row_key_json, a.payload_hash
|
|
14376
|
-
FROM
|
|
14377
|
-
LEFT JOIN
|
|
14671
|
+
FROM snapshot_rows_resolved a
|
|
14672
|
+
LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
|
|
14378
14673
|
WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL
|
|
14379
14674
|
ORDER BY a.row_key_json`).all(beforeId, table, afterId, table);
|
|
14380
14675
|
for (const r of inserted) {
|
|
@@ -14386,8 +14681,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
14386
14681
|
});
|
|
14387
14682
|
}
|
|
14388
14683
|
const deleted = db.prepare(`SELECT b.row_key_json, b.payload_hash
|
|
14389
|
-
FROM
|
|
14390
|
-
LEFT JOIN
|
|
14684
|
+
FROM snapshot_rows_resolved b
|
|
14685
|
+
LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
|
|
14391
14686
|
WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL
|
|
14392
14687
|
ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
|
|
14393
14688
|
for (const r of deleted) {
|
|
@@ -14399,8 +14694,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
14399
14694
|
});
|
|
14400
14695
|
}
|
|
14401
14696
|
const updated = db.prepare(`SELECT b.row_key_json, b.payload_hash AS before_ph, a.payload_hash AS after_ph
|
|
14402
|
-
FROM
|
|
14403
|
-
INNER JOIN
|
|
14697
|
+
FROM snapshot_rows_resolved b
|
|
14698
|
+
INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
|
|
14404
14699
|
WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash
|
|
14405
14700
|
ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
|
|
14406
14701
|
for (const r of updated) {
|
|
@@ -14454,10 +14749,45 @@ CREATE TABLE IF NOT EXISTS snapshot_tables (
|
|
|
14454
14749
|
row_count INTEGER NOT NULL DEFAULT 0,
|
|
14455
14750
|
table_hash TEXT NOT NULL DEFAULT '',
|
|
14456
14751
|
pk_columns_json TEXT NOT NULL DEFAULT '[]',
|
|
14752
|
+
revision_id TEXT,
|
|
14457
14753
|
PRIMARY KEY (snapshot_id, table_name),
|
|
14458
|
-
FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE
|
|
14754
|
+
FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE,
|
|
14755
|
+
FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id)
|
|
14756
|
+
);
|
|
14757
|
+
|
|
14758
|
+
CREATE TABLE IF NOT EXISTS snapshot_table_revisions (
|
|
14759
|
+
id TEXT PRIMARY KEY,
|
|
14760
|
+
source_snapshot_id TEXT,
|
|
14761
|
+
db_id TEXT NOT NULL,
|
|
14762
|
+
schema_name TEXT NOT NULL,
|
|
14763
|
+
table_name TEXT NOT NULL,
|
|
14764
|
+
row_count INTEGER NOT NULL DEFAULT 0,
|
|
14765
|
+
table_hash TEXT NOT NULL DEFAULT '',
|
|
14766
|
+
hash_version INTEGER NOT NULL DEFAULT 2,
|
|
14767
|
+
pk_columns_json TEXT NOT NULL DEFAULT '[]',
|
|
14768
|
+
status TEXT NOT NULL DEFAULT 'building',
|
|
14769
|
+
created_at TEXT NOT NULL
|
|
14770
|
+
);
|
|
14771
|
+
|
|
14772
|
+
CREATE INDEX IF NOT EXISTS idx_snapshot_table_revisions_lookup
|
|
14773
|
+
ON snapshot_table_revisions(db_id, schema_name, table_name, hash_version, table_hash, row_count, pk_columns_json, status);
|
|
14774
|
+
|
|
14775
|
+
CREATE TABLE IF NOT EXISTS snapshot_table_revision_rows (
|
|
14776
|
+
revision_id TEXT NOT NULL,
|
|
14777
|
+
row_key_hash TEXT NOT NULL,
|
|
14778
|
+
row_key_json TEXT NOT NULL,
|
|
14779
|
+
row_hash TEXT NOT NULL,
|
|
14780
|
+
payload_hash TEXT NOT NULL,
|
|
14781
|
+
PRIMARY KEY (revision_id, row_key_hash),
|
|
14782
|
+
FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id) ON DELETE CASCADE
|
|
14459
14783
|
);
|
|
14460
14784
|
|
|
14785
|
+
CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_revision_key
|
|
14786
|
+
ON snapshot_table_revision_rows(revision_id, row_key_hash);
|
|
14787
|
+
|
|
14788
|
+
CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_payload_hash
|
|
14789
|
+
ON snapshot_table_revision_rows(payload_hash);
|
|
14790
|
+
|
|
14461
14791
|
CREATE TABLE IF NOT EXISTS snapshot_rows (
|
|
14462
14792
|
snapshot_id TEXT NOT NULL,
|
|
14463
14793
|
table_name TEXT NOT NULL,
|
|
@@ -14475,14 +14805,40 @@ CREATE TABLE IF NOT EXISTS snapshot_payloads (
|
|
|
14475
14805
|
);
|
|
14476
14806
|
|
|
14477
14807
|
-- deleteSnapshot の orphan cleanup (snapshot_payloads を残さない) は
|
|
14478
|
-
--
|
|
14479
|
-
-- snapshot_payloads 件数 ×
|
|
14808
|
+
-- legacy rows と revision rows の payload_hash で逆引きする。index が無いと
|
|
14809
|
+
-- snapshot_payloads 件数 × rows 全件の相関スキャンになる。
|
|
14480
14810
|
CREATE INDEX IF NOT EXISTS idx_snapshot_rows_payload_hash
|
|
14481
14811
|
ON snapshot_rows(payload_hash);
|
|
14482
14812
|
|
|
14483
|
-
|
|
14813
|
+
DROP VIEW IF EXISTS snapshot_rows_resolved;
|
|
14814
|
+
|
|
14815
|
+
CREATE VIEW snapshot_rows_resolved AS
|
|
14816
|
+
SELECT
|
|
14817
|
+
st.snapshot_id AS snapshot_id,
|
|
14818
|
+
st.table_name AS table_name,
|
|
14819
|
+
rr.row_key_hash AS row_key_hash,
|
|
14820
|
+
rr.row_key_json AS row_key_json,
|
|
14821
|
+
rr.row_hash AS row_hash,
|
|
14822
|
+
rr.payload_hash AS payload_hash
|
|
14823
|
+
FROM snapshot_tables st
|
|
14824
|
+
INNER JOIN snapshot_table_revision_rows rr
|
|
14825
|
+
ON rr.revision_id = st.revision_id
|
|
14826
|
+
WHERE st.revision_id IS NOT NULL
|
|
14827
|
+
UNION ALL
|
|
14828
|
+
SELECT
|
|
14829
|
+
snapshot_id,
|
|
14830
|
+
table_name,
|
|
14831
|
+
row_key_hash,
|
|
14832
|
+
row_key_json,
|
|
14833
|
+
row_hash,
|
|
14834
|
+
payload_hash
|
|
14835
|
+
FROM snapshot_rows;
|
|
14836
|
+
|
|
14837
|
+
`, storeDb = null, storeDbPath = null, SNAPSHOT_TABLE_HASH_VERSION = 2, SNAPSHOT_TABLE_HASH_PREFIX, SNAPSHOT_REVISION_HASH_BATCH_SIZE = 1000;
|
|
14484
14838
|
var init_snapshot_store = __esm(() => {
|
|
14839
|
+
init_discovery();
|
|
14485
14840
|
init_sqlite_driver();
|
|
14841
|
+
SNAPSHOT_TABLE_HASH_PREFIX = `v${SNAPSHOT_TABLE_HASH_VERSION}:`;
|
|
14486
14842
|
});
|
|
14487
14843
|
|
|
14488
14844
|
// web-src/server/database/sources/types.ts
|
|
@@ -14505,7 +14861,6 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
|
|
|
14505
14861
|
const container = containers[i];
|
|
14506
14862
|
throwIfAborted(options.signal, "snapshot cancelled");
|
|
14507
14863
|
onProgress?.({ container, done: false, index: i, total });
|
|
14508
|
-
const collected = [];
|
|
14509
14864
|
let pkColumns = [];
|
|
14510
14865
|
if (snapshotSource.model === "sql") {
|
|
14511
14866
|
try {
|
|
@@ -14517,16 +14872,28 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
|
|
|
14517
14872
|
}
|
|
14518
14873
|
}
|
|
14519
14874
|
throwIfAborted(options.signal, "snapshot cancelled");
|
|
14875
|
+
const revisionId = await beginSnapshotTableRevision(cwd, snapshotId, container, pkColumns);
|
|
14876
|
+
const pendingRows = [];
|
|
14877
|
+
const flushRows = async () => {
|
|
14878
|
+
if (pendingRows.length === 0)
|
|
14879
|
+
return;
|
|
14880
|
+
const rows = pendingRows.splice(0, pendingRows.length);
|
|
14881
|
+
await addSnapshotTableRows(cwd, revisionId, rows);
|
|
14882
|
+
};
|
|
14520
14883
|
for await (const item of snapshotSource.iterateForSnapshot(container, options.signal)) {
|
|
14521
14884
|
throwIfAborted(options.signal, "snapshot cancelled");
|
|
14522
|
-
|
|
14885
|
+
pendingRows.push({
|
|
14523
14886
|
rowKeyJson: item.keyJson,
|
|
14524
14887
|
rowHash: item.rowHash,
|
|
14525
14888
|
payloadJson: item.payloadJson
|
|
14526
14889
|
});
|
|
14890
|
+
if (pendingRows.length >= SNAPSHOT_FLUSH_THRESHOLD) {
|
|
14891
|
+
await flushRows();
|
|
14892
|
+
}
|
|
14527
14893
|
}
|
|
14528
14894
|
throwIfAborted(options.signal, "snapshot cancelled");
|
|
14529
|
-
await
|
|
14895
|
+
await flushRows();
|
|
14896
|
+
await finalizeSnapshotTableRevision(cwd, snapshotId, container, revisionId);
|
|
14530
14897
|
}
|
|
14531
14898
|
await finalizeSnapshot(cwd, snapshotId);
|
|
14532
14899
|
onProgress?.({ container: "", done: true, index: total, total });
|
|
@@ -15083,6 +15450,38 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
15083
15450
|
return handleError("database", "read table", err, signal);
|
|
15084
15451
|
}
|
|
15085
15452
|
}
|
|
15453
|
+
async function handleTableCount(cwd, url, omitDirNames, signal) {
|
|
15454
|
+
const r = await resolveDb(cwd, url.searchParams.get("db"), omitDirNames, url.searchParams.get("schema"), signal);
|
|
15455
|
+
if (r instanceof Response)
|
|
15456
|
+
return r;
|
|
15457
|
+
const table = url.searchParams.get("table");
|
|
15458
|
+
if (!table)
|
|
15459
|
+
return textError("missing table parameter", 400);
|
|
15460
|
+
try {
|
|
15461
|
+
const adapter = await getAdapter(r, cwd, signal);
|
|
15462
|
+
const { result, executedSql } = await captureSql(async () => {
|
|
15463
|
+
const db = asAsync(adapter);
|
|
15464
|
+
const tables = await db.tables(signal);
|
|
15465
|
+
const entry = tables.find((candidate) => candidate.name === table);
|
|
15466
|
+
if (!entry)
|
|
15467
|
+
throw new Error(`unknown table: ${table}`);
|
|
15468
|
+
if (entry.type !== "table")
|
|
15469
|
+
return { rowCount: null };
|
|
15470
|
+
const counts = await db.tableRowCounts([table], signal);
|
|
15471
|
+
return { rowCount: counts.get(table) ?? null };
|
|
15472
|
+
});
|
|
15473
|
+
const body = {
|
|
15474
|
+
dbId: r.dbId,
|
|
15475
|
+
...r.schema ? { schema: r.schema } : {},
|
|
15476
|
+
table,
|
|
15477
|
+
rowCount: result.rowCount,
|
|
15478
|
+
executedSql
|
|
15479
|
+
};
|
|
15480
|
+
return json(body);
|
|
15481
|
+
} catch (err) {
|
|
15482
|
+
return handleError("database", "read table count", err, signal);
|
|
15483
|
+
}
|
|
15484
|
+
}
|
|
15086
15485
|
function makeHistoryId() {
|
|
15087
15486
|
return makeId("qh");
|
|
15088
15487
|
}
|
|
@@ -15821,7 +16220,14 @@ async function handleDiffTables(cwd, url) {
|
|
|
15821
16220
|
return textError("missing before or after parameter", 400);
|
|
15822
16221
|
try {
|
|
15823
16222
|
const tables = await computeDiffTables(cwd, beforeId, afterId);
|
|
15824
|
-
|
|
16223
|
+
const scope = await getSnapshotScopeById(cwd, beforeId);
|
|
16224
|
+
return json({
|
|
16225
|
+
beforeId,
|
|
16226
|
+
afterId,
|
|
16227
|
+
dbId: scope.dbId,
|
|
16228
|
+
schema: scope.schema,
|
|
16229
|
+
tables
|
|
16230
|
+
});
|
|
15825
16231
|
} catch (err) {
|
|
15826
16232
|
return handleError("database", "compute diff", err);
|
|
15827
16233
|
}
|
|
@@ -16004,6 +16410,10 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
16004
16410
|
methods: ["GET"],
|
|
16005
16411
|
handler: () => handleTable(cwd, url, omitDirNames, req.signal)
|
|
16006
16412
|
},
|
|
16413
|
+
"/_db/table-count": {
|
|
16414
|
+
methods: ["GET"],
|
|
16415
|
+
handler: () => handleTableCount(cwd, url, omitDirNames, req.signal)
|
|
16416
|
+
},
|
|
16007
16417
|
"/_db/columns": {
|
|
16008
16418
|
methods: ["GET"],
|
|
16009
16419
|
handler: () => handleColumns(cwd, url, omitDirNames, req.signal)
|
|
@@ -17288,21 +17698,6 @@ function normalizeNewDirectoryName(name) {
|
|
|
17288
17698
|
return trimmed;
|
|
17289
17699
|
}
|
|
17290
17700
|
|
|
17291
|
-
// web-src/core/routes.ts
|
|
17292
|
-
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
17293
|
-
var init_routes = __esm(() => {
|
|
17294
|
-
SPA_PATHS = [
|
|
17295
|
-
"/todif",
|
|
17296
|
-
"/todiff",
|
|
17297
|
-
"/file",
|
|
17298
|
-
"/help",
|
|
17299
|
-
"/history",
|
|
17300
|
-
"/database",
|
|
17301
|
-
"/doctor"
|
|
17302
|
-
];
|
|
17303
|
-
APP_ENTRY_PATHS = ["/", "/index.html"];
|
|
17304
|
-
});
|
|
17305
|
-
|
|
17306
17701
|
// web-src/server/cache.ts
|
|
17307
17702
|
import { lstatSync as lstatSync3 } from "node:fs";
|
|
17308
17703
|
import { join as join14 } from "node:path";
|
|
@@ -19637,6 +20032,8 @@ function fileMetadataForTarget(target, path) {
|
|
|
19637
20032
|
function attachTreeEntryMetadata(target, entry) {
|
|
19638
20033
|
if (entry.type === "tree")
|
|
19639
20034
|
return { ...entry, ...directoryMetadata(target, entry.path) };
|
|
20035
|
+
if (entry.type === "commit" && !entry.submodule && (target === "worktree" || target === ""))
|
|
20036
|
+
return { ...entry, ...directoryMetadata(target, entry.path) };
|
|
19640
20037
|
if (entry.type !== "blob")
|
|
19641
20038
|
return entry;
|
|
19642
20039
|
return { ...entry, ...fileMetadataForTarget(target, entry.path) };
|