@youtyan/code-viewer 0.6.0 → 0.6.1

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.
@@ -3765,6 +3765,93 @@ Default (non --json) output:
3765
3765
  ]);
3766
3766
  });
3767
3767
 
3768
+ // web-src/core/routes.ts
3769
+ function assertNever(value) {
3770
+ throw new Error(`unhandled route: ${JSON.stringify(value)}`);
3771
+ }
3772
+ function formatLineTarget(line) {
3773
+ return typeof line === "number" ? String(line) : `${line.start}-${line.end}`;
3774
+ }
3775
+ function buildRoute(route) {
3776
+ switch (route.screen) {
3777
+ case "repo": {
3778
+ const params = new URLSearchParams;
3779
+ if (route.ref && route.ref !== "worktree")
3780
+ params.set("ref", route.ref);
3781
+ if (route.path)
3782
+ params.set("path", route.path);
3783
+ const qs = params.toString();
3784
+ return `/${qs ? `?${qs}` : ""}`;
3785
+ }
3786
+ case "file":
3787
+ if (route.view === "blob") {
3788
+ 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" : "");
3789
+ }
3790
+ if (route.view === "blame") {
3791
+ const ref = route.ref || "worktree";
3792
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
3793
+ }
3794
+ if (route.view === "history") {
3795
+ 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))}` : "");
3796
+ }
3797
+ 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" : "");
3798
+ case "diff":
3799
+ 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))}` : "");
3800
+ case "help": {
3801
+ const params = new URLSearchParams;
3802
+ if (route.lang && route.lang !== "en")
3803
+ params.set("lang", route.lang);
3804
+ if (route.section && route.section !== "overview")
3805
+ params.set("section", route.section);
3806
+ const qs = params.toString();
3807
+ return `/help${qs ? `?${qs}` : ""}`;
3808
+ }
3809
+ case "history": {
3810
+ const params = new URLSearchParams;
3811
+ if (route.ref && route.ref !== "HEAD")
3812
+ params.set("ref", route.ref);
3813
+ if (route.commit)
3814
+ params.set("commit", route.commit);
3815
+ const qs = params.toString();
3816
+ return `/history${qs ? `?${qs}` : ""}`;
3817
+ }
3818
+ case "database": {
3819
+ const params = new URLSearchParams;
3820
+ if (route.db)
3821
+ params.set("db", route.db);
3822
+ if (route.schema)
3823
+ params.set("schema", route.schema);
3824
+ if (route.table)
3825
+ params.set("table", route.table);
3826
+ if (route.tab)
3827
+ params.set("tab", route.tab);
3828
+ if (route.diffBefore)
3829
+ params.set("diffBefore", route.diffBefore);
3830
+ if (route.diffAfter)
3831
+ params.set("diffAfter", route.diffAfter);
3832
+ const qs = params.toString();
3833
+ return `/database${qs ? `?${qs}` : ""}`;
3834
+ }
3835
+ case "unknown":
3836
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
3837
+ default:
3838
+ return assertNever(route);
3839
+ }
3840
+ }
3841
+ var SPA_PATHS, APP_ENTRY_PATHS;
3842
+ var init_routes = __esm(() => {
3843
+ SPA_PATHS = [
3844
+ "/todif",
3845
+ "/todiff",
3846
+ "/file",
3847
+ "/help",
3848
+ "/history",
3849
+ "/database",
3850
+ "/doctor"
3851
+ ];
3852
+ APP_ENTRY_PATHS = ["/", "/index.html"];
3853
+ });
3854
+
3768
3855
  // web-src/server/query-cli.ts
3769
3856
  var exports_query_cli = {};
3770
3857
  __export(exports_query_cli, {
@@ -5149,6 +5236,18 @@ function buildSnapshotPollCommand(serverUrl, db, schema) {
5149
5236
  const schemaArg = schema ? ` --schema ${shellSingleQuote(schema)}` : "";
5150
5237
  return `${cli} snapshot list --db ${shellSingleQuote(db)}${schemaArg} --json`;
5151
5238
  }
5239
+ function buildSnapshotDiffUrl(serverUrl, dbId, schema, beforeId, afterId) {
5240
+ const path = buildRoute({
5241
+ screen: "database",
5242
+ db: dbId,
5243
+ schema,
5244
+ tab: "snapshot",
5245
+ diffBefore: beforeId,
5246
+ diffAfter: afterId,
5247
+ range: { from: "", to: "" }
5248
+ });
5249
+ return new URL(path, serverUrl).toString();
5250
+ }
5152
5251
  function buildDiffRowsCommand(serverUrl, before, after, table) {
5153
5252
  const cli = `code-viewer query --server ${shellSingleQuote(serverUrl)}`;
5154
5253
  return `${cli} diff rows --before ${shellSingleQuote(before)} ` + `--after ${shellSingleQuote(after)} --table ${shellSingleQuote(table)} --json`;
@@ -5301,6 +5400,7 @@ async function runDiffTables(serverUrl, command) {
5301
5400
  const body = await requestJson(serverUrl, `/_db/snapshot/diff/tables${qs}`, "GET", undefined, "diff tables");
5302
5401
  const enriched = {
5303
5402
  ...body,
5403
+ diffUrl: buildSnapshotDiffUrl(serverUrl, body.dbId, body.schema, body.beforeId, body.afterId),
5304
5404
  tables: body.tables.map((t) => ({
5305
5405
  ...t,
5306
5406
  diffRowsCommand: buildDiffRowsCommand(serverUrl, body.beforeId, body.afterId, t.tableName)
@@ -5312,8 +5412,10 @@ async function runDiffTables(serverUrl, command) {
5312
5412
  }
5313
5413
  if (!enriched.tables.length) {
5314
5414
  console.log("no tables in diff");
5415
+ console.log(`# view in browser: ${enriched.diffUrl}`);
5315
5416
  return;
5316
5417
  }
5418
+ console.log(`# view in browser: ${enriched.diffUrl}`);
5317
5419
  for (const t of enriched.tables) {
5318
5420
  const cov = t.coverage === "both" ? "" : ` (${t.coverage})`;
5319
5421
  console.log(`${t.tableName} +${t.insertedCount} ~${t.updatedCount} -${t.deletedCount} =${t.unchangedCount}${cov}`);
@@ -5570,10 +5672,13 @@ no separate stored diff entity, so you always pass both snapshot ids.
5570
5672
  code-viewer query snapshot list --db app.db --json
5571
5673
 
5572
5674
  5. View the diff (per-table summary, then per-row detail). diff tables
5573
- prints each per-table summary line plus a paste-safe "# diff rows: ..."
5574
- hint right below it, and --json adds a diffRowsCommand field to each
5575
- tables[] element, so you can drill into row detail without rebuilding
5576
- the command yourself:
5675
+ prints a "# view in browser: <url>" hint up top (opens the same diff in
5676
+ the human's browser, Database > Snapshot tab), then each per-table
5677
+ summary line plus a paste-safe "# diff rows: ..." hint right below it.
5678
+ --json adds the same browser link as a diffUrl field and a
5679
+ diffRowsCommand field on each tables[] element, so you can hand the
5680
+ human a direct link or drill into row detail without rebuilding the
5681
+ command yourself:
5577
5682
  code-viewer query diff tables --before snap-abc123 --after snap-def456 --json
5578
5683
  code-viewer query diff rows --before snap-abc123 --after snap-def456 \\
5579
5684
  --table users --json
@@ -5760,10 +5865,13 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5760
5865
  - diff tables: human-readable lines (default) plus a paste-safe
5761
5866
  "# diff rows: code-viewer query --server '<url>' diff rows --before '<id>'
5762
5867
  --after '<id>' --table '<table>' --json" comment line right below each table,
5763
- so AI/human can drill into row detail without rebuilding the command. With
5764
- --json the full /_db/snapshot/diff/tables payload is emitted and each
5765
- tables[] element gains an additive diffRowsCommand field with the same
5766
- literal. server URL / snapshot ids / table names are POSIX single-quoted.
5868
+ so AI/human can drill into row detail without rebuilding the command. A
5869
+ "# view in browser: <url>" hint is printed once up top — opens the same
5870
+ before/after comparison in the human's browser (Database > Snapshot tab).
5871
+ With --json the full /_db/snapshot/diff/tables payload is emitted, the
5872
+ top-level diffUrl field carries the same browser link, and each tables[]
5873
+ element gains an additive diffRowsCommand field with the same literal.
5874
+ server URL / snapshot ids / table names are POSIX single-quoted.
5767
5875
  - snapshot create: prints "snapshot started" immediately with the snapshotId.
5768
5876
  The no-wait output also includes a paste-safe poll command that pins
5769
5877
  --server '<url>' and single-quotes db/schema so AI/human paste does not
@@ -5843,6 +5951,7 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5843
5951
  server only scans text-like columns (faster, cheaper).
5844
5952
  `, 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
5953
  var init_query_cli = __esm(() => {
5954
+ init_routes();
5846
5955
  init_cli_helpers();
5847
5956
  init_cli_helpers();
5848
5957
  VALUE_FLAGS2 = new Set([
@@ -10039,6 +10148,11 @@ var init_source_meta = __esm(() => {
10039
10148
  "conf",
10040
10149
  "env",
10041
10150
  "properties",
10151
+ "rules",
10152
+ "rule",
10153
+ "prompt",
10154
+ "prompts",
10155
+ "instructions",
10042
10156
  "gitignore",
10043
10157
  "dockerignore",
10044
10158
  "editorconfig",
@@ -11775,6 +11889,15 @@ function parseDockerDbId(dbId) {
11775
11889
  return null;
11776
11890
  return { serviceName: rest, relDir: "", database };
11777
11891
  }
11892
+ function canonicalizeDockerDbId(dbId) {
11893
+ const parsed = parseDockerDbId(dbId);
11894
+ if (!parsed)
11895
+ return null;
11896
+ const database = parsed.database ? `:${parsed.database}` : "";
11897
+ if (!parsed.relDir)
11898
+ return `docker:${parsed.serviceName}${database}`;
11899
+ return `docker:${parsed.serviceName}@${encodeURIComponent(parsed.relDir)}${database}`;
11900
+ }
11778
11901
  async function findDockerServiceByDbIdAsync(cwd, dbId, kind, omitDirNames, signal) {
11779
11902
  const parsed = parseDockerDbId(dbId);
11780
11903
  if (!parsed)
@@ -12194,6 +12317,9 @@ function sanitizeSettings(raw) {
12194
12317
  const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
12195
12318
  if (annotationPanelOpen !== undefined)
12196
12319
  out.annotationPanelOpen = annotationPanelOpen;
12320
+ const annotationPanelWidth = optionalNumber(raw.annotationPanelWidth, 260, 720);
12321
+ if (annotationPanelWidth !== undefined)
12322
+ out.annotationPanelWidth = annotationPanelWidth;
12197
12323
  const annotationFollow = optionalBoolean(raw.annotationFollow);
12198
12324
  if (annotationFollow !== undefined)
12199
12325
  out.annotationFollow = annotationFollow;
@@ -14175,6 +14301,9 @@ async function getStoreDb(cwd) {
14175
14301
  try {
14176
14302
  storeDb.exec("ALTER TABLE snapshots ADD COLUMN schema_name TEXT");
14177
14303
  } catch {}
14304
+ try {
14305
+ storeDb.exec("ALTER TABLE snapshot_tables ADD COLUMN revision_id TEXT");
14306
+ } catch {}
14178
14307
  return storeDb;
14179
14308
  }
14180
14309
  function makeId2(prefix) {
@@ -14183,32 +14312,163 @@ function makeId2(prefix) {
14183
14312
  function hashPayload(payloadJson) {
14184
14313
  return createHash5("sha256").update(payloadJson).digest("hex");
14185
14314
  }
14315
+ function hashLengthPrefixed(hasher, value) {
14316
+ hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
14317
+ hasher.update(value);
14318
+ hasher.update(`
14319
+ `);
14320
+ }
14321
+ function deleteOrphanPayloads(db) {
14322
+ db.prepare(`DELETE FROM snapshot_payloads
14323
+ WHERE NOT EXISTS (
14324
+ SELECT 1 FROM snapshot_rows
14325
+ WHERE snapshot_rows.payload_hash = snapshot_payloads.payload_hash
14326
+ )
14327
+ AND NOT EXISTS (
14328
+ SELECT 1 FROM snapshot_table_revision_rows
14329
+ WHERE snapshot_table_revision_rows.payload_hash = snapshot_payloads.payload_hash
14330
+ )`).run();
14331
+ }
14332
+ function dockerDbIdFilterValues(dbId) {
14333
+ const values = [dbId];
14334
+ const canonical = canonicalizeDockerDbId(dbId);
14335
+ if (canonical)
14336
+ values.push(canonical);
14337
+ const parsed = parseDockerDbId(dbId);
14338
+ if (parsed?.relDir) {
14339
+ const database = parsed.database ? `:${parsed.database}` : "";
14340
+ values.push(`docker:${parsed.serviceName}@${parsed.relDir}${database}`);
14341
+ }
14342
+ return [...new Set(values)];
14343
+ }
14344
+ function getSnapshotScopeRow(db, snapshotId) {
14345
+ const row = db.prepare("SELECT db_id, COALESCE(schema_name, 'public') AS schema_name FROM snapshots WHERE id = ?").get(snapshotId);
14346
+ if (!row)
14347
+ throw new Error(`snapshot not found: ${snapshotId}`);
14348
+ return { dbId: row.db_id, schema: row.schema_name };
14349
+ }
14186
14350
  async function createSnapshot(cwd, dbId, kind, tables, note, schema) {
14187
14351
  const db = await getStoreDb(cwd);
14188
14352
  const id = makeId2("snap");
14189
- db.prepare("INSERT INTO snapshots (id, db_id, schema_name, kind, note, created_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, dbId, schema ?? null, kind, note, new Date().toISOString(), "running");
14353
+ const storedDbId = canonicalizeDockerDbId(dbId) ?? dbId;
14354
+ 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
14355
  for (const t of tables) {
14191
14356
  db.prepare("INSERT INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(id, t);
14192
14357
  }
14193
14358
  return id;
14194
14359
  }
14195
- async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows) {
14360
+ async function beginSnapshotTableRevision(cwd, snapshotId, tableName, pkColumns) {
14196
14361
  const db = await getStoreDb(cwd);
14197
- const tableHasher = createHash5("sha256");
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 (?, ?, ?, ?, ?, ?)");
14362
+ const id = makeId2("rev");
14363
+ const scope = getSnapshotScopeRow(db, snapshotId);
14364
+ db.exec("BEGIN");
14365
+ try {
14366
+ db.prepare("INSERT OR IGNORE INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(snapshotId, tableName);
14367
+ db.prepare(`INSERT INTO snapshot_table_revisions
14368
+ (id, source_snapshot_id, db_id, schema_name, table_name, row_count, table_hash, hash_version, pk_columns_json, status, created_at)
14369
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, snapshotId, scope.dbId, scope.schema, tableName, 0, "", SNAPSHOT_TABLE_HASH_VERSION, JSON.stringify(pkColumns), "building", new Date().toISOString());
14370
+ db.exec("COMMIT");
14371
+ } catch (err) {
14372
+ try {
14373
+ db.exec("ROLLBACK");
14374
+ } catch {}
14375
+ throw err;
14376
+ }
14377
+ return id;
14378
+ }
14379
+ async function addSnapshotTableRows(cwd, revisionId, rows) {
14380
+ if (rows.length === 0)
14381
+ return;
14382
+ const db = await getStoreDb(cwd);
14383
+ 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
14384
  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
14385
  db.exec("BEGIN");
14202
14386
  try {
14203
14387
  for (const row of rows) {
14204
14388
  const rowKeyHash = createHash5("sha256").update(row.rowKeyJson).digest("hex");
14205
14389
  const payloadHash = hashPayload(row.payloadJson);
14206
- tableHasher.update(row.rowHash);
14207
- insertRow.run(snapshotId, tableName, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
14390
+ insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
14208
14391
  insertPayload.run(payloadHash, row.payloadJson);
14209
14392
  }
14210
- const tableHash = tableHasher.digest("hex");
14211
- updateTable.run(rows.length, tableHash, JSON.stringify(pkColumns), snapshotId, tableName);
14393
+ db.exec("COMMIT");
14394
+ } catch (err) {
14395
+ try {
14396
+ db.exec("ROLLBACK");
14397
+ } catch {}
14398
+ throw err;
14399
+ }
14400
+ }
14401
+ function computeRevisionTableHash(db, revisionId) {
14402
+ const hasher = createHash5("sha256");
14403
+ hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
14404
+ let rowCount = 0;
14405
+ let last;
14406
+ for (;; ) {
14407
+ const rows = last ? db.prepare(`SELECT row_key_hash, row_key_json, row_hash, payload_hash
14408
+ FROM snapshot_table_revision_rows
14409
+ WHERE revision_id = ?
14410
+ AND (row_key_hash > ? OR (row_key_hash = ? AND row_key_json > ?))
14411
+ ORDER BY row_key_hash, row_key_json
14412
+ 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
14413
+ FROM snapshot_table_revision_rows
14414
+ WHERE revision_id = ?
14415
+ ORDER BY row_key_hash, row_key_json
14416
+ LIMIT ?`).all(revisionId, SNAPSHOT_REVISION_HASH_BATCH_SIZE);
14417
+ if (rows.length === 0)
14418
+ break;
14419
+ for (const row of rows) {
14420
+ hashLengthPrefixed(hasher, row.row_key_hash);
14421
+ hashLengthPrefixed(hasher, row.row_key_json);
14422
+ hashLengthPrefixed(hasher, row.row_hash);
14423
+ hashLengthPrefixed(hasher, row.payload_hash);
14424
+ rowCount++;
14425
+ last = {
14426
+ row_key_hash: row.row_key_hash,
14427
+ row_key_json: row.row_key_json
14428
+ };
14429
+ }
14430
+ if (rows.length < SNAPSHOT_REVISION_HASH_BATCH_SIZE)
14431
+ break;
14432
+ }
14433
+ return {
14434
+ rowCount,
14435
+ tableHash: `${SNAPSHOT_TABLE_HASH_PREFIX}${hasher.digest("hex")}`
14436
+ };
14437
+ }
14438
+ async function finalizeSnapshotTableRevision(cwd, snapshotId, tableName, revisionId) {
14439
+ const db = await getStoreDb(cwd);
14440
+ const revision = db.prepare(`SELECT db_id, schema_name, table_name, pk_columns_json
14441
+ FROM snapshot_table_revisions
14442
+ WHERE id = ?`).get(revisionId);
14443
+ if (!revision)
14444
+ throw new Error(`snapshot table revision not found: ${revisionId}`);
14445
+ const { rowCount, tableHash } = computeRevisionTableHash(db, revisionId);
14446
+ const existing = db.prepare(`SELECT id
14447
+ FROM snapshot_table_revisions
14448
+ WHERE id != ?
14449
+ AND db_id = ?
14450
+ AND schema_name = ?
14451
+ AND table_name = ?
14452
+ AND hash_version = ?
14453
+ AND table_hash = ?
14454
+ AND row_count = ?
14455
+ AND pk_columns_json = ?
14456
+ AND status = 'done'
14457
+ ORDER BY created_at ASC
14458
+ LIMIT 1`).get(revisionId, revision.db_id, revision.schema_name, revision.table_name, SNAPSHOT_TABLE_HASH_VERSION, tableHash, rowCount, revision.pk_columns_json);
14459
+ const adoptedRevisionId = existing?.id ?? revisionId;
14460
+ db.exec("BEGIN");
14461
+ try {
14462
+ if (existing) {
14463
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE id = ?").run(revisionId);
14464
+ } else {
14465
+ db.prepare(`UPDATE snapshot_table_revisions
14466
+ SET row_count = ?, table_hash = ?, status = 'done', source_snapshot_id = NULL
14467
+ WHERE id = ?`).run(rowCount, tableHash, revisionId);
14468
+ }
14469
+ db.prepare(`UPDATE snapshot_tables
14470
+ SET row_count = ?, table_hash = ?, pk_columns_json = ?, revision_id = ?
14471
+ WHERE snapshot_id = ? AND table_name = ?`).run(rowCount, tableHash, revision.pk_columns_json, adoptedRevisionId, snapshotId, tableName);
14212
14472
  db.exec("COMMIT");
14213
14473
  } catch (err) {
14214
14474
  try {
@@ -14220,7 +14480,18 @@ async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows)
14220
14480
  async function finalizeSnapshot(cwd, snapshotId, error) {
14221
14481
  const db = await getStoreDb(cwd);
14222
14482
  if (error) {
14223
- db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
14483
+ db.exec("BEGIN");
14484
+ try {
14485
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
14486
+ deleteOrphanPayloads(db);
14487
+ db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
14488
+ db.exec("COMMIT");
14489
+ } catch (err) {
14490
+ try {
14491
+ db.exec("ROLLBACK");
14492
+ } catch {}
14493
+ throw err;
14494
+ }
14224
14495
  } else {
14225
14496
  db.prepare("UPDATE snapshots SET status = 'done' WHERE id = ?").run(snapshotId);
14226
14497
  }
@@ -14230,8 +14501,9 @@ async function listSnapshots(cwd, dbId, schema) {
14230
14501
  const conditions = [];
14231
14502
  const params = [];
14232
14503
  if (dbId) {
14233
- conditions.push("db_id = ?");
14234
- params.push(dbId);
14504
+ const dbIdValues = dockerDbIdFilterValues(dbId);
14505
+ conditions.push(dbIdValues.length === 1 ? "db_id = ?" : `db_id IN (${dbIdValues.map(() => "?").join(", ")})`);
14506
+ params.push(...dbIdValues);
14235
14507
  }
14236
14508
  if (schema !== undefined) {
14237
14509
  conditions.push("COALESCE(schema_name, 'public') = ?");
@@ -14264,11 +14536,14 @@ async function deleteSnapshot(cwd, snapshotId) {
14264
14536
  db.exec("BEGIN");
14265
14537
  try {
14266
14538
  db.prepare("DELETE FROM snapshots WHERE id = ?").run(snapshotId);
14267
- db.prepare(`DELETE FROM snapshot_payloads
14268
- WHERE NOT EXISTS (
14269
- SELECT 1 FROM snapshot_rows
14270
- WHERE snapshot_rows.payload_hash = snapshot_payloads.payload_hash
14271
- )`).run();
14539
+ db.prepare(`DELETE FROM snapshot_table_revisions
14540
+ WHERE status = 'done'
14541
+ AND NOT EXISTS (
14542
+ SELECT 1 FROM snapshot_tables
14543
+ WHERE snapshot_tables.revision_id = snapshot_table_revisions.id
14544
+ )`).run();
14545
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
14546
+ deleteOrphanPayloads(db);
14272
14547
  db.exec("COMMIT");
14273
14548
  } catch (err) {
14274
14549
  try {
@@ -14278,15 +14553,22 @@ async function deleteSnapshot(cwd, snapshotId) {
14278
14553
  }
14279
14554
  }
14280
14555
  function getSnapshotScope(db, snapshotId) {
14281
- const row = db.prepare("SELECT db_id, COALESCE(schema_name, 'public') AS schema_name FROM snapshots WHERE id = ?").get(snapshotId);
14282
- if (!row)
14283
- throw new Error(`snapshot not found: ${snapshotId}`);
14284
- return { dbId: row.db_id, schema: row.schema_name };
14556
+ return getSnapshotScopeRow(db, snapshotId);
14557
+ }
14558
+ async function getSnapshotScopeById(cwd, snapshotId) {
14559
+ const db = await getStoreDb(cwd);
14560
+ const scope = getSnapshotScope(db, snapshotId);
14561
+ return {
14562
+ dbId: canonicalizeDockerDbId(scope.dbId) ?? scope.dbId,
14563
+ schema: scope.schema
14564
+ };
14285
14565
  }
14286
14566
  function assertSameSnapshotScope(db, beforeId, afterId) {
14287
14567
  const before = getSnapshotScope(db, beforeId);
14288
14568
  const after = getSnapshotScope(db, afterId);
14289
- if (before.dbId !== after.dbId || before.schema !== after.schema) {
14569
+ const beforeDbId = canonicalizeDockerDbId(before.dbId) ?? before.dbId;
14570
+ const afterDbId = canonicalizeDockerDbId(after.dbId) ?? after.dbId;
14571
+ if (beforeDbId !== afterDbId || before.schema !== after.schema) {
14290
14572
  throw new Error(`cannot compare snapshots from different database/schema (${before.dbId}:${before.schema} vs ${after.dbId}:${after.schema})`);
14291
14573
  }
14292
14574
  }
@@ -14338,16 +14620,16 @@ async function computeDiffTables(cwd, beforeId, afterId) {
14338
14620
  continue;
14339
14621
  }
14340
14622
  const insertedCount = db.prepare(`SELECT COUNT(*) AS cnt
14341
- FROM snapshot_rows a
14342
- LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14623
+ FROM snapshot_rows_resolved a
14624
+ LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14343
14625
  WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL`).get(beforeId, table, afterId, table).cnt;
14344
14626
  const deletedCount = db.prepare(`SELECT COUNT(*) AS cnt
14345
- FROM snapshot_rows b
14346
- LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14627
+ FROM snapshot_rows_resolved b
14628
+ LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14347
14629
  WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL`).get(afterId, table, beforeId, table).cnt;
14348
14630
  const updatedCount = db.prepare(`SELECT COUNT(*) AS cnt
14349
- FROM snapshot_rows b
14350
- INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14631
+ FROM snapshot_rows_resolved b
14632
+ INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14351
14633
  WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash`).get(afterId, table, beforeId, table).cnt;
14352
14634
  const unchangedCount = b.row_count - deletedCount - updatedCount;
14353
14635
  results.push({
@@ -14373,8 +14655,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14373
14655
  assertSameSnapshotScope(db, beforeId, afterId);
14374
14656
  const allDiffRows = [];
14375
14657
  const inserted = db.prepare(`SELECT a.row_key_json, a.payload_hash
14376
- FROM snapshot_rows a
14377
- LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14658
+ FROM snapshot_rows_resolved a
14659
+ LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14378
14660
  WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL
14379
14661
  ORDER BY a.row_key_json`).all(beforeId, table, afterId, table);
14380
14662
  for (const r of inserted) {
@@ -14386,8 +14668,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14386
14668
  });
14387
14669
  }
14388
14670
  const deleted = db.prepare(`SELECT b.row_key_json, b.payload_hash
14389
- FROM snapshot_rows b
14390
- LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14671
+ FROM snapshot_rows_resolved b
14672
+ LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14391
14673
  WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL
14392
14674
  ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
14393
14675
  for (const r of deleted) {
@@ -14399,8 +14681,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14399
14681
  });
14400
14682
  }
14401
14683
  const updated = db.prepare(`SELECT b.row_key_json, b.payload_hash AS before_ph, a.payload_hash AS after_ph
14402
- FROM snapshot_rows b
14403
- INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14684
+ FROM snapshot_rows_resolved b
14685
+ INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14404
14686
  WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash
14405
14687
  ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
14406
14688
  for (const r of updated) {
@@ -14454,10 +14736,45 @@ CREATE TABLE IF NOT EXISTS snapshot_tables (
14454
14736
  row_count INTEGER NOT NULL DEFAULT 0,
14455
14737
  table_hash TEXT NOT NULL DEFAULT '',
14456
14738
  pk_columns_json TEXT NOT NULL DEFAULT '[]',
14739
+ revision_id TEXT,
14457
14740
  PRIMARY KEY (snapshot_id, table_name),
14458
- FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE
14741
+ FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE,
14742
+ FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id)
14743
+ );
14744
+
14745
+ CREATE TABLE IF NOT EXISTS snapshot_table_revisions (
14746
+ id TEXT PRIMARY KEY,
14747
+ source_snapshot_id TEXT,
14748
+ db_id TEXT NOT NULL,
14749
+ schema_name TEXT NOT NULL,
14750
+ table_name TEXT NOT NULL,
14751
+ row_count INTEGER NOT NULL DEFAULT 0,
14752
+ table_hash TEXT NOT NULL DEFAULT '',
14753
+ hash_version INTEGER NOT NULL DEFAULT 2,
14754
+ pk_columns_json TEXT NOT NULL DEFAULT '[]',
14755
+ status TEXT NOT NULL DEFAULT 'building',
14756
+ created_at TEXT NOT NULL
14459
14757
  );
14460
14758
 
14759
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revisions_lookup
14760
+ ON snapshot_table_revisions(db_id, schema_name, table_name, hash_version, table_hash, row_count, pk_columns_json, status);
14761
+
14762
+ CREATE TABLE IF NOT EXISTS snapshot_table_revision_rows (
14763
+ revision_id TEXT NOT NULL,
14764
+ row_key_hash TEXT NOT NULL,
14765
+ row_key_json TEXT NOT NULL,
14766
+ row_hash TEXT NOT NULL,
14767
+ payload_hash TEXT NOT NULL,
14768
+ PRIMARY KEY (revision_id, row_key_hash),
14769
+ FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id) ON DELETE CASCADE
14770
+ );
14771
+
14772
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_revision_key
14773
+ ON snapshot_table_revision_rows(revision_id, row_key_hash);
14774
+
14775
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_payload_hash
14776
+ ON snapshot_table_revision_rows(payload_hash);
14777
+
14461
14778
  CREATE TABLE IF NOT EXISTS snapshot_rows (
14462
14779
  snapshot_id TEXT NOT NULL,
14463
14780
  table_name TEXT NOT NULL,
@@ -14475,14 +14792,40 @@ CREATE TABLE IF NOT EXISTS snapshot_payloads (
14475
14792
  );
14476
14793
 
14477
14794
  -- deleteSnapshot の orphan cleanup (snapshot_payloads を残さない) は
14478
- -- snapshot_rows.payload_hash で逆引きする。index が無いと
14479
- -- snapshot_payloads 件数 × snapshot_rows 全件の相関スキャンになる。
14795
+ -- legacy rows と revision rows の payload_hash で逆引きする。index が無いと
14796
+ -- snapshot_payloads 件数 × rows 全件の相関スキャンになる。
14480
14797
  CREATE INDEX IF NOT EXISTS idx_snapshot_rows_payload_hash
14481
14798
  ON snapshot_rows(payload_hash);
14482
14799
 
14483
- `, storeDb = null, storeDbPath = null;
14800
+ DROP VIEW IF EXISTS snapshot_rows_resolved;
14801
+
14802
+ CREATE VIEW snapshot_rows_resolved AS
14803
+ SELECT
14804
+ st.snapshot_id AS snapshot_id,
14805
+ st.table_name AS table_name,
14806
+ rr.row_key_hash AS row_key_hash,
14807
+ rr.row_key_json AS row_key_json,
14808
+ rr.row_hash AS row_hash,
14809
+ rr.payload_hash AS payload_hash
14810
+ FROM snapshot_tables st
14811
+ INNER JOIN snapshot_table_revision_rows rr
14812
+ ON rr.revision_id = st.revision_id
14813
+ WHERE st.revision_id IS NOT NULL
14814
+ UNION ALL
14815
+ SELECT
14816
+ snapshot_id,
14817
+ table_name,
14818
+ row_key_hash,
14819
+ row_key_json,
14820
+ row_hash,
14821
+ payload_hash
14822
+ FROM snapshot_rows;
14823
+
14824
+ `, storeDb = null, storeDbPath = null, SNAPSHOT_TABLE_HASH_VERSION = 2, SNAPSHOT_TABLE_HASH_PREFIX, SNAPSHOT_REVISION_HASH_BATCH_SIZE = 1000;
14484
14825
  var init_snapshot_store = __esm(() => {
14826
+ init_discovery();
14485
14827
  init_sqlite_driver();
14828
+ SNAPSHOT_TABLE_HASH_PREFIX = `v${SNAPSHOT_TABLE_HASH_VERSION}:`;
14486
14829
  });
14487
14830
 
14488
14831
  // web-src/server/database/sources/types.ts
@@ -14505,7 +14848,6 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
14505
14848
  const container = containers[i];
14506
14849
  throwIfAborted(options.signal, "snapshot cancelled");
14507
14850
  onProgress?.({ container, done: false, index: i, total });
14508
- const collected = [];
14509
14851
  let pkColumns = [];
14510
14852
  if (snapshotSource.model === "sql") {
14511
14853
  try {
@@ -14517,16 +14859,28 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
14517
14859
  }
14518
14860
  }
14519
14861
  throwIfAborted(options.signal, "snapshot cancelled");
14862
+ const revisionId = await beginSnapshotTableRevision(cwd, snapshotId, container, pkColumns);
14863
+ const pendingRows = [];
14864
+ const flushRows = async () => {
14865
+ if (pendingRows.length === 0)
14866
+ return;
14867
+ const rows = pendingRows.splice(0, pendingRows.length);
14868
+ await addSnapshotTableRows(cwd, revisionId, rows);
14869
+ };
14520
14870
  for await (const item of snapshotSource.iterateForSnapshot(container, options.signal)) {
14521
14871
  throwIfAborted(options.signal, "snapshot cancelled");
14522
- collected.push({
14872
+ pendingRows.push({
14523
14873
  rowKeyJson: item.keyJson,
14524
14874
  rowHash: item.rowHash,
14525
14875
  payloadJson: item.payloadJson
14526
14876
  });
14877
+ if (pendingRows.length >= SNAPSHOT_FLUSH_THRESHOLD) {
14878
+ await flushRows();
14879
+ }
14527
14880
  }
14528
14881
  throwIfAborted(options.signal, "snapshot cancelled");
14529
- await addSnapshotTableData(cwd, snapshotId, container, pkColumns, collected);
14882
+ await flushRows();
14883
+ await finalizeSnapshotTableRevision(cwd, snapshotId, container, revisionId);
14530
14884
  }
14531
14885
  await finalizeSnapshot(cwd, snapshotId);
14532
14886
  onProgress?.({ container: "", done: true, index: total, total });
@@ -15821,7 +16175,14 @@ async function handleDiffTables(cwd, url) {
15821
16175
  return textError("missing before or after parameter", 400);
15822
16176
  try {
15823
16177
  const tables = await computeDiffTables(cwd, beforeId, afterId);
15824
- return json({ beforeId, afterId, tables });
16178
+ const scope = await getSnapshotScopeById(cwd, beforeId);
16179
+ return json({
16180
+ beforeId,
16181
+ afterId,
16182
+ dbId: scope.dbId,
16183
+ schema: scope.schema,
16184
+ tables
16185
+ });
15825
16186
  } catch (err) {
15826
16187
  return handleError("database", "compute diff", err);
15827
16188
  }
@@ -17288,21 +17649,6 @@ function normalizeNewDirectoryName(name) {
17288
17649
  return trimmed;
17289
17650
  }
17290
17651
 
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
17652
  // web-src/server/cache.ts
17307
17653
  import { lstatSync as lstatSync3 } from "node:fs";
17308
17654
  import { join as join14 } from "node:path";