sqlite-hub 0.3.2 → 0.5.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.
Files changed (75) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +14 -0
  3. package/{js → frontend/js}/api.js +99 -5
  4. package/{js → frontend/js}/app.js +162 -11
  5. package/{js → frontend/js}/components/connectionCard.js +8 -9
  6. package/frontend/js/components/connectionLogo.js +33 -0
  7. package/{js → frontend/js}/components/dataGrid.js +3 -3
  8. package/{js → frontend/js}/components/emptyState.js +25 -11
  9. package/{js → frontend/js}/components/modal.js +57 -0
  10. package/{js → frontend/js}/components/queryEditor.js +33 -55
  11. package/frontend/js/components/queryHistoryDetail.js +263 -0
  12. package/frontend/js/components/queryHistoryPanel.js +228 -0
  13. package/{js → frontend/js}/components/queryResults.js +32 -46
  14. package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
  15. package/{js → frontend/js}/components/sidebar.js +8 -3
  16. package/{js → frontend/js}/store.js +577 -21
  17. package/{js → frontend/js}/utils/format.js +23 -0
  18. package/{js → frontend/js}/views/data.js +136 -10
  19. package/{js → frontend/js}/views/editor.js +34 -10
  20. package/{js → frontend/js}/views/overview.js +15 -0
  21. package/{js → frontend/js}/views/structure.js +10 -12
  22. package/{styles → frontend/styles}/components.css +106 -0
  23. package/{styles → frontend/styles}/structure-graph.css +5 -10
  24. package/package.json +2 -2
  25. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  26. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  27. package/server/data/db_logos/.gitkeep +0 -0
  28. package/server/routes/connections.js +2 -0
  29. package/server/routes/data.js +2 -0
  30. package/server/routes/export.js +4 -1
  31. package/server/routes/overview.js +12 -0
  32. package/server/routes/sql.js +163 -5
  33. package/server/server.js +8 -6
  34. package/server/services/sqlite/connectionManager.js +68 -33
  35. package/server/services/sqlite/dataBrowserService.js +4 -16
  36. package/server/services/sqlite/exportService.js +4 -16
  37. package/server/services/sqlite/overviewService.js +34 -0
  38. package/server/services/sqlite/sqlExecutor.js +83 -63
  39. package/server/services/sqlite/tableSort.js +63 -0
  40. package/server/services/storage/appStateStore.js +832 -20
  41. package/server/services/storage/queryHistoryUtils.js +169 -0
  42. package/server/utils/appPaths.js +42 -18
  43. package/{assets → frontend/assets}/images/logo.webp +0 -0
  44. package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  45. package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  46. package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  47. package/{assets → frontend/assets}/mockups/connections.png +0 -0
  48. package/{assets → frontend/assets}/mockups/data.png +0 -0
  49. package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  50. package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  51. package/{assets → frontend/assets}/mockups/home.png +0 -0
  52. package/{assets → frontend/assets}/mockups/overview.png +0 -0
  53. package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  54. package/{assets → frontend/assets}/mockups/structure.png +0 -0
  55. package/{index.html → frontend/index.html} +0 -0
  56. package/{js → frontend/js}/components/actionBar.js +0 -0
  57. package/{js → frontend/js}/components/appShell.js +0 -0
  58. package/{js → frontend/js}/components/badges.js +0 -0
  59. package/{js → frontend/js}/components/bottomTabs.js +1 -1
  60. /package/{js → frontend/js}/components/metricCard.js +0 -0
  61. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  62. /package/{js → frontend/js}/components/statusBar.js +0 -0
  63. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  64. /package/{js → frontend/js}/components/toast.js +0 -0
  65. /package/{js → frontend/js}/components/topNav.js +0 -0
  66. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  67. /package/{js → frontend/js}/router.js +0 -0
  68. /package/{js → frontend/js}/views/connections.js +0 -0
  69. /package/{js → frontend/js}/views/landing.js +0 -0
  70. /package/{js → frontend/js}/views/settings.js +0 -0
  71. /package/{styles → frontend/styles}/base.css +0 -0
  72. /package/{styles → frontend/styles}/layout.css +0 -0
  73. /package/{styles → frontend/styles}/tokens.css +0 -0
  74. /package/{styles → frontend/styles}/views.css +0 -0
  75. /package/{data → server/data}/.gitkeep +0 -0
@@ -8,12 +8,17 @@ const DEFAULT_SETTINGS = {
8
8
  csvDelimiter: ",",
9
9
  };
10
10
  const DATA_PAGE_SIZES = [25, 50, 100];
11
+ const QUERY_HISTORY_PAGE_SIZE = 30;
12
+ const QUERY_HISTORY_RUN_LIMIT = 8;
11
13
  const MISSING_DATABASE_ERROR = {
12
14
  code: "ACTIVE_DATABASE_REQUIRED",
13
15
  message: "No active SQLite database selected.",
14
16
  };
15
17
 
16
18
  let routeLoadVersion = 0;
19
+ let queryHistoryLoadVersion = 0;
20
+ let queryHistoryDetailLoadVersion = 0;
21
+ let queryHistorySearchTimer = null;
17
22
 
18
23
  const state = {
19
24
  ready: false,
@@ -48,6 +53,10 @@ const state = {
48
53
  deleting: false,
49
54
  page: 1,
50
55
  pageSize: 50,
56
+ sortColumn: null,
57
+ sortDirection: null,
58
+ searchQuery: "",
59
+ searchColumn: "",
51
60
  selectedRowIndex: null,
52
61
  exportLoading: false,
53
62
  error: null,
@@ -57,10 +66,25 @@ const state = {
57
66
  sqlText: "",
58
67
  history: [],
59
68
  historyLoading: false,
69
+ historyLoadingMore: false,
60
70
  historyError: null,
71
+ historyTab: "recent",
72
+ historySearchInput: "",
73
+ historySearch: "",
74
+ historyPageSize: QUERY_HISTORY_PAGE_SIZE,
75
+ historyTotal: 0,
76
+ historyHasMore: false,
77
+ historyActiveId: null,
78
+ historySelectedId: null,
79
+ historyDetail: null,
80
+ historyRuns: [],
81
+ historyDetailLoading: false,
82
+ historyDetailError: null,
61
83
  activeTab: "messages",
62
84
  executing: false,
63
85
  result: null,
86
+ resultSortColumn: null,
87
+ resultSortDirection: null,
64
88
  error: null,
65
89
  exportLoading: false,
66
90
  selectedRowIndex: null,
@@ -100,6 +124,26 @@ function normalizeError(error) {
100
124
  };
101
125
  }
102
126
 
127
+ function setActiveQueryHistoryItem(historyId) {
128
+ const normalizedId = Number(historyId);
129
+
130
+ if (!Number.isInteger(normalizedId) || normalizedId < 1) {
131
+ return null;
132
+ }
133
+
134
+ state.editor.historyActiveId = normalizedId;
135
+ return normalizedId;
136
+ }
137
+
138
+ function clearQueryHistoryDetailState() {
139
+ queryHistoryDetailLoadVersion += 1;
140
+ state.editor.historySelectedId = null;
141
+ state.editor.historyDetail = null;
142
+ state.editor.historyRuns = [];
143
+ state.editor.historyDetailLoading = false;
144
+ state.editor.historyDetailError = null;
145
+ }
146
+
103
147
  function requiresActiveDatabase(routeName) {
104
148
  return ["overview", "data", "editor", "editorResults", "structure"].includes(routeName);
105
149
  }
@@ -114,10 +158,94 @@ function normalizeDataPageSize(value, fallback = 50) {
114
158
  return fallback;
115
159
  }
116
160
 
161
+ function normalizeSortDirection(value) {
162
+ return String(value ?? "").trim().toLowerCase() === "desc" ? "desc" : "asc";
163
+ }
164
+
165
+ function getNextSortDirection(currentColumn, currentDirection, nextColumn) {
166
+ if (currentColumn === nextColumn) {
167
+ return normalizeSortDirection(currentDirection) === "asc" ? "desc" : "asc";
168
+ }
169
+
170
+ return "asc";
171
+ }
172
+
117
173
  function canEditQueryResult(snapshot = state) {
118
174
  return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
119
175
  }
120
176
 
177
+ function resetDataBrowserSearch() {
178
+ state.dataBrowser.searchQuery = "";
179
+ state.dataBrowser.searchColumn = "";
180
+ }
181
+
182
+ function resetDataBrowserSort() {
183
+ state.dataBrowser.sortColumn = null;
184
+ state.dataBrowser.sortDirection = null;
185
+ }
186
+
187
+ function resetEditorResultSort() {
188
+ state.editor.resultSortColumn = null;
189
+ state.editor.resultSortDirection = null;
190
+ }
191
+
192
+ function getSortableEditorValue(value) {
193
+ if (value === null || value === undefined) {
194
+ return { rank: 0, value: "" };
195
+ }
196
+
197
+ if (typeof value === "number") {
198
+ return { rank: 1, value };
199
+ }
200
+
201
+ if (typeof value === "boolean") {
202
+ return { rank: 2, value: value ? 1 : 0 };
203
+ }
204
+
205
+ if (typeof value === "string") {
206
+ return { rank: 3, value };
207
+ }
208
+
209
+ if (value && typeof value === "object" && value.__type === "blob") {
210
+ return {
211
+ rank: 4,
212
+ value: `${value.sizeBytes ?? 0}:${value.hexPreview ?? ""}`,
213
+ };
214
+ }
215
+
216
+ return { rank: 5, value: JSON.stringify(value) };
217
+ }
218
+
219
+ function compareEditorValues(left, right) {
220
+ const leftValue = getSortableEditorValue(left);
221
+ const rightValue = getSortableEditorValue(right);
222
+
223
+ if (leftValue.rank !== rightValue.rank) {
224
+ return leftValue.rank - rightValue.rank;
225
+ }
226
+
227
+ if (typeof leftValue.value === "number" && typeof rightValue.value === "number") {
228
+ return leftValue.value - rightValue.value;
229
+ }
230
+
231
+ return String(leftValue.value).localeCompare(String(rightValue.value), undefined, {
232
+ numeric: true,
233
+ sensitivity: "base",
234
+ });
235
+ }
236
+
237
+ function sortEditorResultRows(rows, sortColumn, sortDirection) {
238
+ if (!sortColumn) {
239
+ return [...(rows ?? [])];
240
+ }
241
+
242
+ const directionMultiplier = normalizeSortDirection(sortDirection) === "desc" ? -1 : 1;
243
+
244
+ return [...(rows ?? [])].sort(
245
+ (left, right) => compareEditorValues(left?.[sortColumn], right?.[sortColumn]) * directionMultiplier
246
+ );
247
+ }
248
+
121
249
  function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
122
250
  const nextRow = {
123
251
  ...existingRow,
@@ -168,6 +296,64 @@ function buildFallbackDeleteRowPreview(row) {
168
296
  );
169
297
  }
170
298
 
299
+ function normalizeQueryHistoryTab(value) {
300
+ return ["recent", "saved", "failed"].includes(value) ? value : "recent";
301
+ }
302
+
303
+ function findQueryHistoryItem(historyId, snapshot = state) {
304
+ return snapshot.editor.history.find((entry) => String(entry.id) === String(historyId)) ?? null;
305
+ }
306
+
307
+ function clearQueryHistorySearchTimer() {
308
+ if (!queryHistorySearchTimer) {
309
+ return;
310
+ }
311
+
312
+ window.clearTimeout(queryHistorySearchTimer);
313
+ queryHistorySearchTimer = null;
314
+ }
315
+
316
+ function resetQueryHistoryState({ preserveSearch = true } = {}) {
317
+ state.editor.history = [];
318
+ state.editor.historyLoading = false;
319
+ state.editor.historyLoadingMore = false;
320
+ state.editor.historyError = null;
321
+ state.editor.historyTotal = 0;
322
+ state.editor.historyHasMore = false;
323
+ state.editor.historyActiveId = null;
324
+ clearQueryHistoryDetailState();
325
+
326
+ if (!preserveSearch) {
327
+ clearQueryHistorySearchTimer();
328
+ state.editor.historySearchInput = "";
329
+ state.editor.historySearch = "";
330
+ }
331
+ }
332
+
333
+ function syncQueryHistoryItem(updatedItem) {
334
+ if (!updatedItem) {
335
+ return;
336
+ }
337
+
338
+ state.editor.history = state.editor.history.map((entry) =>
339
+ entry.id === updatedItem.id ? updatedItem : entry
340
+ );
341
+
342
+ if (state.editor.historyDetail?.id === updatedItem.id) {
343
+ state.editor.historyDetail = updatedItem;
344
+ }
345
+ }
346
+
347
+ function resolveQueryHistorySql(historyId) {
348
+ const historyIdAsString = String(historyId);
349
+
350
+ if (String(state.editor.historyDetail?.id ?? "") === historyIdAsString) {
351
+ return state.editor.historyDetail.rawSql;
352
+ }
353
+
354
+ return findQueryHistoryItem(historyId)?.rawSql ?? null;
355
+ }
356
+
171
357
  function clearRouteSlices() {
172
358
  state.overview.error = null;
173
359
  state.dataBrowser.error = null;
@@ -188,6 +374,8 @@ function setMissingDatabaseState() {
188
374
  state.dataBrowser.selectedTable = null;
189
375
  state.dataBrowser.table = null;
190
376
  state.dataBrowser.page = 1;
377
+ resetDataBrowserSort();
378
+ resetDataBrowserSearch();
191
379
  state.dataBrowser.selectedRowIndex = null;
192
380
  state.dataBrowser.exportLoading = false;
193
381
  state.dataBrowser.error = error;
@@ -198,6 +386,8 @@ function setMissingDatabaseState() {
198
386
  state.structure.data = null;
199
387
  state.structure.detail = null;
200
388
  state.structure.error = error;
389
+
390
+ resetQueryHistoryState({ preserveSearch: false });
201
391
  }
202
392
 
203
393
  function syncRouteContext() {
@@ -205,6 +395,7 @@ function syncRouteContext() {
205
395
 
206
396
  if (route.name === "editorResults") {
207
397
  state.editor.activeTab = "results";
398
+ clearQueryHistoryDetailState();
208
399
  } else if (route.name === "editor" && state.editor.activeTab === "results") {
209
400
  state.editor.activeTab = "messages";
210
401
  }
@@ -221,6 +412,9 @@ function syncRouteContext() {
221
412
  if (route.name !== "data" || route.params?.tableName !== state.dataBrowser.selectedTable) {
222
413
  state.dataBrowser.page = 1;
223
414
  }
415
+ if (route.params?.tableName !== state.dataBrowser.selectedTable) {
416
+ resetDataBrowserSearch();
417
+ }
224
418
  state.dataBrowser.selectedRowIndex = null;
225
419
  state.dataBrowser.saveError = null;
226
420
  }
@@ -273,19 +467,131 @@ async function refreshSettingsState() {
273
467
  }
274
468
  }
275
469
 
276
- async function refreshSqlHistoryState() {
277
- state.editor.historyLoading = true;
278
- state.editor.historyError = null;
470
+ async function loadQueryHistoryDetail(historyId) {
471
+ const normalizedId = String(historyId ?? "").trim();
472
+ const numericId = Number(normalizedId);
473
+ const requestVersion = ++queryHistoryDetailLoadVersion;
474
+
475
+ if (!normalizedId) {
476
+ clearQueryHistoryDetailState();
477
+ emitChange();
478
+ return;
479
+ }
480
+
481
+ state.editor.historyDetailLoading = true;
482
+ state.editor.historyDetailError = null;
279
483
  emitChange();
280
484
 
281
485
  try {
282
- const response = await api.getSqlHistory();
283
- state.editor.history = response.data ?? [];
486
+ const [detailResponse, runsResponse] = await Promise.all([
487
+ api.getQueryHistoryItem(normalizedId),
488
+ api.getQueryHistoryRuns(normalizedId, { limit: QUERY_HISTORY_RUN_LIMIT }),
489
+ ]);
490
+
491
+ if (
492
+ requestVersion !== queryHistoryDetailLoadVersion ||
493
+ state.editor.historySelectedId !== numericId
494
+ ) {
495
+ return;
496
+ }
497
+
498
+ state.editor.historyDetail = detailResponse.data ?? null;
499
+ state.editor.historyRuns = runsResponse.data ?? [];
500
+ state.editor.historyDetailError = null;
501
+ if (detailResponse.data) {
502
+ syncQueryHistoryItem(detailResponse.data);
503
+ }
284
504
  } catch (error) {
285
- state.editor.historyError = normalizeError(error);
505
+ if (
506
+ requestVersion !== queryHistoryDetailLoadVersion ||
507
+ state.editor.historySelectedId !== numericId
508
+ ) {
509
+ return;
510
+ }
511
+
512
+ state.editor.historyDetail = null;
513
+ state.editor.historyRuns = [];
514
+ state.editor.historyDetailError = normalizeError(error);
286
515
  } finally {
287
- state.editor.historyLoading = false;
516
+ if (requestVersion === queryHistoryDetailLoadVersion) {
517
+ state.editor.historyDetailLoading = false;
518
+ emitChange();
519
+ }
520
+ }
521
+ }
522
+
523
+ async function refreshQueryHistoryState({ append = false } = {}) {
524
+ if (!state.connections.active) {
525
+ resetQueryHistoryState({ preserveSearch: false });
288
526
  emitChange();
527
+ return;
528
+ }
529
+
530
+ const requestVersion = ++queryHistoryLoadVersion;
531
+ const nextOffset = append ? state.editor.history.length : 0;
532
+
533
+ if (append) {
534
+ state.editor.historyLoadingMore = true;
535
+ } else {
536
+ state.editor.historyLoading = true;
537
+ state.editor.historyError = null;
538
+ }
539
+ emitChange();
540
+
541
+ try {
542
+ const response = await api.getQueryHistory({
543
+ tab: state.editor.historyTab,
544
+ limit: state.editor.historyPageSize,
545
+ offset: nextOffset,
546
+ search: state.editor.historySearch,
547
+ });
548
+
549
+ if (requestVersion !== queryHistoryLoadVersion) {
550
+ return;
551
+ }
552
+
553
+ const payload = response.data ?? {};
554
+ const items = payload.items ?? [];
555
+ state.editor.history = append ? [...state.editor.history, ...items] : items;
556
+ state.editor.historyTotal = payload.total ?? state.editor.history.length;
557
+ state.editor.historyHasMore = Boolean(payload.hasMore);
558
+ state.editor.historyError = null;
559
+
560
+ if (
561
+ state.editor.historyActiveId &&
562
+ !state.editor.history.some((entry) => entry.id === state.editor.historyActiveId)
563
+ ) {
564
+ state.editor.historyActiveId = null;
565
+ }
566
+
567
+ if (
568
+ state.editor.historySelectedId &&
569
+ !state.editor.history.some((entry) => entry.id === state.editor.historySelectedId)
570
+ ) {
571
+ clearQueryHistoryDetailState();
572
+ }
573
+
574
+ if (
575
+ state.editor.historySelectedId &&
576
+ !state.editor.historyDetailLoading &&
577
+ state.editor.historyDetail?.id !== state.editor.historySelectedId
578
+ ) {
579
+ await loadQueryHistoryDetail(state.editor.historySelectedId);
580
+ } else {
581
+ emitChange();
582
+ }
583
+ } catch (error) {
584
+ if (requestVersion !== queryHistoryLoadVersion) {
585
+ return;
586
+ }
587
+
588
+ state.editor.historyError = normalizeError(error);
589
+ } finally {
590
+ if (requestVersion === queryHistoryLoadVersion) {
591
+ state.editor.historyLoading = false;
592
+ state.editor.historyLoadingMore = false;
593
+ emitChange();
594
+ }
289
595
  }
290
596
  }
291
597
 
@@ -322,6 +628,8 @@ async function loadDataTable(version) {
322
628
  const tableName = state.dataBrowser.selectedTable;
323
629
  const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, 50);
324
630
  const page = Math.max(1, Number(state.dataBrowser.page) || 1);
631
+ const sortColumn = state.dataBrowser.sortColumn;
632
+ const sortDirection = normalizeSortDirection(state.dataBrowser.sortDirection);
325
633
 
326
634
  if (!tableName) {
327
635
  state.dataBrowser.table = null;
@@ -337,6 +645,8 @@ async function loadDataTable(version) {
337
645
  const response = await api.getDataTable(tableName, {
338
646
  limit: pageSize,
339
647
  offset: (page - 1) * pageSize,
648
+ sortColumn,
649
+ sortDirection,
340
650
  });
341
651
 
342
652
  if (version !== routeLoadVersion) {
@@ -346,6 +656,11 @@ async function loadDataTable(version) {
346
656
  state.dataBrowser.table = response.data ?? null;
347
657
  state.dataBrowser.pageSize = pageSize;
348
658
  state.dataBrowser.page = response.data?.page ?? page;
659
+ state.dataBrowser.sortColumn = response.data?.sort?.column ?? null;
660
+ state.dataBrowser.sortDirection = response.data?.sort?.direction ?? null;
661
+ state.dataBrowser.searchColumn = response.data?.columns?.includes(state.dataBrowser.searchColumn)
662
+ ? state.dataBrowser.searchColumn
663
+ : (response.data?.columns?.[0] ?? "");
349
664
  state.dataBrowser.selectedRowIndex = null;
350
665
  } catch (error) {
351
666
  if (version !== routeLoadVersion) {
@@ -383,6 +698,8 @@ async function loadData(version, route) {
383
698
  if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
384
699
  if (requestedTableName !== state.dataBrowser.selectedTable) {
385
700
  state.dataBrowser.page = 1;
701
+ resetDataBrowserSort();
702
+ resetDataBrowserSearch();
386
703
  }
387
704
  state.dataBrowser.selectedTable = requestedTableName;
388
705
  } else if (
@@ -391,10 +708,13 @@ async function loadData(version, route) {
391
708
  ) {
392
709
  state.dataBrowser.selectedTable = tables[0]?.name ?? null;
393
710
  state.dataBrowser.page = 1;
711
+ resetDataBrowserSort();
712
+ resetDataBrowserSearch();
394
713
  }
395
714
 
396
715
  if (!state.dataBrowser.selectedTable) {
397
716
  state.dataBrowser.table = null;
717
+ resetDataBrowserSearch();
398
718
  state.dataBrowser.selectedRowIndex = null;
399
719
  return;
400
720
  }
@@ -510,6 +830,7 @@ function invalidateDatabaseCaches() {
510
830
  state.dataBrowser.selectedTable = null;
511
831
  state.dataBrowser.table = null;
512
832
  state.dataBrowser.page = 1;
833
+ resetDataBrowserSearch();
513
834
  state.dataBrowser.selectedRowIndex = null;
514
835
  state.dataBrowser.exportLoading = false;
515
836
  state.dataBrowser.error = null;
@@ -543,7 +864,7 @@ async function loadRouteData(route) {
543
864
  return;
544
865
  case "editor":
545
866
  case "editorResults":
546
- await refreshSqlHistoryState();
867
+ await refreshQueryHistoryState();
547
868
  return;
548
869
  case "structure":
549
870
  await loadStructure(version);
@@ -606,7 +927,7 @@ export async function initializeApp() {
606
927
  await Promise.all([
607
928
  refreshConnectionsState(),
608
929
  refreshSettingsState(),
609
- refreshSqlHistoryState(),
930
+ refreshQueryHistoryState(),
610
931
  ]);
611
932
 
612
933
  state.ready = true;
@@ -864,6 +1185,17 @@ export async function createActiveConnectionBackup() {
864
1185
  }
865
1186
  }
866
1187
 
1188
+ export async function openOverviewInFinder() {
1189
+ try {
1190
+ const response = await api.openOverviewInFinder();
1191
+ pushToast(response.message || "Database file revealed in Finder.", "muted");
1192
+ return true;
1193
+ } catch (error) {
1194
+ pushToast(normalizeError(error)?.message || "Finder could not be opened.", "alert");
1195
+ return false;
1196
+ }
1197
+ }
1198
+
867
1199
  export function setCurrentQuery(query) {
868
1200
  const nextQuery = String(query ?? "");
869
1201
  const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
@@ -878,12 +1210,22 @@ export function setCurrentQuery(query) {
878
1210
 
879
1211
  export function clearCurrentQuery() {
880
1212
  state.editor.sqlText = "";
1213
+ state.editor.result = null;
1214
+ resetEditorResultSort();
881
1215
  state.editor.error = null;
1216
+ state.editor.selectedRowIndex = null;
1217
+ state.editor.saving = false;
1218
+ state.editor.deleting = false;
1219
+ state.editor.saveError = null;
1220
+ if (state.editor.activeTab === "results" || state.editor.activeTab === "performance") {
1221
+ state.editor.activeTab = "messages";
1222
+ }
882
1223
  emitChange();
883
1224
  }
884
1225
 
885
1226
  export function clearEditorResults() {
886
1227
  state.editor.result = null;
1228
+ resetEditorResultSort();
887
1229
  state.editor.error = null;
888
1230
  state.editor.selectedRowIndex = null;
889
1231
  state.editor.saving = false;
@@ -910,10 +1252,11 @@ export async function executeCurrentQuery() {
910
1252
  try {
911
1253
  const response = await api.executeSql(state.editor.sqlText);
912
1254
  state.editor.result = response.data;
1255
+ resetEditorResultSort();
913
1256
  state.editor.error = null;
914
1257
  state.editor.activeTab = "results";
915
1258
  invalidateDatabaseCaches();
916
- await refreshSqlHistoryState();
1259
+ await refreshQueryHistoryState();
917
1260
  pushToast(
918
1261
  response.message || `Executed ${response.data.statementCount} SQL statement(s).`,
919
1262
  "success"
@@ -922,7 +1265,7 @@ export async function executeCurrentQuery() {
922
1265
  } catch (error) {
923
1266
  state.editor.error = normalizeError(error);
924
1267
  state.editor.activeTab = "messages";
925
- emitChange();
1268
+ await refreshQueryHistoryState();
926
1269
  return false;
927
1270
  } finally {
928
1271
  state.editor.executing = false;
@@ -930,14 +1273,14 @@ export async function executeCurrentQuery() {
930
1273
  }
931
1274
  }
932
1275
 
933
- export async function clearSqlHistoryStateAndData() {
1276
+ export async function clearQueryHistoryStateAndData() {
934
1277
  state.editor.historyLoading = true;
935
1278
  emitChange();
936
1279
 
937
1280
  try {
938
- const response = await api.clearSqlHistory();
939
- state.editor.history = response.data ?? [];
940
- pushToast(response.message || "SQL history cleared.", "muted");
1281
+ const response = await api.clearQueryHistory();
1282
+ resetQueryHistoryState({ preserveSearch: false });
1283
+ pushToast(response.message || "Query history cleared.", "muted");
941
1284
  return true;
942
1285
  } catch (error) {
943
1286
  state.editor.historyError = normalizeError(error);
@@ -949,17 +1292,158 @@ export async function clearSqlHistoryStateAndData() {
949
1292
  }
950
1293
  }
951
1294
 
952
- export function loadQueryFromHistory(id) {
953
- const historyEntry = state.editor.history.find((entry) => entry.id === id);
1295
+ export async function setQueryHistoryTab(tab) {
1296
+ const normalizedTab = normalizeQueryHistoryTab(String(tab ?? "").trim().toLowerCase());
1297
+
1298
+ if (state.editor.historyTab === normalizedTab) {
1299
+ return;
1300
+ }
1301
+
1302
+ state.editor.historyTab = normalizedTab;
1303
+ state.editor.historyActiveId = null;
1304
+ clearQueryHistoryDetailState();
1305
+ emitChange();
1306
+ await refreshQueryHistoryState();
1307
+ }
1308
+
1309
+ export function setQueryHistorySearchInput(query) {
1310
+ state.editor.historySearchInput = String(query ?? "");
1311
+ emitChange();
1312
+
1313
+ clearQueryHistorySearchTimer();
1314
+ queryHistorySearchTimer = window.setTimeout(() => {
1315
+ state.editor.historySearch = state.editor.historySearchInput.trim();
1316
+ state.editor.historyActiveId = null;
1317
+ clearQueryHistoryDetailState();
1318
+ emitChange();
1319
+ void refreshQueryHistoryState();
1320
+ }, 180);
1321
+ }
1322
+
1323
+ export async function loadMoreQueryHistory() {
1324
+ if (
1325
+ state.editor.historyLoading ||
1326
+ state.editor.historyLoadingMore ||
1327
+ !state.editor.historyHasMore
1328
+ ) {
1329
+ return;
1330
+ }
1331
+
1332
+ await refreshQueryHistoryState({ append: true });
1333
+ }
1334
+
1335
+ export async function selectQueryHistoryItem(historyId) {
1336
+ const normalizedId = setActiveQueryHistoryItem(historyId);
1337
+
1338
+ if (normalizedId === null) {
1339
+ return;
1340
+ }
1341
+
1342
+ state.editor.historySelectedId = normalizedId;
1343
+ state.editor.historyDetail = null;
1344
+ state.editor.historyRuns = [];
1345
+ state.editor.historyDetailError = null;
1346
+ emitChange();
1347
+ await loadQueryHistoryDetail(normalizedId);
1348
+ }
954
1349
 
955
- if (!historyEntry) {
1350
+ export function clearQueryHistorySelection() {
1351
+ if (state.editor.historySelectedId === null && !state.editor.historyDetail) {
956
1352
  return;
957
1353
  }
958
1354
 
959
- state.editor.sqlText = historyEntry.sql;
1355
+ clearQueryHistoryDetailState();
960
1356
  emitChange();
961
1357
  }
962
1358
 
1359
+ export function openQueryHistoryInEditor(historyId, options = {}) {
1360
+ const rawSql = resolveQueryHistorySql(historyId);
1361
+
1362
+ if (!rawSql) {
1363
+ pushToast("The selected history query could not be loaded.", "alert");
1364
+ return false;
1365
+ }
1366
+
1367
+ setActiveQueryHistoryItem(historyId);
1368
+ clearQueryHistoryDetailState();
1369
+ state.editor.sqlText = options.append
1370
+ ? [state.editor.sqlText.trim(), rawSql].filter(Boolean).join("\n\n")
1371
+ : rawSql;
1372
+ emitChange();
1373
+ return true;
1374
+ }
1375
+
1376
+ export async function runQueryHistoryItem(historyId) {
1377
+ const loaded = openQueryHistoryInEditor(historyId);
1378
+
1379
+ if (!loaded) {
1380
+ return false;
1381
+ }
1382
+
1383
+ return executeCurrentQuery();
1384
+ }
1385
+
1386
+ export async function toggleQueryHistorySavedState(historyId, nextValue) {
1387
+ try {
1388
+ const response = await api.toggleQueryHistorySaved(historyId, nextValue);
1389
+ syncQueryHistoryItem(response.data);
1390
+ pushToast(response.message || "Query save state updated.", "muted");
1391
+ await refreshQueryHistoryState();
1392
+ return true;
1393
+ } catch (error) {
1394
+ state.editor.historyDetailError = normalizeError(error);
1395
+ emitChange();
1396
+ return false;
1397
+ }
1398
+ }
1399
+
1400
+ export async function saveQueryHistoryTitle(historyId, title) {
1401
+ try {
1402
+ const response = await api.renameQueryHistoryItem(historyId, title);
1403
+ syncQueryHistoryItem(response.data);
1404
+ pushToast(response.message || "Query title updated.", "success");
1405
+ await loadQueryHistoryDetail(historyId);
1406
+ return true;
1407
+ } catch (error) {
1408
+ state.editor.historyDetailError = normalizeError(error);
1409
+ emitChange();
1410
+ return false;
1411
+ }
1412
+ }
1413
+
1414
+ export async function saveQueryHistoryNotes(historyId, notes) {
1415
+ try {
1416
+ const response = await api.updateQueryHistoryNotes(historyId, notes);
1417
+ syncQueryHistoryItem(response.data);
1418
+ pushToast(response.message || "Query notes updated.", "success");
1419
+ await loadQueryHistoryDetail(historyId);
1420
+ return true;
1421
+ } catch (error) {
1422
+ state.editor.historyDetailError = normalizeError(error);
1423
+ emitChange();
1424
+ return false;
1425
+ }
1426
+ }
1427
+
1428
+ export async function deleteQueryHistoryStateItem(historyId) {
1429
+ try {
1430
+ const response = await api.deleteQueryHistoryItem(historyId);
1431
+ if (state.editor.historyActiveId === Number(historyId)) {
1432
+ state.editor.historyActiveId = null;
1433
+ }
1434
+ if (state.editor.historySelectedId === Number(historyId)) {
1435
+ clearQueryHistorySelection();
1436
+ }
1437
+ pushToast(response.message || "Query history item deleted.", "muted");
1438
+ await refreshQueryHistoryState();
1439
+ return true;
1440
+ } catch (error) {
1441
+ state.editor.historyDetailError = normalizeError(error);
1442
+ emitChange();
1443
+ return false;
1444
+ }
1445
+ }
1446
+
963
1447
  export async function selectStructureEntry(name) {
964
1448
  state.structure.selectedName = name;
965
1449
  emitChange();
@@ -1010,6 +1494,20 @@ export function clearDataRowSelection() {
1010
1494
  emitChange();
1011
1495
  }
1012
1496
 
1497
+ export function setDataSearchQuery(query) {
1498
+ state.dataBrowser.searchQuery = String(query ?? "");
1499
+ state.dataBrowser.selectedRowIndex = null;
1500
+ state.dataBrowser.saveError = null;
1501
+ emitChange();
1502
+ }
1503
+
1504
+ export function setDataSearchColumn(columnName) {
1505
+ state.dataBrowser.searchColumn = String(columnName ?? "");
1506
+ state.dataBrowser.selectedRowIndex = null;
1507
+ state.dataBrowser.saveError = null;
1508
+ emitChange();
1509
+ }
1510
+
1013
1511
  export async function setDataPage(page) {
1014
1512
  const numericPage = Number(page);
1015
1513
 
@@ -1031,6 +1529,32 @@ export async function setDataPage(page) {
1031
1529
  }
1032
1530
  }
1033
1531
 
1532
+ export async function sortDataTableByColumn(columnName) {
1533
+ const normalizedColumn = String(columnName ?? "").trim();
1534
+
1535
+ if (
1536
+ !normalizedColumn ||
1537
+ !state.dataBrowser.table?.columns?.includes(normalizedColumn)
1538
+ ) {
1539
+ return;
1540
+ }
1541
+
1542
+ state.dataBrowser.sortDirection = getNextSortDirection(
1543
+ state.dataBrowser.sortColumn,
1544
+ state.dataBrowser.sortDirection,
1545
+ normalizedColumn
1546
+ );
1547
+ state.dataBrowser.sortColumn = normalizedColumn;
1548
+ state.dataBrowser.page = 1;
1549
+ state.dataBrowser.selectedRowIndex = null;
1550
+ state.dataBrowser.saveError = null;
1551
+ emitChange();
1552
+
1553
+ if (state.route.name === "data" && state.dataBrowser.selectedTable) {
1554
+ await loadDataTable(++routeLoadVersion);
1555
+ }
1556
+ }
1557
+
1034
1558
  export async function setDataPageSize(pageSize) {
1035
1559
  const normalizedPageSize = normalizeDataPageSize(pageSize, state.dataBrowser.pageSize);
1036
1560
 
@@ -1158,7 +1682,11 @@ export async function submitEditorRowUpdate(rowIndex, values) {
1158
1682
  );
1159
1683
  state.editor.result = {
1160
1684
  ...result,
1161
- rows: nextRows,
1685
+ rows: sortEditorResultRows(
1686
+ nextRows,
1687
+ state.editor.resultSortColumn,
1688
+ state.editor.resultSortDirection
1689
+ ),
1162
1690
  };
1163
1691
  state.editor.selectedRowIndex = null;
1164
1692
  invalidateDatabaseCaches();
@@ -1272,7 +1800,10 @@ export async function exportCurrentDataTableCsv() {
1272
1800
  emitChange();
1273
1801
 
1274
1802
  try {
1275
- await api.downloadTableCsv(tableName);
1803
+ await api.downloadTableCsv(tableName, {
1804
+ sortColumn: state.dataBrowser.sortColumn,
1805
+ sortDirection: state.dataBrowser.sortDirection,
1806
+ });
1276
1807
  pushToast(`CSV export started for ${tableName}.`, "success");
1277
1808
  return true;
1278
1809
  } catch (error) {
@@ -1285,6 +1816,31 @@ export async function exportCurrentDataTableCsv() {
1285
1816
  }
1286
1817
  }
1287
1818
 
1819
+ export function sortEditorResultsByColumn(columnName) {
1820
+ const normalizedColumn = String(columnName ?? "").trim();
1821
+ const result = state.editor.result;
1822
+
1823
+ if (!normalizedColumn || !result?.columns?.includes(normalizedColumn)) {
1824
+ return;
1825
+ }
1826
+
1827
+ const nextDirection = getNextSortDirection(
1828
+ state.editor.resultSortColumn,
1829
+ state.editor.resultSortDirection,
1830
+ normalizedColumn
1831
+ );
1832
+
1833
+ state.editor.result = {
1834
+ ...result,
1835
+ rows: sortEditorResultRows(result.rows ?? [], normalizedColumn, nextDirection),
1836
+ };
1837
+ state.editor.resultSortColumn = normalizedColumn;
1838
+ state.editor.resultSortDirection = nextDirection;
1839
+ state.editor.selectedRowIndex = null;
1840
+ state.editor.saveError = null;
1841
+ emitChange();
1842
+ }
1843
+
1288
1844
  export async function refreshCurrentRoute() {
1289
1845
  await loadRouteData(state.route);
1290
1846
  }