sqlite-hub 0.4.0 → 0.6.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 (56) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +15 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +114 -5
  12. package/frontend/js/app.js +368 -18
  13. package/frontend/js/components/bottomTabs.js +1 -1
  14. package/frontend/js/components/dataGrid.js +3 -3
  15. package/frontend/js/components/queryEditor.js +33 -55
  16. package/frontend/js/components/queryHistoryDetail.js +263 -0
  17. package/frontend/js/components/queryHistoryPanel.js +228 -0
  18. package/frontend/js/components/queryResults.js +32 -46
  19. package/frontend/js/components/rowEditorPanel.js +73 -14
  20. package/frontend/js/components/sidebar.js +1 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/router.js +10 -0
  25. package/frontend/js/store.js +841 -22
  26. package/frontend/js/utils/format.js +23 -0
  27. package/frontend/js/utils/tableDesigner.js +1192 -0
  28. package/frontend/js/views/data.js +273 -250
  29. package/frontend/js/views/editor.js +34 -10
  30. package/frontend/js/views/overview.js +15 -0
  31. package/frontend/js/views/tableDesigner.js +37 -0
  32. package/frontend/styles/base.css +87 -73
  33. package/frontend/styles/components.css +841 -188
  34. package/frontend/styles/views.css +40 -0
  35. package/package.json +1 -1
  36. package/server/routes/data.js +2 -0
  37. package/server/routes/export.js +4 -1
  38. package/server/routes/overview.js +12 -0
  39. package/server/routes/sql.js +163 -5
  40. package/server/routes/tableDesigner.js +60 -0
  41. package/server/server.js +5 -1
  42. package/server/services/sqlite/dataBrowserService.js +4 -16
  43. package/server/services/sqlite/exportService.js +4 -16
  44. package/server/services/sqlite/overviewService.js +34 -0
  45. package/server/services/sqlite/sqlExecutor.js +83 -63
  46. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  47. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  48. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  49. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  50. package/server/services/sqlite/tableDesignerService.js +181 -0
  51. package/server/services/sqlite/tableSort.js +63 -0
  52. package/server/services/storage/appStateStore.js +674 -1
  53. package/server/services/storage/queryHistoryUtils.js +169 -0
  54. package/frontend/assets/mockups/data_edit.png +0 -0
  55. package/frontend/assets/mockups/graph_visualize.png +0 -0
  56. package/frontend/assets/mockups/overview.png +0 -0
@@ -1,5 +1,14 @@
1
1
  import * as api from "./api.js";
2
2
  import { formatCellValue, inferStatusTone, truncateMiddle } from "./utils/format.js";
3
+ import {
4
+ addTableDesignerColumn,
5
+ createTableDesignerDraftFromCsvImport,
6
+ createNewTableDesignerDraft,
7
+ hydrateTableDesignerDraft,
8
+ removeTableDesignerColumn,
9
+ updateTableDesignerColumnField,
10
+ updateTableDesignerDraftField,
11
+ } from "./utils/tableDesigner.js";
3
12
 
4
13
  const listeners = new Set();
5
14
  const DEFAULT_SETTINGS = {
@@ -8,12 +17,17 @@ const DEFAULT_SETTINGS = {
8
17
  csvDelimiter: ",",
9
18
  };
10
19
  const DATA_PAGE_SIZES = [25, 50, 100];
20
+ const QUERY_HISTORY_PAGE_SIZE = 30;
21
+ const QUERY_HISTORY_RUN_LIMIT = 8;
11
22
  const MISSING_DATABASE_ERROR = {
12
23
  code: "ACTIVE_DATABASE_REQUIRED",
13
24
  message: "No active SQLite database selected.",
14
25
  };
15
26
 
16
27
  let routeLoadVersion = 0;
28
+ let queryHistoryLoadVersion = 0;
29
+ let queryHistoryDetailLoadVersion = 0;
30
+ let queryHistorySearchTimer = null;
17
31
 
18
32
  const state = {
19
33
  ready: false,
@@ -48,6 +62,8 @@ const state = {
48
62
  deleting: false,
49
63
  page: 1,
50
64
  pageSize: 50,
65
+ sortColumn: null,
66
+ sortDirection: null,
51
67
  searchQuery: "",
52
68
  searchColumn: "",
53
69
  selectedRowIndex: null,
@@ -59,10 +75,25 @@ const state = {
59
75
  sqlText: "",
60
76
  history: [],
61
77
  historyLoading: false,
78
+ historyLoadingMore: false,
62
79
  historyError: null,
80
+ historyTab: "recent",
81
+ historySearchInput: "",
82
+ historySearch: "",
83
+ historyPageSize: QUERY_HISTORY_PAGE_SIZE,
84
+ historyTotal: 0,
85
+ historyHasMore: false,
86
+ historyActiveId: null,
87
+ historySelectedId: null,
88
+ historyDetail: null,
89
+ historyRuns: [],
90
+ historyDetailLoading: false,
91
+ historyDetailError: null,
63
92
  activeTab: "messages",
64
93
  executing: false,
65
94
  result: null,
95
+ resultSortColumn: null,
96
+ resultSortDirection: null,
66
97
  error: null,
67
98
  exportLoading: false,
68
99
  selectedRowIndex: null,
@@ -70,6 +101,19 @@ const state = {
70
101
  deleting: false,
71
102
  saveError: null,
72
103
  },
104
+ tableDesigner: {
105
+ tables: [],
106
+ selectedTableName: null,
107
+ draft: null,
108
+ pendingImportedDraft: null,
109
+ loading: false,
110
+ detailLoading: false,
111
+ saving: false,
112
+ searchQuery: "",
113
+ supportedTypes: [],
114
+ error: null,
115
+ saveError: null,
116
+ },
73
117
  structure: {
74
118
  data: null,
75
119
  selectedName: null,
@@ -102,8 +146,35 @@ function normalizeError(error) {
102
146
  };
103
147
  }
104
148
 
149
+ function setActiveQueryHistoryItem(historyId) {
150
+ const normalizedId = Number(historyId);
151
+
152
+ if (!Number.isInteger(normalizedId) || normalizedId < 1) {
153
+ return null;
154
+ }
155
+
156
+ state.editor.historyActiveId = normalizedId;
157
+ return normalizedId;
158
+ }
159
+
160
+ function clearQueryHistoryDetailState() {
161
+ queryHistoryDetailLoadVersion += 1;
162
+ state.editor.historySelectedId = null;
163
+ state.editor.historyDetail = null;
164
+ state.editor.historyRuns = [];
165
+ state.editor.historyDetailLoading = false;
166
+ state.editor.historyDetailError = null;
167
+ }
168
+
105
169
  function requiresActiveDatabase(routeName) {
106
- return ["overview", "data", "editor", "editorResults", "structure"].includes(routeName);
170
+ return [
171
+ "overview",
172
+ "data",
173
+ "editor",
174
+ "editorResults",
175
+ "structure",
176
+ "tableDesigner",
177
+ ].includes(routeName);
107
178
  }
108
179
 
109
180
  function normalizeDataPageSize(value, fallback = 50) {
@@ -116,6 +187,18 @@ function normalizeDataPageSize(value, fallback = 50) {
116
187
  return fallback;
117
188
  }
118
189
 
190
+ function normalizeSortDirection(value) {
191
+ return String(value ?? "").trim().toLowerCase() === "desc" ? "desc" : "asc";
192
+ }
193
+
194
+ function getNextSortDirection(currentColumn, currentDirection, nextColumn) {
195
+ if (currentColumn === nextColumn) {
196
+ return normalizeSortDirection(currentDirection) === "asc" ? "desc" : "asc";
197
+ }
198
+
199
+ return "asc";
200
+ }
201
+
119
202
  function canEditQueryResult(snapshot = state) {
120
203
  return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
121
204
  }
@@ -125,6 +208,73 @@ function resetDataBrowserSearch() {
125
208
  state.dataBrowser.searchColumn = "";
126
209
  }
127
210
 
211
+ function resetDataBrowserSort() {
212
+ state.dataBrowser.sortColumn = null;
213
+ state.dataBrowser.sortDirection = null;
214
+ }
215
+
216
+ function resetEditorResultSort() {
217
+ state.editor.resultSortColumn = null;
218
+ state.editor.resultSortDirection = null;
219
+ }
220
+
221
+ function getSortableEditorValue(value) {
222
+ if (value === null || value === undefined) {
223
+ return { rank: 0, value: "" };
224
+ }
225
+
226
+ if (typeof value === "number") {
227
+ return { rank: 1, value };
228
+ }
229
+
230
+ if (typeof value === "boolean") {
231
+ return { rank: 2, value: value ? 1 : 0 };
232
+ }
233
+
234
+ if (typeof value === "string") {
235
+ return { rank: 3, value };
236
+ }
237
+
238
+ if (value && typeof value === "object" && value.__type === "blob") {
239
+ return {
240
+ rank: 4,
241
+ value: `${value.sizeBytes ?? 0}:${value.hexPreview ?? ""}`,
242
+ };
243
+ }
244
+
245
+ return { rank: 5, value: JSON.stringify(value) };
246
+ }
247
+
248
+ function compareEditorValues(left, right) {
249
+ const leftValue = getSortableEditorValue(left);
250
+ const rightValue = getSortableEditorValue(right);
251
+
252
+ if (leftValue.rank !== rightValue.rank) {
253
+ return leftValue.rank - rightValue.rank;
254
+ }
255
+
256
+ if (typeof leftValue.value === "number" && typeof rightValue.value === "number") {
257
+ return leftValue.value - rightValue.value;
258
+ }
259
+
260
+ return String(leftValue.value).localeCompare(String(rightValue.value), undefined, {
261
+ numeric: true,
262
+ sensitivity: "base",
263
+ });
264
+ }
265
+
266
+ function sortEditorResultRows(rows, sortColumn, sortDirection) {
267
+ if (!sortColumn) {
268
+ return [...(rows ?? [])];
269
+ }
270
+
271
+ const directionMultiplier = normalizeSortDirection(sortDirection) === "desc" ? -1 : 1;
272
+
273
+ return [...(rows ?? [])].sort(
274
+ (left, right) => compareEditorValues(left?.[sortColumn], right?.[sortColumn]) * directionMultiplier
275
+ );
276
+ }
277
+
128
278
  function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
129
279
  const nextRow = {
130
280
  ...existingRow,
@@ -149,6 +299,22 @@ function getCurrentStructureEntry(snapshot = state) {
149
299
  return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
150
300
  }
151
301
 
302
+ function getTableDesignerContext(snapshot = state) {
303
+ return {
304
+ catalogTables: snapshot.tableDesigner.tables ?? [],
305
+ supportedTypes: snapshot.tableDesigner.supportedTypes ?? [],
306
+ readOnly: Boolean(snapshot.connections.active?.readOnly),
307
+ };
308
+ }
309
+
310
+ function decorateTableDesignerDraft(draft, snapshot = state) {
311
+ if (!draft) {
312
+ return null;
313
+ }
314
+
315
+ return hydrateTableDesignerDraft(draft, getTableDesignerContext(snapshot));
316
+ }
317
+
152
318
  function buildDeleteRowPreview(fields = []) {
153
319
  return fields
154
320
  .filter((field) => field && field.label)
@@ -175,10 +341,70 @@ function buildFallbackDeleteRowPreview(row) {
175
341
  );
176
342
  }
177
343
 
344
+ function normalizeQueryHistoryTab(value) {
345
+ return ["recent", "saved", "failed"].includes(value) ? value : "recent";
346
+ }
347
+
348
+ function findQueryHistoryItem(historyId, snapshot = state) {
349
+ return snapshot.editor.history.find((entry) => String(entry.id) === String(historyId)) ?? null;
350
+ }
351
+
352
+ function clearQueryHistorySearchTimer() {
353
+ if (!queryHistorySearchTimer) {
354
+ return;
355
+ }
356
+
357
+ window.clearTimeout(queryHistorySearchTimer);
358
+ queryHistorySearchTimer = null;
359
+ }
360
+
361
+ function resetQueryHistoryState({ preserveSearch = true } = {}) {
362
+ state.editor.history = [];
363
+ state.editor.historyLoading = false;
364
+ state.editor.historyLoadingMore = false;
365
+ state.editor.historyError = null;
366
+ state.editor.historyTotal = 0;
367
+ state.editor.historyHasMore = false;
368
+ state.editor.historyActiveId = null;
369
+ clearQueryHistoryDetailState();
370
+
371
+ if (!preserveSearch) {
372
+ clearQueryHistorySearchTimer();
373
+ state.editor.historySearchInput = "";
374
+ state.editor.historySearch = "";
375
+ }
376
+ }
377
+
378
+ function syncQueryHistoryItem(updatedItem) {
379
+ if (!updatedItem) {
380
+ return;
381
+ }
382
+
383
+ state.editor.history = state.editor.history.map((entry) =>
384
+ entry.id === updatedItem.id ? updatedItem : entry
385
+ );
386
+
387
+ if (state.editor.historyDetail?.id === updatedItem.id) {
388
+ state.editor.historyDetail = updatedItem;
389
+ }
390
+ }
391
+
392
+ function resolveQueryHistorySql(historyId) {
393
+ const historyIdAsString = String(historyId);
394
+
395
+ if (String(state.editor.historyDetail?.id ?? "") === historyIdAsString) {
396
+ return state.editor.historyDetail.rawSql;
397
+ }
398
+
399
+ return findQueryHistoryItem(historyId)?.rawSql ?? null;
400
+ }
401
+
178
402
  function clearRouteSlices() {
179
403
  state.overview.error = null;
180
404
  state.dataBrowser.error = null;
181
405
  state.dataBrowser.saveError = null;
406
+ state.tableDesigner.error = null;
407
+ state.tableDesigner.saveError = null;
182
408
  state.structure.error = null;
183
409
  }
184
410
 
@@ -195,6 +421,7 @@ function setMissingDatabaseState() {
195
421
  state.dataBrowser.selectedTable = null;
196
422
  state.dataBrowser.table = null;
197
423
  state.dataBrowser.page = 1;
424
+ resetDataBrowserSort();
198
425
  resetDataBrowserSearch();
199
426
  state.dataBrowser.selectedRowIndex = null;
200
427
  state.dataBrowser.exportLoading = false;
@@ -206,6 +433,20 @@ function setMissingDatabaseState() {
206
433
  state.structure.data = null;
207
434
  state.structure.detail = null;
208
435
  state.structure.error = error;
436
+
437
+ state.tableDesigner.loading = false;
438
+ state.tableDesigner.detailLoading = false;
439
+ state.tableDesigner.tables = [];
440
+ state.tableDesigner.selectedTableName = null;
441
+ state.tableDesigner.draft = null;
442
+ state.tableDesigner.pendingImportedDraft = null;
443
+ state.tableDesigner.saving = false;
444
+ state.tableDesigner.searchQuery = "";
445
+ state.tableDesigner.supportedTypes = [];
446
+ state.tableDesigner.error = error;
447
+ state.tableDesigner.saveError = null;
448
+
449
+ resetQueryHistoryState({ preserveSearch: false });
209
450
  }
210
451
 
211
452
  function syncRouteContext() {
@@ -213,6 +454,7 @@ function syncRouteContext() {
213
454
 
214
455
  if (route.name === "editorResults") {
215
456
  state.editor.activeTab = "results";
457
+ clearQueryHistoryDetailState();
216
458
  } else if (route.name === "editor" && state.editor.activeTab === "results") {
217
459
  state.editor.activeTab = "messages";
218
460
  }
@@ -240,6 +482,10 @@ function syncRouteContext() {
240
482
  state.structure.detail = null;
241
483
  state.structure.selectedName = null;
242
484
  }
485
+
486
+ if (route.name !== "tableDesigner") {
487
+ state.tableDesigner.saveError = null;
488
+ }
243
489
  }
244
490
 
245
491
  async function refreshConnectionsState() {
@@ -284,19 +530,131 @@ async function refreshSettingsState() {
284
530
  }
285
531
  }
286
532
 
287
- async function refreshSqlHistoryState() {
288
- state.editor.historyLoading = true;
289
- state.editor.historyError = null;
533
+ async function loadQueryHistoryDetail(historyId) {
534
+ const normalizedId = String(historyId ?? "").trim();
535
+ const numericId = Number(normalizedId);
536
+ const requestVersion = ++queryHistoryDetailLoadVersion;
537
+
538
+ if (!normalizedId) {
539
+ clearQueryHistoryDetailState();
540
+ emitChange();
541
+ return;
542
+ }
543
+
544
+ state.editor.historyDetailLoading = true;
545
+ state.editor.historyDetailError = null;
290
546
  emitChange();
291
547
 
292
548
  try {
293
- const response = await api.getSqlHistory();
294
- state.editor.history = response.data ?? [];
549
+ const [detailResponse, runsResponse] = await Promise.all([
550
+ api.getQueryHistoryItem(normalizedId),
551
+ api.getQueryHistoryRuns(normalizedId, { limit: QUERY_HISTORY_RUN_LIMIT }),
552
+ ]);
553
+
554
+ if (
555
+ requestVersion !== queryHistoryDetailLoadVersion ||
556
+ state.editor.historySelectedId !== numericId
557
+ ) {
558
+ return;
559
+ }
560
+
561
+ state.editor.historyDetail = detailResponse.data ?? null;
562
+ state.editor.historyRuns = runsResponse.data ?? [];
563
+ state.editor.historyDetailError = null;
564
+ if (detailResponse.data) {
565
+ syncQueryHistoryItem(detailResponse.data);
566
+ }
295
567
  } catch (error) {
296
- state.editor.historyError = normalizeError(error);
568
+ if (
569
+ requestVersion !== queryHistoryDetailLoadVersion ||
570
+ state.editor.historySelectedId !== numericId
571
+ ) {
572
+ return;
573
+ }
574
+
575
+ state.editor.historyDetail = null;
576
+ state.editor.historyRuns = [];
577
+ state.editor.historyDetailError = normalizeError(error);
297
578
  } finally {
298
- state.editor.historyLoading = false;
579
+ if (requestVersion === queryHistoryDetailLoadVersion) {
580
+ state.editor.historyDetailLoading = false;
581
+ emitChange();
582
+ }
583
+ }
584
+ }
585
+
586
+ async function refreshQueryHistoryState({ append = false } = {}) {
587
+ if (!state.connections.active) {
588
+ resetQueryHistoryState({ preserveSearch: false });
299
589
  emitChange();
590
+ return;
591
+ }
592
+
593
+ const requestVersion = ++queryHistoryLoadVersion;
594
+ const nextOffset = append ? state.editor.history.length : 0;
595
+
596
+ if (append) {
597
+ state.editor.historyLoadingMore = true;
598
+ } else {
599
+ state.editor.historyLoading = true;
600
+ state.editor.historyError = null;
601
+ }
602
+ emitChange();
603
+
604
+ try {
605
+ const response = await api.getQueryHistory({
606
+ tab: state.editor.historyTab,
607
+ limit: state.editor.historyPageSize,
608
+ offset: nextOffset,
609
+ search: state.editor.historySearch,
610
+ });
611
+
612
+ if (requestVersion !== queryHistoryLoadVersion) {
613
+ return;
614
+ }
615
+
616
+ const payload = response.data ?? {};
617
+ const items = payload.items ?? [];
618
+ state.editor.history = append ? [...state.editor.history, ...items] : items;
619
+ state.editor.historyTotal = payload.total ?? state.editor.history.length;
620
+ state.editor.historyHasMore = Boolean(payload.hasMore);
621
+ state.editor.historyError = null;
622
+
623
+ if (
624
+ state.editor.historyActiveId &&
625
+ !state.editor.history.some((entry) => entry.id === state.editor.historyActiveId)
626
+ ) {
627
+ state.editor.historyActiveId = null;
628
+ }
629
+
630
+ if (
631
+ state.editor.historySelectedId &&
632
+ !state.editor.history.some((entry) => entry.id === state.editor.historySelectedId)
633
+ ) {
634
+ clearQueryHistoryDetailState();
635
+ }
636
+
637
+ if (
638
+ state.editor.historySelectedId &&
639
+ !state.editor.historyDetailLoading &&
640
+ state.editor.historyDetail?.id !== state.editor.historySelectedId
641
+ ) {
642
+ await loadQueryHistoryDetail(state.editor.historySelectedId);
643
+ } else {
644
+ emitChange();
645
+ }
646
+ } catch (error) {
647
+ if (requestVersion !== queryHistoryLoadVersion) {
648
+ return;
649
+ }
650
+
651
+ state.editor.historyError = normalizeError(error);
652
+ } finally {
653
+ if (requestVersion === queryHistoryLoadVersion) {
654
+ state.editor.historyLoading = false;
655
+ state.editor.historyLoadingMore = false;
656
+ emitChange();
657
+ }
300
658
  }
301
659
  }
302
660
 
@@ -333,6 +691,8 @@ async function loadDataTable(version) {
333
691
  const tableName = state.dataBrowser.selectedTable;
334
692
  const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, 50);
335
693
  const page = Math.max(1, Number(state.dataBrowser.page) || 1);
694
+ const sortColumn = state.dataBrowser.sortColumn;
695
+ const sortDirection = normalizeSortDirection(state.dataBrowser.sortDirection);
336
696
 
337
697
  if (!tableName) {
338
698
  state.dataBrowser.table = null;
@@ -348,6 +708,8 @@ async function loadDataTable(version) {
348
708
  const response = await api.getDataTable(tableName, {
349
709
  limit: pageSize,
350
710
  offset: (page - 1) * pageSize,
711
+ sortColumn,
712
+ sortDirection,
351
713
  });
352
714
 
353
715
  if (version !== routeLoadVersion) {
@@ -357,6 +719,8 @@ async function loadDataTable(version) {
357
719
  state.dataBrowser.table = response.data ?? null;
358
720
  state.dataBrowser.pageSize = pageSize;
359
721
  state.dataBrowser.page = response.data?.page ?? page;
722
+ state.dataBrowser.sortColumn = response.data?.sort?.column ?? null;
723
+ state.dataBrowser.sortDirection = response.data?.sort?.direction ?? null;
360
724
  state.dataBrowser.searchColumn = response.data?.columns?.includes(state.dataBrowser.searchColumn)
361
725
  ? state.dataBrowser.searchColumn
362
726
  : (response.data?.columns?.[0] ?? "");
@@ -397,6 +761,7 @@ async function loadData(version, route) {
397
761
  if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
398
762
  if (requestedTableName !== state.dataBrowser.selectedTable) {
399
763
  state.dataBrowser.page = 1;
764
+ resetDataBrowserSort();
400
765
  resetDataBrowserSearch();
401
766
  }
402
767
  state.dataBrowser.selectedTable = requestedTableName;
@@ -406,6 +771,7 @@ async function loadData(version, route) {
406
771
  ) {
407
772
  state.dataBrowser.selectedTable = tables[0]?.name ?? null;
408
773
  state.dataBrowser.page = 1;
774
+ resetDataBrowserSort();
409
775
  resetDataBrowserSearch();
410
776
  }
411
777
 
@@ -521,6 +887,103 @@ async function loadStructure(version) {
521
887
  }
522
888
  }
523
889
 
890
+ async function loadTableDesignerDetail(version, tableName) {
891
+ if (!tableName) {
892
+ state.tableDesigner.draft = null;
893
+ return;
894
+ }
895
+
896
+ state.tableDesigner.detailLoading = true;
897
+ state.tableDesigner.saveError = null;
898
+ emitChange();
899
+
900
+ try {
901
+ const response = await api.getTableDesignerTable(tableName);
902
+
903
+ if (version !== routeLoadVersion) {
904
+ return;
905
+ }
906
+
907
+ state.tableDesigner.selectedTableName = tableName;
908
+ state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
909
+ } catch (error) {
910
+ if (version !== routeLoadVersion) {
911
+ return;
912
+ }
913
+
914
+ state.tableDesigner.error = normalizeError(error);
915
+ state.tableDesigner.draft = null;
916
+ } finally {
917
+ if (version === routeLoadVersion) {
918
+ state.tableDesigner.detailLoading = false;
919
+ emitChange();
920
+ }
921
+ }
922
+ }
923
+
924
+ async function loadTableDesigner(version, route) {
925
+ state.tableDesigner.loading = true;
926
+ state.tableDesigner.error = null;
927
+ emitChange();
928
+
929
+ try {
930
+ const response = await api.getTableDesignerOverview();
931
+
932
+ if (version !== routeLoadVersion) {
933
+ return;
934
+ }
935
+
936
+ state.tableDesigner.tables = response.data?.tables ?? [];
937
+ state.tableDesigner.supportedTypes = response.data?.supportedTypes ?? [];
938
+
939
+ if (route.params?.isNew) {
940
+ const importedDraft = state.tableDesigner.pendingImportedDraft;
941
+ state.tableDesigner.selectedTableName = null;
942
+ state.tableDesigner.detailLoading = false;
943
+ state.tableDesigner.pendingImportedDraft = null;
944
+ state.tableDesigner.draft = decorateTableDesignerDraft(
945
+ importedDraft ?? createNewTableDesignerDraft()
946
+ );
947
+ return;
948
+ }
949
+
950
+ const requestedTableName = route.params?.tableName ?? null;
951
+ const tableName =
952
+ requestedTableName &&
953
+ state.tableDesigner.tables.some((table) => table.name === requestedTableName)
954
+ ? requestedTableName
955
+ : state.tableDesigner.selectedTableName &&
956
+ state.tableDesigner.tables.some(
957
+ (table) => table.name === state.tableDesigner.selectedTableName
958
+ )
959
+ ? state.tableDesigner.selectedTableName
960
+ : state.tableDesigner.tables[0]?.name ?? null;
961
+
962
+ if (!tableName) {
963
+ state.tableDesigner.selectedTableName = null;
964
+ state.tableDesigner.detailLoading = false;
965
+ state.tableDesigner.draft = null;
966
+ return;
967
+ }
968
+
969
+ await loadTableDesignerDetail(version, tableName);
970
+ } catch (error) {
971
+ if (version !== routeLoadVersion) {
972
+ return;
973
+ }
974
+
975
+ state.tableDesigner.tables = [];
976
+ state.tableDesigner.selectedTableName = null;
977
+ state.tableDesigner.draft = null;
978
+ state.tableDesigner.error = normalizeError(error);
979
+ } finally {
980
+ if (version === routeLoadVersion) {
981
+ state.tableDesigner.loading = false;
982
+ emitChange();
983
+ }
984
+ }
985
+ }
986
+
524
987
  function invalidateDatabaseCaches() {
525
988
  state.overview.data = null;
526
989
  state.dataBrowser.tables = [];
@@ -532,6 +995,15 @@ function invalidateDatabaseCaches() {
532
995
  state.dataBrowser.exportLoading = false;
533
996
  state.dataBrowser.error = null;
534
997
  state.dataBrowser.saveError = null;
998
+ state.tableDesigner.tables = [];
999
+ state.tableDesigner.selectedTableName = null;
1000
+ state.tableDesigner.draft = null;
1001
+ state.tableDesigner.pendingImportedDraft = null;
1002
+ state.tableDesigner.saving = false;
1003
+ state.tableDesigner.searchQuery = "";
1004
+ state.tableDesigner.supportedTypes = [];
1005
+ state.tableDesigner.error = null;
1006
+ state.tableDesigner.saveError = null;
535
1007
  state.structure.data = null;
536
1008
  state.structure.detail = null;
537
1009
  }
@@ -561,11 +1033,14 @@ async function loadRouteData(route) {
561
1033
  return;
562
1034
  case "editor":
563
1035
  case "editorResults":
564
- await refreshSqlHistoryState();
1036
+ await refreshQueryHistoryState();
565
1037
  return;
566
1038
  case "structure":
567
1039
  await loadStructure(version);
568
1040
  return;
1041
+ case "tableDesigner":
1042
+ await loadTableDesigner(version, route);
1043
+ return;
569
1044
  case "settings":
570
1045
  await refreshSettingsState();
571
1046
  return;
@@ -624,7 +1099,7 @@ export async function initializeApp() {
624
1099
  await Promise.all([
625
1100
  refreshConnectionsState(),
626
1101
  refreshSettingsState(),
627
- refreshSqlHistoryState(),
1102
+ refreshQueryHistoryState(),
628
1103
  ]);
629
1104
 
630
1105
  state.ready = true;
@@ -882,6 +1357,17 @@ export async function createActiveConnectionBackup() {
882
1357
  }
883
1358
  }
884
1359
 
1360
+ export async function openOverviewInFinder() {
1361
+ try {
1362
+ const response = await api.openOverviewInFinder();
1363
+ pushToast(response.message || "Database file revealed in Finder.", "muted");
1364
+ return true;
1365
+ } catch (error) {
1366
+ pushToast(normalizeError(error)?.message || "Finder could not be opened.", "alert");
1367
+ return false;
1368
+ }
1369
+ }
1370
+
885
1371
  export function setCurrentQuery(query) {
886
1372
  const nextQuery = String(query ?? "");
887
1373
  const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
@@ -896,12 +1382,22 @@ export function setCurrentQuery(query) {
896
1382
 
897
1383
  export function clearCurrentQuery() {
898
1384
  state.editor.sqlText = "";
1385
+ state.editor.result = null;
1386
+ resetEditorResultSort();
899
1387
  state.editor.error = null;
1388
+ state.editor.selectedRowIndex = null;
1389
+ state.editor.saving = false;
1390
+ state.editor.deleting = false;
1391
+ state.editor.saveError = null;
1392
+ if (state.editor.activeTab === "results" || state.editor.activeTab === "performance") {
1393
+ state.editor.activeTab = "messages";
1394
+ }
900
1395
  emitChange();
901
1396
  }
902
1397
 
903
1398
  export function clearEditorResults() {
904
1399
  state.editor.result = null;
1400
+ resetEditorResultSort();
905
1401
  state.editor.error = null;
906
1402
  state.editor.selectedRowIndex = null;
907
1403
  state.editor.saving = false;
@@ -928,10 +1424,11 @@ export async function executeCurrentQuery() {
928
1424
  try {
929
1425
  const response = await api.executeSql(state.editor.sqlText);
930
1426
  state.editor.result = response.data;
1427
+ resetEditorResultSort();
931
1428
  state.editor.error = null;
932
1429
  state.editor.activeTab = "results";
933
1430
  invalidateDatabaseCaches();
934
- await refreshSqlHistoryState();
1431
+ await refreshQueryHistoryState();
935
1432
  pushToast(
936
1433
  response.message || `Executed ${response.data.statementCount} SQL statement(s).`,
937
1434
  "success"
@@ -940,7 +1437,7 @@ export async function executeCurrentQuery() {
940
1437
  } catch (error) {
941
1438
  state.editor.error = normalizeError(error);
942
1439
  state.editor.activeTab = "messages";
943
- emitChange();
1440
+ await refreshQueryHistoryState();
944
1441
  return false;
945
1442
  } finally {
946
1443
  state.editor.executing = false;
@@ -948,14 +1445,14 @@ export async function executeCurrentQuery() {
948
1445
  }
949
1446
  }
950
1447
 
951
- export async function clearSqlHistoryStateAndData() {
1448
+ export async function clearQueryHistoryStateAndData() {
952
1449
  state.editor.historyLoading = true;
953
1450
  emitChange();
954
1451
 
955
1452
  try {
956
- const response = await api.clearSqlHistory();
957
- state.editor.history = response.data ?? [];
958
- pushToast(response.message || "SQL history cleared.", "muted");
1453
+ const response = await api.clearQueryHistory();
1454
+ resetQueryHistoryState({ preserveSearch: false });
1455
+ pushToast(response.message || "Query history cleared.", "muted");
959
1456
  return true;
960
1457
  } catch (error) {
961
1458
  state.editor.historyError = normalizeError(error);
@@ -967,15 +1464,156 @@ export async function clearSqlHistoryStateAndData() {
967
1464
  }
968
1465
  }
969
1466
 
970
- export function loadQueryFromHistory(id) {
971
- const historyEntry = state.editor.history.find((entry) => entry.id === id);
1467
+ export async function setQueryHistoryTab(tab) {
1468
+ const normalizedTab = normalizeQueryHistoryTab(String(tab ?? "").trim().toLowerCase());
1469
+
1470
+ if (state.editor.historyTab === normalizedTab) {
1471
+ return;
1472
+ }
1473
+
1474
+ state.editor.historyTab = normalizedTab;
1475
+ state.editor.historyActiveId = null;
1476
+ clearQueryHistoryDetailState();
1477
+ emitChange();
1478
+ await refreshQueryHistoryState();
1479
+ }
1480
+
1481
+ export function setQueryHistorySearchInput(query) {
1482
+ state.editor.historySearchInput = String(query ?? "");
1483
+ emitChange();
1484
+
1485
+ clearQueryHistorySearchTimer();
1486
+ queryHistorySearchTimer = window.setTimeout(() => {
1487
+ state.editor.historySearch = state.editor.historySearchInput.trim();
1488
+ state.editor.historyActiveId = null;
1489
+ clearQueryHistoryDetailState();
1490
+ emitChange();
1491
+ void refreshQueryHistoryState();
1492
+ }, 180);
1493
+ }
1494
+
1495
+ export async function loadMoreQueryHistory() {
1496
+ if (
1497
+ state.editor.historyLoading ||
1498
+ state.editor.historyLoadingMore ||
1499
+ !state.editor.historyHasMore
1500
+ ) {
1501
+ return;
1502
+ }
1503
+
1504
+ await refreshQueryHistoryState({ append: true });
1505
+ }
1506
+
1507
+ export async function selectQueryHistoryItem(historyId) {
1508
+ const normalizedId = setActiveQueryHistoryItem(historyId);
1509
+
1510
+ if (normalizedId === null) {
1511
+ return;
1512
+ }
1513
+
1514
+ state.editor.historySelectedId = normalizedId;
1515
+ state.editor.historyDetail = null;
1516
+ state.editor.historyRuns = [];
1517
+ state.editor.historyDetailError = null;
1518
+ emitChange();
1519
+ await loadQueryHistoryDetail(normalizedId);
1520
+ }
972
1521
 
973
- if (!historyEntry) {
1522
+ export function clearQueryHistorySelection() {
1523
+ if (state.editor.historySelectedId === null && !state.editor.historyDetail) {
974
1524
  return;
975
1525
  }
976
1526
 
977
- state.editor.sqlText = historyEntry.sql;
1527
+ clearQueryHistoryDetailState();
1528
+ emitChange();
1529
+ }
1530
+
1531
+ export function openQueryHistoryInEditor(historyId, options = {}) {
1532
+ const rawSql = resolveQueryHistorySql(historyId);
1533
+
1534
+ if (!rawSql) {
1535
+ pushToast("The selected history query could not be loaded.", "alert");
1536
+ return false;
1537
+ }
1538
+
1539
+ setActiveQueryHistoryItem(historyId);
1540
+ clearQueryHistoryDetailState();
1541
+ state.editor.sqlText = options.append
1542
+ ? [state.editor.sqlText.trim(), rawSql].filter(Boolean).join("\n\n")
1543
+ : rawSql;
978
1544
  emitChange();
1545
+ return true;
1546
+ }
1547
+
1548
+ export async function runQueryHistoryItem(historyId) {
1549
+ const loaded = openQueryHistoryInEditor(historyId);
1550
+
1551
+ if (!loaded) {
1552
+ return false;
1553
+ }
1554
+
1555
+ return executeCurrentQuery();
1556
+ }
1557
+
1558
+ export async function toggleQueryHistorySavedState(historyId, nextValue) {
1559
+ try {
1560
+ const response = await api.toggleQueryHistorySaved(historyId, nextValue);
1561
+ syncQueryHistoryItem(response.data);
1562
+ pushToast(response.message || "Query save state updated.", "muted");
1563
+ await refreshQueryHistoryState();
1564
+ return true;
1565
+ } catch (error) {
1566
+ state.editor.historyDetailError = normalizeError(error);
1567
+ emitChange();
1568
+ return false;
1569
+ }
1570
+ }
1571
+
1572
+ export async function saveQueryHistoryTitle(historyId, title) {
1573
+ try {
1574
+ const response = await api.renameQueryHistoryItem(historyId, title);
1575
+ syncQueryHistoryItem(response.data);
1576
+ pushToast(response.message || "Query title updated.", "success");
1577
+ await loadQueryHistoryDetail(historyId);
1578
+ return true;
1579
+ } catch (error) {
1580
+ state.editor.historyDetailError = normalizeError(error);
1581
+ emitChange();
1582
+ return false;
1583
+ }
1584
+ }
1585
+
1586
+ export async function saveQueryHistoryNotes(historyId, notes) {
1587
+ try {
1588
+ const response = await api.updateQueryHistoryNotes(historyId, notes);
1589
+ syncQueryHistoryItem(response.data);
1590
+ pushToast(response.message || "Query notes updated.", "success");
1591
+ await loadQueryHistoryDetail(historyId);
1592
+ return true;
1593
+ } catch (error) {
1594
+ state.editor.historyDetailError = normalizeError(error);
1595
+ emitChange();
1596
+ return false;
1597
+ }
1598
+ }
1599
+
1600
+ export async function deleteQueryHistoryStateItem(historyId) {
1601
+ try {
1602
+ const response = await api.deleteQueryHistoryItem(historyId);
1603
+ if (state.editor.historyActiveId === Number(historyId)) {
1604
+ state.editor.historyActiveId = null;
1605
+ }
1606
+ if (state.editor.historySelectedId === Number(historyId)) {
1607
+ clearQueryHistorySelection();
1608
+ }
1609
+ pushToast(response.message || "Query history item deleted.", "muted");
1610
+ await refreshQueryHistoryState();
1611
+ return true;
1612
+ } catch (error) {
1613
+ state.editor.historyDetailError = normalizeError(error);
1614
+ emitChange();
1615
+ return false;
1616
+ }
979
1617
  }
980
1618
 
981
1619
  export async function selectStructureEntry(name) {
@@ -984,6 +1622,125 @@ export async function selectStructureEntry(name) {
984
1622
  await loadStructureDetail(++routeLoadVersion);
985
1623
  }
986
1624
 
1625
+ export function setTableDesignerSearchQuery(query) {
1626
+ state.tableDesigner.searchQuery = String(query ?? "");
1627
+ emitChange();
1628
+ }
1629
+
1630
+ export function updateCurrentTableDesignerField(field, value) {
1631
+ if (!state.tableDesigner.draft) {
1632
+ return;
1633
+ }
1634
+
1635
+ state.tableDesigner.draft = updateTableDesignerDraftField(
1636
+ state.tableDesigner.draft,
1637
+ field,
1638
+ value,
1639
+ getTableDesignerContext()
1640
+ );
1641
+ state.tableDesigner.saveError = null;
1642
+ emitChange();
1643
+ }
1644
+
1645
+ export function updateCurrentTableDesignerColumnField(columnId, field, value) {
1646
+ if (!state.tableDesigner.draft) {
1647
+ return;
1648
+ }
1649
+
1650
+ state.tableDesigner.draft = updateTableDesignerColumnField(
1651
+ state.tableDesigner.draft,
1652
+ columnId,
1653
+ field,
1654
+ value,
1655
+ getTableDesignerContext()
1656
+ );
1657
+ state.tableDesigner.saveError = null;
1658
+ emitChange();
1659
+ }
1660
+
1661
+ export function addCurrentTableDesignerColumn() {
1662
+ if (!state.tableDesigner.draft) {
1663
+ return null;
1664
+ }
1665
+
1666
+ const previousColumnIds = new Set(state.tableDesigner.draft.columns.map((column) => column.id));
1667
+ state.tableDesigner.draft = addTableDesignerColumn(
1668
+ state.tableDesigner.draft,
1669
+ getTableDesignerContext()
1670
+ );
1671
+ state.tableDesigner.saveError = null;
1672
+ emitChange();
1673
+
1674
+ const nextColumn = state.tableDesigner.draft.columns.find(
1675
+ (column) => !previousColumnIds.has(column.id)
1676
+ );
1677
+
1678
+ return nextColumn?.id ?? null;
1679
+ }
1680
+
1681
+ export function queueTableDesignerCsvImport(fileName, csvText) {
1682
+ try {
1683
+ const imported = createTableDesignerDraftFromCsvImport(
1684
+ { fileName, csvText },
1685
+ getTableDesignerContext()
1686
+ );
1687
+
1688
+ state.tableDesigner.pendingImportedDraft = imported.draft;
1689
+ state.tableDesigner.selectedTableName = null;
1690
+ state.tableDesigner.saveError = null;
1691
+ state.tableDesigner.error = null;
1692
+ emitChange();
1693
+ return imported;
1694
+ } catch (error) {
1695
+ pushToast(error?.message || "CSV import failed.", "alert");
1696
+ return null;
1697
+ }
1698
+ }
1699
+
1700
+ export function removeCurrentTableDesignerColumn(columnId) {
1701
+ if (!state.tableDesigner.draft) {
1702
+ return;
1703
+ }
1704
+
1705
+ state.tableDesigner.draft = removeTableDesignerColumn(
1706
+ state.tableDesigner.draft,
1707
+ columnId,
1708
+ getTableDesignerContext()
1709
+ );
1710
+ state.tableDesigner.saveError = null;
1711
+ emitChange();
1712
+ }
1713
+
1714
+ export async function saveCurrentTableDesignerDraft() {
1715
+ if (!state.tableDesigner.draft) {
1716
+ return null;
1717
+ }
1718
+
1719
+ state.tableDesigner.saving = true;
1720
+ state.tableDesigner.saveError = null;
1721
+ emitChange();
1722
+
1723
+ try {
1724
+ const response = await api.saveTableDesignerDraft({
1725
+ draft: state.tableDesigner.draft,
1726
+ });
1727
+
1728
+ state.tableDesigner.tables = response.data?.tables ?? state.tableDesigner.tables;
1729
+ state.tableDesigner.selectedTableName =
1730
+ response.data?.savedTableName ?? state.tableDesigner.selectedTableName;
1731
+ state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
1732
+ pushToast(response.message || "Table schema saved.", "success");
1733
+ return response.data?.savedTableName ?? null;
1734
+ } catch (error) {
1735
+ state.tableDesigner.saveError = normalizeError(error);
1736
+ emitChange();
1737
+ return null;
1738
+ } finally {
1739
+ state.tableDesigner.saving = false;
1740
+ emitChange();
1741
+ }
1742
+ }
1743
+
987
1744
  export function selectDataRow(index) {
988
1745
  const numericIndex = Number(index);
989
1746
 
@@ -1063,6 +1820,32 @@ export async function setDataPage(page) {
1063
1820
  }
1064
1821
  }
1065
1822
 
1823
+ export async function sortDataTableByColumn(columnName) {
1824
+ const normalizedColumn = String(columnName ?? "").trim();
1825
+
1826
+ if (
1827
+ !normalizedColumn ||
1828
+ !state.dataBrowser.table?.columns?.includes(normalizedColumn)
1829
+ ) {
1830
+ return;
1831
+ }
1832
+
1833
+ state.dataBrowser.sortDirection = getNextSortDirection(
1834
+ state.dataBrowser.sortColumn,
1835
+ state.dataBrowser.sortDirection,
1836
+ normalizedColumn
1837
+ );
1838
+ state.dataBrowser.sortColumn = normalizedColumn;
1839
+ state.dataBrowser.page = 1;
1840
+ state.dataBrowser.selectedRowIndex = null;
1841
+ state.dataBrowser.saveError = null;
1842
+ emitChange();
1843
+
1844
+ if (state.route.name === "data" && state.dataBrowser.selectedTable) {
1845
+ await loadDataTable(++routeLoadVersion);
1846
+ }
1847
+ }
1848
+
1066
1849
  export async function setDataPageSize(pageSize) {
1067
1850
  const normalizedPageSize = normalizeDataPageSize(pageSize, state.dataBrowser.pageSize);
1068
1851
 
@@ -1190,7 +1973,11 @@ export async function submitEditorRowUpdate(rowIndex, values) {
1190
1973
  );
1191
1974
  state.editor.result = {
1192
1975
  ...result,
1193
- rows: nextRows,
1976
+ rows: sortEditorResultRows(
1977
+ nextRows,
1978
+ state.editor.resultSortColumn,
1979
+ state.editor.resultSortDirection
1980
+ ),
1194
1981
  };
1195
1982
  state.editor.selectedRowIndex = null;
1196
1983
  invalidateDatabaseCaches();
@@ -1304,7 +2091,10 @@ export async function exportCurrentDataTableCsv() {
1304
2091
  emitChange();
1305
2092
 
1306
2093
  try {
1307
- await api.downloadTableCsv(tableName);
2094
+ await api.downloadTableCsv(tableName, {
2095
+ sortColumn: state.dataBrowser.sortColumn,
2096
+ sortDirection: state.dataBrowser.sortDirection,
2097
+ });
1308
2098
  pushToast(`CSV export started for ${tableName}.`, "success");
1309
2099
  return true;
1310
2100
  } catch (error) {
@@ -1317,10 +2107,39 @@ export async function exportCurrentDataTableCsv() {
1317
2107
  }
1318
2108
  }
1319
2109
 
2110
+ export function sortEditorResultsByColumn(columnName) {
2111
+ const normalizedColumn = String(columnName ?? "").trim();
2112
+ const result = state.editor.result;
2113
+
2114
+ if (!normalizedColumn || !result?.columns?.includes(normalizedColumn)) {
2115
+ return;
2116
+ }
2117
+
2118
+ const nextDirection = getNextSortDirection(
2119
+ state.editor.resultSortColumn,
2120
+ state.editor.resultSortDirection,
2121
+ normalizedColumn
2122
+ );
2123
+
2124
+ state.editor.result = {
2125
+ ...result,
2126
+ rows: sortEditorResultRows(result.rows ?? [], normalizedColumn, nextDirection),
2127
+ };
2128
+ state.editor.resultSortColumn = normalizedColumn;
2129
+ state.editor.resultSortDirection = nextDirection;
2130
+ state.editor.selectedRowIndex = null;
2131
+ state.editor.saveError = null;
2132
+ emitChange();
2133
+ }
2134
+
1320
2135
  export async function refreshCurrentRoute() {
1321
2136
  await loadRouteData(state.route);
1322
2137
  }
1323
2138
 
2139
+ export function showToast(message, tone = "muted") {
2140
+ pushToast(message, tone);
2141
+ }
2142
+
1324
2143
  export function getCurrentConnection(snapshot = state) {
1325
2144
  return snapshot.connections.active;
1326
2145
  }