sqlite-hub 0.4.0 → 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.
@@ -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,8 @@ const state = {
48
53
  deleting: false,
49
54
  page: 1,
50
55
  pageSize: 50,
56
+ sortColumn: null,
57
+ sortDirection: null,
51
58
  searchQuery: "",
52
59
  searchColumn: "",
53
60
  selectedRowIndex: null,
@@ -59,10 +66,25 @@ const state = {
59
66
  sqlText: "",
60
67
  history: [],
61
68
  historyLoading: false,
69
+ historyLoadingMore: false,
62
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,
63
83
  activeTab: "messages",
64
84
  executing: false,
65
85
  result: null,
86
+ resultSortColumn: null,
87
+ resultSortDirection: null,
66
88
  error: null,
67
89
  exportLoading: false,
68
90
  selectedRowIndex: null,
@@ -102,6 +124,26 @@ function normalizeError(error) {
102
124
  };
103
125
  }
104
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
+
105
147
  function requiresActiveDatabase(routeName) {
106
148
  return ["overview", "data", "editor", "editorResults", "structure"].includes(routeName);
107
149
  }
@@ -116,6 +158,18 @@ function normalizeDataPageSize(value, fallback = 50) {
116
158
  return fallback;
117
159
  }
118
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
+
119
173
  function canEditQueryResult(snapshot = state) {
120
174
  return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
121
175
  }
@@ -125,6 +179,73 @@ function resetDataBrowserSearch() {
125
179
  state.dataBrowser.searchColumn = "";
126
180
  }
127
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
+
128
249
  function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
129
250
  const nextRow = {
130
251
  ...existingRow,
@@ -175,6 +296,64 @@ function buildFallbackDeleteRowPreview(row) {
175
296
  );
176
297
  }
177
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
+
178
357
  function clearRouteSlices() {
179
358
  state.overview.error = null;
180
359
  state.dataBrowser.error = null;
@@ -195,6 +374,7 @@ function setMissingDatabaseState() {
195
374
  state.dataBrowser.selectedTable = null;
196
375
  state.dataBrowser.table = null;
197
376
  state.dataBrowser.page = 1;
377
+ resetDataBrowserSort();
198
378
  resetDataBrowserSearch();
199
379
  state.dataBrowser.selectedRowIndex = null;
200
380
  state.dataBrowser.exportLoading = false;
@@ -206,6 +386,8 @@ function setMissingDatabaseState() {
206
386
  state.structure.data = null;
207
387
  state.structure.detail = null;
208
388
  state.structure.error = error;
389
+
390
+ resetQueryHistoryState({ preserveSearch: false });
209
391
  }
210
392
 
211
393
  function syncRouteContext() {
@@ -213,6 +395,7 @@ function syncRouteContext() {
213
395
 
214
396
  if (route.name === "editorResults") {
215
397
  state.editor.activeTab = "results";
398
+ clearQueryHistoryDetailState();
216
399
  } else if (route.name === "editor" && state.editor.activeTab === "results") {
217
400
  state.editor.activeTab = "messages";
218
401
  }
@@ -284,19 +467,131 @@ async function refreshSettingsState() {
284
467
  }
285
468
  }
286
469
 
287
- async function refreshSqlHistoryState() {
288
- state.editor.historyLoading = true;
289
- 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;
290
483
  emitChange();
291
484
 
292
485
  try {
293
- const response = await api.getSqlHistory();
294
- 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
+ }
295
504
  } catch (error) {
296
- 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);
297
515
  } finally {
298
- 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 });
299
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
+ }
300
595
  }
301
596
  }
302
597
 
@@ -333,6 +628,8 @@ async function loadDataTable(version) {
333
628
  const tableName = state.dataBrowser.selectedTable;
334
629
  const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, 50);
335
630
  const page = Math.max(1, Number(state.dataBrowser.page) || 1);
631
+ const sortColumn = state.dataBrowser.sortColumn;
632
+ const sortDirection = normalizeSortDirection(state.dataBrowser.sortDirection);
336
633
 
337
634
  if (!tableName) {
338
635
  state.dataBrowser.table = null;
@@ -348,6 +645,8 @@ async function loadDataTable(version) {
348
645
  const response = await api.getDataTable(tableName, {
349
646
  limit: pageSize,
350
647
  offset: (page - 1) * pageSize,
648
+ sortColumn,
649
+ sortDirection,
351
650
  });
352
651
 
353
652
  if (version !== routeLoadVersion) {
@@ -357,6 +656,8 @@ async function loadDataTable(version) {
357
656
  state.dataBrowser.table = response.data ?? null;
358
657
  state.dataBrowser.pageSize = pageSize;
359
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;
360
661
  state.dataBrowser.searchColumn = response.data?.columns?.includes(state.dataBrowser.searchColumn)
361
662
  ? state.dataBrowser.searchColumn
362
663
  : (response.data?.columns?.[0] ?? "");
@@ -397,6 +698,7 @@ async function loadData(version, route) {
397
698
  if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
398
699
  if (requestedTableName !== state.dataBrowser.selectedTable) {
399
700
  state.dataBrowser.page = 1;
701
+ resetDataBrowserSort();
400
702
  resetDataBrowserSearch();
401
703
  }
402
704
  state.dataBrowser.selectedTable = requestedTableName;
@@ -406,6 +708,7 @@ async function loadData(version, route) {
406
708
  ) {
407
709
  state.dataBrowser.selectedTable = tables[0]?.name ?? null;
408
710
  state.dataBrowser.page = 1;
711
+ resetDataBrowserSort();
409
712
  resetDataBrowserSearch();
410
713
  }
411
714
 
@@ -561,7 +864,7 @@ async function loadRouteData(route) {
561
864
  return;
562
865
  case "editor":
563
866
  case "editorResults":
564
- await refreshSqlHistoryState();
867
+ await refreshQueryHistoryState();
565
868
  return;
566
869
  case "structure":
567
870
  await loadStructure(version);
@@ -624,7 +927,7 @@ export async function initializeApp() {
624
927
  await Promise.all([
625
928
  refreshConnectionsState(),
626
929
  refreshSettingsState(),
627
- refreshSqlHistoryState(),
930
+ refreshQueryHistoryState(),
628
931
  ]);
629
932
 
630
933
  state.ready = true;
@@ -882,6 +1185,17 @@ export async function createActiveConnectionBackup() {
882
1185
  }
883
1186
  }
884
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
+
885
1199
  export function setCurrentQuery(query) {
886
1200
  const nextQuery = String(query ?? "");
887
1201
  const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
@@ -896,12 +1210,22 @@ export function setCurrentQuery(query) {
896
1210
 
897
1211
  export function clearCurrentQuery() {
898
1212
  state.editor.sqlText = "";
1213
+ state.editor.result = null;
1214
+ resetEditorResultSort();
899
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
+ }
900
1223
  emitChange();
901
1224
  }
902
1225
 
903
1226
  export function clearEditorResults() {
904
1227
  state.editor.result = null;
1228
+ resetEditorResultSort();
905
1229
  state.editor.error = null;
906
1230
  state.editor.selectedRowIndex = null;
907
1231
  state.editor.saving = false;
@@ -928,10 +1252,11 @@ export async function executeCurrentQuery() {
928
1252
  try {
929
1253
  const response = await api.executeSql(state.editor.sqlText);
930
1254
  state.editor.result = response.data;
1255
+ resetEditorResultSort();
931
1256
  state.editor.error = null;
932
1257
  state.editor.activeTab = "results";
933
1258
  invalidateDatabaseCaches();
934
- await refreshSqlHistoryState();
1259
+ await refreshQueryHistoryState();
935
1260
  pushToast(
936
1261
  response.message || `Executed ${response.data.statementCount} SQL statement(s).`,
937
1262
  "success"
@@ -940,7 +1265,7 @@ export async function executeCurrentQuery() {
940
1265
  } catch (error) {
941
1266
  state.editor.error = normalizeError(error);
942
1267
  state.editor.activeTab = "messages";
943
- emitChange();
1268
+ await refreshQueryHistoryState();
944
1269
  return false;
945
1270
  } finally {
946
1271
  state.editor.executing = false;
@@ -948,14 +1273,14 @@ export async function executeCurrentQuery() {
948
1273
  }
949
1274
  }
950
1275
 
951
- export async function clearSqlHistoryStateAndData() {
1276
+ export async function clearQueryHistoryStateAndData() {
952
1277
  state.editor.historyLoading = true;
953
1278
  emitChange();
954
1279
 
955
1280
  try {
956
- const response = await api.clearSqlHistory();
957
- state.editor.history = response.data ?? [];
958
- 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");
959
1284
  return true;
960
1285
  } catch (error) {
961
1286
  state.editor.historyError = normalizeError(error);
@@ -967,15 +1292,156 @@ export async function clearSqlHistoryStateAndData() {
967
1292
  }
968
1293
  }
969
1294
 
970
- export function loadQueryFromHistory(id) {
971
- const historyEntry = state.editor.history.find((entry) => entry.id === id);
1295
+ export async function setQueryHistoryTab(tab) {
1296
+ const normalizedTab = normalizeQueryHistoryTab(String(tab ?? "").trim().toLowerCase());
972
1297
 
973
- if (!historyEntry) {
1298
+ if (state.editor.historyTab === normalizedTab) {
974
1299
  return;
975
1300
  }
976
1301
 
977
- state.editor.sqlText = historyEntry.sql;
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 ?? "");
978
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
+ }
1349
+
1350
+ export function clearQueryHistorySelection() {
1351
+ if (state.editor.historySelectedId === null && !state.editor.historyDetail) {
1352
+ return;
1353
+ }
1354
+
1355
+ clearQueryHistoryDetailState();
1356
+ emitChange();
1357
+ }
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
+ }
979
1445
  }
980
1446
 
981
1447
  export async function selectStructureEntry(name) {
@@ -1063,6 +1529,32 @@ export async function setDataPage(page) {
1063
1529
  }
1064
1530
  }
1065
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
+
1066
1558
  export async function setDataPageSize(pageSize) {
1067
1559
  const normalizedPageSize = normalizeDataPageSize(pageSize, state.dataBrowser.pageSize);
1068
1560
 
@@ -1190,7 +1682,11 @@ export async function submitEditorRowUpdate(rowIndex, values) {
1190
1682
  );
1191
1683
  state.editor.result = {
1192
1684
  ...result,
1193
- rows: nextRows,
1685
+ rows: sortEditorResultRows(
1686
+ nextRows,
1687
+ state.editor.resultSortColumn,
1688
+ state.editor.resultSortDirection
1689
+ ),
1194
1690
  };
1195
1691
  state.editor.selectedRowIndex = null;
1196
1692
  invalidateDatabaseCaches();
@@ -1304,7 +1800,10 @@ export async function exportCurrentDataTableCsv() {
1304
1800
  emitChange();
1305
1801
 
1306
1802
  try {
1307
- await api.downloadTableCsv(tableName);
1803
+ await api.downloadTableCsv(tableName, {
1804
+ sortColumn: state.dataBrowser.sortColumn,
1805
+ sortDirection: state.dataBrowser.sortDirection,
1806
+ });
1308
1807
  pushToast(`CSV export started for ${tableName}.`, "success");
1309
1808
  return true;
1310
1809
  } catch (error) {
@@ -1317,6 +1816,31 @@ export async function exportCurrentDataTableCsv() {
1317
1816
  }
1318
1817
  }
1319
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
+
1320
1844
  export async function refreshCurrentRoute() {
1321
1845
  await loadRouteData(state.route);
1322
1846
  }