@youtyan/code-viewer 0.3.0 → 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.
@@ -4768,52 +4768,37 @@ function serializeDbRow(row) {
4768
4768
  function serializeDbRows(rows) {
4769
4769
  return rows.map(serializeDbRow);
4770
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
+ }
4771
4796
  var MIN_SAFE, MAX_SAFE;
4772
4797
  var init_serialize = __esm(() => {
4773
4798
  MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
4774
4799
  MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
4775
4800
  });
4776
4801
 
4777
- // web-src/server/database/sources/sql-snapshot.ts
4778
- import { createHash as createHash2 } from "node:crypto";
4779
- function normalizeRawValue(v) {
4780
- if (v === null)
4781
- return "\\N";
4782
- if (typeof v === "bigint")
4783
- return v.toString();
4784
- if (v instanceof Uint8Array) {
4785
- return `\\x${Buffer.from(v).toString("hex")}`;
4786
- }
4787
- return String(v);
4788
- }
4789
- function rowToPayloadJson(columns, row) {
4790
- const obj = {};
4791
- for (let i = 0;i < columns.length; i++) {
4792
- obj[columns[i]] = serializeDbValue(row[i]);
4793
- }
4794
- return JSON.stringify(obj);
4795
- }
4796
- function computeRowHash(columns, row) {
4797
- const parts = columns.map((_, i) => normalizeRawValue(row[i]));
4798
- return createHash2("sha256").update(parts.join("\t")).digest("hex");
4799
- }
4800
- function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
4801
- if (pkColumns.length === 0) {
4802
- return JSON.stringify({ __rowIndex: rowIndex });
4803
- }
4804
- const keyObj = {};
4805
- for (const pk of pkColumns) {
4806
- const idx = allColumns.indexOf(pk);
4807
- if (idx >= 0)
4808
- keyObj[pk] = serializeDbValue(row[idx]);
4809
- }
4810
- return JSON.stringify(keyObj);
4811
- }
4812
- var SQL_SNAPSHOT_BATCH_SIZE = 500;
4813
- var init_sql_snapshot = __esm(() => {
4814
- init_serialize();
4815
- });
4816
-
4817
4802
  // web-src/server/database/sql-utils.ts
4818
4803
  function sanitizeIdentifier(name, kind = "sqlite") {
4819
4804
  if (kind === "mysql")
@@ -4875,6 +4860,189 @@ function filterOrderByColumns(orderBy, columnNames) {
4875
4860
  const filtered = orderBy.filter((order) => validColumns.has(order.column));
4876
4861
  return filtered.length > 0 ? filtered : undefined;
4877
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
+ });
4878
5046
 
4879
5047
  // web-src/server/database/adapters/spawn-runner.ts
4880
5048
  import { spawn as spawn2 } from "node:child_process";
@@ -5124,6 +5292,24 @@ var init_docker_utils = __esm(() => {
5124
5292
  };
5125
5293
  });
5126
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
+
5127
5313
  // web-src/server/database/adapters/docker.ts
5128
5314
  import { spawnSync as spawnSync3 } from "node:child_process";
5129
5315
  function dockerDatabasesCacheKey(serviceName, kind, cwd) {
@@ -5279,6 +5465,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
5279
5465
  });
5280
5466
  }
5281
5467
  async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
5468
+ recordSql(sql);
5282
5469
  if (spawnSyncImpl2 !== spawnSync3)
5283
5470
  return execInContainer(config, sql, timeoutMs);
5284
5471
  const args = buildExecArgs(config, sql);
@@ -5366,12 +5553,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
5366
5553
  const rows = lines.map((line) => splitTsvLine(line, false));
5367
5554
  return { columns: [], rows };
5368
5555
  }
5369
- function buildOrderClause(orderBy, kind) {
5370
- if (!orderBy?.length)
5371
- return "";
5372
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
5373
- return ` ORDER BY ${parts.join(", ")}`;
5374
- }
5375
5556
  function isMysqlSpatialType(type) {
5376
5557
  const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
5377
5558
  return MYSQL_SPATIAL_TYPES.has(baseType);
@@ -5460,7 +5641,7 @@ function createDockerAdapter(config) {
5460
5641
  const tableLiteral = table.replace(/'/g, "''");
5461
5642
  if (config.kind === "postgresql") {
5462
5643
  const schemaLiteral = postgresSchemaLiteral();
5463
- 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 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`;
5464
5645
  }
5465
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`;
5466
5647
  }
@@ -5794,6 +5975,19 @@ function createDockerAdapter(config) {
5794
5975
  rowCount: Math.min(result.rows.length, maxRows)
5795
5976
  };
5796
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
+ },
5797
5991
  invalidateTableMetaCache(table) {
5798
5992
  tableMetaCache.invalidate(table);
5799
5993
  if (table) {
@@ -5975,9 +6169,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
5975
6169
  }
5976
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;
5977
6171
  var init_docker = __esm(() => {
6172
+ init_mutate();
5978
6173
  init_sql_snapshot();
6174
+ init_sql_utils();
5979
6175
  init_docker_utils();
5980
6176
  init_spawn_runner();
6177
+ init_sql_capture();
5981
6178
  dockerDatabasesCache = new Map;
5982
6179
  dockerSchemasCache = new Map;
5983
6180
  spawnSyncImpl2 = spawnSync3;
@@ -5994,17 +6191,8 @@ var init_docker = __esm(() => {
5994
6191
  ]);
5995
6192
  });
5996
6193
 
5997
- // web-src/server/database/adapters/sqlite.ts
5998
- function safePrepare(db, sql) {
5999
- const stmt = db.prepare(sql);
6000
- if (typeof stmt.safeIntegers === "function") {
6001
- try {
6002
- stmt.safeIntegers(true);
6003
- } catch {}
6004
- }
6005
- return stmt;
6006
- }
6007
- async function getSqliteClass() {
6194
+ // web-src/server/database/sqlite-driver.ts
6195
+ async function loadSqliteClass() {
6008
6196
  if (cachedDbClass)
6009
6197
  return cachedDbClass;
6010
6198
  try {
@@ -6019,11 +6207,17 @@ async function getSqliteClass() {
6019
6207
  } catch {}
6020
6208
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
6021
6209
  }
6022
- function buildOrderClause2(orderBy) {
6023
- if (!orderBy?.length)
6024
- return "";
6025
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
6026
- 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;
6027
6221
  }
6028
6222
  function queryRowsToResult(rows, columns) {
6029
6223
  const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
@@ -6045,7 +6239,30 @@ function queryColumns(db, table) {
6045
6239
  defaultValue: row.dflt_value
6046
6240
  }));
6047
6241
  }
6048
- 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
+ };
6049
6266
  const adapter = {
6050
6267
  kind: "sqlite",
6051
6268
  model: "sql",
@@ -6145,7 +6362,7 @@ function createSqliteAdapter(db) {
6145
6362
  return this.getTableRowCounts(tables);
6146
6363
  },
6147
6364
  getTablePage(table, options) {
6148
- const order = buildOrderClause2(options.orderBy);
6365
+ const order = buildOrderClause(options.orderBy);
6149
6366
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
6150
6367
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
6151
6368
  const cols = queryColumns(db, table);
@@ -6157,7 +6374,7 @@ function createSqliteAdapter(db) {
6157
6374
  async getTablePageWithMeta(table, options) {
6158
6375
  const columns = queryColumns(db, table);
6159
6376
  const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
6160
- const order = buildOrderClause2(orderBy);
6377
+ const order = buildOrderClause(orderBy);
6161
6378
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
6162
6379
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
6163
6380
  const result = queryRowsToResult(rows, columns);
@@ -6173,7 +6390,7 @@ function createSqliteAdapter(db) {
6173
6390
  const columns = queryColumns(db, table);
6174
6391
  const columnNames = columns.map((column) => column.name);
6175
6392
  const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
6176
- const order = buildOrderClause2(filterOrderByColumns(options.orderBy, columnNames));
6393
+ const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
6177
6394
  const tableId = sanitizeIdentifier(table);
6178
6395
  const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
6179
6396
  const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
@@ -6241,7 +6458,36 @@ function createSqliteAdapter(db) {
6241
6458
  async getTriggersAsync(table) {
6242
6459
  return this.getTriggers(table);
6243
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
+ },
6244
6484
  close() {
6485
+ if (writeDb) {
6486
+ try {
6487
+ writeDb.close();
6488
+ } catch {}
6489
+ writeDb = null;
6490
+ }
6245
6491
  db.close();
6246
6492
  },
6247
6493
  async* iterateForSnapshot(table, signal) {
@@ -6279,14 +6525,18 @@ function createSqliteAdapter(db) {
6279
6525
  };
6280
6526
  return adapter;
6281
6527
  }
6282
- var cachedDbClass = null, sqliteAdapterFactory;
6528
+ var sqliteAdapterFactory;
6283
6529
  var init_sqlite = __esm(() => {
6530
+ init_mutate();
6284
6531
  init_sql_snapshot();
6532
+ init_sql_utils();
6533
+ init_sql_capture();
6285
6534
  sqliteAdapterFactory = {
6286
6535
  async open(path) {
6287
- const DbClass = await getSqliteClass();
6536
+ const DbClass = await loadSqliteClass();
6288
6537
  const db = new DbClass(path, { readonly: true, create: false });
6289
- return createSqliteAdapter(db);
6538
+ const openWriteDb = () => new DbClass(path);
6539
+ return createSqliteAdapter(db, openWriteDb);
6290
6540
  }
6291
6541
  };
6292
6542
  });
@@ -7055,6 +7305,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
7055
7305
  }
7056
7306
  var init_global_search = __esm(() => {
7057
7307
  init_serialize();
7308
+ init_sql_utils();
7058
7309
  });
7059
7310
 
7060
7311
  // web-src/server/database/adapters/elasticsearch.ts
@@ -7254,6 +7505,38 @@ function createElasticsearchAdapter(config) {
7254
7505
  primaryTerm: parsed._primary_term
7255
7506
  };
7256
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
+ }
7257
7540
  async function* iterateForSnapshot(container, signal) {
7258
7541
  const { index, query: query2 } = parseEsSnapshotContainer(container);
7259
7542
  const PAGE = 1000;
@@ -7327,6 +7610,8 @@ function createElasticsearchAdapter(config) {
7327
7610
  getMappingAsync,
7328
7611
  searchDocsAsync,
7329
7612
  getDocAsync,
7613
+ writeDocAsync,
7614
+ deleteDocAsync,
7330
7615
  iterateForSnapshot,
7331
7616
  listSnapshotContainers,
7332
7617
  query,
@@ -7806,6 +8091,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
7806
8091
  return handleError("elasticsearch", "read elasticsearch mapping", err);
7807
8092
  }
7808
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
+ }
7809
8139
  async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
7810
8140
  const wrap = createQueryStrippedLogger("elasticsearch", req, url);
7811
8141
  return dispatchRoutes(req, url, {
@@ -7825,6 +8155,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
7825
8155
  methods: ["GET"],
7826
8156
  handler: () => handleDoc(req, cwd, url, omitDirNames)
7827
8157
  },
8158
+ "/_db/elasticsearch/write": {
8159
+ methods: ["POST"],
8160
+ sideEffect: true,
8161
+ handler: () => handleWrite(req, cwd, omitDirNames)
8162
+ },
7828
8163
  "/_db/elasticsearch/search": {
7829
8164
  methods: ["GET", "POST"],
7830
8165
  sideEffect: (m) => m === "POST",
@@ -7843,6 +8178,7 @@ var init_handle_elasticsearch = __esm(() => {
7843
8178
  var exports_redis = {};
7844
8179
  __export(exports_redis, {
7845
8180
  openRedisExplorerAsync: () => openRedisExplorerAsync,
8181
+ createRedisAdapter: () => createRedisAdapter,
7846
8182
  canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
7847
8183
  });
7848
8184
  import { createHash as createHash3 } from "node:crypto";
@@ -8364,6 +8700,41 @@ function createRedisAdapter(config) {
8364
8700
  async function listSnapshotContainers() {
8365
8701
  return [];
8366
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
+ }
8367
8738
  return {
8368
8739
  kind: "redis",
8369
8740
  model: "kv",
@@ -8371,6 +8742,11 @@ function createRedisAdapter(config) {
8371
8742
  listDatabasesAsync,
8372
8743
  listKeysAsync,
8373
8744
  getValueAsync,
8745
+ setStringAsync,
8746
+ createStringAsync,
8747
+ setHashFieldAsync,
8748
+ setListIndexAsync,
8749
+ deleteKeyAsync,
8374
8750
  iterateForSnapshot,
8375
8751
  listSnapshotContainers,
8376
8752
  close() {}
@@ -8447,6 +8823,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
8447
8823
  return handleError("redis", "list redis keys", err);
8448
8824
  }
8449
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
+ }
8450
8898
  async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
8451
8899
  const wrap = createQueryStrippedLogger("redis", req, url);
8452
8900
  return dispatchRoutes(req, url, {
@@ -8461,6 +8909,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
8461
8909
  "/_db/redis/value": {
8462
8910
  methods: ["GET"],
8463
8911
  handler: () => handleValue(req, cwd, url, omitDirNames)
8912
+ },
8913
+ "/_db/redis/write": {
8914
+ methods: ["POST"],
8915
+ sideEffect: true,
8916
+ handler: () => handleWrite2(req, cwd, omitDirNames)
8464
8917
  }
8465
8918
  }, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
8466
8919
  }
@@ -8538,6 +8991,9 @@ function hmac(key, value) {
8538
8991
  function sha256(value) {
8539
8992
  return createHash4("sha256").update(value, "utf8").digest("hex");
8540
8993
  }
8994
+ function sha256Bytes(value) {
8995
+ return createHash4("sha256").update(value).digest("hex");
8996
+ }
8541
8997
  function encodeRfc3986(value) {
8542
8998
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
8543
8999
  }
@@ -8907,9 +9363,10 @@ function createS3Adapter(config) {
8907
9363
  const path = buildPath(opts.bucket, opts.key);
8908
9364
  const query = canonicalQuery(opts.query);
8909
9365
  const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
9366
+ const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
8910
9367
  const headers = {
8911
9368
  host: endpoint.host,
8912
- "x-amz-content-sha256": EMPTY_SHA256,
9369
+ "x-amz-content-sha256": payloadHash,
8913
9370
  "x-amz-date": requestDate,
8914
9371
  ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
8915
9372
  ...opts.headers || {}
@@ -8921,7 +9378,7 @@ function createS3Adapter(config) {
8921
9378
  query,
8922
9379
  canonicalHeaders(headers),
8923
9380
  signedNames,
8924
- EMPTY_SHA256
9381
+ payloadHash
8925
9382
  ].join(`
8926
9383
  `);
8927
9384
  const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
@@ -8940,6 +9397,9 @@ function createS3Adapter(config) {
8940
9397
  }
8941
9398
  requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
8942
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
+ }
8943
9403
  if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
8944
9404
  throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8945
9405
  }
@@ -8954,6 +9414,7 @@ function createS3Adapter(config) {
8954
9414
  return fetch(url, {
8955
9415
  method: opts.method,
8956
9416
  headers: requestHeaders,
9417
+ ...opts.body !== undefined ? { body: opts.body } : {},
8957
9418
  signal: transportSignal
8958
9419
  });
8959
9420
  }, deadline);
@@ -9036,6 +9497,32 @@ function createS3Adapter(config) {
9036
9497
  headers: rawObjectHeaders(opts.key, res)
9037
9498
  });
9038
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
+ }
9039
9526
  return {
9040
9527
  kind: "s3",
9041
9528
  model: "object",
@@ -9044,7 +9531,9 @@ function createS3Adapter(config) {
9044
9531
  listObjects,
9045
9532
  headObject,
9046
9533
  getObjectText,
9047
- getObjectResponse
9534
+ getObjectResponse,
9535
+ putObjectAsync,
9536
+ deleteObjectAsync
9048
9537
  };
9049
9538
  }
9050
9539
  async function s3ConfigFromDockerInfoAsync(info, signal) {
@@ -9447,6 +9936,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
9447
9936
  return s3ErrorResponse(err, "stream s3 object");
9448
9937
  }
9449
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
+ }
9450
9993
  async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9451
9994
  const wrap = createQueryStrippedLogger("s3", req, url);
9452
9995
  return dispatchRoutes(req, url, {
@@ -9473,6 +10016,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9473
10016
  "/_db/s3/raw": {
9474
10017
  methods: ["GET", "HEAD"],
9475
10018
  handler: () => handleRaw(cwd, req, url, omitDirNames)
10019
+ },
10020
+ "/_db/s3/write": {
10021
+ methods: ["POST"],
10022
+ sideEffect: true,
10023
+ handler: () => handleWrite3(req, cwd, omitDirNames)
9476
10024
  }
9477
10025
  }, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
9478
10026
  }
@@ -9639,21 +10187,6 @@ var init_query_history = __esm(() => {
9639
10187
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
9640
10188
  import { mkdirSync as mkdirSync3 } from "node:fs";
9641
10189
  import { join as join11 } from "node:path";
9642
- async function getSqliteClass2() {
9643
- if (cachedDbClass2)
9644
- return cachedDbClass2;
9645
- try {
9646
- const mod = await import("bun:sqlite");
9647
- cachedDbClass2 = mod.Database;
9648
- return cachedDbClass2;
9649
- } catch {}
9650
- try {
9651
- const mod = await Function('return import("better-sqlite3")')();
9652
- cachedDbClass2 = mod.default || mod;
9653
- return cachedDbClass2;
9654
- } catch {}
9655
- throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
9656
- }
9657
10190
  async function getStoreDb(cwd) {
9658
10191
  const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
9659
10192
  if (storeDb && storeDbPath === dbPath)
@@ -9664,7 +10197,7 @@ async function getStoreDb(cwd) {
9664
10197
  } catch {}
9665
10198
  }
9666
10199
  mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
9667
- const DbClass = await getSqliteClass2();
10200
+ const DbClass = await loadSqliteClass();
9668
10201
  storeDb = new DbClass(dbPath);
9669
10202
  storeDbPath = dbPath;
9670
10203
  storeDb.exec("PRAGMA journal_mode=WAL");
@@ -9923,7 +10456,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
9923
10456
  });
9924
10457
  return { rows, total };
9925
10458
  }
9926
- 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 = `
9927
10460
  CREATE TABLE IF NOT EXISTS snapshots (
9928
10461
  id TEXT PRIMARY KEY,
9929
10462
  db_id TEXT NOT NULL,
@@ -10153,6 +10686,9 @@ function sanitize(input) {
10153
10686
  const historyHeight = sanitizeCssSize(tab.historyHeight);
10154
10687
  if (historyHeight !== undefined)
10155
10688
  out.historyHeight = historyHeight;
10689
+ if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
10690
+ out.activeHistoryTab = tab.activeHistoryTab;
10691
+ }
10156
10692
  const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
10157
10693
  if (sidebarWidth !== undefined)
10158
10694
  out.sidebarWidth = sidebarWidth;
@@ -10343,11 +10879,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
10343
10879
  const body2 = { dbId: r.dbId, schemas: [] };
10344
10880
  return json(body2);
10345
10881
  }
10346
- 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));
10347
10884
  const body = {
10348
10885
  dbId: r.dbId,
10349
10886
  schemas: schemas.map((name) => ({ name })),
10350
- selectedSchema: r.schema
10887
+ selectedSchema: r.schema,
10888
+ executedSql
10351
10889
  };
10352
10890
  return json(body);
10353
10891
  }
@@ -10359,37 +10897,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
10359
10897
  const linkedAbort = createLinkedAbortController(signal);
10360
10898
  try {
10361
10899
  const adapter = await getAdapter(r, cwd, signal);
10362
- const db = asAsync(adapter);
10363
- const tables = await db.tables(linkedAbort.signal);
10364
- const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
10365
- const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
10366
- const indexesPromise = db.indexes(linkedAbort.signal);
10367
- const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
10368
- const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
10369
- const schemaPromises = [
10370
- countMapPromise,
10371
- indexesPromise,
10372
- foreignKeysPromise,
10373
- columnsMapPromise
10374
- ];
10375
- const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
10376
- linkedAbort.abort();
10377
- await Promise.allSettled(schemaPromises);
10378
- 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 };
10379
10920
  });
10380
- const tablesWithCount = tables.map((t) => ({
10921
+ const tablesWithCount = result.tables.map((t) => ({
10381
10922
  ...t,
10382
- rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
10923
+ rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
10383
10924
  }));
10384
10925
  const body = {
10385
10926
  dbId: r.dbId,
10386
10927
  ...r.schema ? { schema: r.schema } : {},
10387
10928
  tables: tablesWithCount,
10388
- indexes,
10389
- foreignKeys
10929
+ indexes: result.indexes,
10930
+ foreignKeys: result.foreignKeys,
10931
+ executedSql
10390
10932
  };
10391
- if (colsMap) {
10392
- body.columnsMap = Object.fromEntries(colsMap);
10933
+ if (result.colsMap) {
10934
+ body.columnsMap = Object.fromEntries(result.colsMap);
10393
10935
  }
10394
10936
  return json(body);
10395
10937
  } catch (err) {
@@ -10451,36 +10993,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10451
10993
  const exact = parseExactConditions(url);
10452
10994
  try {
10453
10995
  const adapter = await getAdapter(r, cwd, signal);
10454
- if (filters.length > 0 || exact.length > 0) {
10455
- const meta2 = await adapter.getFilteredTablePageWithMeta(table, {
10456
- offset,
10457
- limit,
10458
- orderBy,
10459
- grouped: groupFiltersByValue(filters),
10460
- ...exact.length > 0 ? { exact } : {}
10461
- }, signal);
10462
- const colNames2 = new Set(meta2.columns.map((c) => c.name));
10463
- if (sortCol && !colNames2.has(sortCol)) {
10464
- return textError(`invalid sort column: ${sortCol}`, 400);
10465
- }
10466
- const body2 = {
10467
- dbId: r.dbId,
10468
- ...r.schema ? { schema: r.schema } : {},
10469
- table,
10470
- columns: meta2.columns,
10471
- rows: serializeDbRows(meta2.rows),
10472
- totalRows: meta2.totalRows,
10473
- offset,
10474
- limit,
10475
- hasMore: offset + meta2.rowCount < meta2.totalRows
10476
- };
10477
- return json(body2);
10478
- }
10479
- 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, {
10480
11003
  offset,
10481
11004
  limit,
10482
11005
  orderBy
10483
- }, signal);
11006
+ }, signal));
10484
11007
  const colNames = new Set(meta.columns.map((c) => c.name));
10485
11008
  if (sortCol && !colNames.has(sortCol)) {
10486
11009
  return textError(`invalid sort column: ${sortCol}`, 400);
@@ -10494,7 +11017,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10494
11017
  totalRows: meta.totalRows,
10495
11018
  offset,
10496
11019
  limit,
10497
- hasMore: offset + meta.rowCount < meta.totalRows
11020
+ hasMore: offset + meta.rowCount < meta.totalRows,
11021
+ executedSql
10498
11022
  };
10499
11023
  return json(body);
10500
11024
  } catch (err) {
@@ -10571,8 +11095,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10571
11095
  const start = Date.now();
10572
11096
  try {
10573
11097
  const adapter = await getAdapter(r, cwd, req.signal);
10574
- const db = asAsync(adapter);
10575
- 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));
10576
11099
  const elapsed = Date.now() - start;
10577
11100
  const serializedRows = serializeDbRows(result.rows);
10578
11101
  const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
@@ -10586,7 +11109,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10586
11109
  rows: serializedRows,
10587
11110
  rowCount: result.rowCount,
10588
11111
  truncated: result.rowCount >= maxRows,
10589
- elapsedMs: elapsed
11112
+ elapsedMs: elapsed,
11113
+ executedSql
10590
11114
  };
10591
11115
  if (body.saveHistory) {
10592
11116
  const entry = {
@@ -10805,13 +11329,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
10805
11329
  return textError("missing table parameter", 400);
10806
11330
  try {
10807
11331
  const adapter = await getAdapter(r, cwd, signal);
10808
- const db = asAsync(adapter);
10809
- const columns = await db.columns(table, signal);
11332
+ const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
10810
11333
  return json({
10811
11334
  dbId: r.dbId,
10812
11335
  ...r.schema ? { schema: r.schema } : {},
10813
11336
  table,
10814
- columns
11337
+ columns,
11338
+ executedSql
10815
11339
  });
10816
11340
  } catch (err) {
10817
11341
  return handleError("database", "get columns", err);
@@ -10826,17 +11350,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
10826
11350
  return textError("missing table parameter", 400);
10827
11351
  try {
10828
11352
  const adapter = await getAdapter(r, cwd, signal);
10829
- const db = asAsync(adapter);
10830
- const [sql, triggers] = await Promise.all([
10831
- db.createStatement(table, signal),
10832
- db.triggers(table, signal)
10833
- ]);
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
+ });
10834
11361
  return json({
10835
11362
  dbId: r.dbId,
10836
11363
  ...r.schema ? { schema: r.schema } : {},
10837
11364
  table,
10838
- sql,
10839
- triggers
11365
+ sql: result.sql,
11366
+ triggers: result.triggers,
11367
+ executedSql
10840
11368
  });
10841
11369
  } catch (err) {
10842
11370
  return handleError("database", "get DDL", err);
@@ -11248,6 +11776,48 @@ async function handleClose(cwd, req, omitDirNames) {
11248
11776
  }
11249
11777
  return json({ ok: true });
11250
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
+ }
11251
11821
  async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
11252
11822
  ensureInit();
11253
11823
  if (url.pathname.startsWith("/_db/redis/")) {
@@ -11308,6 +11878,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
11308
11878
  sideEffect: true,
11309
11879
  handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
11310
11880
  },
11881
+ "/_db/mutate": {
11882
+ methods: ["POST"],
11883
+ sideEffect: true,
11884
+ handler: () => handleMutate(cwd, req, omitDirNames)
11885
+ },
11311
11886
  "/_db/close": {
11312
11887
  methods: ["POST"],
11313
11888
  sideEffect: true,
@@ -11390,6 +11965,7 @@ var init_handle = __esm(() => {
11390
11965
  init_state_store();
11391
11966
  init_docker();
11392
11967
  init_docker_utils();
11968
+ init_sql_capture();
11393
11969
  init_sqlite();
11394
11970
  init_connection_pool();
11395
11971
  init_discovery();