sqlite-hub 1.1.1 → 1.1.2

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 (48) hide show
  1. package/README.md +17 -196
  2. package/bin/sqlite-hub.js +7 -2
  3. package/frontend/js/api.js +60 -0
  4. package/frontend/js/app.js +110 -0
  5. package/frontend/js/components/dropdownButton.js +92 -0
  6. package/frontend/js/components/modal.js +991 -876
  7. package/frontend/js/components/queryEditor.js +24 -15
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +2 -0
  13. package/frontend/js/store.js +383 -37
  14. package/frontend/js/utils/riskySql.js +165 -0
  15. package/frontend/js/views/backups.js +176 -0
  16. package/frontend/js/views/charts.js +1 -1
  17. package/frontend/js/views/documents.js +184 -154
  18. package/frontend/js/views/structure.js +26 -24
  19. package/frontend/styles/components.css +128 -0
  20. package/frontend/styles/tailwind.generated.css +1 -1
  21. package/frontend/styles/views.css +33 -21
  22. package/package.json +2 -1
  23. package/server/routes/backups.js +120 -0
  24. package/server/routes/connections.js +26 -3
  25. package/server/routes/externalApi.js +6 -2
  26. package/server/server.js +3 -1
  27. package/server/services/databaseCommandService.js +4 -3
  28. package/server/services/sqlite/backupService.js +497 -66
  29. package/server/services/sqlite/connectionManager.js +2 -2
  30. package/server/services/sqlite/importService.js +25 -0
  31. package/server/services/sqlite/sqlExecutor.js +2 -0
  32. package/server/services/storage/appStateStore.js +379 -88
  33. package/tests/api-token-auth.test.js +45 -2
  34. package/tests/backup-manager.test.js +140 -0
  35. package/tests/backups-view.test.js +64 -0
  36. package/tests/charts-height-preset-storage.test.js +60 -0
  37. package/tests/charts-route-state.test.js +144 -0
  38. package/tests/cli-service-delegation.test.js +97 -0
  39. package/tests/connection-removal.test.js +52 -0
  40. package/tests/database-command-service.test.js +37 -2
  41. package/tests/documents-view.test.js +132 -0
  42. package/tests/dropdown-button.test.js +75 -0
  43. package/tests/query-editor.test.js +28 -0
  44. package/tests/query-history-detail.test.js +37 -0
  45. package/tests/query-history-header.test.js +30 -0
  46. package/tests/risky-sql.test.js +30 -0
  47. package/tests/row-editor-null-values.test.js +24 -0
  48. package/tests/structure-view.test.js +56 -0
@@ -19,12 +19,10 @@ import {
19
19
  suggestQueryChartType,
20
20
  validateQueryChartConfig,
21
21
  } from './lib/queryCharts.js';
22
- import {
23
- MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
24
- MEDIA_TAGGING_DEFAULT_TAG_TABLE,
25
- } from './lib/mediaTaggingDefaults.js';
22
+ import { MEDIA_TAGGING_DEFAULT_MAPPING_TABLE, MEDIA_TAGGING_DEFAULT_TAG_TABLE } from './lib/mediaTaggingDefaults.js';
26
23
  import { buildTextExportFilename } from './utils/exportFilenames.js';
27
24
  import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
25
+ import { buildRiskySqlBackupContext, detectRiskySqlOperations } from './utils/riskySql.js';
28
26
 
29
27
  const listeners = new Set();
30
28
  const DEFAULT_SETTINGS = {
@@ -51,8 +49,10 @@ const UI_PREFERENCE_STORAGE_KEYS = {
51
49
  chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
52
50
  chartsQueryVisible: 'sqlite_hub_charts_query_visible',
53
51
  chartsResultsVisible: 'sqlite_hub_charts_results_visible',
52
+ chartsHeightPreset: 'sqlite_hub_charts_height_preset',
54
53
  tableDesignerTablesVisible: 'sqlite_hub_table_designer_tables_visible',
55
54
  tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
55
+ documentsVisible: 'sqlite_hub_documents_visible',
56
56
  documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
57
57
  documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
58
58
  mediaTaggingViewerVisible: 'sqlite_hub_media_tagging_viewer_visible',
@@ -228,6 +228,15 @@ function storeChartsHistoryTab(tab) {
228
228
  }
229
229
  }
230
230
 
231
+ function readStoredChartsHeightPreset(fallback = 'medium') {
232
+ return normalizeChartsHeightPreset(readStoredString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, fallback));
233
+ }
234
+
235
+ function storeChartsHeightPreset(preset) {
236
+ const normalizedPreset = normalizeChartsHeightPreset(preset);
237
+ storeString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, normalizedPreset);
238
+ }
239
+
231
240
  function readStoredQueryHistoryTab(fallback = 'recent') {
232
241
  try {
233
242
  return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
@@ -256,6 +265,12 @@ const state = {
256
265
  backupLoading: false,
257
266
  error: null,
258
267
  },
268
+ backups: {
269
+ items: [],
270
+ loading: false,
271
+ operationLoading: false,
272
+ error: null,
273
+ },
259
274
  settings: {
260
275
  data: { ...DEFAULT_SETTINGS },
261
276
  section: 'information',
@@ -344,7 +359,7 @@ const state = {
344
359
  historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
345
360
  detailPanelVisible: false,
346
361
  selectedHistoryId: null,
347
- chartHeightPreset: 'medium',
362
+ chartHeightPreset: readStoredChartsHeightPreset(),
348
363
  queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
349
364
  resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
350
365
  detail: null,
@@ -362,6 +377,7 @@ const state = {
362
377
  draftFilename: '',
363
378
  draftContent: '',
364
379
  dirty: false,
380
+ documentsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, true),
365
381
  editorVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, true),
366
382
  previewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, true),
367
383
  loading: false,
@@ -573,6 +589,7 @@ function clearQueryHistoryDetailState() {
573
589
  function requiresActiveDatabase(routeName) {
574
590
  return [
575
591
  'overview',
592
+ 'backups',
576
593
  'data',
577
594
  'editor',
578
595
  'editorResults',
@@ -611,7 +628,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
611
628
  return 0;
612
629
  }
613
630
 
614
- return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
631
+ return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
615
632
  }
616
633
 
617
634
  function normalizeDataPageSize(value, fallback = 50) {
@@ -818,8 +835,10 @@ function areRowIdentitiesEqual(left, right) {
818
835
  return false;
819
836
  }
820
837
 
821
- return JSON.stringify(left.columns ?? []) === JSON.stringify(right.columns ?? [])
822
- && JSON.stringify(left.values ?? null) === JSON.stringify(right.values ?? null);
838
+ return (
839
+ JSON.stringify(left.columns ?? []) === JSON.stringify(right.columns ?? []) &&
840
+ JSON.stringify(left.values ?? null) === JSON.stringify(right.values ?? null)
841
+ );
823
842
  }
824
843
 
825
844
  function getSelectedDataBrowserRow(snapshot = state) {
@@ -851,9 +870,7 @@ function clearDataBrowserRowSelectionState() {
851
870
 
852
871
  function resolveDataBrowserRowSelection(rowIndex, identity = null) {
853
872
  const hasRowIndex =
854
- rowIndex !== null &&
855
- rowIndex !== undefined &&
856
- (typeof rowIndex !== 'string' || rowIndex.trim() !== '');
873
+ rowIndex !== null && rowIndex !== undefined && (typeof rowIndex !== 'string' || rowIndex.trim() !== '');
857
874
  const numericIndex = hasRowIndex ? Number(rowIndex) : NaN;
858
875
 
859
876
  if (Number.isInteger(numericIndex) && numericIndex >= 0) {
@@ -871,7 +888,11 @@ function resolveDataBrowserRowSelection(rowIndex, identity = null) {
871
888
  const selectedRow = getSelectedDataBrowserRow();
872
889
  const selectedIdentity = identity ?? selectedRow?.__identity ?? null;
873
890
 
874
- if (!selectedRow?.__identity || !selectedIdentity || !areRowIdentitiesEqual(selectedRow.__identity, selectedIdentity)) {
891
+ if (
892
+ !selectedRow?.__identity ||
893
+ !selectedIdentity ||
894
+ !areRowIdentitiesEqual(selectedRow.__identity, selectedIdentity)
895
+ ) {
875
896
  return {
876
897
  row: null,
877
898
  rowIndex: null,
@@ -947,7 +968,7 @@ function resetChartsState() {
947
968
  state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
948
969
  state.charts.detailPanelVisible = false;
949
970
  state.charts.selectedHistoryId = null;
950
- state.charts.chartHeightPreset = 'medium';
971
+ state.charts.chartHeightPreset = readStoredChartsHeightPreset(state.charts.chartHeightPreset);
951
972
  state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
952
973
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
953
974
  state.charts.detail = null;
@@ -1066,7 +1087,9 @@ function syncQueryHistoryItem(updatedItem) {
1066
1087
 
1067
1088
  const updatedItemId = String(updatedItem.id);
1068
1089
 
1069
- state.editor.history = state.editor.history.map(entry => (String(entry.id) === updatedItemId ? updatedItem : entry));
1090
+ state.editor.history = state.editor.history.map(entry =>
1091
+ String(entry.id) === updatedItemId ? updatedItem : entry,
1092
+ );
1070
1093
 
1071
1094
  if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
1072
1095
  state.editor.historyDetail = updatedItem;
@@ -1195,7 +1218,10 @@ function setMissingDatabaseState() {
1195
1218
  state.tableDesigner.selectedTableName = null;
1196
1219
  state.tableDesigner.draft = null;
1197
1220
  state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
1198
- state.tableDesigner.sqlPreviewVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true);
1221
+ state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
1222
+ UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
1223
+ true,
1224
+ );
1199
1225
  state.tableDesigner.pendingImportedDraft = null;
1200
1226
  state.tableDesigner.saving = false;
1201
1227
  state.tableDesigner.searchQuery = '';
@@ -1344,6 +1370,30 @@ async function refreshSettingsState() {
1344
1370
  }
1345
1371
  }
1346
1372
 
1373
+ async function refreshBackupsState() {
1374
+ if (!state.connections.active) {
1375
+ state.backups.items = [];
1376
+ state.backups.error = null;
1377
+ emitChange();
1378
+ return;
1379
+ }
1380
+
1381
+ state.backups.loading = true;
1382
+ state.backups.error = null;
1383
+ emitChange();
1384
+
1385
+ try {
1386
+ const response = await api.getBackups();
1387
+ state.backups.items = response.data ?? [];
1388
+ state.backups.error = null;
1389
+ } catch (error) {
1390
+ state.backups.error = normalizeError(error);
1391
+ } finally {
1392
+ state.backups.loading = false;
1393
+ emitChange();
1394
+ }
1395
+ }
1396
+
1347
1397
  export async function createSettingsApiToken(name) {
1348
1398
  state.settings.tokenSaving = true;
1349
1399
  state.settings.error = null;
@@ -1387,9 +1437,7 @@ export async function checkSettingsAppVersion() {
1387
1437
 
1388
1438
  state.settings.versionCheck = result;
1389
1439
  pushToast(
1390
- result?.updateAvailable
1391
- ? `SQLite Hub v${result.latestVersion} is available.`
1392
- : 'SQLite Hub is up to date.',
1440
+ result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
1393
1441
  'success',
1394
1442
  );
1395
1443
  return result;
@@ -1671,11 +1719,17 @@ function getRequestedChartsHistoryId(route) {
1671
1719
  function resolveLoadableChartsHistoryId(route) {
1672
1720
  const requestedHistoryId = getRequestedChartsHistoryId(route);
1673
1721
 
1674
- if (!requestedHistoryId) {
1675
- return null;
1722
+ if (requestedHistoryId) {
1723
+ return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
1676
1724
  }
1677
1725
 
1678
- return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
1726
+ const selectedHistoryId = Number(state.charts.selectedHistoryId);
1727
+
1728
+ return Number.isInteger(selectedHistoryId) &&
1729
+ selectedHistoryId > 0 &&
1730
+ state.charts.queries.some(item => item.id === selectedHistoryId)
1731
+ ? selectedHistoryId
1732
+ : null;
1679
1733
  }
1680
1734
 
1681
1735
  function hasSettledChartsDetail(historyId) {
@@ -2391,7 +2445,10 @@ function invalidateDatabaseCaches(options = {}) {
2391
2445
  state.tableDesigner.selectedTableName = null;
2392
2446
  state.tableDesigner.draft = null;
2393
2447
  state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
2394
- state.tableDesigner.sqlPreviewVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true);
2448
+ state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
2449
+ UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
2450
+ true,
2451
+ );
2395
2452
  state.tableDesigner.pendingImportedDraft = null;
2396
2453
  state.tableDesigner.saving = false;
2397
2454
  state.tableDesigner.searchQuery = '';
@@ -2468,6 +2525,9 @@ async function loadRouteData(route, options = {}) {
2468
2525
  case 'overview':
2469
2526
  await loadOverview(version);
2470
2527
  return;
2528
+ case 'backups':
2529
+ await refreshBackupsState();
2530
+ return;
2471
2531
  case 'data':
2472
2532
  await loadData(version, route);
2473
2533
  return;
@@ -2505,7 +2565,7 @@ function pushToast(message, tone = 'muted') {
2505
2565
 
2506
2566
  window.setTimeout(() => {
2507
2567
  dismissToast(id);
2508
- }, 3600);
2568
+ }, 4500);
2509
2569
  }
2510
2570
 
2511
2571
  function withModalError(error) {
@@ -2845,7 +2905,9 @@ function normalizeDocumentInsertionRange(range = null) {
2845
2905
  const start = Number(range?.start);
2846
2906
  const end = Number(range?.end);
2847
2907
  const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
2848
- const normalizedEnd = Number.isInteger(end) ? Math.max(normalizedStart, Math.min(contentLength, end)) : normalizedStart;
2908
+ const normalizedEnd = Number.isInteger(end)
2909
+ ? Math.max(normalizedStart, Math.min(contentLength, end))
2910
+ : normalizedStart;
2849
2911
 
2850
2912
  return {
2851
2913
  start: normalizedStart,
@@ -3154,7 +3216,8 @@ export async function deleteCurrentDocument(options = {}) {
3154
3216
  try {
3155
3217
  await api.deleteDocument(documentId);
3156
3218
  state.documents.items = state.documents.items.filter(item => item.id !== documentId);
3157
- const nextDocument = state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
3219
+ const nextDocument =
3220
+ state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
3158
3221
  if (isSelectedDocument) {
3159
3222
  applyCurrentDocument(null);
3160
3223
  }
@@ -3244,9 +3307,7 @@ export function openDeleteDataRowModal(rowIndex) {
3244
3307
  const tableName = state.dataBrowser.selectedTable;
3245
3308
  const numericIndex = Number(rowIndex);
3246
3309
  const hasNumericIndex = Number.isInteger(numericIndex) && numericIndex >= 0;
3247
- const row = hasNumericIndex
3248
- ? state.dataBrowser.table?.rows?.[numericIndex] ?? null
3249
- : getSelectedDataBrowserRow();
3310
+ const row = hasNumericIndex ? (state.dataBrowser.table?.rows?.[numericIndex] ?? null) : getSelectedDataBrowserRow();
3250
3311
  const rowPreview = buildDeleteRowPreview(
3251
3312
  (state.dataBrowser.table?.columnMeta ?? [])
3252
3313
  .filter(column => column.visible)
@@ -3541,6 +3602,7 @@ export function setChartsHeightPreset(preset) {
3541
3602
  }
3542
3603
 
3543
3604
  state.charts.chartHeightPreset = nextPreset;
3605
+ storeChartsHeightPreset(nextPreset);
3544
3606
  emitChange();
3545
3607
  }
3546
3608
 
@@ -3691,10 +3753,36 @@ export async function chooseCreateDatabasePath() {
3691
3753
  }
3692
3754
 
3693
3755
  export async function submitImportSql(payload) {
3756
+ if (!payload?.skipSafety && !payload?.createNew && !payload?.targetConnectionId && !payload?.targetPath) {
3757
+ try {
3758
+ const preview = await api.previewImportSql(payload);
3759
+
3760
+ if (preview.data?.requiresSafetyBackup) {
3761
+ state.modal = {
3762
+ kind: 'backup-safety',
3763
+ operation: 'import',
3764
+ importPayload: payload,
3765
+ backupType: 'pre_import',
3766
+ backupName: 'Before import',
3767
+ notes: `SQL import, ${preview.data.statementCount ?? 0} statements, ${preview.data.sizeBytes ?? 0} bytes`,
3768
+ description: 'This SQL dump is large enough to change substantial data. Creating a backup allows you to restore the current state if the import goes wrong.',
3769
+ error: null,
3770
+ submitting: false,
3771
+ };
3772
+ emitChange();
3773
+ return null;
3774
+ }
3775
+ } catch (error) {
3776
+ withModalError(error);
3777
+ return null;
3778
+ }
3779
+ }
3780
+
3694
3781
  startModalSubmission();
3695
3782
 
3696
3783
  try {
3697
- const response = await api.importSql(payload);
3784
+ const { skipSafety, ...requestPayload } = payload ?? {};
3785
+ const response = await api.importSql(requestPayload);
3698
3786
  closeModalInternal();
3699
3787
  pushToast(response.message || 'SQL dump imported.', 'success');
3700
3788
  await refreshConnectionsState();
@@ -3770,28 +3858,257 @@ export async function removeConnection(id) {
3770
3858
  }
3771
3859
 
3772
3860
  export async function createActiveConnectionBackup() {
3861
+ openCreateBackupModal();
3862
+ return null;
3863
+ }
3864
+
3865
+ export function openCreateBackupModal(defaults = {}) {
3773
3866
  if (!state.connections.active) {
3774
3867
  pushToast('No active SQLite database selected for backup.', 'alert');
3868
+ return;
3869
+ }
3870
+
3871
+ state.modal = {
3872
+ kind: 'create-backup',
3873
+ name: defaults.name ?? '',
3874
+ notes: defaults.notes ?? '',
3875
+ backupType: defaults.type ?? 'manual',
3876
+ error: null,
3877
+ submitting: false,
3878
+ };
3879
+ emitChange();
3880
+ }
3881
+
3882
+ export async function submitCreateBackupConfirmation({ name = '', notes = '', type = 'manual' } = {}) {
3883
+ if (state.modal?.kind !== 'create-backup') {
3775
3884
  return null;
3776
3885
  }
3777
3886
 
3887
+ startModalSubmission();
3778
3888
  state.connections.backupLoading = true;
3779
- emitChange();
3889
+ state.backups.operationLoading = true;
3780
3890
 
3781
3891
  try {
3782
- const response = await api.createActiveConnectionBackup();
3783
- await refreshConnectionsState();
3892
+ const response = await api.createBackup({ name, notes, type });
3893
+ state.backups.items = [
3894
+ response.data,
3895
+ ...state.backups.items.filter(item => item.id !== response.data.id),
3896
+ ];
3897
+ closeModalInternal();
3784
3898
  pushToast(response.message || 'Backup created.', 'success');
3899
+ if (state.route.name === 'backups') {
3900
+ await refreshBackupsState();
3901
+ }
3785
3902
  return response.data;
3786
3903
  } catch (error) {
3787
- pushToast(normalizeError(error)?.message || 'Backup could not be created.', 'alert');
3904
+ withModalError(error);
3788
3905
  return null;
3789
3906
  } finally {
3790
3907
  state.connections.backupLoading = false;
3908
+ state.backups.operationLoading = false;
3909
+ emitChange();
3910
+ }
3911
+ }
3912
+
3913
+ export function openEditBackupNotesModal(backupId) {
3914
+ const backup = state.backups.items.find(item => String(item.id) === String(backupId));
3915
+
3916
+ if (!backup) {
3917
+ pushToast('The selected backup could not be loaded.', 'alert');
3918
+ return;
3919
+ }
3920
+
3921
+ state.modal = {
3922
+ kind: 'edit-backup-notes',
3923
+ backupId: backup.id,
3924
+ backupName: backup.name,
3925
+ notes: backup.notes ?? '',
3926
+ error: null,
3927
+ submitting: false,
3928
+ };
3929
+ emitChange();
3930
+ }
3931
+
3932
+ export async function submitEditBackupNotesConfirmation(notes = '') {
3933
+ if (state.modal?.kind !== 'edit-backup-notes') {
3934
+ return null;
3935
+ }
3936
+
3937
+ const backupId = state.modal.backupId;
3938
+ startModalSubmission();
3939
+ state.backups.operationLoading = true;
3940
+
3941
+ try {
3942
+ const response = await api.updateBackup(backupId, { notes });
3943
+ state.backups.items = state.backups.items.map(item =>
3944
+ String(item.id) === String(response.data.id) ? response.data : item
3945
+ );
3946
+ closeModalInternal();
3947
+ pushToast(response.message || 'Backup notes updated.', 'success');
3948
+ return response.data;
3949
+ } catch (error) {
3950
+ withModalError(error);
3951
+ return null;
3952
+ } finally {
3953
+ state.backups.operationLoading = false;
3791
3954
  emitChange();
3792
3955
  }
3793
3956
  }
3794
3957
 
3958
+ export function openDeleteBackupModal(backupId) {
3959
+ const backup = state.backups.items.find(item => String(item.id) === String(backupId));
3960
+
3961
+ if (!backup) {
3962
+ pushToast('The selected backup could not be loaded.', 'alert');
3963
+ return;
3964
+ }
3965
+
3966
+ state.modal = {
3967
+ kind: 'delete-backup',
3968
+ backupId: backup.id,
3969
+ backupName: backup.name,
3970
+ fileName: backup.fileName,
3971
+ error: null,
3972
+ submitting: false,
3973
+ };
3974
+ emitChange();
3975
+ }
3976
+
3977
+ export async function submitDeleteBackupConfirmation() {
3978
+ if (state.modal?.kind !== 'delete-backup') {
3979
+ return false;
3980
+ }
3981
+
3982
+ const backupId = state.modal.backupId;
3983
+ startModalSubmission();
3984
+ state.backups.operationLoading = true;
3985
+
3986
+ try {
3987
+ const response = await api.deleteBackup(backupId);
3988
+ state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
3989
+ closeModalInternal();
3990
+ pushToast(response.message || 'Backup deleted.', 'muted');
3991
+ return true;
3992
+ } catch (error) {
3993
+ withModalError(error);
3994
+ return false;
3995
+ } finally {
3996
+ state.backups.operationLoading = false;
3997
+ emitChange();
3998
+ }
3999
+ }
4000
+
4001
+ export async function downloadBackup(backupId) {
4002
+ state.backups.operationLoading = true;
4003
+ emitChange();
4004
+
4005
+ try {
4006
+ await api.downloadBackup(backupId);
4007
+ pushToast('Backup download started.', 'success');
4008
+ return true;
4009
+ } catch (error) {
4010
+ state.backups.error = normalizeError(error);
4011
+ pushToast(state.backups.error.message || 'Backup could not be downloaded.', 'alert');
4012
+ return false;
4013
+ } finally {
4014
+ state.backups.operationLoading = false;
4015
+ emitChange();
4016
+ }
4017
+ }
4018
+
4019
+ export function openRestoreBackupModal(backupId) {
4020
+ const backup = state.backups.items.find(item => String(item.id) === String(backupId));
4021
+
4022
+ if (!backup) {
4023
+ pushToast('The selected backup could not be loaded.', 'alert');
4024
+ return;
4025
+ }
4026
+
4027
+ state.modal = {
4028
+ kind: 'backup-safety',
4029
+ operation: 'restore',
4030
+ backupId: backup.id,
4031
+ backupName: backup.name,
4032
+ backupType: 'pre_restore',
4033
+ backupNameBeforeOperation: `Before restore of ${backup.name}`,
4034
+ description: 'Restoring replaces the current database file with this backup.',
4035
+ error: null,
4036
+ submitting: false,
4037
+ };
4038
+ emitChange();
4039
+ }
4040
+
4041
+ async function restoreBackupById(backupId) {
4042
+ state.backups.operationLoading = true;
4043
+
4044
+ try {
4045
+ const response = await api.restoreBackup(backupId);
4046
+ pushToast(response.message || 'Backup restored.', 'success');
4047
+ await Promise.all([refreshConnectionsState(), refreshBackupsState()]);
4048
+ invalidateDatabaseCaches();
4049
+ return response.data;
4050
+ } finally {
4051
+ state.backups.operationLoading = false;
4052
+ emitChange();
4053
+ }
4054
+ }
4055
+
4056
+ async function createSafetyBackupFromModal(modal) {
4057
+ const response = await api.createBackup({
4058
+ name: modal.backupNameBeforeOperation ?? modal.backupName,
4059
+ notes: modal.notes,
4060
+ type: modal.backupType,
4061
+ });
4062
+
4063
+ if (state.route.name === 'backups') {
4064
+ await refreshBackupsState();
4065
+ }
4066
+
4067
+ return response.data;
4068
+ }
4069
+
4070
+ export async function submitBackupSafetyChoice(choice) {
4071
+ const modal = state.modal;
4072
+
4073
+ if (modal?.kind !== 'backup-safety') {
4074
+ return false;
4075
+ }
4076
+
4077
+ if (choice === 'cancel') {
4078
+ closeModalInternal();
4079
+ return false;
4080
+ }
4081
+
4082
+ startModalSubmission();
4083
+
4084
+ try {
4085
+ if (choice === 'create') {
4086
+ await createSafetyBackupFromModal(modal);
4087
+ }
4088
+
4089
+ if (modal.operation === 'restore') {
4090
+ await restoreBackupById(modal.backupId);
4091
+ } else if (modal.operation === 'sql') {
4092
+ await executeCurrentQuery({ skipSafety: true });
4093
+ } else if (modal.operation === 'import') {
4094
+ await submitImportSql({
4095
+ ...(modal.importPayload ?? {}),
4096
+ skipSafety: true,
4097
+ });
4098
+ }
4099
+
4100
+ closeModalInternal();
4101
+ return true;
4102
+ } catch (error) {
4103
+ withModalError(error);
4104
+ return false;
4105
+ }
4106
+ }
4107
+
4108
+ export async function refreshBackups() {
4109
+ await refreshBackupsState();
4110
+ }
4111
+
3795
4112
  export async function openOverviewInFinder() {
3796
4113
  try {
3797
4114
  const response = await api.openOverviewInFinder();
@@ -3864,7 +4181,26 @@ export function setEditorTab(tab) {
3864
4181
  emitChange();
3865
4182
  }
3866
4183
 
3867
- export async function executeCurrentQuery() {
4184
+ export async function executeCurrentQuery(options = {}) {
4185
+ const riskyOperations = detectRiskySqlOperations(state.editor.sqlText);
4186
+
4187
+ if (!options.skipSafety && riskyOperations.length) {
4188
+ const context = buildRiskySqlBackupContext(riskyOperations);
4189
+ state.modal = {
4190
+ kind: 'backup-safety',
4191
+ operation: 'sql',
4192
+ backupType: context.type,
4193
+ backupName: context.name,
4194
+ notes: state.editor.sqlText.slice(0, 600),
4195
+ description: context.description,
4196
+ sqlText: state.editor.sqlText,
4197
+ error: null,
4198
+ submitting: false,
4199
+ };
4200
+ emitChange();
4201
+ return false;
4202
+ }
4203
+
3868
4204
  state.editor.executing = true;
3869
4205
  state.editor.lastExecutedSql = state.editor.sqlText;
3870
4206
  state.editor.error = null;
@@ -4140,6 +4476,12 @@ export function setDocumentsSearchQuery(query) {
4140
4476
  emitChange();
4141
4477
  }
4142
4478
 
4479
+ export function toggleDocumentsPanel() {
4480
+ state.documents.documentsVisible = state.documents.documentsVisible === false;
4481
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, state.documents.documentsVisible);
4482
+ emitChange();
4483
+ }
4484
+
4143
4485
  export function setTableDesignerSqlPreviewVisibility(visible) {
4144
4486
  const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
4145
4487
 
@@ -4707,8 +5049,7 @@ export function openDataRowByIdentity(tableName, identity) {
4707
5049
  export function preserveCurrentDataRowSelectionForReload() {
4708
5050
  const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
4709
5051
  const row = getSelectedDataBrowserRow();
4710
- const rowIndex =
4711
- typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
5052
+ const rowIndex = typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
4712
5053
 
4713
5054
  if (!tableName || !row) {
4714
5055
  return false;
@@ -5517,7 +5858,12 @@ export function getQueryMessages(snapshot = state) {
5517
5858
 
5518
5859
  return [
5519
5860
  ...snapshot.editor.result.statements.map(statement => ({
5520
- tone: statement.kind === 'resultSet' && statement.truncated ? 'warning' : statement.kind === 'resultSet' ? 'success' : inferStatusTone(statement.keyword),
5861
+ tone:
5862
+ statement.kind === 'resultSet' && statement.truncated
5863
+ ? 'warning'
5864
+ : statement.kind === 'resultSet'
5865
+ ? 'success'
5866
+ : inferStatusTone(statement.keyword),
5521
5867
  label: `${statement.keyword} #${statement.index + 1}`,
5522
5868
  value:
5523
5869
  statement.kind === 'resultSet'