@youtyan/code-viewer 0.2.11 → 0.4.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.
@@ -759,6 +759,16 @@ import {
759
759
  statSync
760
760
  } from "node:fs";
761
761
  import { join as join2 } from "node:path";
762
+ function normalizeBlameRef(ref, base) {
763
+ const rawRef = ref || "worktree";
764
+ if (base === "worktree" && rawRef !== "worktree") {
765
+ return { base: "HEAD", ref: rawRef };
766
+ }
767
+ if (base === "HEAD" && rawRef === "worktree") {
768
+ return { base: "HEAD", ref: "HEAD" };
769
+ }
770
+ return { base, ref: rawRef };
771
+ }
762
772
  function run(args, cwd) {
763
773
  return runSync(args, cwd);
764
774
  }
@@ -1063,6 +1073,13 @@ function commitHistory(cwd, options) {
1063
1073
  const skip = Math.max(0, Math.floor(options.skip) || 0);
1064
1074
  const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
1065
1075
  const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
1076
+ const pathFilter = (options.path || "").trim();
1077
+ const pathArgs = [];
1078
+ if (pathFilter && !pathFilter.includes("\x00") && !pathFilter.startsWith("-")) {
1079
+ if (!pathFilter.endsWith("/"))
1080
+ pathArgs.push("--follow");
1081
+ pathArgs.push("--", pathFilter);
1082
+ }
1066
1083
  const res = run([
1067
1084
  "git",
1068
1085
  "log",
@@ -1072,7 +1089,8 @@ function commitHistory(cwd, options) {
1072
1089
  `--format=${HISTORY_FORMAT}`,
1073
1090
  ...filterArgs,
1074
1091
  verified.stdout.trim(),
1075
- ...pathspec
1092
+ ...pathspec,
1093
+ ...pathArgs
1076
1094
  ], cwd);
1077
1095
  if (res.code !== 0)
1078
1096
  return { commits: [], hasMore: false, error: "git log failed" };
@@ -1173,6 +1191,130 @@ function numstatZ(args, cwd) {
1173
1191
  function isToolInternalPath(path) {
1174
1192
  return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
1175
1193
  }
1194
+ function syntheticUncommittedBlameFromWorktree(cwd, path) {
1195
+ const filePath = join2(cwd, path);
1196
+ try {
1197
+ const stat = statSync(filePath);
1198
+ if (!stat.isFile())
1199
+ return { lines: [], commits: {}, error: "not a file" };
1200
+ const text = readFileSync(filePath, "utf8");
1201
+ const normalized = text.replace(/\r\n/g, `
1202
+ `).replace(/\r/g, `
1203
+ `);
1204
+ const lineCount = normalized.length ? normalized.endsWith(`
1205
+ `) ? normalized.length - 1 === 0 ? 1 : normalized.split(`
1206
+ `).length - 1 : normalized.split(`
1207
+ `).length : 1;
1208
+ const lines = [];
1209
+ for (let i = 1;i <= lineCount; i++) {
1210
+ lines.push({ lineNo: i, sha: BLAME_ZERO_SHA, isUncommitted: true });
1211
+ }
1212
+ return {
1213
+ lines,
1214
+ commits: {
1215
+ [BLAME_ZERO_SHA]: {
1216
+ sha: BLAME_ZERO_SHA,
1217
+ author: "Not Committed Yet",
1218
+ authorMail: "",
1219
+ authorTime: 0,
1220
+ summary: "Working tree",
1221
+ isUncommitted: true
1222
+ }
1223
+ },
1224
+ isUntracked: true,
1225
+ isSynthetic: true
1226
+ };
1227
+ } catch (err) {
1228
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
1229
+ return { lines: [], commits: {}, error: "file not found" };
1230
+ }
1231
+ return { lines: [], commits: {}, error: "file not readable" };
1232
+ }
1233
+ }
1234
+ function blame(cwd, options) {
1235
+ const path = options.path;
1236
+ if (!path || path.includes("\x00") || path.startsWith("-")) {
1237
+ return { lines: [], commits: {}, error: "invalid path" };
1238
+ }
1239
+ const normalized = normalizeBlameRef(options.ref, options.base);
1240
+ const args = ["git", "blame", "--porcelain"];
1241
+ if (normalized.base === "HEAD") {
1242
+ if (normalized.ref.startsWith("-") || normalized.ref.includes("\x00"))
1243
+ return { lines: [], commits: {}, error: "invalid ref" };
1244
+ args.push(normalized.ref);
1245
+ }
1246
+ args.push("--", path);
1247
+ const res = run(args, cwd);
1248
+ if (res.code !== 0) {
1249
+ if (normalized.base === "worktree") {
1250
+ return syntheticUncommittedBlameFromWorktree(cwd, path);
1251
+ }
1252
+ return {
1253
+ lines: [],
1254
+ commits: {},
1255
+ error: res.stderr.trim() || "blame failed"
1256
+ };
1257
+ }
1258
+ const lines = [];
1259
+ const commits = {};
1260
+ const rawLines = res.stdout.split(`
1261
+ `);
1262
+ let i = 0;
1263
+ while (i < rawLines.length) {
1264
+ const headerLine = rawLines[i];
1265
+ if (!headerLine) {
1266
+ i++;
1267
+ continue;
1268
+ }
1269
+ const headerMatch = /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(headerLine);
1270
+ if (!headerMatch) {
1271
+ i++;
1272
+ continue;
1273
+ }
1274
+ const sha = headerMatch[1];
1275
+ const finalLine = Number(headerMatch[3]);
1276
+ i++;
1277
+ let commit = commits[sha];
1278
+ if (!commit) {
1279
+ commit = {
1280
+ sha,
1281
+ author: "",
1282
+ authorMail: "",
1283
+ authorTime: 0,
1284
+ summary: "",
1285
+ isUncommitted: sha === BLAME_ZERO_SHA
1286
+ };
1287
+ commits[sha] = commit;
1288
+ }
1289
+ while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
1290
+ const metaLine = rawLines[i++];
1291
+ if (!metaLine)
1292
+ continue;
1293
+ const sp = metaLine.indexOf(" ");
1294
+ const key = sp >= 0 ? metaLine.slice(0, sp) : metaLine;
1295
+ const val = sp >= 0 ? metaLine.slice(sp + 1) : "";
1296
+ if (key === "author" && !commit.author)
1297
+ commit.author = val;
1298
+ else if (key === "author-mail" && !commit.authorMail)
1299
+ commit.authorMail = val.replace(/^</, "").replace(/>$/, "");
1300
+ else if (key === "author-time" && !commit.authorTime)
1301
+ commit.authorTime = Number(val) || 0;
1302
+ else if (key === "summary" && !commit.summary)
1303
+ commit.summary = val;
1304
+ }
1305
+ if (i < rawLines.length && rawLines[i].startsWith("\t"))
1306
+ i++;
1307
+ if (Number.isFinite(finalLine) && finalLine > 0) {
1308
+ lines.push({
1309
+ lineNo: finalLine,
1310
+ sha,
1311
+ isUncommitted: sha === BLAME_ZERO_SHA
1312
+ });
1313
+ }
1314
+ }
1315
+ lines.sort((a, b) => a.lineNo - b.lineNo);
1316
+ return { lines, commits };
1317
+ }
1176
1318
  function untracked(cwd, path = "") {
1177
1319
  const args = ["git", "ls-files", "--others", "--exclude-standard"];
1178
1320
  if (path)
@@ -1509,7 +1651,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
1509
1651
  lineTruncated
1510
1652
  };
1511
1653
  }
1512
- var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
1654
+ var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
1513
1655
  var init_git = __esm(() => {
1514
1656
  init_runtime();
1515
1657
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
@@ -4016,12 +4158,41 @@ function sanitizeDbUiPrefs(raw) {
4016
4158
  }
4017
4159
  return Object.keys(out).length > 0 ? out : undefined;
4018
4160
  }
4161
+ function sanitizeDbUiExpandedTables(raw) {
4162
+ if (!isRecord(raw))
4163
+ return;
4164
+ const out = {};
4165
+ let scopeCount = 0;
4166
+ for (const [scopeRaw, tablesRaw] of Object.entries(raw)) {
4167
+ if (scopeCount >= MAX_DB_UI_EXPANDED_SCOPES)
4168
+ break;
4169
+ const scope = safeObjectKey(scopeRaw);
4170
+ if (!scope)
4171
+ continue;
4172
+ const tables = normalizeStringList(tablesRaw, {
4173
+ maxItems: MAX_DB_UI_TABLES,
4174
+ maxLen: MAX_KEY_LEN,
4175
+ sort: true
4176
+ });
4177
+ if (!tables || tables.length === 0)
4178
+ continue;
4179
+ out[scope] = tables;
4180
+ scopeCount++;
4181
+ }
4182
+ return Object.keys(out).length > 0 ? out : undefined;
4183
+ }
4019
4184
  function sanitizeDbUiState(raw) {
4020
4185
  if (!isRecord(raw))
4021
4186
  return emptyDbUiState();
4022
4187
  const prefs = sanitizeDbUiPrefs(raw.prefs);
4188
+ const expandedTables = sanitizeDbUiExpandedTables(raw.expandedTables);
4023
4189
  if (!isRecord(raw.columnWidths)) {
4024
- return prefs ? { ...emptyDbUiState(), prefs } : emptyDbUiState();
4190
+ const out2 = emptyDbUiState();
4191
+ if (expandedTables)
4192
+ out2.expandedTables = expandedTables;
4193
+ if (prefs)
4194
+ out2.prefs = prefs;
4195
+ return out2;
4025
4196
  }
4026
4197
  const columnWidths = {};
4027
4198
  let dbCount = 0;
@@ -4062,6 +4233,8 @@ function sanitizeDbUiState(raw) {
4062
4233
  dbCount++;
4063
4234
  }
4064
4235
  const out = { version: 1, columnWidths };
4236
+ if (expandedTables)
4237
+ out.expandedTables = expandedTables;
4065
4238
  if (prefs)
4066
4239
  out.prefs = prefs;
4067
4240
  return out;
@@ -4079,12 +4252,30 @@ function mergeDbUiPrefs(current, patch) {
4079
4252
  }
4080
4253
  return Object.keys(next).length > 0 ? next : undefined;
4081
4254
  }
4255
+ function mergeDbUiExpandedTables(current, patch) {
4256
+ if (!isRecord(patch))
4257
+ return current;
4258
+ const next = { ...current ?? {} };
4259
+ for (const [scope, tablesRaw] of Object.entries(patch)) {
4260
+ if (tablesRaw === null) {
4261
+ delete next[scope];
4262
+ continue;
4263
+ }
4264
+ next[scope] = tablesRaw;
4265
+ }
4266
+ return sanitizeDbUiExpandedTables(next);
4267
+ }
4082
4268
  function mergeDbUiState(current, patch) {
4083
4269
  if (!isRecord(patch))
4084
4270
  return current;
4085
4271
  const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
4272
+ const mergedExpandedTables = "expandedTables" in patch ? mergeDbUiExpandedTables(current.expandedTables, patch.expandedTables) : current.expandedTables;
4086
4273
  if (!isRecord(patch.columnWidths)) {
4087
4274
  const merged = { ...current, version: 1 };
4275
+ if (mergedExpandedTables)
4276
+ merged.expandedTables = mergedExpandedTables;
4277
+ else
4278
+ delete merged.expandedTables;
4088
4279
  if (mergedPrefs)
4089
4280
  merged.prefs = mergedPrefs;
4090
4281
  else
@@ -4124,6 +4315,7 @@ function mergeDbUiState(current, patch) {
4124
4315
  ...current,
4125
4316
  ...patch,
4126
4317
  columnWidths,
4318
+ expandedTables: mergedExpandedTables,
4127
4319
  prefs: mergedPrefs,
4128
4320
  version: 1
4129
4321
  });
@@ -4155,7 +4347,7 @@ async function patchDbUiState(root, patch) {
4155
4347
  return { state: next, result: next };
4156
4348
  });
4157
4349
  }
4158
- var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
4350
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
4159
4351
  var init_state_store = __esm(() => {
4160
4352
  init_json_store();
4161
4353
  DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
@@ -4576,52 +4768,37 @@ function serializeDbRow(row) {
4576
4768
  function serializeDbRows(rows) {
4577
4769
  return rows.map(serializeDbRow);
4578
4770
  }
4771
+ function coerceDbValue(value, columnType) {
4772
+ if (value === null)
4773
+ return null;
4774
+ const t = (columnType || "").toLowerCase();
4775
+ if (/bool/.test(t)) {
4776
+ const v = value.trim().toLowerCase();
4777
+ if (v === "")
4778
+ return null;
4779
+ if (v === "true" || v === "t" || v === "1")
4780
+ return true;
4781
+ if (v === "false" || v === "f" || v === "0")
4782
+ return false;
4783
+ return value;
4784
+ }
4785
+ if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
4786
+ const trimmed = value.trim();
4787
+ if (trimmed === "")
4788
+ return null;
4789
+ const n = Number(trimmed);
4790
+ if (Number.isFinite(n) && String(n) === trimmed)
4791
+ return n;
4792
+ return value;
4793
+ }
4794
+ return value;
4795
+ }
4579
4796
  var MIN_SAFE, MAX_SAFE;
4580
4797
  var init_serialize = __esm(() => {
4581
4798
  MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
4582
4799
  MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
4583
4800
  });
4584
4801
 
4585
- // web-src/server/database/sources/sql-snapshot.ts
4586
- import { createHash as createHash2 } from "node:crypto";
4587
- function normalizeRawValue(v) {
4588
- if (v === null)
4589
- return "\\N";
4590
- if (typeof v === "bigint")
4591
- return v.toString();
4592
- if (v instanceof Uint8Array) {
4593
- return `\\x${Buffer.from(v).toString("hex")}`;
4594
- }
4595
- return String(v);
4596
- }
4597
- function rowToPayloadJson(columns, row) {
4598
- const obj = {};
4599
- for (let i = 0;i < columns.length; i++) {
4600
- obj[columns[i]] = serializeDbValue(row[i]);
4601
- }
4602
- return JSON.stringify(obj);
4603
- }
4604
- function computeRowHash(columns, row) {
4605
- const parts = columns.map((_, i) => normalizeRawValue(row[i]));
4606
- return createHash2("sha256").update(parts.join("\t")).digest("hex");
4607
- }
4608
- function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
4609
- if (pkColumns.length === 0) {
4610
- return JSON.stringify({ __rowIndex: rowIndex });
4611
- }
4612
- const keyObj = {};
4613
- for (const pk of pkColumns) {
4614
- const idx = allColumns.indexOf(pk);
4615
- if (idx >= 0)
4616
- keyObj[pk] = serializeDbValue(row[idx]);
4617
- }
4618
- return JSON.stringify(keyObj);
4619
- }
4620
- var SQL_SNAPSHOT_BATCH_SIZE = 500;
4621
- var init_sql_snapshot = __esm(() => {
4622
- init_serialize();
4623
- });
4624
-
4625
4802
  // web-src/server/database/sql-utils.ts
4626
4803
  function sanitizeIdentifier(name, kind = "sqlite") {
4627
4804
  if (kind === "mysql")
@@ -4683,6 +4860,189 @@ function filterOrderByColumns(orderBy, columnNames) {
4683
4860
  const filtered = orderBy.filter((order) => validColumns.has(order.column));
4684
4861
  return filtered.length > 0 ? filtered : undefined;
4685
4862
  }
4863
+ function buildOrderClause(orderBy, kind = "sqlite") {
4864
+ if (!orderBy?.length)
4865
+ return "";
4866
+ const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
4867
+ return ` ORDER BY ${parts.join(", ")}`;
4868
+ }
4869
+ function useParamsFor(kind) {
4870
+ return kind === "sqlite";
4871
+ }
4872
+ function placeValue(coerced, kind, useParams, params) {
4873
+ if (useParams) {
4874
+ params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
4875
+ return "?";
4876
+ }
4877
+ if (coerced === null)
4878
+ return "NULL";
4879
+ if (typeof coerced === "number")
4880
+ return String(coerced);
4881
+ if (typeof coerced === "boolean")
4882
+ return coerced ? "TRUE" : "FALSE";
4883
+ const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
4884
+ return escapeSqlString(text, kind);
4885
+ }
4886
+ function coerceCell(cell, columnType) {
4887
+ return coerceDbValue(cell.value, columnType);
4888
+ }
4889
+ function buildInsertSql(table, cells, columnTypes, kind) {
4890
+ if (cells.length === 0) {
4891
+ throw new Error("insert requires at least one column value");
4892
+ }
4893
+ const useParams = useParamsFor(kind);
4894
+ const params = [];
4895
+ const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
4896
+ const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
4897
+ const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
4898
+ return { sql, params };
4899
+ }
4900
+ function buildUpdateSql(table, set, pk, columnTypes, kind) {
4901
+ if (set.length === 0) {
4902
+ throw new Error("update requires at least one column to set");
4903
+ }
4904
+ if (pk.length === 0) {
4905
+ throw new Error("update requires a primary key condition");
4906
+ }
4907
+ const useParams = useParamsFor(kind);
4908
+ const params = [];
4909
+ const setSql = set.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(", ");
4910
+ const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
4911
+ const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
4912
+ return { sql, params };
4913
+ }
4914
+ function buildDeleteSql(table, pk, columnTypes, kind) {
4915
+ if (pk.length === 0) {
4916
+ throw new Error("delete requires a primary key condition");
4917
+ }
4918
+ const useParams = useParamsFor(kind);
4919
+ const params = [];
4920
+ const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
4921
+ const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
4922
+ return { sql, params };
4923
+ }
4924
+ var init_sql_utils = __esm(() => {
4925
+ init_serialize();
4926
+ });
4927
+
4928
+ // web-src/server/database/mutate.ts
4929
+ function assertCells(cells, label) {
4930
+ if (!Array.isArray(cells)) {
4931
+ throw new Error(`${label} must be an array`);
4932
+ }
4933
+ for (const cell of cells) {
4934
+ if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
4935
+ throw new Error(`${label} contains an invalid cell`);
4936
+ }
4937
+ }
4938
+ return cells;
4939
+ }
4940
+ function buildMutationStatements(table, mutations, columns, kind) {
4941
+ if (!Array.isArray(mutations) || mutations.length === 0) {
4942
+ throw new Error("no mutations provided");
4943
+ }
4944
+ if (mutations.length > MAX_MUTATIONS) {
4945
+ throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
4946
+ }
4947
+ const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
4948
+ const columnNames = new Set(columns.map((c) => c.name));
4949
+ const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
4950
+ const pkNames = new Set(pkColumns);
4951
+ const requireKnownColumns = (cells, label) => {
4952
+ for (const cell of cells) {
4953
+ if (!columnNames.has(cell.column)) {
4954
+ throw new Error(`unknown column: ${cell.column}`);
4955
+ }
4956
+ }
4957
+ };
4958
+ const requirePrimaryKey = (pk) => {
4959
+ if (pkColumns.length === 0) {
4960
+ throw new Error("table has no primary key; row update/delete is not supported");
4961
+ }
4962
+ const provided = new Set(pk.map((c) => c.column));
4963
+ for (const name of pkColumns) {
4964
+ if (!provided.has(name)) {
4965
+ throw new Error(`missing primary key column: ${name}`);
4966
+ }
4967
+ }
4968
+ for (const cell of pk) {
4969
+ if (!pkNames.has(cell.column)) {
4970
+ throw new Error(`not a primary key column: ${cell.column}`);
4971
+ }
4972
+ if (cell.value === null) {
4973
+ throw new Error(`primary key column cannot be null: ${cell.column}`);
4974
+ }
4975
+ }
4976
+ };
4977
+ const statements = [];
4978
+ for (const mutation of mutations) {
4979
+ if (!mutation || typeof mutation !== "object") {
4980
+ throw new Error("invalid mutation");
4981
+ }
4982
+ if (mutation.kind === "insert") {
4983
+ const values = assertCells(mutation.values, "insert values");
4984
+ requireKnownColumns(values, "insert values");
4985
+ statements.push(buildInsertSql(table, values, columnTypes, kind));
4986
+ } else if (mutation.kind === "update") {
4987
+ const pk = assertCells(mutation.pk, "update pk");
4988
+ const values = assertCells(mutation.values, "update values");
4989
+ requirePrimaryKey(pk);
4990
+ requireKnownColumns(values, "update values");
4991
+ statements.push(buildUpdateSql(table, values, pk, columnTypes, kind));
4992
+ } else if (mutation.kind === "delete") {
4993
+ const pk = assertCells(mutation.pk, "delete pk");
4994
+ requirePrimaryKey(pk);
4995
+ statements.push(buildDeleteSql(table, pk, columnTypes, kind));
4996
+ } else {
4997
+ throw new Error(`unknown mutation kind: ${mutation.kind}`);
4998
+ }
4999
+ }
5000
+ return statements;
5001
+ }
5002
+ var MAX_MUTATIONS = 1000;
5003
+ var init_mutate = __esm(() => {
5004
+ init_sql_utils();
5005
+ });
5006
+
5007
+ // web-src/server/database/sources/sql-snapshot.ts
5008
+ import { createHash as createHash2 } from "node:crypto";
5009
+ function normalizeRawValue(v) {
5010
+ if (v === null)
5011
+ return "\\N";
5012
+ if (typeof v === "bigint")
5013
+ return v.toString();
5014
+ if (v instanceof Uint8Array) {
5015
+ return `\\x${Buffer.from(v).toString("hex")}`;
5016
+ }
5017
+ return String(v);
5018
+ }
5019
+ function rowToPayloadJson(columns, row) {
5020
+ const obj = {};
5021
+ for (let i = 0;i < columns.length; i++) {
5022
+ obj[columns[i]] = serializeDbValue(row[i]);
5023
+ }
5024
+ return JSON.stringify(obj);
5025
+ }
5026
+ function computeRowHash(columns, row) {
5027
+ const parts = columns.map((_, i) => normalizeRawValue(row[i]));
5028
+ return createHash2("sha256").update(parts.join("\t")).digest("hex");
5029
+ }
5030
+ function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
5031
+ if (pkColumns.length === 0) {
5032
+ return JSON.stringify({ __rowIndex: rowIndex });
5033
+ }
5034
+ const keyObj = {};
5035
+ for (const pk of pkColumns) {
5036
+ const idx = allColumns.indexOf(pk);
5037
+ if (idx >= 0)
5038
+ keyObj[pk] = serializeDbValue(row[idx]);
5039
+ }
5040
+ return JSON.stringify(keyObj);
5041
+ }
5042
+ var SQL_SNAPSHOT_BATCH_SIZE = 500;
5043
+ var init_sql_snapshot = __esm(() => {
5044
+ init_serialize();
5045
+ });
4686
5046
 
4687
5047
  // web-src/server/database/adapters/spawn-runner.ts
4688
5048
  import { spawn as spawn2 } from "node:child_process";
@@ -4932,6 +5292,24 @@ var init_docker_utils = __esm(() => {
4932
5292
  };
4933
5293
  });
4934
5294
 
5295
+ // web-src/server/database/adapters/sql-capture.ts
5296
+ import { AsyncLocalStorage } from "node:async_hooks";
5297
+ function recordSql(sql) {
5298
+ const bucket = storage.getStore();
5299
+ if (!bucket)
5300
+ return;
5301
+ bucket.sqls.push(sql);
5302
+ }
5303
+ async function captureSql(fn) {
5304
+ const bucket = { sqls: [] };
5305
+ const result = await storage.run(bucket, fn);
5306
+ return { result, executedSql: bucket.sqls };
5307
+ }
5308
+ var storage;
5309
+ var init_sql_capture = __esm(() => {
5310
+ storage = new AsyncLocalStorage;
5311
+ });
5312
+
4935
5313
  // web-src/server/database/adapters/docker.ts
4936
5314
  import { spawnSync as spawnSync3 } from "node:child_process";
4937
5315
  function dockerDatabasesCacheKey(serviceName, kind, cwd) {
@@ -5087,6 +5465,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
5087
5465
  });
5088
5466
  }
5089
5467
  async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
5468
+ recordSql(sql);
5090
5469
  if (spawnSyncImpl2 !== spawnSync3)
5091
5470
  return execInContainer(config, sql, timeoutMs);
5092
5471
  const args = buildExecArgs(config, sql);
@@ -5174,12 +5553,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
5174
5553
  const rows = lines.map((line) => splitTsvLine(line, false));
5175
5554
  return { columns: [], rows };
5176
5555
  }
5177
- function buildOrderClause(orderBy, kind) {
5178
- if (!orderBy?.length)
5179
- return "";
5180
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
5181
- return ` ORDER BY ${parts.join(", ")}`;
5182
- }
5183
5556
  function isMysqlSpatialType(type) {
5184
5557
  const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
5185
5558
  return MYSQL_SPATIAL_TYPES.has(baseType);
@@ -5268,9 +5641,12 @@ function createDockerAdapter(config) {
5268
5641
  const tableLiteral = table.replace(/'/g, "''");
5269
5642
  if (config.kind === "postgresql") {
5270
5643
  const schemaLiteral = postgresSchemaLiteral();
5271
- return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END FROM information_schema.columns c LEFT JOIN (SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name WHERE tc.table_schema = ${schemaLiteral} AND tc.table_name = '${tableLiteral}' AND tc.constraint_type = 'PRIMARY KEY') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
5644
+ return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum LEFT JOIN (SELECT att.attname AS column_name FROM pg_index ix JOIN pg_class clp ON clp.oid = ix.indrelid JOIN pg_namespace nn ON nn.oid = clp.relnamespace JOIN pg_attribute att ON att.attrelid = ix.indrelid AND att.attnum = ANY(ix.indkey) WHERE ix.indisprimary AND nn.nspname = ${schemaLiteral} AND clp.relname = '${tableLiteral}') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
5272
5645
  }
5273
- return `SELECT column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
5646
+ return `SELECT column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
5647
+ }
5648
+ function columnCommentFromInfoValue(value) {
5649
+ return value ? value : null;
5274
5650
  }
5275
5651
  function columnsFromInfoRows(rows) {
5276
5652
  if (config.kind === "postgresql") {
@@ -5279,7 +5655,8 @@ function createDockerAdapter(config) {
5279
5655
  type: row[1],
5280
5656
  nullable: row[2] === "YES",
5281
5657
  primaryKey: row[4] === "YES",
5282
- defaultValue: row[3] === "" ? null : row[3]
5658
+ defaultValue: row[3] === "" ? null : row[3],
5659
+ comment: columnCommentFromInfoValue(row[5])
5283
5660
  }));
5284
5661
  }
5285
5662
  return rows.map((row) => ({
@@ -5287,7 +5664,8 @@ function createDockerAdapter(config) {
5287
5664
  type: row[1],
5288
5665
  nullable: row[2] === "YES",
5289
5666
  primaryKey: row[4] === "PRI",
5290
- defaultValue: row[3] === "NULL" ? null : row[3]
5667
+ defaultValue: row[3] === "NULL" ? null : row[3],
5668
+ comment: columnCommentFromInfoValue(row[5])
5291
5669
  }));
5292
5670
  }
5293
5671
  async function fetchColumnsAsyncUncached(table, signal) {
@@ -5386,10 +5764,10 @@ function createDockerAdapter(config) {
5386
5764
  let sql;
5387
5765
  if (config.kind === "postgresql") {
5388
5766
  const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
5389
- sql = `SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ${postgresSchemaLiteral()} AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5767
+ sql = `SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum WHERE c.table_schema = ${postgresSchemaLiteral()} AND c.table_name IN (${inList}) ORDER BY c.table_name, c.ordinal_position`;
5390
5768
  } else {
5391
5769
  const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
5392
- sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5770
+ sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
5393
5771
  }
5394
5772
  try {
5395
5773
  const queryResult = await execAsync(sql, signal);
@@ -5425,7 +5803,8 @@ function createDockerAdapter(config) {
5425
5803
  type: row[2],
5426
5804
  nullable: row[3] === "YES",
5427
5805
  primaryKey: pkCols.has(row[1]),
5428
- defaultValue: row[4] === "" ? null : row[4]
5806
+ defaultValue: row[4] === "" ? null : row[4],
5807
+ comment: columnCommentFromInfoValue(row[5])
5429
5808
  }));
5430
5809
  } else {
5431
5810
  cols = rows.map((row) => ({
@@ -5433,7 +5812,8 @@ function createDockerAdapter(config) {
5433
5812
  type: row[2],
5434
5813
  nullable: row[3] === "YES",
5435
5814
  primaryKey: row[5] === "PRI",
5436
- defaultValue: row[4] === "NULL" ? null : row[4]
5815
+ defaultValue: row[4] === "NULL" ? null : row[4],
5816
+ comment: columnCommentFromInfoValue(row[6])
5437
5817
  }));
5438
5818
  }
5439
5819
  columnCache.set(tbl, cols);
@@ -5526,12 +5906,7 @@ function createDockerAdapter(config) {
5526
5906
  async getFilteredTablePageWithMeta(table, options, signal) {
5527
5907
  const id = tableIdentifier(table);
5528
5908
  const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
5529
- let columns;
5530
- try {
5531
- columns = await columnsPromise;
5532
- } catch (err) {
5533
- throw err;
5534
- }
5909
+ const columns = await columnsPromise;
5535
5910
  const columnNames = columns.map((column) => column.name);
5536
5911
  const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
5537
5912
  const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
@@ -5600,6 +5975,19 @@ function createDockerAdapter(config) {
5600
5975
  rowCount: Math.min(result.rows.length, maxRows)
5601
5976
  };
5602
5977
  },
5978
+ async applyMutations(table, mutations, signal) {
5979
+ const columns = await this.getColumnsAsync(table, signal);
5980
+ if (columns.length === 0) {
5981
+ throw new Error(`unknown table: ${table}`);
5982
+ }
5983
+ const statements = buildMutationStatements(table, mutations, columns, config.kind);
5984
+ const body = statements.map((s) => s.sql).join(`;
5985
+ `);
5986
+ const wrapped = config.kind === "postgresql" ? `BEGIN; SET LOCAL search_path = ${sanitizeIdentifier(currentPostgresSchema(), config.kind)}; ${body}; COMMIT` : `START TRANSACTION; ${body}; COMMIT`;
5987
+ await execAsync(wrapped, signal);
5988
+ this.invalidateTableMetaCache?.(table);
5989
+ return { affected: statements.length };
5990
+ },
5603
5991
  invalidateTableMetaCache(table) {
5604
5992
  tableMetaCache.invalidate(table);
5605
5993
  if (table) {
@@ -5781,9 +6169,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
5781
6169
  }
5782
6170
  var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES;
5783
6171
  var init_docker = __esm(() => {
6172
+ init_mutate();
5784
6173
  init_sql_snapshot();
6174
+ init_sql_utils();
5785
6175
  init_docker_utils();
5786
6176
  init_spawn_runner();
6177
+ init_sql_capture();
5787
6178
  dockerDatabasesCache = new Map;
5788
6179
  dockerSchemasCache = new Map;
5789
6180
  spawnSyncImpl2 = spawnSync3;
@@ -5800,17 +6191,8 @@ var init_docker = __esm(() => {
5800
6191
  ]);
5801
6192
  });
5802
6193
 
5803
- // web-src/server/database/adapters/sqlite.ts
5804
- function safePrepare(db, sql) {
5805
- const stmt = db.prepare(sql);
5806
- if (typeof stmt.safeIntegers === "function") {
5807
- try {
5808
- stmt.safeIntegers(true);
5809
- } catch {}
5810
- }
5811
- return stmt;
5812
- }
5813
- async function getSqliteClass() {
6194
+ // web-src/server/database/sqlite-driver.ts
6195
+ async function loadSqliteClass() {
5814
6196
  if (cachedDbClass)
5815
6197
  return cachedDbClass;
5816
6198
  try {
@@ -5825,11 +6207,17 @@ async function getSqliteClass() {
5825
6207
  } catch {}
5826
6208
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
5827
6209
  }
5828
- function buildOrderClause2(orderBy) {
5829
- if (!orderBy?.length)
5830
- return "";
5831
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
5832
- return ` ORDER BY ${parts.join(", ")}`;
6210
+ var cachedDbClass = null;
6211
+
6212
+ // web-src/server/database/adapters/sqlite.ts
6213
+ function safePrepare(db, sql) {
6214
+ const stmt = db.prepare(sql);
6215
+ if (typeof stmt.safeIntegers === "function") {
6216
+ try {
6217
+ stmt.safeIntegers(true);
6218
+ } catch {}
6219
+ }
6220
+ return stmt;
5833
6221
  }
5834
6222
  function queryRowsToResult(rows, columns) {
5835
6223
  const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
@@ -5851,7 +6239,30 @@ function queryColumns(db, table) {
5851
6239
  defaultValue: row.dflt_value
5852
6240
  }));
5853
6241
  }
5854
- function createSqliteAdapter(db) {
6242
+ function wrapDbWithSqlCapture(rawDb) {
6243
+ return new Proxy(rawDb, {
6244
+ get(target, prop, receiver) {
6245
+ if (prop === "prepare") {
6246
+ return (sql) => {
6247
+ recordSql(sql);
6248
+ return target.prepare(sql);
6249
+ };
6250
+ }
6251
+ return Reflect.get(target, prop, receiver);
6252
+ }
6253
+ });
6254
+ }
6255
+ function createSqliteAdapter(rawDb, openRawWriteDb) {
6256
+ const db = wrapDbWithSqlCapture(rawDb);
6257
+ let writeDb = null;
6258
+ const getWriteDb = () => {
6259
+ if (!openRawWriteDb) {
6260
+ throw new Error("writes are not supported for this connection");
6261
+ }
6262
+ if (!writeDb)
6263
+ writeDb = wrapDbWithSqlCapture(openRawWriteDb());
6264
+ return writeDb;
6265
+ };
5855
6266
  const adapter = {
5856
6267
  kind: "sqlite",
5857
6268
  model: "sql",
@@ -5951,7 +6362,7 @@ function createSqliteAdapter(db) {
5951
6362
  return this.getTableRowCounts(tables);
5952
6363
  },
5953
6364
  getTablePage(table, options) {
5954
- const order = buildOrderClause2(options.orderBy);
6365
+ const order = buildOrderClause(options.orderBy);
5955
6366
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
5956
6367
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
5957
6368
  const cols = queryColumns(db, table);
@@ -5963,7 +6374,7 @@ function createSqliteAdapter(db) {
5963
6374
  async getTablePageWithMeta(table, options) {
5964
6375
  const columns = queryColumns(db, table);
5965
6376
  const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
5966
- const order = buildOrderClause2(orderBy);
6377
+ const order = buildOrderClause(orderBy);
5967
6378
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
5968
6379
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
5969
6380
  const result = queryRowsToResult(rows, columns);
@@ -5979,7 +6390,7 @@ function createSqliteAdapter(db) {
5979
6390
  const columns = queryColumns(db, table);
5980
6391
  const columnNames = columns.map((column) => column.name);
5981
6392
  const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
5982
- const order = buildOrderClause2(filterOrderByColumns(options.orderBy, columnNames));
6393
+ const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
5983
6394
  const tableId = sanitizeIdentifier(table);
5984
6395
  const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
5985
6396
  const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
@@ -6047,7 +6458,36 @@ function createSqliteAdapter(db) {
6047
6458
  async getTriggersAsync(table) {
6048
6459
  return this.getTriggers(table);
6049
6460
  },
6461
+ async applyMutations(table, mutations) {
6462
+ const columns = queryColumns(db, table);
6463
+ if (columns.length === 0) {
6464
+ throw new Error(`unknown table: ${table}`);
6465
+ }
6466
+ const statements = buildMutationStatements(table, mutations, columns, "sqlite");
6467
+ const wdb = getWriteDb();
6468
+ let affected = 0;
6469
+ wdb.prepare("BEGIN").run();
6470
+ try {
6471
+ for (const stmt of statements) {
6472
+ const result = wdb.prepare(stmt.sql).run(...stmt.params);
6473
+ affected += result.changes ?? 0;
6474
+ }
6475
+ wdb.prepare("COMMIT").run();
6476
+ } catch (err) {
6477
+ try {
6478
+ wdb.prepare("ROLLBACK").run();
6479
+ } catch {}
6480
+ throw err;
6481
+ }
6482
+ return { affected };
6483
+ },
6050
6484
  close() {
6485
+ if (writeDb) {
6486
+ try {
6487
+ writeDb.close();
6488
+ } catch {}
6489
+ writeDb = null;
6490
+ }
6051
6491
  db.close();
6052
6492
  },
6053
6493
  async* iterateForSnapshot(table, signal) {
@@ -6085,14 +6525,18 @@ function createSqliteAdapter(db) {
6085
6525
  };
6086
6526
  return adapter;
6087
6527
  }
6088
- var cachedDbClass = null, sqliteAdapterFactory;
6528
+ var sqliteAdapterFactory;
6089
6529
  var init_sqlite = __esm(() => {
6530
+ init_mutate();
6090
6531
  init_sql_snapshot();
6532
+ init_sql_utils();
6533
+ init_sql_capture();
6091
6534
  sqliteAdapterFactory = {
6092
6535
  async open(path) {
6093
- const DbClass = await getSqliteClass();
6536
+ const DbClass = await loadSqliteClass();
6094
6537
  const db = new DbClass(path, { readonly: true, create: false });
6095
- return createSqliteAdapter(db);
6538
+ const openWriteDb = () => new DbClass(path);
6539
+ return createSqliteAdapter(db, openWriteDb);
6096
6540
  }
6097
6541
  };
6098
6542
  });
@@ -6861,6 +7305,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
6861
7305
  }
6862
7306
  var init_global_search = __esm(() => {
6863
7307
  init_serialize();
7308
+ init_sql_utils();
6864
7309
  });
6865
7310
 
6866
7311
  // web-src/server/database/adapters/elasticsearch.ts
@@ -7060,6 +7505,38 @@ function createElasticsearchAdapter(config) {
7060
7505
  primaryTerm: parsed._primary_term
7061
7506
  };
7062
7507
  }
7508
+ function assertIndex(index) {
7509
+ if (!index || index.includes("/") || index.includes("?")) {
7510
+ throw new Error(`invalid index name: ${index}`);
7511
+ }
7512
+ }
7513
+ async function writeDocAsync(opts) {
7514
+ assertIndex(opts.index);
7515
+ const idGiven = typeof opts.id === "string" && opts.id !== "";
7516
+ let path;
7517
+ let method;
7518
+ if (idGiven && opts.create) {
7519
+ path = `/${encodeURIComponent(opts.index)}/_create/${encodeURIComponent(opts.id)}`;
7520
+ method = "PUT";
7521
+ } else if (idGiven) {
7522
+ path = `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`;
7523
+ method = "PUT";
7524
+ if (opts.seqNo !== undefined && opts.primaryTerm !== undefined) {
7525
+ path += `?if_seq_no=${opts.seqNo}&if_primary_term=${opts.primaryTerm}`;
7526
+ }
7527
+ } else {
7528
+ path = `/${encodeURIComponent(opts.index)}/_doc`;
7529
+ method = "POST";
7530
+ }
7531
+ const resp = await callJsonAsync(method, path, opts.source, "_doc write", opts.signal);
7532
+ return { id: resp._id ?? opts.id ?? "", result: resp.result ?? "" };
7533
+ }
7534
+ async function deleteDocAsync(opts) {
7535
+ assertIndex(opts.index);
7536
+ if (!opts.id)
7537
+ throw new Error("missing doc id");
7538
+ await callJsonAsync("DELETE", `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`, undefined, "_doc delete", opts.signal);
7539
+ }
7063
7540
  async function* iterateForSnapshot(container, signal) {
7064
7541
  const { index, query: query2 } = parseEsSnapshotContainer(container);
7065
7542
  const PAGE = 1000;
@@ -7133,6 +7610,8 @@ function createElasticsearchAdapter(config) {
7133
7610
  getMappingAsync,
7134
7611
  searchDocsAsync,
7135
7612
  getDocAsync,
7613
+ writeDocAsync,
7614
+ deleteDocAsync,
7136
7615
  iterateForSnapshot,
7137
7616
  listSnapshotContainers,
7138
7617
  query,
@@ -7612,6 +8091,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
7612
8091
  return handleError("elasticsearch", "read elasticsearch mapping", err);
7613
8092
  }
7614
8093
  }
8094
+ async function handleWrite(req, cwd, omitDirNames) {
8095
+ const parsed = await parseBoundedJsonBody(req, 4 * 1024 * 1024, "payload too large");
8096
+ if (parsed instanceof Response)
8097
+ return parsed;
8098
+ const body = parsed;
8099
+ if (typeof body.db !== "string" || body.db === "") {
8100
+ return textError("missing db", 400);
8101
+ }
8102
+ if (typeof body.index !== "string" || body.index === "") {
8103
+ return textError("missing index", 400);
8104
+ }
8105
+ const id = typeof body.id === "string" ? body.id : undefined;
8106
+ const r = await resolveEs(cwd, body.db, req.signal, omitDirNames);
8107
+ if (r instanceof Response)
8108
+ return r;
8109
+ try {
8110
+ if (body.op === "delete") {
8111
+ if (!id)
8112
+ return textError("missing id", 400);
8113
+ await r.explorer.deleteDocAsync({
8114
+ index: body.index,
8115
+ id,
8116
+ signal: req.signal
8117
+ });
8118
+ return json({ ok: true });
8119
+ }
8120
+ if (body.source === null || typeof body.source !== "object" || Array.isArray(body.source)) {
8121
+ return textError("source must be a JSON object", 400);
8122
+ }
8123
+ const seqNo = typeof body.seqNo === "number" ? body.seqNo : undefined;
8124
+ const primaryTerm = typeof body.primaryTerm === "number" ? body.primaryTerm : undefined;
8125
+ const result = await r.explorer.writeDocAsync({
8126
+ index: body.index,
8127
+ id,
8128
+ source: body.source,
8129
+ seqNo,
8130
+ primaryTerm,
8131
+ create: body.op === "create",
8132
+ signal: req.signal
8133
+ });
8134
+ return json({ ok: true, id: result.id, result: result.result });
8135
+ } catch (err) {
8136
+ return handleError("elasticsearch", "write elasticsearch doc", err);
8137
+ }
8138
+ }
7615
8139
  async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
7616
8140
  const wrap = createQueryStrippedLogger("elasticsearch", req, url);
7617
8141
  return dispatchRoutes(req, url, {
@@ -7631,6 +8155,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
7631
8155
  methods: ["GET"],
7632
8156
  handler: () => handleDoc(req, cwd, url, omitDirNames)
7633
8157
  },
8158
+ "/_db/elasticsearch/write": {
8159
+ methods: ["POST"],
8160
+ sideEffect: true,
8161
+ handler: () => handleWrite(req, cwd, omitDirNames)
8162
+ },
7634
8163
  "/_db/elasticsearch/search": {
7635
8164
  methods: ["GET", "POST"],
7636
8165
  sideEffect: (m) => m === "POST",
@@ -7649,6 +8178,7 @@ var init_handle_elasticsearch = __esm(() => {
7649
8178
  var exports_redis = {};
7650
8179
  __export(exports_redis, {
7651
8180
  openRedisExplorerAsync: () => openRedisExplorerAsync,
8181
+ createRedisAdapter: () => createRedisAdapter,
7652
8182
  canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
7653
8183
  });
7654
8184
  import { createHash as createHash3 } from "node:crypto";
@@ -8170,6 +8700,41 @@ function createRedisAdapter(config) {
8170
8700
  async function listSnapshotContainers() {
8171
8701
  return [];
8172
8702
  }
8703
+ async function runWriteAsync(args, signal) {
8704
+ const res = await execRedisCliAsync(config, args, 1e4, signal);
8705
+ if (res.code !== 0) {
8706
+ throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
8707
+ }
8708
+ const out = res.stdout.trim();
8709
+ if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
8710
+ throw new Error(out);
8711
+ }
8712
+ }
8713
+ async function setStringAsync(opts) {
8714
+ await runWriteAsync(["-n", String(opts.db), "SET", opts.key, opts.value], opts.signal);
8715
+ }
8716
+ async function createStringAsync(opts) {
8717
+ const res = await execRedisCliAsync(config, ["-n", String(opts.db), "SET", opts.key, opts.value, "NX"], 1e4, opts.signal);
8718
+ if (res.code !== 0) {
8719
+ throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
8720
+ }
8721
+ const out = res.stdout.trim();
8722
+ if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
8723
+ throw new Error(out);
8724
+ }
8725
+ if (!/\bOK\b/i.test(out)) {
8726
+ throw new Error(`key already exists: ${opts.key}`);
8727
+ }
8728
+ }
8729
+ async function setHashFieldAsync(opts) {
8730
+ await runWriteAsync(["-n", String(opts.db), "HSET", opts.key, opts.field, opts.value], opts.signal);
8731
+ }
8732
+ async function setListIndexAsync(opts) {
8733
+ await runWriteAsync(["-n", String(opts.db), "LSET", opts.key, String(opts.index), opts.value], opts.signal);
8734
+ }
8735
+ async function deleteKeyAsync(opts) {
8736
+ await runWriteAsync(["-n", String(opts.db), "DEL", opts.key], opts.signal);
8737
+ }
8173
8738
  return {
8174
8739
  kind: "redis",
8175
8740
  model: "kv",
@@ -8177,6 +8742,11 @@ function createRedisAdapter(config) {
8177
8742
  listDatabasesAsync,
8178
8743
  listKeysAsync,
8179
8744
  getValueAsync,
8745
+ setStringAsync,
8746
+ createStringAsync,
8747
+ setHashFieldAsync,
8748
+ setListIndexAsync,
8749
+ deleteKeyAsync,
8180
8750
  iterateForSnapshot,
8181
8751
  listSnapshotContainers,
8182
8752
  close() {}
@@ -8253,6 +8823,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
8253
8823
  return handleError("redis", "list redis keys", err);
8254
8824
  }
8255
8825
  }
8826
+ async function handleWrite2(req, cwd, omitDirNames) {
8827
+ const parsed = await parseBoundedJsonBody(req, 1024 * 1024, "payload too large");
8828
+ if (parsed instanceof Response)
8829
+ return parsed;
8830
+ const body = parsed;
8831
+ if (typeof body.db !== "string" || body.db === "") {
8832
+ return textError("missing db", 400);
8833
+ }
8834
+ const dbIndex = Number(body.dbIndex);
8835
+ if (!Number.isInteger(dbIndex) || dbIndex < 0 || dbIndex > 15) {
8836
+ return textError("dbIndex must be an integer in 0..15", 400);
8837
+ }
8838
+ if (typeof body.key !== "string" || body.key === "") {
8839
+ return textError("missing key", 400);
8840
+ }
8841
+ const op = body.op;
8842
+ const value = typeof body.value === "string" ? body.value : "";
8843
+ const r = await resolveRedis(cwd, body.db, req.signal, omitDirNames);
8844
+ if (r instanceof Response)
8845
+ return r;
8846
+ try {
8847
+ if (op === "setString") {
8848
+ await r.explorer.setStringAsync({
8849
+ db: dbIndex,
8850
+ key: body.key,
8851
+ value,
8852
+ signal: req.signal
8853
+ });
8854
+ } else if (op === "createString") {
8855
+ await r.explorer.createStringAsync({
8856
+ db: dbIndex,
8857
+ key: body.key,
8858
+ value,
8859
+ signal: req.signal
8860
+ });
8861
+ } else if (op === "setHashField") {
8862
+ if (typeof body.field !== "string" || body.field === "") {
8863
+ return textError("missing field", 400);
8864
+ }
8865
+ await r.explorer.setHashFieldAsync({
8866
+ db: dbIndex,
8867
+ key: body.key,
8868
+ field: body.field,
8869
+ value,
8870
+ signal: req.signal
8871
+ });
8872
+ } else if (op === "setListIndex") {
8873
+ const index = Number(body.index);
8874
+ if (!Number.isInteger(index) || index < 0) {
8875
+ return textError("index must be a non-negative integer", 400);
8876
+ }
8877
+ await r.explorer.setListIndexAsync({
8878
+ db: dbIndex,
8879
+ key: body.key,
8880
+ index,
8881
+ value,
8882
+ signal: req.signal
8883
+ });
8884
+ } else if (op === "delete") {
8885
+ await r.explorer.deleteKeyAsync({
8886
+ db: dbIndex,
8887
+ key: body.key,
8888
+ signal: req.signal
8889
+ });
8890
+ } else {
8891
+ return textError(`unknown op: ${String(op)}`, 400);
8892
+ }
8893
+ return json({ ok: true });
8894
+ } catch (err) {
8895
+ return handleError("redis", "write redis value", err);
8896
+ }
8897
+ }
8256
8898
  async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
8257
8899
  const wrap = createQueryStrippedLogger("redis", req, url);
8258
8900
  return dispatchRoutes(req, url, {
@@ -8267,6 +8909,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
8267
8909
  "/_db/redis/value": {
8268
8910
  methods: ["GET"],
8269
8911
  handler: () => handleValue(req, cwd, url, omitDirNames)
8912
+ },
8913
+ "/_db/redis/write": {
8914
+ methods: ["POST"],
8915
+ sideEffect: true,
8916
+ handler: () => handleWrite2(req, cwd, omitDirNames)
8270
8917
  }
8271
8918
  }, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
8272
8919
  }
@@ -8344,6 +8991,9 @@ function hmac(key, value) {
8344
8991
  function sha256(value) {
8345
8992
  return createHash4("sha256").update(value, "utf8").digest("hex");
8346
8993
  }
8994
+ function sha256Bytes(value) {
8995
+ return createHash4("sha256").update(value).digest("hex");
8996
+ }
8347
8997
  function encodeRfc3986(value) {
8348
8998
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
8349
8999
  }
@@ -8713,9 +9363,10 @@ function createS3Adapter(config) {
8713
9363
  const path = buildPath(opts.bucket, opts.key);
8714
9364
  const query = canonicalQuery(opts.query);
8715
9365
  const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
9366
+ const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
8716
9367
  const headers = {
8717
9368
  host: endpoint.host,
8718
- "x-amz-content-sha256": EMPTY_SHA256,
9369
+ "x-amz-content-sha256": payloadHash,
8719
9370
  "x-amz-date": requestDate,
8720
9371
  ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
8721
9372
  ...opts.headers || {}
@@ -8727,7 +9378,7 @@ function createS3Adapter(config) {
8727
9378
  query,
8728
9379
  canonicalHeaders(headers),
8729
9380
  signedNames,
8730
- EMPTY_SHA256
9381
+ payloadHash
8731
9382
  ].join(`
8732
9383
  `);
8733
9384
  const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
@@ -8746,6 +9397,9 @@ function createS3Adapter(config) {
8746
9397
  }
8747
9398
  requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
8748
9399
  if (config.dockerContainerName) {
9400
+ if (opts.body !== undefined) {
9401
+ throw new S3HttpError(503, "S3 object writes require a published host port (docker-exec transport cannot stream a request body)");
9402
+ }
8749
9403
  if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
8750
9404
  throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8751
9405
  }
@@ -8760,6 +9414,7 @@ function createS3Adapter(config) {
8760
9414
  return fetch(url, {
8761
9415
  method: opts.method,
8762
9416
  headers: requestHeaders,
9417
+ ...opts.body !== undefined ? { body: opts.body } : {},
8763
9418
  signal: transportSignal
8764
9419
  });
8765
9420
  }, deadline);
@@ -8842,6 +9497,32 @@ function createS3Adapter(config) {
8842
9497
  headers: rawObjectHeaders(opts.key, res)
8843
9498
  });
8844
9499
  }
9500
+ async function putObjectAsync(opts) {
9501
+ const deadline = createS3TransportDeadline(config);
9502
+ const res = await signedFetch({
9503
+ method: "PUT",
9504
+ bucket: opts.bucket,
9505
+ key: opts.key,
9506
+ body: opts.body,
9507
+ headers: opts.contentType ? { "content-type": opts.contentType } : undefined,
9508
+ signal: opts.signal
9509
+ }, deadline);
9510
+ if (!res.ok) {
9511
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
9512
+ }
9513
+ }
9514
+ async function deleteObjectAsync(opts) {
9515
+ const deadline = createS3TransportDeadline(config);
9516
+ const res = await signedFetch({
9517
+ method: "DELETE",
9518
+ bucket: opts.bucket,
9519
+ key: opts.key,
9520
+ signal: opts.signal
9521
+ }, deadline);
9522
+ if (!res.ok) {
9523
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
9524
+ }
9525
+ }
8845
9526
  return {
8846
9527
  kind: "s3",
8847
9528
  model: "object",
@@ -8850,7 +9531,9 @@ function createS3Adapter(config) {
8850
9531
  listObjects,
8851
9532
  headObject,
8852
9533
  getObjectText,
8853
- getObjectResponse
9534
+ getObjectResponse,
9535
+ putObjectAsync,
9536
+ deleteObjectAsync
8854
9537
  };
8855
9538
  }
8856
9539
  async function s3ConfigFromDockerInfoAsync(info, signal) {
@@ -9253,6 +9936,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
9253
9936
  return s3ErrorResponse(err, "stream s3 object");
9254
9937
  }
9255
9938
  }
9939
+ async function handleWrite3(req, cwd, omitDirNames) {
9940
+ const parsed = await parseBoundedJsonBody(req, 8 * 1024 * 1024, "payload too large");
9941
+ if (parsed instanceof Response)
9942
+ return parsed;
9943
+ const body = parsed;
9944
+ if (typeof body.db !== "string" || body.db === "") {
9945
+ return textError("missing db", 400);
9946
+ }
9947
+ const bucket = validateBucket(typeof body.bucket === "string" ? body.bucket : null);
9948
+ if (bucket instanceof Response)
9949
+ return bucket;
9950
+ const key = validateKey(typeof body.key === "string" ? body.key : null);
9951
+ if (key instanceof Response)
9952
+ return key;
9953
+ const r = await resolveS3(cwd, body.db, req.signal, omitDirNames);
9954
+ if (r instanceof Response)
9955
+ return r;
9956
+ try {
9957
+ if (body.op === "delete") {
9958
+ await r.explorer.deleteObjectAsync({
9959
+ bucket,
9960
+ key,
9961
+ signal: req.signal
9962
+ });
9963
+ return json({ ok: true });
9964
+ }
9965
+ if (body.op === "create") {
9966
+ let exists = false;
9967
+ try {
9968
+ await r.explorer.headObject({ bucket, key, signal: req.signal });
9969
+ exists = true;
9970
+ } catch (err) {
9971
+ if (isS3HttpError(err) && err.status === 404)
9972
+ exists = false;
9973
+ else
9974
+ throw err;
9975
+ }
9976
+ if (exists)
9977
+ return textError(`object already exists: ${key}`, 409);
9978
+ }
9979
+ const content = typeof body.content === "string" ? body.content : "";
9980
+ const contentType = typeof body.contentType === "string" && body.contentType ? body.contentType : "application/octet-stream";
9981
+ await r.explorer.putObjectAsync({
9982
+ bucket,
9983
+ key,
9984
+ body: new TextEncoder().encode(content),
9985
+ contentType,
9986
+ signal: req.signal
9987
+ });
9988
+ return json({ ok: true });
9989
+ } catch (err) {
9990
+ return handleError("s3", "write s3 object", err);
9991
+ }
9992
+ }
9256
9993
  async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9257
9994
  const wrap = createQueryStrippedLogger("s3", req, url);
9258
9995
  return dispatchRoutes(req, url, {
@@ -9279,6 +10016,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9279
10016
  "/_db/s3/raw": {
9280
10017
  methods: ["GET", "HEAD"],
9281
10018
  handler: () => handleRaw(cwd, req, url, omitDirNames)
10019
+ },
10020
+ "/_db/s3/write": {
10021
+ methods: ["POST"],
10022
+ sideEffect: true,
10023
+ handler: () => handleWrite3(req, cwd, omitDirNames)
9282
10024
  }
9283
10025
  }, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
9284
10026
  }
@@ -9445,21 +10187,6 @@ var init_query_history = __esm(() => {
9445
10187
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
9446
10188
  import { mkdirSync as mkdirSync3 } from "node:fs";
9447
10189
  import { join as join11 } from "node:path";
9448
- async function getSqliteClass2() {
9449
- if (cachedDbClass2)
9450
- return cachedDbClass2;
9451
- try {
9452
- const mod = await import("bun:sqlite");
9453
- cachedDbClass2 = mod.Database;
9454
- return cachedDbClass2;
9455
- } catch {}
9456
- try {
9457
- const mod = await Function('return import("better-sqlite3")')();
9458
- cachedDbClass2 = mod.default || mod;
9459
- return cachedDbClass2;
9460
- } catch {}
9461
- throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
9462
- }
9463
10190
  async function getStoreDb(cwd) {
9464
10191
  const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
9465
10192
  if (storeDb && storeDbPath === dbPath)
@@ -9470,7 +10197,7 @@ async function getStoreDb(cwd) {
9470
10197
  } catch {}
9471
10198
  }
9472
10199
  mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
9473
- const DbClass = await getSqliteClass2();
10200
+ const DbClass = await loadSqliteClass();
9474
10201
  storeDb = new DbClass(dbPath);
9475
10202
  storeDbPath = dbPath;
9476
10203
  storeDb.exec("PRAGMA journal_mode=WAL");
@@ -9729,7 +10456,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
9729
10456
  });
9730
10457
  return { rows, total };
9731
10458
  }
9732
- var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
10459
+ var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", SCHEMA_SQL = `
9733
10460
  CREATE TABLE IF NOT EXISTS snapshots (
9734
10461
  id TEXT PRIMARY KEY,
9735
10462
  db_id TEXT NOT NULL,
@@ -9959,6 +10686,9 @@ function sanitize(input) {
9959
10686
  const historyHeight = sanitizeCssSize(tab.historyHeight);
9960
10687
  if (historyHeight !== undefined)
9961
10688
  out.historyHeight = historyHeight;
10689
+ if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
10690
+ out.activeHistoryTab = tab.activeHistoryTab;
10691
+ }
9962
10692
  const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
9963
10693
  if (sidebarWidth !== undefined)
9964
10694
  out.sidebarWidth = sidebarWidth;
@@ -10149,11 +10879,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
10149
10879
  const body2 = { dbId: r.dbId, schemas: [] };
10150
10880
  return json(body2);
10151
10881
  }
10152
- const schemas = await listDockerSchemasAsync(r.docker.serviceName, "postgresql", r.docker.env, r.docker.composeDir, r.docker.database, signal);
10882
+ const docker = r.docker;
10883
+ const { result: schemas, executedSql } = await captureSql(() => listDockerSchemasAsync(docker.serviceName, "postgresql", docker.env, docker.composeDir, docker.database, signal));
10153
10884
  const body = {
10154
10885
  dbId: r.dbId,
10155
10886
  schemas: schemas.map((name) => ({ name })),
10156
- selectedSchema: r.schema
10887
+ selectedSchema: r.schema,
10888
+ executedSql
10157
10889
  };
10158
10890
  return json(body);
10159
10891
  }
@@ -10165,37 +10897,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
10165
10897
  const linkedAbort = createLinkedAbortController(signal);
10166
10898
  try {
10167
10899
  const adapter = await getAdapter(r, cwd, signal);
10168
- const db = asAsync(adapter);
10169
- const tables = await db.tables(linkedAbort.signal);
10170
- const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
10171
- const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
10172
- const indexesPromise = db.indexes(linkedAbort.signal);
10173
- const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
10174
- const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
10175
- const schemaPromises = [
10176
- countMapPromise,
10177
- indexesPromise,
10178
- foreignKeysPromise,
10179
- columnsMapPromise
10180
- ];
10181
- const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
10182
- linkedAbort.abort();
10183
- await Promise.allSettled(schemaPromises);
10184
- throw err;
10900
+ const { result, executedSql } = await captureSql(async () => {
10901
+ const db = asAsync(adapter);
10902
+ const tables = await db.tables(linkedAbort.signal);
10903
+ const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
10904
+ const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
10905
+ const indexesPromise = db.indexes(linkedAbort.signal);
10906
+ const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
10907
+ const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
10908
+ const schemaPromises = [
10909
+ countMapPromise,
10910
+ indexesPromise,
10911
+ foreignKeysPromise,
10912
+ columnsMapPromise
10913
+ ];
10914
+ const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
10915
+ linkedAbort.abort();
10916
+ await Promise.allSettled(schemaPromises);
10917
+ throw err;
10918
+ });
10919
+ return { tables, countMap, indexes, foreignKeys, colsMap };
10185
10920
  });
10186
- const tablesWithCount = tables.map((t) => ({
10921
+ const tablesWithCount = result.tables.map((t) => ({
10187
10922
  ...t,
10188
- rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
10923
+ rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
10189
10924
  }));
10190
10925
  const body = {
10191
10926
  dbId: r.dbId,
10192
10927
  ...r.schema ? { schema: r.schema } : {},
10193
10928
  tables: tablesWithCount,
10194
- indexes,
10195
- foreignKeys
10929
+ indexes: result.indexes,
10930
+ foreignKeys: result.foreignKeys,
10931
+ executedSql
10196
10932
  };
10197
- if (colsMap) {
10198
- body.columnsMap = Object.fromEntries(colsMap);
10933
+ if (result.colsMap) {
10934
+ body.columnsMap = Object.fromEntries(result.colsMap);
10199
10935
  }
10200
10936
  return json(body);
10201
10937
  } catch (err) {
@@ -10257,36 +10993,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10257
10993
  const exact = parseExactConditions(url);
10258
10994
  try {
10259
10995
  const adapter = await getAdapter(r, cwd, signal);
10260
- if (filters.length > 0 || exact.length > 0) {
10261
- const meta2 = await adapter.getFilteredTablePageWithMeta(table, {
10262
- offset,
10263
- limit,
10264
- orderBy,
10265
- grouped: groupFiltersByValue(filters),
10266
- ...exact.length > 0 ? { exact } : {}
10267
- }, signal);
10268
- const colNames2 = new Set(meta2.columns.map((c) => c.name));
10269
- if (sortCol && !colNames2.has(sortCol)) {
10270
- return textError(`invalid sort column: ${sortCol}`, 400);
10271
- }
10272
- const body2 = {
10273
- dbId: r.dbId,
10274
- ...r.schema ? { schema: r.schema } : {},
10275
- table,
10276
- columns: meta2.columns,
10277
- rows: serializeDbRows(meta2.rows),
10278
- totalRows: meta2.totalRows,
10279
- offset,
10280
- limit,
10281
- hasMore: offset + meta2.rowCount < meta2.totalRows
10282
- };
10283
- return json(body2);
10284
- }
10285
- const meta = await adapter.getTablePageWithMeta(table, {
10996
+ const { result: meta, executedSql } = await captureSql(() => filters.length > 0 || exact.length > 0 ? adapter.getFilteredTablePageWithMeta(table, {
10997
+ offset,
10998
+ limit,
10999
+ orderBy,
11000
+ grouped: groupFiltersByValue(filters),
11001
+ ...exact.length > 0 ? { exact } : {}
11002
+ }, signal) : adapter.getTablePageWithMeta(table, {
10286
11003
  offset,
10287
11004
  limit,
10288
11005
  orderBy
10289
- }, signal);
11006
+ }, signal));
10290
11007
  const colNames = new Set(meta.columns.map((c) => c.name));
10291
11008
  if (sortCol && !colNames.has(sortCol)) {
10292
11009
  return textError(`invalid sort column: ${sortCol}`, 400);
@@ -10300,7 +11017,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10300
11017
  totalRows: meta.totalRows,
10301
11018
  offset,
10302
11019
  limit,
10303
- hasMore: offset + meta.rowCount < meta.totalRows
11020
+ hasMore: offset + meta.rowCount < meta.totalRows,
11021
+ executedSql
10304
11022
  };
10305
11023
  return json(body);
10306
11024
  } catch (err) {
@@ -10377,8 +11095,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10377
11095
  const start = Date.now();
10378
11096
  try {
10379
11097
  const adapter = await getAdapter(r, cwd, req.signal);
10380
- const db = asAsync(adapter);
10381
- const result = await db.readonlyQuery(body.sql, undefined, maxRows, req.signal);
11098
+ const { result, executedSql } = await captureSql(() => asAsync(adapter).readonlyQuery(body.sql ?? "", undefined, maxRows, req.signal));
10382
11099
  const elapsed = Date.now() - start;
10383
11100
  const serializedRows = serializeDbRows(result.rows);
10384
11101
  const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
@@ -10392,7 +11109,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10392
11109
  rows: serializedRows,
10393
11110
  rowCount: result.rowCount,
10394
11111
  truncated: result.rowCount >= maxRows,
10395
- elapsedMs: elapsed
11112
+ elapsedMs: elapsed,
11113
+ executedSql
10396
11114
  };
10397
11115
  if (body.saveHistory) {
10398
11116
  const entry = {
@@ -10611,13 +11329,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
10611
11329
  return textError("missing table parameter", 400);
10612
11330
  try {
10613
11331
  const adapter = await getAdapter(r, cwd, signal);
10614
- const db = asAsync(adapter);
10615
- const columns = await db.columns(table, signal);
11332
+ const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
10616
11333
  return json({
10617
11334
  dbId: r.dbId,
10618
11335
  ...r.schema ? { schema: r.schema } : {},
10619
11336
  table,
10620
- columns
11337
+ columns,
11338
+ executedSql
10621
11339
  });
10622
11340
  } catch (err) {
10623
11341
  return handleError("database", "get columns", err);
@@ -10632,17 +11350,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
10632
11350
  return textError("missing table parameter", 400);
10633
11351
  try {
10634
11352
  const adapter = await getAdapter(r, cwd, signal);
10635
- const db = asAsync(adapter);
10636
- const [sql, triggers] = await Promise.all([
10637
- db.createStatement(table, signal),
10638
- db.triggers(table, signal)
10639
- ]);
11353
+ const { result, executedSql } = await captureSql(async () => {
11354
+ const db = asAsync(adapter);
11355
+ const [sql, triggers] = await Promise.all([
11356
+ db.createStatement(table, signal),
11357
+ db.triggers(table, signal)
11358
+ ]);
11359
+ return { sql, triggers };
11360
+ });
10640
11361
  return json({
10641
11362
  dbId: r.dbId,
10642
11363
  ...r.schema ? { schema: r.schema } : {},
10643
11364
  table,
10644
- sql,
10645
- triggers
11365
+ sql: result.sql,
11366
+ triggers: result.triggers,
11367
+ executedSql
10646
11368
  });
10647
11369
  } catch (err) {
10648
11370
  return handleError("database", "get DDL", err);
@@ -11054,6 +11776,48 @@ async function handleClose(cwd, req, omitDirNames) {
11054
11776
  }
11055
11777
  return json({ ok: true });
11056
11778
  }
11779
+ async function handleMutate(cwd, req, omitDirNames) {
11780
+ const parsed = await parseBoundedJsonBody(req, 1048576, "payload too large");
11781
+ if (parsed instanceof Response)
11782
+ return parsed;
11783
+ const body = parsed;
11784
+ if (typeof body.db !== "string" || body.db === "") {
11785
+ return textError("missing db", 400);
11786
+ }
11787
+ if (typeof body.table !== "string" || body.table === "") {
11788
+ return textError("missing table", 400);
11789
+ }
11790
+ if (!Array.isArray(body.mutations) || body.mutations.length === 0) {
11791
+ return textError("missing mutations", 400);
11792
+ }
11793
+ const schemaParam = typeof body.schema === "string" ? body.schema : undefined;
11794
+ const r = await resolveDb(cwd, body.db, omitDirNames, schemaParam, req.signal);
11795
+ if (r instanceof Response)
11796
+ return r;
11797
+ const adapter = await getAdapter(r, cwd, req.signal);
11798
+ if (!adapter.applyMutations) {
11799
+ return textError("writes are not supported for this datastore", 400);
11800
+ }
11801
+ try {
11802
+ const tableName = body.table;
11803
+ const { result, executedSql } = await captureSql(() => adapter.applyMutations?.(tableName, body.mutations, req.signal) ?? Promise.reject(new Error("applyMutations not supported")));
11804
+ adapter.invalidateTableMetaCache?.(tableName);
11805
+ const response = {
11806
+ dbId: r.dbId,
11807
+ ...r.schema ? { schema: r.schema } : {},
11808
+ table: tableName,
11809
+ affected: result.affected,
11810
+ executedSql
11811
+ };
11812
+ return json(response);
11813
+ } catch (err) {
11814
+ if (isAbortLikeError(err, req.signal)) {
11815
+ return textError("mutation aborted", 503);
11816
+ }
11817
+ const message = err instanceof Error ? err.message : String(err);
11818
+ return textError(message, 400);
11819
+ }
11820
+ }
11057
11821
  async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
11058
11822
  ensureInit();
11059
11823
  if (url.pathname.startsWith("/_db/redis/")) {
@@ -11114,6 +11878,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
11114
11878
  sideEffect: true,
11115
11879
  handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
11116
11880
  },
11881
+ "/_db/mutate": {
11882
+ methods: ["POST"],
11883
+ sideEffect: true,
11884
+ handler: () => handleMutate(cwd, req, omitDirNames)
11885
+ },
11117
11886
  "/_db/close": {
11118
11887
  methods: ["POST"],
11119
11888
  sideEffect: true,
@@ -11196,6 +11965,7 @@ var init_handle = __esm(() => {
11196
11965
  init_state_store();
11197
11966
  init_docker();
11198
11967
  init_docker_utils();
11968
+ init_sql_capture();
11199
11969
  init_sqlite();
11200
11970
  init_connection_pool();
11201
11971
  init_discovery();
@@ -11607,7 +12377,7 @@ function fileToMeta(file, range, extraQs) {
11607
12377
  untracked: file.untracked || false
11608
12378
  };
11609
12379
  }
11610
- function computePayload(extras, range) {
12380
+ function computePayload(extras, range, pathFilter = "") {
11611
12381
  if (isSameWorktreeRange(range)) {
11612
12382
  return {
11613
12383
  files: [],
@@ -11623,8 +12393,9 @@ function computePayload(extras, range) {
11623
12393
  const files = fileMeta(fullArgs, cwd, false);
11624
12394
  if (includeUntracked(range, refs2))
11625
12395
  files.push(...untrackedMeta(cwd));
11626
- files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
11627
- files.forEach((file, i) => {
12396
+ const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
12397
+ filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
12398
+ filteredFiles.forEach((file, i) => {
11628
12399
  file.order = i + 1;
11629
12400
  });
11630
12401
  const extraQs = {};
@@ -11634,7 +12405,7 @@ function computePayload(extras, range) {
11634
12405
  if (e === "--ignore-blank-lines")
11635
12406
  extraQs.ignore_blank = "1";
11636
12407
  }
11637
- const meta = files.map((file) => fileToMeta(file, range, extraQs));
12408
+ const meta = filteredFiles.map((file) => fileToMeta(file, range, extraQs));
11638
12409
  const totals = meta.reduce((acc, file) => {
11639
12410
  acc.additions += file.additions || 0;
11640
12411
  acc.deletions += file.deletions || 0;
@@ -11661,9 +12432,12 @@ function handleDiffJson(url) {
11661
12432
  from: url.searchParams.get("from") || "",
11662
12433
  to: url.searchParams.get("to") || ""
11663
12434
  };
11664
- const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}`;
12435
+ const path = url.searchParams.get("path") || "";
12436
+ if (path && !safePath(path))
12437
+ return text("invalid path", 400);
12438
+ const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}|${path}`;
11665
12439
  if (url.searchParams.get("nocache") === "1") {
11666
- const payload2 = computePayload(extras, range);
12440
+ const payload2 = computePayload(extras, range, path);
11667
12441
  const sig = JSON.stringify({ ...payload2, generation: undefined });
11668
12442
  const cached2 = metaCache.get(key);
11669
12443
  if (!cached2 || cached2.sig !== sig) {
@@ -11689,7 +12463,7 @@ function handleDiffJson(url) {
11689
12463
  "Cache-Control": "no-store"
11690
12464
  }
11691
12465
  });
11692
- const payload = computePayload(extras, range);
12466
+ const payload = computePayload(extras, range, path);
11693
12467
  const body = JSON.stringify(payload);
11694
12468
  setTimedCacheEntry(metaCache, key, {
11695
12469
  body,
@@ -12106,15 +12880,109 @@ function handleLog(url) {
12106
12880
  const ref = url.searchParams.get("ref") || "HEAD";
12107
12881
  const skip = Number(url.searchParams.get("skip") || "0");
12108
12882
  const limit = Number(url.searchParams.get("limit") || "50");
12883
+ const path = url.searchParams.get("path") || "";
12884
+ if (path && !safePath(path))
12885
+ return text("invalid path", 400);
12109
12886
  const result = commitHistory(cwd, {
12110
12887
  ref,
12111
12888
  skip: Number.isFinite(skip) ? skip : 0,
12112
12889
  limit: Number.isFinite(limit) ? limit : 50,
12113
- query: url.searchParams.get("q") || ""
12890
+ query: url.searchParams.get("q") || "",
12891
+ ...path ? { path } : {}
12114
12892
  });
12115
12893
  if (result.error)
12116
12894
  return text(result.error, 400);
12117
- return json2({ commits: result.commits, hasMore: result.hasMore });
12895
+ const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
12896
+ let commits = result.commits;
12897
+ let hasWorktree = false;
12898
+ if (wantsWorktreeHead) {
12899
+ const status = runSync([
12900
+ "git",
12901
+ "-c",
12902
+ "core.quotepath=false",
12903
+ "status",
12904
+ "--porcelain=v1",
12905
+ "-z",
12906
+ "--untracked-files=normal",
12907
+ "--",
12908
+ path
12909
+ ], cwd);
12910
+ if (status.code === 0 && status.stdout.length > 0) {
12911
+ const parts = status.stdout.split("\x00").filter(Boolean);
12912
+ if (parts.length > 0) {
12913
+ hasWorktree = true;
12914
+ commits = [
12915
+ {
12916
+ sha: "worktree",
12917
+ subject: "未コミット変更 (Working tree)",
12918
+ author: "",
12919
+ when: "",
12920
+ parents: [],
12921
+ body: ""
12922
+ },
12923
+ ...commits
12924
+ ];
12925
+ }
12926
+ }
12927
+ }
12928
+ return json2({
12929
+ commits,
12930
+ hasMore: result.hasMore,
12931
+ generation,
12932
+ ...hasWorktree ? { hasWorktree: true } : {}
12933
+ });
12934
+ }
12935
+ function blamePathKey(p) {
12936
+ try {
12937
+ const st = statSync3(join13(cwd, p));
12938
+ return `${st.mtimeMs}:${st.size}`;
12939
+ } catch {
12940
+ return "missing";
12941
+ }
12942
+ }
12943
+ function rememberBlame(key, value) {
12944
+ if (blameCache.has(key))
12945
+ blameCache.delete(key);
12946
+ blameCache.set(key, value);
12947
+ while (blameCache.size > BLAME_CACHE_MAX) {
12948
+ const oldest = blameCache.keys().next();
12949
+ if (oldest.done)
12950
+ break;
12951
+ blameCache.delete(oldest.value);
12952
+ }
12953
+ }
12954
+ function handleFileBlame(url) {
12955
+ const path = url.searchParams.get("path") || "";
12956
+ if (!safePath(path))
12957
+ return text("invalid path", 400);
12958
+ const ref = url.searchParams.get("ref") || "worktree";
12959
+ if (!ref || ref.startsWith("-") || ref.includes("\x00"))
12960
+ return text("invalid ref", 400);
12961
+ const rawBase = url.searchParams.get("base");
12962
+ const requestedBase = rawBase === "HEAD" ? "HEAD" : rawBase === "worktree" ? "worktree" : ref === "worktree" ? "worktree" : "HEAD";
12963
+ const normalized = normalizeBlameRef(ref, requestedBase);
12964
+ const { base } = normalized;
12965
+ let cacheKey;
12966
+ if (base === "worktree") {
12967
+ cacheKey = `worktree|${path}|${blamePathKey(path)}`;
12968
+ } else {
12969
+ const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
12970
+ if (resolved.code !== 0)
12971
+ return text("unknown ref", 400);
12972
+ cacheKey = `HEAD|${path}|${resolved.stdout.trim()}`;
12973
+ }
12974
+ const cached = blameCache.get(cacheKey);
12975
+ if (cached) {
12976
+ if (blameCache.has(cacheKey)) {
12977
+ blameCache.delete(cacheKey);
12978
+ blameCache.set(cacheKey, cached);
12979
+ }
12980
+ return json2({ ...cached, base, ref, generation });
12981
+ }
12982
+ const result = blame(cwd, { path, ref: normalized.ref, base });
12983
+ if (!result.error)
12984
+ rememberBlame(cacheKey, result);
12985
+ return json2({ ...result, base, ref, generation });
12118
12986
  }
12119
12987
  function handleFileDiff(url) {
12120
12988
  const path = url.searchParams.get("path") || "";
@@ -13123,7 +13991,7 @@ function restartWorktreeWatch() {
13123
13991
  }
13124
13992
  worktreeWatch = startScopedWorktreeWatch();
13125
13993
  }
13126
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13994
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
13127
13995
  var init_preview = __esm(async () => {
13128
13996
  init_routes();
13129
13997
  init_annotations();
@@ -13191,6 +14059,7 @@ var init_preview = __esm(async () => {
13191
14059
  sseClients = new Set;
13192
14060
  sseKeepalives = new Map;
13193
14061
  fileCache = new Map;
14062
+ blameCache = new Map;
13194
14063
  metaCache = new Map;
13195
14064
  fileListCache = new Map;
13196
14065
  lineIndexCache = new Map;
@@ -13223,6 +14092,8 @@ var init_preview = __esm(async () => {
13223
14092
  return handleRefCommits(url);
13224
14093
  if (url.pathname === "/_log")
13225
14094
  return handleLog(url);
14095
+ if (url.pathname === "/_file_blame")
14096
+ return handleFileBlame(url);
13226
14097
  if (url.pathname === "/file_diff")
13227
14098
  return handleFileDiff(url);
13228
14099
  if (url.pathname === "/file_range")