@youtyan/code-viewer 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3888,10 +3888,260 @@ var init_search = __esm(() => {
3888
3888
  DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
3889
3889
  });
3890
3890
 
3891
+ // web-src/server/worktree-watcher.ts
3892
+ import {
3893
+ lstatSync as lstatSync3,
3894
+ readdirSync as nodeReaddirSync,
3895
+ watch as nodeWatch
3896
+ } from "node:fs";
3897
+ import { join as join7, relative } from "node:path";
3898
+ function normalizeRelativePath(path) {
3899
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
3900
+ }
3901
+ function isInsideRoot(root, path) {
3902
+ const rel = relative(root, path).replace(/\\/g, "/");
3903
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
3904
+ }
3905
+ function startWorktreeUpdateWatch(options) {
3906
+ const watch = options.watch || nodeWatch;
3907
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
3908
+ const isDirectory = options.isDirectory || ((path) => {
3909
+ try {
3910
+ return lstatSync3(path).isDirectory();
3911
+ } catch {
3912
+ return false;
3913
+ }
3914
+ });
3915
+ const directorySignature = options.directorySignature || ((path) => {
3916
+ try {
3917
+ const stats = lstatSync3(path);
3918
+ if (!stats.isDirectory())
3919
+ return null;
3920
+ return `${stats.dev}:${stats.ino}`;
3921
+ } catch {
3922
+ return null;
3923
+ }
3924
+ });
3925
+ const setTimer = options.setTimeoutFn || setTimeout;
3926
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
3927
+ const debounceMs = options.debounceMs ?? 250;
3928
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
3929
+ const watchers = new Map;
3930
+ const signatures = new Map;
3931
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
3932
+ const initialScanQueue = [];
3933
+ let initialScanTimer = null;
3934
+ const pendingPathInspections = new Map;
3935
+ let pathInspectionTimer = null;
3936
+ let timer = null;
3937
+ const pendingChangedPaths = new Set;
3938
+ let watchLimitReported = false;
3939
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
3940
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
3941
+ const ignoredDirectory = (dir) => {
3942
+ const rel = directoryRelativePath(dir);
3943
+ return Boolean(rel && ignored(rel));
3944
+ };
3945
+ const scheduleUpdate = (changedPath) => {
3946
+ if (changedPath)
3947
+ pendingChangedPaths.add(changedPath);
3948
+ if (timer)
3949
+ clearTimer(timer);
3950
+ timer = setTimer(() => {
3951
+ timer = null;
3952
+ const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
3953
+ pendingChangedPaths.clear();
3954
+ options.onUpdate(paths);
3955
+ }, debounceMs);
3956
+ };
3957
+ const reportWatchLimit = () => {
3958
+ if (watchLimitReported)
3959
+ return;
3960
+ watchLimitReported = true;
3961
+ options.onWatchLimit?.(maxWatchedDirectories);
3962
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
3963
+ };
3964
+ const closeSubtree = (dir) => {
3965
+ for (const [watchedDir, watcher] of [...watchers]) {
3966
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
3967
+ continue;
3968
+ try {
3969
+ watcher.close?.();
3970
+ } catch {}
3971
+ watchers.delete(watchedDir);
3972
+ signatures.delete(watchedDir);
3973
+ }
3974
+ };
3975
+ const closeAll = () => {
3976
+ if (initialScanTimer) {
3977
+ clearTimer(initialScanTimer);
3978
+ initialScanTimer = null;
3979
+ }
3980
+ if (pathInspectionTimer) {
3981
+ clearTimer(pathInspectionTimer);
3982
+ pathInspectionTimer = null;
3983
+ }
3984
+ initialScanQueue.length = 0;
3985
+ pendingPathInspections.clear();
3986
+ for (const watcher of [...watchers.values()]) {
3987
+ try {
3988
+ watcher.close?.();
3989
+ } catch {}
3990
+ }
3991
+ watchers.clear();
3992
+ signatures.clear();
3993
+ };
3994
+ const readChildDirectories = (dir) => {
3995
+ let entries;
3996
+ try {
3997
+ entries = readDirs(dir);
3998
+ } catch (error) {
3999
+ options.onError?.(error);
4000
+ return [];
4001
+ }
4002
+ const children = [];
4003
+ for (const entry of entries) {
4004
+ if (!entry.isDirectory())
4005
+ continue;
4006
+ const child = join7(dir, entry.name);
4007
+ if (ignoredDirectory(child))
4008
+ continue;
4009
+ children.push(child);
4010
+ }
4011
+ return children;
4012
+ };
4013
+ const processInitialScanQueue = () => {
4014
+ initialScanTimer = null;
4015
+ if (watchers.size >= maxWatchedDirectories) {
4016
+ reportWatchLimit();
4017
+ initialScanQueue.length = 0;
4018
+ return;
4019
+ }
4020
+ const next = initialScanQueue.shift();
4021
+ if (next)
4022
+ watchDirectory(next, true);
4023
+ if (watchers.size >= maxWatchedDirectories) {
4024
+ reportWatchLimit();
4025
+ initialScanQueue.length = 0;
4026
+ }
4027
+ if (initialScanQueue.length)
4028
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
4029
+ };
4030
+ const queueInitialChildren = (dir) => {
4031
+ const remaining = maxWatchedDirectories - watchers.size;
4032
+ if (remaining <= 0) {
4033
+ reportWatchLimit();
4034
+ return;
4035
+ }
4036
+ const children = readChildDirectories(dir);
4037
+ if (children.length > remaining)
4038
+ reportWatchLimit();
4039
+ initialScanQueue.push(...children.slice(0, remaining));
4040
+ if (!initialScanTimer)
4041
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
4042
+ };
4043
+ const processChangedPath = (changed, fullChangedPath) => {
4044
+ const known = watchers.has(fullChangedPath);
4045
+ if (isDirectory(fullChangedPath)) {
4046
+ if (known) {
4047
+ const signature = directorySignature(fullChangedPath);
4048
+ if (signature && signature !== signatures.get(fullChangedPath)) {
4049
+ closeSubtree(fullChangedPath);
4050
+ watchDirectory(fullChangedPath, initialScanAsync);
4051
+ }
4052
+ scheduleUpdate(changed);
4053
+ return;
4054
+ }
4055
+ watchDirectory(fullChangedPath, initialScanAsync);
4056
+ } else if (known) {
4057
+ closeSubtree(fullChangedPath);
4058
+ }
4059
+ scheduleUpdate(changed);
4060
+ };
4061
+ const processPathInspections = () => {
4062
+ pathInspectionTimer = null;
4063
+ const entries = [...pendingPathInspections];
4064
+ pendingPathInspections.clear();
4065
+ for (const [changed, fullChangedPath] of entries) {
4066
+ processChangedPath(changed, fullChangedPath);
4067
+ }
4068
+ };
4069
+ const queuePathInspection = (changed, fullChangedPath) => {
4070
+ pendingPathInspections.set(changed, fullChangedPath);
4071
+ if (!pathInspectionTimer)
4072
+ pathInspectionTimer = setTimer(processPathInspections, 25);
4073
+ };
4074
+ const watchDirectory = (dir, initialScan = false) => {
4075
+ if (watchers.has(dir))
4076
+ return;
4077
+ if (watchers.size >= maxWatchedDirectories) {
4078
+ reportWatchLimit();
4079
+ return;
4080
+ }
4081
+ const rel = directoryRelativePath(dir);
4082
+ if (rel && ignored(rel))
4083
+ return;
4084
+ try {
4085
+ const watcher = watch(dir, { persistent: false }, (_event, filename) => {
4086
+ if (!filename) {
4087
+ scheduleUpdate();
4088
+ return;
4089
+ }
4090
+ const changed = normalizeRelativePath(join7(rel, filename.toString()));
4091
+ if (ignored(changed))
4092
+ return;
4093
+ const fullChangedPath = join7(options.root, changed);
4094
+ if (!isInsideRoot(options.root, fullChangedPath))
4095
+ return;
4096
+ if (initialScanAsync) {
4097
+ queuePathInspection(changed, fullChangedPath);
4098
+ return;
4099
+ }
4100
+ processChangedPath(changed, fullChangedPath);
4101
+ }) || {};
4102
+ watchers.set(dir, watcher);
4103
+ const signature = directorySignature(dir);
4104
+ if (signature)
4105
+ signatures.set(dir, signature);
4106
+ watcher.on?.("error", () => {
4107
+ if (watchers.get(dir) === watcher) {
4108
+ watchers.delete(dir);
4109
+ signatures.delete(dir);
4110
+ }
4111
+ });
4112
+ watcher.on?.("close", () => {
4113
+ if (watchers.get(dir) === watcher) {
4114
+ watchers.delete(dir);
4115
+ signatures.delete(dir);
4116
+ }
4117
+ });
4118
+ } catch (error) {
4119
+ options.onError?.(error);
4120
+ return;
4121
+ }
4122
+ if (initialScanAsync && initialScan) {
4123
+ queueInitialChildren(dir);
4124
+ return;
4125
+ }
4126
+ if (watchers.size >= maxWatchedDirectories) {
4127
+ reportWatchLimit();
4128
+ return;
4129
+ }
4130
+ for (const child of readChildDirectories(dir))
4131
+ watchDirectory(child);
4132
+ };
4133
+ watchDirectory(options.root, true);
4134
+ return { started: watchers.size > 0, close: closeAll };
4135
+ }
4136
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
4137
+ var init_worktree_watcher = __esm(() => {
4138
+ init_search();
4139
+ });
4140
+
3891
4141
  // web-src/server/state-store.ts
3892
- import { join as join7 } from "node:path";
4142
+ import { join as join8 } from "node:path";
3893
4143
  function codeViewerPath(root, fileName) {
3894
- return join7(root, CODE_VIEWER_DIR2, fileName);
4144
+ return join8(root, CODE_VIEWER_DIR2, fileName);
3895
4145
  }
3896
4146
  function isRecord(value) {
3897
4147
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -4029,6 +4279,9 @@ function sanitizeSettings(raw) {
4029
4279
  });
4030
4280
  if (scopeExcludeNames)
4031
4281
  out.scopeExcludeNames = scopeExcludeNames;
4282
+ const scopeWatchLimit = optionalNumber(raw.scopeWatchLimit, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT);
4283
+ if (scopeWatchLimit !== undefined)
4284
+ out.scopeWatchLimit = scopeWatchLimit;
4032
4285
  const uploadEnabled = optionalBoolean(raw.uploadEnabled);
4033
4286
  if (uploadEnabled !== undefined)
4034
4287
  out.uploadEnabled = uploadEnabled;
@@ -4350,6 +4603,7 @@ async function patchDbUiState(root, patch) {
4350
4603
  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;
4351
4604
  var init_state_store = __esm(() => {
4352
4605
  init_json_store();
4606
+ init_worktree_watcher();
4353
4607
  DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
4354
4608
  settingsStore = createJsonFileStore({
4355
4609
  filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
@@ -4377,256 +4631,6 @@ var init_state_store = __esm(() => {
4377
4631
  });
4378
4632
  });
4379
4633
 
4380
- // web-src/server/worktree-watcher.ts
4381
- import {
4382
- lstatSync as lstatSync3,
4383
- readdirSync as nodeReaddirSync,
4384
- watch as nodeWatch
4385
- } from "node:fs";
4386
- import { join as join8, relative } from "node:path";
4387
- function normalizeRelativePath(path) {
4388
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
4389
- }
4390
- function isInsideRoot(root, path) {
4391
- const rel = relative(root, path).replace(/\\/g, "/");
4392
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
4393
- }
4394
- function startWorktreeUpdateWatch(options) {
4395
- const watch = options.watch || nodeWatch;
4396
- const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
4397
- const isDirectory = options.isDirectory || ((path) => {
4398
- try {
4399
- return lstatSync3(path).isDirectory();
4400
- } catch {
4401
- return false;
4402
- }
4403
- });
4404
- const directorySignature = options.directorySignature || ((path) => {
4405
- try {
4406
- const stats = lstatSync3(path);
4407
- if (!stats.isDirectory())
4408
- return null;
4409
- return `${stats.dev}:${stats.ino}`;
4410
- } catch {
4411
- return null;
4412
- }
4413
- });
4414
- const setTimer = options.setTimeoutFn || setTimeout;
4415
- const clearTimer = options.clearTimeoutFn || clearTimeout;
4416
- const debounceMs = options.debounceMs ?? 250;
4417
- const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
4418
- const watchers = new Map;
4419
- const signatures = new Map;
4420
- const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
4421
- const initialScanQueue = [];
4422
- let initialScanTimer = null;
4423
- const pendingPathInspections = new Map;
4424
- let pathInspectionTimer = null;
4425
- let timer = null;
4426
- const pendingChangedPaths = new Set;
4427
- let watchLimitReported = false;
4428
- const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
4429
- const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
4430
- const ignoredDirectory = (dir) => {
4431
- const rel = directoryRelativePath(dir);
4432
- return Boolean(rel && ignored(rel));
4433
- };
4434
- const scheduleUpdate = (changedPath) => {
4435
- if (changedPath)
4436
- pendingChangedPaths.add(changedPath);
4437
- if (timer)
4438
- clearTimer(timer);
4439
- timer = setTimer(() => {
4440
- timer = null;
4441
- const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
4442
- pendingChangedPaths.clear();
4443
- options.onUpdate(paths);
4444
- }, debounceMs);
4445
- };
4446
- const reportWatchLimit = () => {
4447
- if (watchLimitReported)
4448
- return;
4449
- watchLimitReported = true;
4450
- options.onWatchLimit?.(maxWatchedDirectories);
4451
- options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
4452
- };
4453
- const closeSubtree = (dir) => {
4454
- for (const [watchedDir, watcher] of [...watchers]) {
4455
- if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
4456
- continue;
4457
- try {
4458
- watcher.close?.();
4459
- } catch {}
4460
- watchers.delete(watchedDir);
4461
- signatures.delete(watchedDir);
4462
- }
4463
- };
4464
- const closeAll = () => {
4465
- if (initialScanTimer) {
4466
- clearTimer(initialScanTimer);
4467
- initialScanTimer = null;
4468
- }
4469
- if (pathInspectionTimer) {
4470
- clearTimer(pathInspectionTimer);
4471
- pathInspectionTimer = null;
4472
- }
4473
- initialScanQueue.length = 0;
4474
- pendingPathInspections.clear();
4475
- for (const watcher of [...watchers.values()]) {
4476
- try {
4477
- watcher.close?.();
4478
- } catch {}
4479
- }
4480
- watchers.clear();
4481
- signatures.clear();
4482
- };
4483
- const readChildDirectories = (dir) => {
4484
- let entries;
4485
- try {
4486
- entries = readDirs(dir);
4487
- } catch (error) {
4488
- options.onError?.(error);
4489
- return [];
4490
- }
4491
- const children = [];
4492
- for (const entry of entries) {
4493
- if (!entry.isDirectory())
4494
- continue;
4495
- const child = join8(dir, entry.name);
4496
- if (ignoredDirectory(child))
4497
- continue;
4498
- children.push(child);
4499
- }
4500
- return children;
4501
- };
4502
- const processInitialScanQueue = () => {
4503
- initialScanTimer = null;
4504
- if (watchers.size >= maxWatchedDirectories) {
4505
- reportWatchLimit();
4506
- initialScanQueue.length = 0;
4507
- return;
4508
- }
4509
- const next = initialScanQueue.shift();
4510
- if (next)
4511
- watchDirectory(next, true);
4512
- if (watchers.size >= maxWatchedDirectories) {
4513
- reportWatchLimit();
4514
- initialScanQueue.length = 0;
4515
- }
4516
- if (initialScanQueue.length)
4517
- initialScanTimer = setTimer(processInitialScanQueue, 50);
4518
- };
4519
- const queueInitialChildren = (dir) => {
4520
- const remaining = maxWatchedDirectories - watchers.size;
4521
- if (remaining <= 0) {
4522
- reportWatchLimit();
4523
- return;
4524
- }
4525
- const children = readChildDirectories(dir);
4526
- if (children.length > remaining)
4527
- reportWatchLimit();
4528
- initialScanQueue.push(...children.slice(0, remaining));
4529
- if (!initialScanTimer)
4530
- initialScanTimer = setTimer(processInitialScanQueue, 5000);
4531
- };
4532
- const processChangedPath = (changed, fullChangedPath) => {
4533
- const known = watchers.has(fullChangedPath);
4534
- if (isDirectory(fullChangedPath)) {
4535
- if (known) {
4536
- const signature = directorySignature(fullChangedPath);
4537
- if (signature && signature !== signatures.get(fullChangedPath)) {
4538
- closeSubtree(fullChangedPath);
4539
- watchDirectory(fullChangedPath, initialScanAsync);
4540
- }
4541
- scheduleUpdate(changed);
4542
- return;
4543
- }
4544
- watchDirectory(fullChangedPath, initialScanAsync);
4545
- } else if (known) {
4546
- closeSubtree(fullChangedPath);
4547
- }
4548
- scheduleUpdate(changed);
4549
- };
4550
- const processPathInspections = () => {
4551
- pathInspectionTimer = null;
4552
- const entries = [...pendingPathInspections];
4553
- pendingPathInspections.clear();
4554
- for (const [changed, fullChangedPath] of entries) {
4555
- processChangedPath(changed, fullChangedPath);
4556
- }
4557
- };
4558
- const queuePathInspection = (changed, fullChangedPath) => {
4559
- pendingPathInspections.set(changed, fullChangedPath);
4560
- if (!pathInspectionTimer)
4561
- pathInspectionTimer = setTimer(processPathInspections, 25);
4562
- };
4563
- const watchDirectory = (dir, initialScan = false) => {
4564
- if (watchers.has(dir))
4565
- return;
4566
- if (watchers.size >= maxWatchedDirectories) {
4567
- reportWatchLimit();
4568
- return;
4569
- }
4570
- const rel = directoryRelativePath(dir);
4571
- if (rel && ignored(rel))
4572
- return;
4573
- try {
4574
- const watcher = watch(dir, { persistent: false }, (_event, filename) => {
4575
- if (!filename) {
4576
- scheduleUpdate();
4577
- return;
4578
- }
4579
- const changed = normalizeRelativePath(join8(rel, filename.toString()));
4580
- if (ignored(changed))
4581
- return;
4582
- const fullChangedPath = join8(options.root, changed);
4583
- if (!isInsideRoot(options.root, fullChangedPath))
4584
- return;
4585
- if (initialScanAsync) {
4586
- queuePathInspection(changed, fullChangedPath);
4587
- return;
4588
- }
4589
- processChangedPath(changed, fullChangedPath);
4590
- }) || {};
4591
- watchers.set(dir, watcher);
4592
- const signature = directorySignature(dir);
4593
- if (signature)
4594
- signatures.set(dir, signature);
4595
- watcher.on?.("error", () => {
4596
- if (watchers.get(dir) === watcher) {
4597
- watchers.delete(dir);
4598
- signatures.delete(dir);
4599
- }
4600
- });
4601
- watcher.on?.("close", () => {
4602
- if (watchers.get(dir) === watcher) {
4603
- watchers.delete(dir);
4604
- signatures.delete(dir);
4605
- }
4606
- });
4607
- } catch (error) {
4608
- options.onError?.(error);
4609
- return;
4610
- }
4611
- if (initialScanAsync && initialScan) {
4612
- queueInitialChildren(dir);
4613
- return;
4614
- }
4615
- if (watchers.size >= maxWatchedDirectories) {
4616
- reportWatchLimit();
4617
- return;
4618
- }
4619
- for (const child of readChildDirectories(dir))
4620
- watchDirectory(child);
4621
- };
4622
- watchDirectory(options.root, true);
4623
- return { started: watchers.size > 0, close: closeAll };
4624
- }
4625
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
4626
- var init_worktree_watcher = __esm(() => {
4627
- init_search();
4628
- });
4629
-
4630
4634
  // web-src/core/control-chars.ts
4631
4635
  function hasControlCharacter(value) {
4632
4636
  for (const ch of value) {
@@ -4768,52 +4772,37 @@ function serializeDbRow(row) {
4768
4772
  function serializeDbRows(rows) {
4769
4773
  return rows.map(serializeDbRow);
4770
4774
  }
4775
+ function coerceDbValue(value, columnType) {
4776
+ if (value === null)
4777
+ return null;
4778
+ const t = (columnType || "").toLowerCase();
4779
+ if (/bool/.test(t)) {
4780
+ const v = value.trim().toLowerCase();
4781
+ if (v === "")
4782
+ return null;
4783
+ if (v === "true" || v === "t" || v === "1")
4784
+ return true;
4785
+ if (v === "false" || v === "f" || v === "0")
4786
+ return false;
4787
+ return value;
4788
+ }
4789
+ if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
4790
+ const trimmed = value.trim();
4791
+ if (trimmed === "")
4792
+ return null;
4793
+ const n = Number(trimmed);
4794
+ if (Number.isFinite(n) && String(n) === trimmed)
4795
+ return n;
4796
+ return value;
4797
+ }
4798
+ return value;
4799
+ }
4771
4800
  var MIN_SAFE, MAX_SAFE;
4772
4801
  var init_serialize = __esm(() => {
4773
4802
  MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
4774
4803
  MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
4775
4804
  });
4776
4805
 
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
4806
  // web-src/server/database/sql-utils.ts
4818
4807
  function sanitizeIdentifier(name, kind = "sqlite") {
4819
4808
  if (kind === "mysql")
@@ -4875,6 +4864,189 @@ function filterOrderByColumns(orderBy, columnNames) {
4875
4864
  const filtered = orderBy.filter((order) => validColumns.has(order.column));
4876
4865
  return filtered.length > 0 ? filtered : undefined;
4877
4866
  }
4867
+ function buildOrderClause(orderBy, kind = "sqlite") {
4868
+ if (!orderBy?.length)
4869
+ return "";
4870
+ const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
4871
+ return ` ORDER BY ${parts.join(", ")}`;
4872
+ }
4873
+ function useParamsFor(kind) {
4874
+ return kind === "sqlite";
4875
+ }
4876
+ function placeValue(coerced, kind, useParams, params) {
4877
+ if (useParams) {
4878
+ params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
4879
+ return "?";
4880
+ }
4881
+ if (coerced === null)
4882
+ return "NULL";
4883
+ if (typeof coerced === "number")
4884
+ return String(coerced);
4885
+ if (typeof coerced === "boolean")
4886
+ return coerced ? "TRUE" : "FALSE";
4887
+ const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
4888
+ return escapeSqlString(text, kind);
4889
+ }
4890
+ function coerceCell(cell, columnType) {
4891
+ return coerceDbValue(cell.value, columnType);
4892
+ }
4893
+ function buildInsertSql(table, cells, columnTypes, kind) {
4894
+ if (cells.length === 0) {
4895
+ throw new Error("insert requires at least one column value");
4896
+ }
4897
+ const useParams = useParamsFor(kind);
4898
+ const params = [];
4899
+ const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
4900
+ const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
4901
+ const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
4902
+ return { sql, params };
4903
+ }
4904
+ function buildUpdateSql(table, set, pk, columnTypes, kind) {
4905
+ if (set.length === 0) {
4906
+ throw new Error("update requires at least one column to set");
4907
+ }
4908
+ if (pk.length === 0) {
4909
+ throw new Error("update requires a primary key condition");
4910
+ }
4911
+ const useParams = useParamsFor(kind);
4912
+ const params = [];
4913
+ const setSql = set.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(", ");
4914
+ const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
4915
+ const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
4916
+ return { sql, params };
4917
+ }
4918
+ function buildDeleteSql(table, pk, columnTypes, kind) {
4919
+ if (pk.length === 0) {
4920
+ throw new Error("delete requires a primary key condition");
4921
+ }
4922
+ const useParams = useParamsFor(kind);
4923
+ const params = [];
4924
+ const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
4925
+ const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
4926
+ return { sql, params };
4927
+ }
4928
+ var init_sql_utils = __esm(() => {
4929
+ init_serialize();
4930
+ });
4931
+
4932
+ // web-src/server/database/mutate.ts
4933
+ function assertCells(cells, label) {
4934
+ if (!Array.isArray(cells)) {
4935
+ throw new Error(`${label} must be an array`);
4936
+ }
4937
+ for (const cell of cells) {
4938
+ if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
4939
+ throw new Error(`${label} contains an invalid cell`);
4940
+ }
4941
+ }
4942
+ return cells;
4943
+ }
4944
+ function buildMutationStatements(table, mutations, columns, kind) {
4945
+ if (!Array.isArray(mutations) || mutations.length === 0) {
4946
+ throw new Error("no mutations provided");
4947
+ }
4948
+ if (mutations.length > MAX_MUTATIONS) {
4949
+ throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
4950
+ }
4951
+ const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
4952
+ const columnNames = new Set(columns.map((c) => c.name));
4953
+ const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
4954
+ const pkNames = new Set(pkColumns);
4955
+ const requireKnownColumns = (cells, label) => {
4956
+ for (const cell of cells) {
4957
+ if (!columnNames.has(cell.column)) {
4958
+ throw new Error(`unknown column: ${cell.column}`);
4959
+ }
4960
+ }
4961
+ };
4962
+ const requirePrimaryKey = (pk) => {
4963
+ if (pkColumns.length === 0) {
4964
+ throw new Error("table has no primary key; row update/delete is not supported");
4965
+ }
4966
+ const provided = new Set(pk.map((c) => c.column));
4967
+ for (const name of pkColumns) {
4968
+ if (!provided.has(name)) {
4969
+ throw new Error(`missing primary key column: ${name}`);
4970
+ }
4971
+ }
4972
+ for (const cell of pk) {
4973
+ if (!pkNames.has(cell.column)) {
4974
+ throw new Error(`not a primary key column: ${cell.column}`);
4975
+ }
4976
+ if (cell.value === null) {
4977
+ throw new Error(`primary key column cannot be null: ${cell.column}`);
4978
+ }
4979
+ }
4980
+ };
4981
+ const statements = [];
4982
+ for (const mutation of mutations) {
4983
+ if (!mutation || typeof mutation !== "object") {
4984
+ throw new Error("invalid mutation");
4985
+ }
4986
+ if (mutation.kind === "insert") {
4987
+ const values = assertCells(mutation.values, "insert values");
4988
+ requireKnownColumns(values, "insert values");
4989
+ statements.push(buildInsertSql(table, values, columnTypes, kind));
4990
+ } else if (mutation.kind === "update") {
4991
+ const pk = assertCells(mutation.pk, "update pk");
4992
+ const values = assertCells(mutation.values, "update values");
4993
+ requirePrimaryKey(pk);
4994
+ requireKnownColumns(values, "update values");
4995
+ statements.push(buildUpdateSql(table, values, pk, columnTypes, kind));
4996
+ } else if (mutation.kind === "delete") {
4997
+ const pk = assertCells(mutation.pk, "delete pk");
4998
+ requirePrimaryKey(pk);
4999
+ statements.push(buildDeleteSql(table, pk, columnTypes, kind));
5000
+ } else {
5001
+ throw new Error(`unknown mutation kind: ${mutation.kind}`);
5002
+ }
5003
+ }
5004
+ return statements;
5005
+ }
5006
+ var MAX_MUTATIONS = 1000;
5007
+ var init_mutate = __esm(() => {
5008
+ init_sql_utils();
5009
+ });
5010
+
5011
+ // web-src/server/database/sources/sql-snapshot.ts
5012
+ import { createHash as createHash2 } from "node:crypto";
5013
+ function normalizeRawValue(v) {
5014
+ if (v === null)
5015
+ return "\\N";
5016
+ if (typeof v === "bigint")
5017
+ return v.toString();
5018
+ if (v instanceof Uint8Array) {
5019
+ return `\\x${Buffer.from(v).toString("hex")}`;
5020
+ }
5021
+ return String(v);
5022
+ }
5023
+ function rowToPayloadJson(columns, row) {
5024
+ const obj = {};
5025
+ for (let i = 0;i < columns.length; i++) {
5026
+ obj[columns[i]] = serializeDbValue(row[i]);
5027
+ }
5028
+ return JSON.stringify(obj);
5029
+ }
5030
+ function computeRowHash(columns, row) {
5031
+ const parts = columns.map((_, i) => normalizeRawValue(row[i]));
5032
+ return createHash2("sha256").update(parts.join("\t")).digest("hex");
5033
+ }
5034
+ function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
5035
+ if (pkColumns.length === 0) {
5036
+ return JSON.stringify({ __rowIndex: rowIndex });
5037
+ }
5038
+ const keyObj = {};
5039
+ for (const pk of pkColumns) {
5040
+ const idx = allColumns.indexOf(pk);
5041
+ if (idx >= 0)
5042
+ keyObj[pk] = serializeDbValue(row[idx]);
5043
+ }
5044
+ return JSON.stringify(keyObj);
5045
+ }
5046
+ var SQL_SNAPSHOT_BATCH_SIZE = 500;
5047
+ var init_sql_snapshot = __esm(() => {
5048
+ init_serialize();
5049
+ });
4878
5050
 
4879
5051
  // web-src/server/database/adapters/spawn-runner.ts
4880
5052
  import { spawn as spawn2 } from "node:child_process";
@@ -5124,6 +5296,24 @@ var init_docker_utils = __esm(() => {
5124
5296
  };
5125
5297
  });
5126
5298
 
5299
+ // web-src/server/database/adapters/sql-capture.ts
5300
+ import { AsyncLocalStorage } from "node:async_hooks";
5301
+ function recordSql(sql) {
5302
+ const bucket = storage.getStore();
5303
+ if (!bucket)
5304
+ return;
5305
+ bucket.sqls.push(sql);
5306
+ }
5307
+ async function captureSql(fn) {
5308
+ const bucket = { sqls: [] };
5309
+ const result = await storage.run(bucket, fn);
5310
+ return { result, executedSql: bucket.sqls };
5311
+ }
5312
+ var storage;
5313
+ var init_sql_capture = __esm(() => {
5314
+ storage = new AsyncLocalStorage;
5315
+ });
5316
+
5127
5317
  // web-src/server/database/adapters/docker.ts
5128
5318
  import { spawnSync as spawnSync3 } from "node:child_process";
5129
5319
  function dockerDatabasesCacheKey(serviceName, kind, cwd) {
@@ -5279,6 +5469,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
5279
5469
  });
5280
5470
  }
5281
5471
  async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
5472
+ recordSql(sql);
5282
5473
  if (spawnSyncImpl2 !== spawnSync3)
5283
5474
  return execInContainer(config, sql, timeoutMs);
5284
5475
  const args = buildExecArgs(config, sql);
@@ -5366,12 +5557,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
5366
5557
  const rows = lines.map((line) => splitTsvLine(line, false));
5367
5558
  return { columns: [], rows };
5368
5559
  }
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
5560
  function isMysqlSpatialType(type) {
5376
5561
  const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
5377
5562
  return MYSQL_SPATIAL_TYPES.has(baseType);
@@ -5460,7 +5645,7 @@ function createDockerAdapter(config) {
5460
5645
  const tableLiteral = table.replace(/'/g, "''");
5461
5646
  if (config.kind === "postgresql") {
5462
5647
  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`;
5648
+ 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
5649
  }
5465
5650
  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
5651
  }
@@ -5794,6 +5979,19 @@ function createDockerAdapter(config) {
5794
5979
  rowCount: Math.min(result.rows.length, maxRows)
5795
5980
  };
5796
5981
  },
5982
+ async applyMutations(table, mutations, signal) {
5983
+ const columns = await this.getColumnsAsync(table, signal);
5984
+ if (columns.length === 0) {
5985
+ throw new Error(`unknown table: ${table}`);
5986
+ }
5987
+ const statements = buildMutationStatements(table, mutations, columns, config.kind);
5988
+ const body = statements.map((s) => s.sql).join(`;
5989
+ `);
5990
+ const wrapped = config.kind === "postgresql" ? `BEGIN; SET LOCAL search_path = ${sanitizeIdentifier(currentPostgresSchema(), config.kind)}; ${body}; COMMIT` : `START TRANSACTION; ${body}; COMMIT`;
5991
+ await execAsync(wrapped, signal);
5992
+ this.invalidateTableMetaCache?.(table);
5993
+ return { affected: statements.length };
5994
+ },
5797
5995
  invalidateTableMetaCache(table) {
5798
5996
  tableMetaCache.invalidate(table);
5799
5997
  if (table) {
@@ -5975,9 +6173,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
5975
6173
  }
5976
6174
  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
6175
  var init_docker = __esm(() => {
6176
+ init_mutate();
5978
6177
  init_sql_snapshot();
6178
+ init_sql_utils();
5979
6179
  init_docker_utils();
5980
6180
  init_spawn_runner();
6181
+ init_sql_capture();
5981
6182
  dockerDatabasesCache = new Map;
5982
6183
  dockerSchemasCache = new Map;
5983
6184
  spawnSyncImpl2 = spawnSync3;
@@ -5994,17 +6195,8 @@ var init_docker = __esm(() => {
5994
6195
  ]);
5995
6196
  });
5996
6197
 
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() {
6198
+ // web-src/server/database/sqlite-driver.ts
6199
+ async function loadSqliteClass() {
6008
6200
  if (cachedDbClass)
6009
6201
  return cachedDbClass;
6010
6202
  try {
@@ -6019,11 +6211,17 @@ async function getSqliteClass() {
6019
6211
  } catch {}
6020
6212
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
6021
6213
  }
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(", ")}`;
6214
+ var cachedDbClass = null;
6215
+
6216
+ // web-src/server/database/adapters/sqlite.ts
6217
+ function safePrepare(db, sql) {
6218
+ const stmt = db.prepare(sql);
6219
+ if (typeof stmt.safeIntegers === "function") {
6220
+ try {
6221
+ stmt.safeIntegers(true);
6222
+ } catch {}
6223
+ }
6224
+ return stmt;
6027
6225
  }
6028
6226
  function queryRowsToResult(rows, columns) {
6029
6227
  const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
@@ -6045,7 +6243,30 @@ function queryColumns(db, table) {
6045
6243
  defaultValue: row.dflt_value
6046
6244
  }));
6047
6245
  }
6048
- function createSqliteAdapter(db) {
6246
+ function wrapDbWithSqlCapture(rawDb) {
6247
+ return new Proxy(rawDb, {
6248
+ get(target, prop, receiver) {
6249
+ if (prop === "prepare") {
6250
+ return (sql) => {
6251
+ recordSql(sql);
6252
+ return target.prepare(sql);
6253
+ };
6254
+ }
6255
+ return Reflect.get(target, prop, receiver);
6256
+ }
6257
+ });
6258
+ }
6259
+ function createSqliteAdapter(rawDb, openRawWriteDb) {
6260
+ const db = wrapDbWithSqlCapture(rawDb);
6261
+ let writeDb = null;
6262
+ const getWriteDb = () => {
6263
+ if (!openRawWriteDb) {
6264
+ throw new Error("writes are not supported for this connection");
6265
+ }
6266
+ if (!writeDb)
6267
+ writeDb = wrapDbWithSqlCapture(openRawWriteDb());
6268
+ return writeDb;
6269
+ };
6049
6270
  const adapter = {
6050
6271
  kind: "sqlite",
6051
6272
  model: "sql",
@@ -6145,7 +6366,7 @@ function createSqliteAdapter(db) {
6145
6366
  return this.getTableRowCounts(tables);
6146
6367
  },
6147
6368
  getTablePage(table, options) {
6148
- const order = buildOrderClause2(options.orderBy);
6369
+ const order = buildOrderClause(options.orderBy);
6149
6370
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
6150
6371
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
6151
6372
  const cols = queryColumns(db, table);
@@ -6157,7 +6378,7 @@ function createSqliteAdapter(db) {
6157
6378
  async getTablePageWithMeta(table, options) {
6158
6379
  const columns = queryColumns(db, table);
6159
6380
  const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
6160
- const order = buildOrderClause2(orderBy);
6381
+ const order = buildOrderClause(orderBy);
6161
6382
  const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
6162
6383
  const rows = safePrepare(db, sql).all(options.limit, options.offset);
6163
6384
  const result = queryRowsToResult(rows, columns);
@@ -6173,7 +6394,7 @@ function createSqliteAdapter(db) {
6173
6394
  const columns = queryColumns(db, table);
6174
6395
  const columnNames = columns.map((column) => column.name);
6175
6396
  const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
6176
- const order = buildOrderClause2(filterOrderByColumns(options.orderBy, columnNames));
6397
+ const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
6177
6398
  const tableId = sanitizeIdentifier(table);
6178
6399
  const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
6179
6400
  const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
@@ -6241,7 +6462,36 @@ function createSqliteAdapter(db) {
6241
6462
  async getTriggersAsync(table) {
6242
6463
  return this.getTriggers(table);
6243
6464
  },
6465
+ async applyMutations(table, mutations) {
6466
+ const columns = queryColumns(db, table);
6467
+ if (columns.length === 0) {
6468
+ throw new Error(`unknown table: ${table}`);
6469
+ }
6470
+ const statements = buildMutationStatements(table, mutations, columns, "sqlite");
6471
+ const wdb = getWriteDb();
6472
+ let affected = 0;
6473
+ wdb.prepare("BEGIN").run();
6474
+ try {
6475
+ for (const stmt of statements) {
6476
+ const result = wdb.prepare(stmt.sql).run(...stmt.params);
6477
+ affected += result.changes ?? 0;
6478
+ }
6479
+ wdb.prepare("COMMIT").run();
6480
+ } catch (err) {
6481
+ try {
6482
+ wdb.prepare("ROLLBACK").run();
6483
+ } catch {}
6484
+ throw err;
6485
+ }
6486
+ return { affected };
6487
+ },
6244
6488
  close() {
6489
+ if (writeDb) {
6490
+ try {
6491
+ writeDb.close();
6492
+ } catch {}
6493
+ writeDb = null;
6494
+ }
6245
6495
  db.close();
6246
6496
  },
6247
6497
  async* iterateForSnapshot(table, signal) {
@@ -6279,14 +6529,18 @@ function createSqliteAdapter(db) {
6279
6529
  };
6280
6530
  return adapter;
6281
6531
  }
6282
- var cachedDbClass = null, sqliteAdapterFactory;
6532
+ var sqliteAdapterFactory;
6283
6533
  var init_sqlite = __esm(() => {
6534
+ init_mutate();
6284
6535
  init_sql_snapshot();
6536
+ init_sql_utils();
6537
+ init_sql_capture();
6285
6538
  sqliteAdapterFactory = {
6286
6539
  async open(path) {
6287
- const DbClass = await getSqliteClass();
6540
+ const DbClass = await loadSqliteClass();
6288
6541
  const db = new DbClass(path, { readonly: true, create: false });
6289
- return createSqliteAdapter(db);
6542
+ const openWriteDb = () => new DbClass(path);
6543
+ return createSqliteAdapter(db, openWriteDb);
6290
6544
  }
6291
6545
  };
6292
6546
  });
@@ -7055,6 +7309,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
7055
7309
  }
7056
7310
  var init_global_search = __esm(() => {
7057
7311
  init_serialize();
7312
+ init_sql_utils();
7058
7313
  });
7059
7314
 
7060
7315
  // web-src/server/database/adapters/elasticsearch.ts
@@ -7254,6 +7509,38 @@ function createElasticsearchAdapter(config) {
7254
7509
  primaryTerm: parsed._primary_term
7255
7510
  };
7256
7511
  }
7512
+ function assertIndex(index) {
7513
+ if (!index || index.includes("/") || index.includes("?")) {
7514
+ throw new Error(`invalid index name: ${index}`);
7515
+ }
7516
+ }
7517
+ async function writeDocAsync(opts) {
7518
+ assertIndex(opts.index);
7519
+ const idGiven = typeof opts.id === "string" && opts.id !== "";
7520
+ let path;
7521
+ let method;
7522
+ if (idGiven && opts.create) {
7523
+ path = `/${encodeURIComponent(opts.index)}/_create/${encodeURIComponent(opts.id)}`;
7524
+ method = "PUT";
7525
+ } else if (idGiven) {
7526
+ path = `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`;
7527
+ method = "PUT";
7528
+ if (opts.seqNo !== undefined && opts.primaryTerm !== undefined) {
7529
+ path += `?if_seq_no=${opts.seqNo}&if_primary_term=${opts.primaryTerm}`;
7530
+ }
7531
+ } else {
7532
+ path = `/${encodeURIComponent(opts.index)}/_doc`;
7533
+ method = "POST";
7534
+ }
7535
+ const resp = await callJsonAsync(method, path, opts.source, "_doc write", opts.signal);
7536
+ return { id: resp._id ?? opts.id ?? "", result: resp.result ?? "" };
7537
+ }
7538
+ async function deleteDocAsync(opts) {
7539
+ assertIndex(opts.index);
7540
+ if (!opts.id)
7541
+ throw new Error("missing doc id");
7542
+ await callJsonAsync("DELETE", `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`, undefined, "_doc delete", opts.signal);
7543
+ }
7257
7544
  async function* iterateForSnapshot(container, signal) {
7258
7545
  const { index, query: query2 } = parseEsSnapshotContainer(container);
7259
7546
  const PAGE = 1000;
@@ -7327,6 +7614,8 @@ function createElasticsearchAdapter(config) {
7327
7614
  getMappingAsync,
7328
7615
  searchDocsAsync,
7329
7616
  getDocAsync,
7617
+ writeDocAsync,
7618
+ deleteDocAsync,
7330
7619
  iterateForSnapshot,
7331
7620
  listSnapshotContainers,
7332
7621
  query,
@@ -7806,6 +8095,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
7806
8095
  return handleError("elasticsearch", "read elasticsearch mapping", err);
7807
8096
  }
7808
8097
  }
8098
+ async function handleWrite(req, cwd, omitDirNames) {
8099
+ const parsed = await parseBoundedJsonBody(req, 4 * 1024 * 1024, "payload too large");
8100
+ if (parsed instanceof Response)
8101
+ return parsed;
8102
+ const body = parsed;
8103
+ if (typeof body.db !== "string" || body.db === "") {
8104
+ return textError("missing db", 400);
8105
+ }
8106
+ if (typeof body.index !== "string" || body.index === "") {
8107
+ return textError("missing index", 400);
8108
+ }
8109
+ const id = typeof body.id === "string" ? body.id : undefined;
8110
+ const r = await resolveEs(cwd, body.db, req.signal, omitDirNames);
8111
+ if (r instanceof Response)
8112
+ return r;
8113
+ try {
8114
+ if (body.op === "delete") {
8115
+ if (!id)
8116
+ return textError("missing id", 400);
8117
+ await r.explorer.deleteDocAsync({
8118
+ index: body.index,
8119
+ id,
8120
+ signal: req.signal
8121
+ });
8122
+ return json({ ok: true });
8123
+ }
8124
+ if (body.source === null || typeof body.source !== "object" || Array.isArray(body.source)) {
8125
+ return textError("source must be a JSON object", 400);
8126
+ }
8127
+ const seqNo = typeof body.seqNo === "number" ? body.seqNo : undefined;
8128
+ const primaryTerm = typeof body.primaryTerm === "number" ? body.primaryTerm : undefined;
8129
+ const result = await r.explorer.writeDocAsync({
8130
+ index: body.index,
8131
+ id,
8132
+ source: body.source,
8133
+ seqNo,
8134
+ primaryTerm,
8135
+ create: body.op === "create",
8136
+ signal: req.signal
8137
+ });
8138
+ return json({ ok: true, id: result.id, result: result.result });
8139
+ } catch (err) {
8140
+ return handleError("elasticsearch", "write elasticsearch doc", err);
8141
+ }
8142
+ }
7809
8143
  async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
7810
8144
  const wrap = createQueryStrippedLogger("elasticsearch", req, url);
7811
8145
  return dispatchRoutes(req, url, {
@@ -7825,6 +8159,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
7825
8159
  methods: ["GET"],
7826
8160
  handler: () => handleDoc(req, cwd, url, omitDirNames)
7827
8161
  },
8162
+ "/_db/elasticsearch/write": {
8163
+ methods: ["POST"],
8164
+ sideEffect: true,
8165
+ handler: () => handleWrite(req, cwd, omitDirNames)
8166
+ },
7828
8167
  "/_db/elasticsearch/search": {
7829
8168
  methods: ["GET", "POST"],
7830
8169
  sideEffect: (m) => m === "POST",
@@ -7843,6 +8182,7 @@ var init_handle_elasticsearch = __esm(() => {
7843
8182
  var exports_redis = {};
7844
8183
  __export(exports_redis, {
7845
8184
  openRedisExplorerAsync: () => openRedisExplorerAsync,
8185
+ createRedisAdapter: () => createRedisAdapter,
7846
8186
  canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
7847
8187
  });
7848
8188
  import { createHash as createHash3 } from "node:crypto";
@@ -8364,6 +8704,41 @@ function createRedisAdapter(config) {
8364
8704
  async function listSnapshotContainers() {
8365
8705
  return [];
8366
8706
  }
8707
+ async function runWriteAsync(args, signal) {
8708
+ const res = await execRedisCliAsync(config, args, 1e4, signal);
8709
+ if (res.code !== 0) {
8710
+ throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
8711
+ }
8712
+ const out = res.stdout.trim();
8713
+ if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
8714
+ throw new Error(out);
8715
+ }
8716
+ }
8717
+ async function setStringAsync(opts) {
8718
+ await runWriteAsync(["-n", String(opts.db), "SET", opts.key, opts.value], opts.signal);
8719
+ }
8720
+ async function createStringAsync(opts) {
8721
+ const res = await execRedisCliAsync(config, ["-n", String(opts.db), "SET", opts.key, opts.value, "NX"], 1e4, opts.signal);
8722
+ if (res.code !== 0) {
8723
+ throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
8724
+ }
8725
+ const out = res.stdout.trim();
8726
+ if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
8727
+ throw new Error(out);
8728
+ }
8729
+ if (!/\bOK\b/i.test(out)) {
8730
+ throw new Error(`key already exists: ${opts.key}`);
8731
+ }
8732
+ }
8733
+ async function setHashFieldAsync(opts) {
8734
+ await runWriteAsync(["-n", String(opts.db), "HSET", opts.key, opts.field, opts.value], opts.signal);
8735
+ }
8736
+ async function setListIndexAsync(opts) {
8737
+ await runWriteAsync(["-n", String(opts.db), "LSET", opts.key, String(opts.index), opts.value], opts.signal);
8738
+ }
8739
+ async function deleteKeyAsync(opts) {
8740
+ await runWriteAsync(["-n", String(opts.db), "DEL", opts.key], opts.signal);
8741
+ }
8367
8742
  return {
8368
8743
  kind: "redis",
8369
8744
  model: "kv",
@@ -8371,6 +8746,11 @@ function createRedisAdapter(config) {
8371
8746
  listDatabasesAsync,
8372
8747
  listKeysAsync,
8373
8748
  getValueAsync,
8749
+ setStringAsync,
8750
+ createStringAsync,
8751
+ setHashFieldAsync,
8752
+ setListIndexAsync,
8753
+ deleteKeyAsync,
8374
8754
  iterateForSnapshot,
8375
8755
  listSnapshotContainers,
8376
8756
  close() {}
@@ -8447,6 +8827,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
8447
8827
  return handleError("redis", "list redis keys", err);
8448
8828
  }
8449
8829
  }
8830
+ async function handleWrite2(req, cwd, omitDirNames) {
8831
+ const parsed = await parseBoundedJsonBody(req, 1024 * 1024, "payload too large");
8832
+ if (parsed instanceof Response)
8833
+ return parsed;
8834
+ const body = parsed;
8835
+ if (typeof body.db !== "string" || body.db === "") {
8836
+ return textError("missing db", 400);
8837
+ }
8838
+ const dbIndex = Number(body.dbIndex);
8839
+ if (!Number.isInteger(dbIndex) || dbIndex < 0 || dbIndex > 15) {
8840
+ return textError("dbIndex must be an integer in 0..15", 400);
8841
+ }
8842
+ if (typeof body.key !== "string" || body.key === "") {
8843
+ return textError("missing key", 400);
8844
+ }
8845
+ const op = body.op;
8846
+ const value = typeof body.value === "string" ? body.value : "";
8847
+ const r = await resolveRedis(cwd, body.db, req.signal, omitDirNames);
8848
+ if (r instanceof Response)
8849
+ return r;
8850
+ try {
8851
+ if (op === "setString") {
8852
+ await r.explorer.setStringAsync({
8853
+ db: dbIndex,
8854
+ key: body.key,
8855
+ value,
8856
+ signal: req.signal
8857
+ });
8858
+ } else if (op === "createString") {
8859
+ await r.explorer.createStringAsync({
8860
+ db: dbIndex,
8861
+ key: body.key,
8862
+ value,
8863
+ signal: req.signal
8864
+ });
8865
+ } else if (op === "setHashField") {
8866
+ if (typeof body.field !== "string" || body.field === "") {
8867
+ return textError("missing field", 400);
8868
+ }
8869
+ await r.explorer.setHashFieldAsync({
8870
+ db: dbIndex,
8871
+ key: body.key,
8872
+ field: body.field,
8873
+ value,
8874
+ signal: req.signal
8875
+ });
8876
+ } else if (op === "setListIndex") {
8877
+ const index = Number(body.index);
8878
+ if (!Number.isInteger(index) || index < 0) {
8879
+ return textError("index must be a non-negative integer", 400);
8880
+ }
8881
+ await r.explorer.setListIndexAsync({
8882
+ db: dbIndex,
8883
+ key: body.key,
8884
+ index,
8885
+ value,
8886
+ signal: req.signal
8887
+ });
8888
+ } else if (op === "delete") {
8889
+ await r.explorer.deleteKeyAsync({
8890
+ db: dbIndex,
8891
+ key: body.key,
8892
+ signal: req.signal
8893
+ });
8894
+ } else {
8895
+ return textError(`unknown op: ${String(op)}`, 400);
8896
+ }
8897
+ return json({ ok: true });
8898
+ } catch (err) {
8899
+ return handleError("redis", "write redis value", err);
8900
+ }
8901
+ }
8450
8902
  async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
8451
8903
  const wrap = createQueryStrippedLogger("redis", req, url);
8452
8904
  return dispatchRoutes(req, url, {
@@ -8461,6 +8913,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
8461
8913
  "/_db/redis/value": {
8462
8914
  methods: ["GET"],
8463
8915
  handler: () => handleValue(req, cwd, url, omitDirNames)
8916
+ },
8917
+ "/_db/redis/write": {
8918
+ methods: ["POST"],
8919
+ sideEffect: true,
8920
+ handler: () => handleWrite2(req, cwd, omitDirNames)
8464
8921
  }
8465
8922
  }, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
8466
8923
  }
@@ -8538,6 +8995,9 @@ function hmac(key, value) {
8538
8995
  function sha256(value) {
8539
8996
  return createHash4("sha256").update(value, "utf8").digest("hex");
8540
8997
  }
8998
+ function sha256Bytes(value) {
8999
+ return createHash4("sha256").update(value).digest("hex");
9000
+ }
8541
9001
  function encodeRfc3986(value) {
8542
9002
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
8543
9003
  }
@@ -8907,9 +9367,10 @@ function createS3Adapter(config) {
8907
9367
  const path = buildPath(opts.bucket, opts.key);
8908
9368
  const query = canonicalQuery(opts.query);
8909
9369
  const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
9370
+ const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
8910
9371
  const headers = {
8911
9372
  host: endpoint.host,
8912
- "x-amz-content-sha256": EMPTY_SHA256,
9373
+ "x-amz-content-sha256": payloadHash,
8913
9374
  "x-amz-date": requestDate,
8914
9375
  ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
8915
9376
  ...opts.headers || {}
@@ -8921,7 +9382,7 @@ function createS3Adapter(config) {
8921
9382
  query,
8922
9383
  canonicalHeaders(headers),
8923
9384
  signedNames,
8924
- EMPTY_SHA256
9385
+ payloadHash
8925
9386
  ].join(`
8926
9387
  `);
8927
9388
  const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
@@ -8940,6 +9401,9 @@ function createS3Adapter(config) {
8940
9401
  }
8941
9402
  requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
8942
9403
  if (config.dockerContainerName) {
9404
+ if (opts.body !== undefined) {
9405
+ throw new S3HttpError(503, "S3 object writes require a published host port (docker-exec transport cannot stream a request body)");
9406
+ }
8943
9407
  if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
8944
9408
  throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8945
9409
  }
@@ -8954,6 +9418,7 @@ function createS3Adapter(config) {
8954
9418
  return fetch(url, {
8955
9419
  method: opts.method,
8956
9420
  headers: requestHeaders,
9421
+ ...opts.body !== undefined ? { body: opts.body } : {},
8957
9422
  signal: transportSignal
8958
9423
  });
8959
9424
  }, deadline);
@@ -9036,6 +9501,32 @@ function createS3Adapter(config) {
9036
9501
  headers: rawObjectHeaders(opts.key, res)
9037
9502
  });
9038
9503
  }
9504
+ async function putObjectAsync(opts) {
9505
+ const deadline = createS3TransportDeadline(config);
9506
+ const res = await signedFetch({
9507
+ method: "PUT",
9508
+ bucket: opts.bucket,
9509
+ key: opts.key,
9510
+ body: opts.body,
9511
+ headers: opts.contentType ? { "content-type": opts.contentType } : undefined,
9512
+ signal: opts.signal
9513
+ }, deadline);
9514
+ if (!res.ok) {
9515
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
9516
+ }
9517
+ }
9518
+ async function deleteObjectAsync(opts) {
9519
+ const deadline = createS3TransportDeadline(config);
9520
+ const res = await signedFetch({
9521
+ method: "DELETE",
9522
+ bucket: opts.bucket,
9523
+ key: opts.key,
9524
+ signal: opts.signal
9525
+ }, deadline);
9526
+ if (!res.ok) {
9527
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
9528
+ }
9529
+ }
9039
9530
  return {
9040
9531
  kind: "s3",
9041
9532
  model: "object",
@@ -9044,7 +9535,9 @@ function createS3Adapter(config) {
9044
9535
  listObjects,
9045
9536
  headObject,
9046
9537
  getObjectText,
9047
- getObjectResponse
9538
+ getObjectResponse,
9539
+ putObjectAsync,
9540
+ deleteObjectAsync
9048
9541
  };
9049
9542
  }
9050
9543
  async function s3ConfigFromDockerInfoAsync(info, signal) {
@@ -9447,6 +9940,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
9447
9940
  return s3ErrorResponse(err, "stream s3 object");
9448
9941
  }
9449
9942
  }
9943
+ async function handleWrite3(req, cwd, omitDirNames) {
9944
+ const parsed = await parseBoundedJsonBody(req, 8 * 1024 * 1024, "payload too large");
9945
+ if (parsed instanceof Response)
9946
+ return parsed;
9947
+ const body = parsed;
9948
+ if (typeof body.db !== "string" || body.db === "") {
9949
+ return textError("missing db", 400);
9950
+ }
9951
+ const bucket = validateBucket(typeof body.bucket === "string" ? body.bucket : null);
9952
+ if (bucket instanceof Response)
9953
+ return bucket;
9954
+ const key = validateKey(typeof body.key === "string" ? body.key : null);
9955
+ if (key instanceof Response)
9956
+ return key;
9957
+ const r = await resolveS3(cwd, body.db, req.signal, omitDirNames);
9958
+ if (r instanceof Response)
9959
+ return r;
9960
+ try {
9961
+ if (body.op === "delete") {
9962
+ await r.explorer.deleteObjectAsync({
9963
+ bucket,
9964
+ key,
9965
+ signal: req.signal
9966
+ });
9967
+ return json({ ok: true });
9968
+ }
9969
+ if (body.op === "create") {
9970
+ let exists = false;
9971
+ try {
9972
+ await r.explorer.headObject({ bucket, key, signal: req.signal });
9973
+ exists = true;
9974
+ } catch (err) {
9975
+ if (isS3HttpError(err) && err.status === 404)
9976
+ exists = false;
9977
+ else
9978
+ throw err;
9979
+ }
9980
+ if (exists)
9981
+ return textError(`object already exists: ${key}`, 409);
9982
+ }
9983
+ const content = typeof body.content === "string" ? body.content : "";
9984
+ const contentType = typeof body.contentType === "string" && body.contentType ? body.contentType : "application/octet-stream";
9985
+ await r.explorer.putObjectAsync({
9986
+ bucket,
9987
+ key,
9988
+ body: new TextEncoder().encode(content),
9989
+ contentType,
9990
+ signal: req.signal
9991
+ });
9992
+ return json({ ok: true });
9993
+ } catch (err) {
9994
+ return handleError("s3", "write s3 object", err);
9995
+ }
9996
+ }
9450
9997
  async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9451
9998
  const wrap = createQueryStrippedLogger("s3", req, url);
9452
9999
  return dispatchRoutes(req, url, {
@@ -9473,6 +10020,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
9473
10020
  "/_db/s3/raw": {
9474
10021
  methods: ["GET", "HEAD"],
9475
10022
  handler: () => handleRaw(cwd, req, url, omitDirNames)
10023
+ },
10024
+ "/_db/s3/write": {
10025
+ methods: ["POST"],
10026
+ sideEffect: true,
10027
+ handler: () => handleWrite3(req, cwd, omitDirNames)
9476
10028
  }
9477
10029
  }, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
9478
10030
  }
@@ -9639,21 +10191,6 @@ var init_query_history = __esm(() => {
9639
10191
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
9640
10192
  import { mkdirSync as mkdirSync3 } from "node:fs";
9641
10193
  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
10194
  async function getStoreDb(cwd) {
9658
10195
  const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
9659
10196
  if (storeDb && storeDbPath === dbPath)
@@ -9664,7 +10201,7 @@ async function getStoreDb(cwd) {
9664
10201
  } catch {}
9665
10202
  }
9666
10203
  mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
9667
- const DbClass = await getSqliteClass2();
10204
+ const DbClass = await loadSqliteClass();
9668
10205
  storeDb = new DbClass(dbPath);
9669
10206
  storeDbPath = dbPath;
9670
10207
  storeDb.exec("PRAGMA journal_mode=WAL");
@@ -9923,7 +10460,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
9923
10460
  });
9924
10461
  return { rows, total };
9925
10462
  }
9926
- var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
10463
+ var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", SCHEMA_SQL = `
9927
10464
  CREATE TABLE IF NOT EXISTS snapshots (
9928
10465
  id TEXT PRIMARY KEY,
9929
10466
  db_id TEXT NOT NULL,
@@ -10153,6 +10690,9 @@ function sanitize(input) {
10153
10690
  const historyHeight = sanitizeCssSize(tab.historyHeight);
10154
10691
  if (historyHeight !== undefined)
10155
10692
  out.historyHeight = historyHeight;
10693
+ if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
10694
+ out.activeHistoryTab = tab.activeHistoryTab;
10695
+ }
10156
10696
  const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
10157
10697
  if (sidebarWidth !== undefined)
10158
10698
  out.sidebarWidth = sidebarWidth;
@@ -10343,11 +10883,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
10343
10883
  const body2 = { dbId: r.dbId, schemas: [] };
10344
10884
  return json(body2);
10345
10885
  }
10346
- const schemas = await listDockerSchemasAsync(r.docker.serviceName, "postgresql", r.docker.env, r.docker.composeDir, r.docker.database, signal);
10886
+ const docker = r.docker;
10887
+ const { result: schemas, executedSql } = await captureSql(() => listDockerSchemasAsync(docker.serviceName, "postgresql", docker.env, docker.composeDir, docker.database, signal));
10347
10888
  const body = {
10348
10889
  dbId: r.dbId,
10349
10890
  schemas: schemas.map((name) => ({ name })),
10350
- selectedSchema: r.schema
10891
+ selectedSchema: r.schema,
10892
+ executedSql
10351
10893
  };
10352
10894
  return json(body);
10353
10895
  }
@@ -10359,37 +10901,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
10359
10901
  const linkedAbort = createLinkedAbortController(signal);
10360
10902
  try {
10361
10903
  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;
10904
+ const { result, executedSql } = await captureSql(async () => {
10905
+ const db = asAsync(adapter);
10906
+ const tables = await db.tables(linkedAbort.signal);
10907
+ const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
10908
+ const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
10909
+ const indexesPromise = db.indexes(linkedAbort.signal);
10910
+ const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
10911
+ const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
10912
+ const schemaPromises = [
10913
+ countMapPromise,
10914
+ indexesPromise,
10915
+ foreignKeysPromise,
10916
+ columnsMapPromise
10917
+ ];
10918
+ const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
10919
+ linkedAbort.abort();
10920
+ await Promise.allSettled(schemaPromises);
10921
+ throw err;
10922
+ });
10923
+ return { tables, countMap, indexes, foreignKeys, colsMap };
10379
10924
  });
10380
- const tablesWithCount = tables.map((t) => ({
10925
+ const tablesWithCount = result.tables.map((t) => ({
10381
10926
  ...t,
10382
- rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
10927
+ rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
10383
10928
  }));
10384
10929
  const body = {
10385
10930
  dbId: r.dbId,
10386
10931
  ...r.schema ? { schema: r.schema } : {},
10387
10932
  tables: tablesWithCount,
10388
- indexes,
10389
- foreignKeys
10933
+ indexes: result.indexes,
10934
+ foreignKeys: result.foreignKeys,
10935
+ executedSql
10390
10936
  };
10391
- if (colsMap) {
10392
- body.columnsMap = Object.fromEntries(colsMap);
10937
+ if (result.colsMap) {
10938
+ body.columnsMap = Object.fromEntries(result.colsMap);
10393
10939
  }
10394
10940
  return json(body);
10395
10941
  } catch (err) {
@@ -10451,36 +10997,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10451
10997
  const exact = parseExactConditions(url);
10452
10998
  try {
10453
10999
  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, {
11000
+ const { result: meta, executedSql } = await captureSql(() => filters.length > 0 || exact.length > 0 ? adapter.getFilteredTablePageWithMeta(table, {
11001
+ offset,
11002
+ limit,
11003
+ orderBy,
11004
+ grouped: groupFiltersByValue(filters),
11005
+ ...exact.length > 0 ? { exact } : {}
11006
+ }, signal) : adapter.getTablePageWithMeta(table, {
10480
11007
  offset,
10481
11008
  limit,
10482
11009
  orderBy
10483
- }, signal);
11010
+ }, signal));
10484
11011
  const colNames = new Set(meta.columns.map((c) => c.name));
10485
11012
  if (sortCol && !colNames.has(sortCol)) {
10486
11013
  return textError(`invalid sort column: ${sortCol}`, 400);
@@ -10494,7 +11021,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
10494
11021
  totalRows: meta.totalRows,
10495
11022
  offset,
10496
11023
  limit,
10497
- hasMore: offset + meta.rowCount < meta.totalRows
11024
+ hasMore: offset + meta.rowCount < meta.totalRows,
11025
+ executedSql
10498
11026
  };
10499
11027
  return json(body);
10500
11028
  } catch (err) {
@@ -10571,8 +11099,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10571
11099
  const start = Date.now();
10572
11100
  try {
10573
11101
  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);
11102
+ const { result, executedSql } = await captureSql(() => asAsync(adapter).readonlyQuery(body.sql ?? "", undefined, maxRows, req.signal));
10576
11103
  const elapsed = Date.now() - start;
10577
11104
  const serializedRows = serializeDbRows(result.rows);
10578
11105
  const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
@@ -10586,7 +11113,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
10586
11113
  rows: serializedRows,
10587
11114
  rowCount: result.rowCount,
10588
11115
  truncated: result.rowCount >= maxRows,
10589
- elapsedMs: elapsed
11116
+ elapsedMs: elapsed,
11117
+ executedSql
10590
11118
  };
10591
11119
  if (body.saveHistory) {
10592
11120
  const entry = {
@@ -10805,13 +11333,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
10805
11333
  return textError("missing table parameter", 400);
10806
11334
  try {
10807
11335
  const adapter = await getAdapter(r, cwd, signal);
10808
- const db = asAsync(adapter);
10809
- const columns = await db.columns(table, signal);
11336
+ const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
10810
11337
  return json({
10811
11338
  dbId: r.dbId,
10812
11339
  ...r.schema ? { schema: r.schema } : {},
10813
11340
  table,
10814
- columns
11341
+ columns,
11342
+ executedSql
10815
11343
  });
10816
11344
  } catch (err) {
10817
11345
  return handleError("database", "get columns", err);
@@ -10826,17 +11354,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
10826
11354
  return textError("missing table parameter", 400);
10827
11355
  try {
10828
11356
  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
- ]);
11357
+ const { result, executedSql } = await captureSql(async () => {
11358
+ const db = asAsync(adapter);
11359
+ const [sql, triggers] = await Promise.all([
11360
+ db.createStatement(table, signal),
11361
+ db.triggers(table, signal)
11362
+ ]);
11363
+ return { sql, triggers };
11364
+ });
10834
11365
  return json({
10835
11366
  dbId: r.dbId,
10836
11367
  ...r.schema ? { schema: r.schema } : {},
10837
11368
  table,
10838
- sql,
10839
- triggers
11369
+ sql: result.sql,
11370
+ triggers: result.triggers,
11371
+ executedSql
10840
11372
  });
10841
11373
  } catch (err) {
10842
11374
  return handleError("database", "get DDL", err);
@@ -11248,6 +11780,48 @@ async function handleClose(cwd, req, omitDirNames) {
11248
11780
  }
11249
11781
  return json({ ok: true });
11250
11782
  }
11783
+ async function handleMutate(cwd, req, omitDirNames) {
11784
+ const parsed = await parseBoundedJsonBody(req, 1048576, "payload too large");
11785
+ if (parsed instanceof Response)
11786
+ return parsed;
11787
+ const body = parsed;
11788
+ if (typeof body.db !== "string" || body.db === "") {
11789
+ return textError("missing db", 400);
11790
+ }
11791
+ if (typeof body.table !== "string" || body.table === "") {
11792
+ return textError("missing table", 400);
11793
+ }
11794
+ if (!Array.isArray(body.mutations) || body.mutations.length === 0) {
11795
+ return textError("missing mutations", 400);
11796
+ }
11797
+ const schemaParam = typeof body.schema === "string" ? body.schema : undefined;
11798
+ const r = await resolveDb(cwd, body.db, omitDirNames, schemaParam, req.signal);
11799
+ if (r instanceof Response)
11800
+ return r;
11801
+ const adapter = await getAdapter(r, cwd, req.signal);
11802
+ if (!adapter.applyMutations) {
11803
+ return textError("writes are not supported for this datastore", 400);
11804
+ }
11805
+ try {
11806
+ const tableName = body.table;
11807
+ const { result, executedSql } = await captureSql(() => adapter.applyMutations?.(tableName, body.mutations, req.signal) ?? Promise.reject(new Error("applyMutations not supported")));
11808
+ adapter.invalidateTableMetaCache?.(tableName);
11809
+ const response = {
11810
+ dbId: r.dbId,
11811
+ ...r.schema ? { schema: r.schema } : {},
11812
+ table: tableName,
11813
+ affected: result.affected,
11814
+ executedSql
11815
+ };
11816
+ return json(response);
11817
+ } catch (err) {
11818
+ if (isAbortLikeError(err, req.signal)) {
11819
+ return textError("mutation aborted", 503);
11820
+ }
11821
+ const message = err instanceof Error ? err.message : String(err);
11822
+ return textError(message, 400);
11823
+ }
11824
+ }
11251
11825
  async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
11252
11826
  ensureInit();
11253
11827
  if (url.pathname.startsWith("/_db/redis/")) {
@@ -11308,6 +11882,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
11308
11882
  sideEffect: true,
11309
11883
  handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
11310
11884
  },
11885
+ "/_db/mutate": {
11886
+ methods: ["POST"],
11887
+ sideEffect: true,
11888
+ handler: () => handleMutate(cwd, req, omitDirNames)
11889
+ },
11311
11890
  "/_db/close": {
11312
11891
  methods: ["POST"],
11313
11892
  sideEffect: true,
@@ -11390,6 +11969,7 @@ var init_handle = __esm(() => {
11390
11969
  init_state_store();
11391
11970
  init_docker();
11392
11971
  init_docker_utils();
11972
+ init_sql_capture();
11393
11973
  init_sqlite();
11394
11974
  init_connection_pool();
11395
11975
  init_discovery();