diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 9f1cf70c42..d3842874ca 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -54,7 +54,7 @@ import { isMacOS, isWindows } from "@/lib/backend/platform"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { openQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; import { externalSqlFileOpenErrorMessage, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen"; -import type { ConnectionConfig, ObjectSourceKind, QueryTab } from "@/types/database"; +import type { ConnectionConfig, ObjectSourceKind, QueryTab, SavedSqlFile } from "@/types/database"; import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; import { isBrowserReloadShortcut, @@ -92,6 +92,7 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor import { apiUrl, webPath } from "@/lib/common/webPath"; import { APP_FONT_SANS_CSS_VAR, DATA_GRID_FONT_FAMILY_CSS_VAR, DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts"; import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory"; +import { savedSqlDefaultTargetForWrite } from "@/lib/savedSql/savedSqlExecutionTarget"; import { countActiveUpdateBlockingTasks } from "@/lib/app/appUpdateTaskGuard"; import { initSavedSqlEditorPositions } from "@/lib/app/savedSqlEditorPosition"; import { isSchemaAware, isSingleDatabase, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; @@ -826,6 +827,18 @@ async function saveExternalSqlPath(tab: QueryTab, options: { closeAfterSave?: bo } } +function savedSqlTargetForSave(tab: QueryTab, existing?: SavedSqlFile) { + return savedSqlDefaultTargetForWrite( + { + connectionId: tab.connectionId, + database: tab.database, + schema: tab.schema, + catalog: tab.catalog, + }, + existing, + ); +} + async function saveTabForCloseAll(tabId: string): Promise { const tab = queryStore.tabs.find((t) => t.id === tabId); if (!tab) return true; @@ -842,14 +855,15 @@ async function saveTabForCloseAll(tabId: string): Promise { if (await saveExternalSqlPath(tab)) return !queryStore.isTabDirty(tab); const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; + const target = savedSqlTargetForSave(tab, existing); try { const saved = await savedSqlStore.saveFile({ id: existing?.id, - connectionId: tab.connectionId, + connectionId: target.connectionId, folderId: existing?.folderId, name: existing?.name || defaultSavedSqlName(tab.title), - database: tab.database, - schema: tab.schema, + database: target.database, + schema: target.schema, sql: tab.sql, }); queryStore.linkSavedSql(tab.id, saved.id, saved.name); @@ -908,13 +922,14 @@ async function handleSaveTab(tabId: string) { } const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; if (existing) { + const target = savedSqlTargetForSave(tab, existing); const updated = await savedSqlStore.saveFile({ id: existing.id, - connectionId: tab.connectionId, + connectionId: target.connectionId, folderId: existing.folderId, name: existing.name, - database: tab.database, - schema: tab.schema, + database: target.database, + schema: target.schema, sql: tab.sql, }); queryStore.linkSavedSql(tab.id, updated.id, updated.name); @@ -943,13 +958,14 @@ async function openSaveSqlDialog() { if (await saveExternalSqlPath(tab)) return; const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; if (existing) { + const target = savedSqlTargetForSave(tab, existing); const updated = await savedSqlStore.saveFile({ id: existing.id, - connectionId: tab.connectionId, + connectionId: target.connectionId, folderId: existing.folderId, name: existing.name, - database: tab.database, - schema: tab.schema, + database: target.database, + schema: target.schema, sql: tab.sql, }); queryStore.linkSavedSql(tab.id, updated.id, updated.name); @@ -1036,13 +1052,15 @@ async function confirmSaveSqlToLibrary() { const name = saveSqlName.value.trim(); if (!tab || !tab.sql.trim() || !name) return; try { + const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; + const target = savedSqlTargetForSave(tab, existing); const saved = await savedSqlStore.saveFile({ id: tab.savedSqlId, - connectionId: tab.connectionId, + connectionId: target.connectionId, folderId: saveSqlFolderId.value === ROOT_SAVED_SQL_FOLDER ? undefined : saveSqlFolderId.value, name: defaultSavedSqlName(name), - database: tab.database, - schema: tab.schema, + database: target.database, + schema: target.schema, sql: tab.sql, }); queryStore.linkSavedSql(tab.id, saved.id, saved.name); @@ -1384,8 +1402,8 @@ async function openConnectionQuery(connectionId: string) { async function openSavedSqlFromWelcome(fileId: string) { const file = await savedSqlStore.ensureFileContent(fileId); if (!file) return; - queryStore.openSavedSql(file); - connectionStore.activeConnectionId = file.connectionId; + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; void savedSqlStore.recordFileUsage(file.id); toast(t("welcome.fileOpened", { name: file.name }), 2000); } @@ -1685,8 +1703,8 @@ async function handleQuickOpenSelect(item: any) { if (item.type === "sql_library_file" && item.sqlFileId) { const file = await savedSqlStore.ensureFileContent(item.sqlFileId); if (!file) return; - queryStore.openSavedSql(file); - connectionStore.activeConnectionId = file.connectionId; + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; void savedSqlStore.recordFileUsage(file.id); return; } diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 285232158c..8b08a11b1b 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -1404,7 +1404,10 @@ async function openMessageMention(mention: AiMessageMention) { try { if (mention.kind === "sqlFile") { const file = await savedSqlStore.ensureFileContent(mention.id); - if (file) queryStore.openSavedSql(file); + if (file) { + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; + } return; } await openTableTarget({ diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 1e10e7e9bd..506e95e96c 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -40,6 +40,7 @@ import { type OpenTabsRestoreMode, type SidebarObjectInfoMode, type SqlSemanticDiagnosticsMode, + type SavedSqlOpenTargetMode, type UpdateDownloadSource, type CustomThemeColors, type CustomTheme, @@ -306,6 +307,7 @@ const editSqlSemanticDiagnosticsEnabled = ref(settingsStore.editorSettings.sqlSe const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution); const editContinueOnErrorOnBatch = ref(settingsStore.editorSettings.continueOnErrorOnBatch); const editConfirmUnsavedSqlClose = ref(settingsStore.editorSettings.confirmUnsavedSqlClose); +const editSavedSqlOpenTargetMode = ref(settingsStore.editorSettings.savedSqlOpenTargetMode); const editAppLayout = ref(settingsStore.editorSettings.appLayout); const editTabLayout = ref(settingsStore.editorSettings.tabLayout); const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon); @@ -465,6 +467,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value, continueOnErrorOnBatch: editContinueOnErrorOnBatch.value, confirmUnsavedSqlClose: editConfirmUnsavedSqlClose.value, + savedSqlOpenTargetMode: editSavedSqlOpenTargetMode.value, appLayout: editAppLayout.value, tabLayout: editTabLayout.value, showColumnCommentsInHeader: editShowColumnCommentsInHeader.value, @@ -730,6 +733,7 @@ function syncEditorSettingsDraftFromStore() { editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution; editContinueOnErrorOnBatch.value = settingsStore.editorSettings.continueOnErrorOnBatch; editConfirmUnsavedSqlClose.value = settingsStore.editorSettings.confirmUnsavedSqlClose; + editSavedSqlOpenTargetMode.value = settingsStore.editorSettings.savedSqlOpenTargetMode; editAppLayout.value = settingsStore.editorSettings.appLayout; editTabLayout.value = settingsStore.editorSettings.tabLayout; editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader; @@ -930,6 +934,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editContinueOnErrorOnBatch.value = DEFAULT_EDITOR_SETTINGS.continueOnErrorOnBatch; editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; + editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); } else if (tab === "formatter") { @@ -1018,6 +1023,7 @@ function resetAllDefaults() { editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled; editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; + editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon; @@ -3334,6 +3340,24 @@ onUnmounted(cleanupPreviewEditor);
+
+
+ +

+ {{ editSavedSqlOpenTargetMode === "current" ? t("settings.savedSqlOpenTargetCurrentDescription") : t("settings.savedSqlOpenTargetSavedDescription") }} +

+
+ +
+
diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue index 09c7854490..29471d3b5a 100644 --- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue +++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue @@ -2,7 +2,7 @@ import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from "vue"; import type { CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; -import { ArrowDownWideNarrow, Download, FilePlus, FileText, FolderCog, FolderClosed, FolderOpen, FolderPlus, Library, LocateFixed, Pencil, Search, Trash2, Upload, X } from "@lucide/vue"; +import { ArrowDownWideNarrow, Download, FilePlus, FileText, FolderCog, FolderClosed, FolderOpen, FolderPlus, Library, LocateFixed, Pencil, Play, Search, Trash2, Upload, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import CustomContextMenu, { type ContextMenuItem as CtxMenuItem } from "@/components/ui/CustomContextMenu.vue"; @@ -18,6 +18,7 @@ import { useSettingsStore } from "@/stores/settingsStore"; import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus"; import { savedSqlFolderBranchFileCount } from "@/lib/savedSql/savedSqlFolderCounts"; import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName"; +import { savedSqlExecutionTargetFromTab, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { SavedSqlFile, SavedSqlFolder } from "@/types/database"; const { t } = useI18n(); @@ -456,7 +457,8 @@ async function openNewQueryInFolder(folder?: SavedSqlFolder) { database: "", sql: "", }); - queryStore.openSavedSql(file); + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; } // Batch selection state @@ -468,6 +470,11 @@ const lastClickedItemIndex = ref(null); // Unified index for both const activeItemId = ref(null); const activeItemType = ref<"file" | "folder" | null>(null); const activeSavedSqlId = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId)?.savedSqlId ?? null); +const hasCurrentSavedSqlExecutionTarget = computed(() => { + const activeTab = queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId); + const target = savedSqlExecutionTargetFromTab(activeTab); + return !!target && activeConnectionIds.value.has(target.connectionId); +}); watch( activeSavedSqlId, @@ -676,12 +683,12 @@ async function moveFilesToFolder(fileIds: string[], folderId?: string) { toast(t("sqlLibrary.moveSuccess", { count: movableIds.length }), 2000); } -async function openFile(file: SavedSqlFile) { +async function openFile(file: SavedSqlFile, targetMode?: SavedSqlOpenTargetMode) { if (suppressNextRowClick.value) return; const loadedFile = await savedSqlStore.ensureFileContent(file.id); if (!loadedFile) return; - queryStore.openSavedSql(loadedFile); - connectionStore.activeConnectionId = loadedFile.connectionId; + const tabId = queryStore.openSavedSql(loadedFile, { targetMode }); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? loadedFile.connectionId; void savedSqlStore.recordFileUsage(loadedFile.id); } @@ -884,6 +891,12 @@ const contextMenuItems = computed(() => { if ("sql" in target) { return [ { label: t("savedSql.open"), action: () => openFile(target), icon: FileText }, + { + label: t("sqlLibrary.openInCurrentDatabase"), + action: () => openFile(target, "current"), + icon: Play, + disabled: !hasCurrentSavedSqlExecutionTarget.value, + }, { label: t("sqlLibrary.exportFile"), action: () => exportSingleFile(target), icon: Upload }, { label: t("sqlLibrary.moveToFolder"), icon: FolderClosed, children: folderMoveMenuItems([target.id]) }, { label: "", separator: true }, diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 5836d03323..ab05886394 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -1083,8 +1083,8 @@ async function openSavedSqlFile() { if (node.type !== "saved-sql-file" || !node.savedSqlId) return; const file = await savedSqlStore.ensureFileContent(node.savedSqlId); if (!file) return; - queryStore.openSavedSql(file); - connectionStore.activeConnectionId = file.connectionId; + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; void savedSqlStore.recordFileUsage(file.id); } @@ -3744,8 +3744,8 @@ function savedSqlHistoryScopeForNode(node: TreeNode): SavedSqlHistoryScope | nul async function openSavedSqlHistoryFile(fileId: string) { const file = await savedSqlStore.ensureFileContent(fileId); if (!file) return; - queryStore.openSavedSql(file); - connectionStore.activeConnectionId = file.connectionId; + const tabId = queryStore.openSavedSql(file); + connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId; void savedSqlStore.recordFileUsage(file.id); } diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 8ffdfdefe6..9af2083c7d 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -144,6 +144,7 @@ export default { }, sqlLibrary: { title: "SQL Library", + openInCurrentDatabase: "Open in Current Database", empty: "No saved queries yet", emptyFolder: "Empty folder", unfiled: "Unfiled", @@ -4825,6 +4826,11 @@ export default { continueOnErrorOnBatchDescription: "When enabled, multi-statement execution continues after an error instead of stopping at the first failure.", confirmUnsavedSqlClose: "Confirm before closing unsaved SQL", confirmUnsavedSqlCloseDescription: "When disabled, SQL tabs with unsaved edits close or quit without the save confirmation dialog.", + savedSqlOpenTarget: "Default target for saved SQL", + savedSqlOpenTargetSaved: "Saved target", + savedSqlOpenTargetSavedDescription: "Use the connection, database, and schema stored with the SQL file.", + savedSqlOpenTargetCurrent: "Current tab", + savedSqlOpenTargetCurrentDescription: "Use the active tab’s connection, database, schema, and catalog when available; otherwise use the saved target.", prefillNewQueryWithSelect: "Prefill new query with SELECT *", prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM based on the active table tab or the table selected in the sidebar.", clickTableNavigationTarget: "Ctrl+Click Table Opens DDL", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 24827bcd5c..1278ca14ce 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -146,6 +146,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "Biblioteca SQL", + openInCurrentDatabase: "Abrir en la base de datos actual", empty: "Sin consultas guardadas", emptyFolder: "Carpeta vacía", unfiled: "Sin categoría", @@ -4586,6 +4587,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "Cuando está habilitado, la ejecución de múltiples sentencias continúa después de un error en lugar de detenerse en el primer fallo.", confirmUnsavedSqlClose: "Confirmar antes de cerrar SQL sin guardar", confirmUnsavedSqlCloseDescription: "Cuando se desactiva, las pestañas SQL con ediciones sin guardar se cierran o salen sin el diálogo de confirmación de guardado.", + savedSqlOpenTarget: "Destino predeterminado del SQL guardado", + savedSqlOpenTargetSaved: "Destino guardado", + savedSqlOpenTargetSavedDescription: "Usar la conexión, la base de datos y el esquema guardados con el archivo SQL.", + savedSqlOpenTargetCurrent: "Pestaña actual", + savedSqlOpenTargetCurrentDescription: "Usar la conexión, la base de datos, el esquema y el catálogo de la pestaña activa cuando estén disponibles; de lo contrario, usar el destino guardado.", prefillNewQueryWithSelect: "Rellenar nueva consulta con SELECT *", prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM según la pestaña de tabla activa o la tabla seleccionada en la barra lateral.", clickTableNavigationTarget: "Ctrl+Clic abre DDL de tabla", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 2c87bf8296..4dcb5c77d7 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -145,6 +145,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "Libreria SQL", + openInCurrentDatabase: "Apri nel database corrente", empty: "Nessuna query salvata", emptyFolder: "Cartella vuota", unfiled: "Non categorizzati", @@ -4586,6 +4587,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "Se abilitato, l'esecuzione di più istruzioni continua dopo un errore invece di interrompersi al primo fallimento.", confirmUnsavedSqlClose: "Conferma prima di chiudere SQL non salvato", confirmUnsavedSqlCloseDescription: "Se disattivato, le schede SQL con modifiche non salvate verranno chiuse o usciranno senza la finestra di conferma di salvataggio.", + savedSqlOpenTarget: "Destinazione predefinita per SQL salvato", + savedSqlOpenTargetSaved: "Destinazione salvata", + savedSqlOpenTargetSavedDescription: "Usa la connessione, il database e lo schema salvati con il file SQL.", + savedSqlOpenTargetCurrent: "Scheda corrente", + savedSqlOpenTargetCurrentDescription: "Usa la connessione, il database, lo schema e il catalogo della scheda attiva quando disponibili; altrimenti usa la destinazione salvata.", prefillNewQueryWithSelect: "Precompila nuova query con SELECT *", prefillNewQueryWithSelectDescription: "Alla creazione di una nuova query, precompila l'editor con SELECT * FROM in base alla scheda tabella attiva o alla tabella selezionata nella barra laterale.", clickTableNavigationTarget: "Ctrl+Clic apre DDL tabella", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 45f66c7b0c..6af5bbd9c8 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -146,6 +146,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "SQLライブラリ", + openInCurrentDatabase: "現在のデータベースで開く", empty: "保存されたクエリはまだありません", emptyFolder: "空のフォルダ", unfiled: "未分類", @@ -4577,6 +4578,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "有効にすると、複数SQL文の実行時にエラーが発生しても、最初の失敗で停止せずに後続の文を実行し続けます。", confirmUnsavedSqlClose: "未保存のSQLを閉じる前に確認", confirmUnsavedSqlCloseDescription: "無効時、未保存の編集があるSQLタブは保存確認ダイアログなしで閉じるか終了します。", + savedSqlOpenTarget: "保存済みSQLのデフォルト実行先", + savedSqlOpenTargetSaved: "保存時の実行先", + savedSqlOpenTargetSavedDescription: "SQLファイルに保存された接続、データベース、スキーマを使用します。", + savedSqlOpenTargetCurrent: "現在のタブ", + savedSqlOpenTargetCurrentDescription: "利用可能な場合は現在のタブの接続、データベース、スキーマ、カタログを使用し、それ以外は保存時の実行先を使用します。", prefillNewQueryWithSelect: "新規クエリに SELECT を自動入力", prefillNewQueryWithSelectDescription: "新規クエリ作成時、アクティブなテーブルタブまたはサイドバーで選択したテーブルに基づき、エディタに SELECT * FROM <テーブル名> を自動入力します。", clickTableNavigationTarget: "Ctrl+クリックでテーブル構造を開く", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 80a44fb78a..45a0ded754 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -141,6 +141,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "SQL 라이브러리", + openInCurrentDatabase: "현재 데이터베이스에서 열기", empty: "아직 저장된 쿼리가 없습니다", emptyFolder: "빈 폴더", unfiled: "분류 안 됨", @@ -4574,6 +4575,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "활성화하면 다중 구문 실행이 첫 번째 실패에서 중지하는 대신 오류 후에도 계속됩니다.", confirmUnsavedSqlClose: "저장되지 않은 SQL 닫기 전 확인", confirmUnsavedSqlCloseDescription: "비활성화하면 저장되지 않은 편집이 있는 SQL 탭이 저장 확인 대화상자 없이 닫히거나 종료됩니다.", + savedSqlOpenTarget: "저장된 SQL의 기본 실행 대상", + savedSqlOpenTargetSaved: "저장된 대상", + savedSqlOpenTargetSavedDescription: "SQL 파일에 저장된 연결, 데이터베이스 및 스키마를 사용합니다.", + savedSqlOpenTargetCurrent: "현재 탭", + savedSqlOpenTargetCurrentDescription: "가능한 경우 현재 탭의 연결, 데이터베이스, 스키마 및 카탈로그를 사용하고, 그렇지 않으면 저장된 대상을 사용합니다.", prefillNewQueryWithSelect: "새 쿼리에 SELECT * 미리 채우기", prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM
을 미리 채웁니다.", sqlVariableSyntax: "SQL 변수 및 자리표시자 치환", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 3e2ef8f7ef..22faca7b06 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -146,6 +146,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "Biblioteca SQL", + openInCurrentDatabase: "Abrir no banco de dados atual", empty: "Nenhuma consulta salva", emptyFolder: "Pasta vazia", unfiled: "Sem categoria", @@ -4588,6 +4589,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "Quando ativado, a execução de múltiplas instruções continua após um erro em vez de parar na primeira falha.", confirmUnsavedSqlClose: "Confirmar antes de fechar SQL não salvo", confirmUnsavedSqlCloseDescription: "Quando desativado, abas SQL com edições não salvas são fechadas ou o app é encerrado sem a caixa de diálogo de confirmação de salvamento.", + savedSqlOpenTarget: "Destino padrão do SQL salvo", + savedSqlOpenTargetSaved: "Destino salvo", + savedSqlOpenTargetSavedDescription: "Usar a conexão, o banco de dados e o esquema salvos com o arquivo SQL.", + savedSqlOpenTargetCurrent: "Aba atual", + savedSqlOpenTargetCurrentDescription: "Usar a conexão, o banco de dados, o esquema e o catálogo da aba ativa quando disponíveis; caso contrário, usar o destino salvo.", prefillNewQueryWithSelect: "Preencher nova consulta com SELECT *", prefillNewQueryWithSelectDescription: "Ao criar uma nova consulta, preenche o editor com SELECT * FROM com base na aba de tabela ativa ou na tabela selecionada na barra lateral.", clickTableNavigationTarget: "Ctrl+Clique abre DDL da tabela", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 401bbe3b5b..aa0c5d98ac 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -146,6 +146,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "SQL 库", + openInCurrentDatabase: "在当前库中打开", empty: "暂无已保存的查询", emptyFolder: "空文件夹", unfiled: "未分类", @@ -4824,6 +4825,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "开启后,多条 SQL 批量执行时遇到错误将继续执行后续语句,而非中断。", confirmUnsavedSqlClose: "关闭未保存 SQL 前弹出确认", confirmUnsavedSqlCloseDescription: "关闭后,有未保存内容的 SQL 标签页会直接关闭或退出,不再弹出保存确认。", + savedSqlOpenTarget: "打开已保存 SQL 时的默认目标", + savedSqlOpenTargetSaved: "保存时的目标", + savedSqlOpenTargetSavedDescription: "使用 SQL 文件保存时记录的连接、数据库和 Schema。", + savedSqlOpenTargetCurrent: "当前标签页", + savedSqlOpenTargetCurrentDescription: "优先使用当前标签页的连接、数据库、Schema 和 Catalog;不可用时回退到保存时的目标。", prefillNewQueryWithSelect: "新建查询时预填充 SELECT 语句", prefillNewQueryWithSelectDescription: "新建查询时,根据当前激活的数据表标签页或侧边栏选中的表,自动在编辑器中填充 SELECT * FROM <表名>。", clickTableNavigationTarget: "Ctrl+点击表名打开 DDL", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 095b56170f..b5e970956e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -146,6 +146,7 @@ export default withEnglishFallback({ }, sqlLibrary: { title: "SQL 庫", + openInCurrentDatabase: "在目前資料庫中開啟", empty: "暫無已儲存的查詢", emptyFolder: "空資料夾", unfiled: "未分類", @@ -4048,6 +4049,11 @@ export default withEnglishFallback({ continueOnErrorOnBatchDescription: "開啟後,多條 SQL 批次執行時遇到錯誤將繼續執行後續語句,而非中斷。", confirmUnsavedSqlClose: "關閉未儲存 SQL 前彈出確認", confirmUnsavedSqlCloseDescription: "關閉後,有未儲存內容的 SQL 分頁會直接關閉或結束,不再彈出儲存確認。", + savedSqlOpenTarget: "開啟已儲存 SQL 時的預設目標", + savedSqlOpenTargetSaved: "儲存時的目標", + savedSqlOpenTargetSavedDescription: "使用 SQL 檔案儲存時記錄的連線、資料庫和 Schema。", + savedSqlOpenTargetCurrent: "目前分頁", + savedSqlOpenTargetCurrentDescription: "優先使用目前分頁的連線、資料庫、Schema 和 Catalog;無法使用時回復至儲存時的目標。", prefillNewQueryWithSelect: "新建查詢時預填 SELECT 語句", prefillNewQueryWithSelectDescription: "新建查詢時,根據目前啟用的資料表分頁或側邊欄選取的資料表,自動在編輯器中填入 SELECT * FROM <資料表名稱>。", clickTableNavigationTarget: "Ctrl+點擊表名開啟 DDL", diff --git a/apps/desktop/src/lib/__tests__/savedSql/savedSqlExecutionTarget.spec.ts b/apps/desktop/src/lib/__tests__/savedSql/savedSqlExecutionTarget.spec.ts new file mode 100644 index 0000000000..9cb0c39458 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/savedSql/savedSqlExecutionTarget.spec.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { resolveSavedSqlExecutionTarget, savedSqlDefaultTargetForWrite, savedSqlExecutionTargetFromTab } from "@/lib/savedSql/savedSqlExecutionTarget"; + +const savedTarget = { + connectionId: "saved-connection", + database: "saved_database", + schema: "saved_schema", +}; + +describe("saved SQL execution targets", () => { + it("uses the saved target by default", () => { + expect( + resolveSavedSqlExecutionTarget(savedTarget, "saved", { + connectionId: "current-connection", + database: "current_database", + schema: "current_schema", + catalog: "current_catalog", + }), + ).toEqual(savedTarget); + }); + + it("uses the current tab target when requested", () => { + expect( + resolveSavedSqlExecutionTarget(savedTarget, "current", { + connectionId: "current-connection", + database: "current_database", + schema: "current_schema", + catalog: "current_catalog", + }), + ).toEqual({ + connectionId: "current-connection", + database: "current_database", + schema: "current_schema", + catalog: "current_catalog", + }); + }); + + it("falls back to the saved target when no current tab is available", () => { + expect(resolveSavedSqlExecutionTarget(savedTarget, "current")).toEqual(savedTarget); + expect(savedSqlExecutionTargetFromTab(undefined)).toBeUndefined(); + }); + + it("preserves the saved default target when an existing file is updated", () => { + expect( + savedSqlDefaultTargetForWrite( + { + connectionId: "runtime-connection", + database: "runtime_database", + schema: "runtime_schema", + }, + savedTarget, + ), + ).toEqual(savedTarget); + }); +}); diff --git a/apps/desktop/src/lib/savedSql/savedSqlExecutionTarget.ts b/apps/desktop/src/lib/savedSql/savedSqlExecutionTarget.ts new file mode 100644 index 0000000000..cd1131f6c7 --- /dev/null +++ b/apps/desktop/src/lib/savedSql/savedSqlExecutionTarget.ts @@ -0,0 +1,45 @@ +import type { QueryTab, SavedSqlFile } from "@/types/database"; + +export type SavedSqlOpenTargetMode = "saved" | "current"; + +export interface SavedSqlExecutionTarget { + connectionId: string; + database: string; + schema?: string; + catalog?: string; +} + +type SavedSqlFileTarget = Pick; +type QueryTabTarget = Pick; + +export function savedSqlExecutionTargetFromFile(file: SavedSqlFileTarget): SavedSqlExecutionTarget { + return { + connectionId: file.connectionId, + database: file.database, + schema: file.schema, + }; +} + +export function savedSqlExecutionTargetFromTab(tab: QueryTabTarget | undefined): SavedSqlExecutionTarget | undefined { + if (!tab?.connectionId) return undefined; + return { + connectionId: tab.connectionId, + database: tab.database, + schema: tab.schema, + catalog: tab.catalog, + }; +} + +export function resolveSavedSqlExecutionTarget(file: SavedSqlFileTarget, mode: SavedSqlOpenTargetMode, currentTarget?: SavedSqlExecutionTarget): SavedSqlExecutionTarget { + if (mode === "current" && currentTarget) return { ...currentTarget }; + return savedSqlExecutionTargetFromFile(file); +} + +export function savedSqlDefaultTargetForWrite(currentTarget: SavedSqlExecutionTarget, existingFile?: SavedSqlFileTarget): SavedSqlFileTarget { + if (existingFile) return savedSqlExecutionTargetFromFile(existingFile); + return { + connectionId: currentTarget.connectionId, + database: currentTarget.database, + schema: currentTarget.schema, + }; +} diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index e664006190..72cbbdbee2 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -21,6 +21,7 @@ function makeSettings(overrides: Partial = {}): EditorSettings { confirmDangerousSqlExecution: true, continueOnErrorOnBatch: false, confirmUnsavedSqlClose: true, + savedSqlOpenTargetMode: "saved", objectBrowserViewMode: "list", sqlVariableSyntaxOverrides: {}, tabLayout: "scroll", @@ -36,6 +37,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { it("includes the table-open page size", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tableOpenPageSize"); }); + + it("includes the saved SQL open target mode", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("savedSqlOpenTargetMode"); + }); }); describe("editorSettingsDraftFromSettings", () => { @@ -54,6 +59,10 @@ describe("editorSettingsDraftFromSettings", () => { delete (settings as Partial).tableOpenPageSize; expect(editorSettingsDraftFromSettings(settings).tableOpenPageSize).toBe(100); }); + + it("maps the saved SQL open target mode", () => { + expect(editorSettingsDraftFromSettings(makeSettings({ savedSqlOpenTargetMode: "current" })).savedSqlOpenTargetMode).toBe("current"); + }); }); describe("normalizeTableOpenPageSizeDraft", () => { @@ -94,6 +103,14 @@ describe("editorSettingsDraftChanged", () => { draft.tableOpenPageSize = Number.NaN; expect(editorSettingsDraftChanged(draft, base)).toBe(false); }); + + it("detects a saved SQL open target change", () => { + const settings = makeSettings({ savedSqlOpenTargetMode: "saved" }); + const draft = editorSettingsDraftFromSettings(settings); + const base = editorSettingsDraftFromSettings(settings); + draft.savedSqlOpenTargetMode = "current"; + expect(editorSettingsDraftChanged(draft, base)).toBe(true); + }); }); describe("editorSettingsPatchFromDraft", () => { @@ -121,6 +138,14 @@ describe("editorSettingsPatchFromDraft", () => { draft.tableOpenPageSize = 200000.9; expect(editorSettingsPatchFromDraft(draft, base).tableOpenPageSize).toBe(100000); }); + + it("includes the saved SQL open target when changed", () => { + const settings = makeSettings({ savedSqlOpenTargetMode: "saved" }); + const draft = editorSettingsDraftFromSettings(settings); + const base = editorSettingsDraftFromSettings(settings); + draft.savedSqlOpenTargetMode = "current"; + expect(editorSettingsPatchFromDraft(draft, base).savedSqlOpenTargetMode).toBe("current"); + }); }); describe("EDITOR_SETTINGS_DRAFT_KEYS - tabLayout", () => { diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index f5fd119c39..0b4739c200 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -23,6 +23,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "sqlSemanticDiagnosticsMode", "confirmDangerousSqlExecution", "confirmUnsavedSqlClose", + "savedSqlOpenTargetMode", "appLayout", "tabLayout", "showColumnCommentsInHeader", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 6b4f750a00..d4775e6509 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -64,6 +64,7 @@ import { appendDebugLog } from "@/lib/backend/debugLog"; import { formatError } from "@/lib/backend/errorUtils"; import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition"; import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName"; +import { resolveSavedSqlExecutionTarget, savedSqlExecutionTargetFromTab, type SavedSqlExecutionTarget, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage"; import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint"; import type { SavedSqlFile } from "@/types/database"; @@ -95,6 +96,10 @@ interface BuildQueryResultExportRequestOptions { exportColumnTypes?: Array; } +interface OpenSavedSqlOptions { + targetMode?: SavedSqlOpenTargetMode; +} + type DroppedTableObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW"; interface DroppedTableObjectTarget { @@ -2437,7 +2442,26 @@ export const useQueryStore = defineStore("query", () => { refreshExternalSqlFileTitles(); } - function openSavedSql(file: SavedSqlFile) { + function currentSavedSqlExecutionTarget(): SavedSqlExecutionTarget | undefined { + const activeTab = tabs.value.find((tab) => tab.id === activeTabId.value); + const target = savedSqlExecutionTargetFromTab(activeTab); + if (!target || !useConnectionStore().getConfig(target.connectionId)) return undefined; + return target; + } + + function applySavedSqlExecutionTarget(tab: QueryTab, target: SavedSqlExecutionTarget) { + updateConnection(tab.id, target.connectionId, target.database); + if (tab.catalog !== target.catalog || tab.database !== target.database) { + if (tab.catalog !== undefined || target.catalog !== undefined) updateCatalog(tab.id, target.catalog, target.database); + else updateDatabase(tab.id, target.database); + } + updateSchema(tab.id, target.schema); + } + + function openSavedSql(file: SavedSqlFile, options: OpenSavedSqlOptions = {}) { + const targetMode = options.targetMode ?? useSettingsStore().editorSettings.savedSqlOpenTargetMode; + const currentTarget = targetMode === "current" ? currentSavedSqlExecutionTarget() : undefined; + const target = resolveSavedSqlExecutionTarget(file, targetMode, currentTarget); const existing = tabs.value.find((tab) => tab.savedSqlId === file.id); if (existing) { persistSavedSqlEditorPosition(existing); @@ -2448,6 +2472,7 @@ export const useQueryStore = defineStore("query", () => { existing.editorSelection = restored.selection; existing.editorViewport = restored.viewport; } + applySavedSqlExecutionTarget(existing, target); switchTab(existing.id); return existing.id; } @@ -2458,9 +2483,10 @@ export const useQueryStore = defineStore("query", () => { id, title: file.name, customTitle: true, - connectionId: file.connectionId, - database: file.database, - schema: file.schema, + connectionId: target.connectionId, + database: target.database, + schema: target.schema, + catalog: target.catalog, sql: file.sql, savedSqlId: file.id, originalSql: file.sql, @@ -2484,9 +2510,6 @@ export const useQueryStore = defineStore("query", () => { const file = await savedSqlStore.ensureFileContent(tab.savedSqlId!); if (!file) continue; tab.title = tab.customTitle ? tab.title : file.name; - tab.connectionId = file.connectionId; - tab.database = file.database; - tab.schema = file.schema; tab.sql = file.sql; tab.originalSql = file.sql; const restored = restoreSavedSqlEditorPosition(file.id, file.sql); @@ -2582,22 +2605,6 @@ export const useQueryStore = defineStore("query", () => { tab.resultSortedSql = undefined; clearExplain(tab); tab.tableMeta = undefined; - - // Sync connection change back to the saved SQL file if this tab is linked - if (tab.savedSqlId) { - const savedSqlStore = useSavedSqlStore(); - void savedSqlStore.ensureFileContent(tab.savedSqlId).then((existing) => { - if (!existing) return; - return savedSqlStore.saveFile({ - id: existing.id, - connectionId, - name: existing.name, - database, - schema: existing.schema, - sql: existing.sql, - }); - }); - } } function clearInvalidDataTabSortState(tab: QueryTab, columns: NonNullable["columns"]): boolean { diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 14ab410e69..cbebdbc990 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -10,6 +10,7 @@ import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableN import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax"; +import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick"; import type { SqlSnippet, TableInfoTab } from "@/types/database"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; @@ -20,6 +21,7 @@ import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeS import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection, AiModelEffortPreference } from "@/types/ai"; export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection }; +export type { SavedSqlOpenTargetMode }; export interface DesktopSettings { show_tray_icon: boolean; @@ -452,6 +454,7 @@ export interface EditorSettings { sqlSemanticDiagnosticsEnabled: boolean; confirmDangerousSqlExecution: boolean; confirmUnsavedSqlClose: boolean; + savedSqlOpenTargetMode: SavedSqlOpenTargetMode; compactTabTitle: boolean; tabLayout: TabLayoutMode; appLayout: "separated" | "classic"; @@ -628,6 +631,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { sqlSemanticDiagnosticsEnabled: SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED, confirmDangerousSqlExecution: true, confirmUnsavedSqlClose: true, + savedSqlOpenTargetMode: "saved", compactTabTitle: false, tabLayout: "scroll", appLayout: "classic", @@ -935,6 +939,7 @@ export function normalizeEditorSettings(settings: Partial, exist sqlSemanticDiagnosticsEnabled: sqlSemanticDiagnosticsEnabledForMode(sqlSemanticDiagnosticsMode), confirmDangerousSqlExecution: settings.confirmDangerousSqlExecution ?? DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution, confirmUnsavedSqlClose: settings.confirmUnsavedSqlClose ?? DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose, + savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode, compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle, tabLayout: normalizeTabLayout(settings.tabLayout), appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout, @@ -1416,6 +1421,7 @@ export const useSettingsStore = defineStore("settings", () => { } if (partial.confirmDangerousSqlExecution !== undefined) editorSettings.value.confirmDangerousSqlExecution = partial.confirmDangerousSqlExecution; if (partial.confirmUnsavedSqlClose !== undefined) editorSettings.value.confirmUnsavedSqlClose = partial.confirmUnsavedSqlClose; + if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved"; if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle; if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout); if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout; diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 8c6e4a8d7f..cb3c6bed20 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from "node:assert"; import { afterEach, test } from "vitest"; import { createPinia, disposePinia, getActivePinia, setActivePinia } from "pinia"; -import { isReactive, toRaw } from "vue"; +import { isReactive, nextTick, toRaw } from "vue"; import { decodeQueryResultArchive } from "../../apps/desktop/src/lib/query/queryResultArchive.ts"; import { analyzeEditableQueryEditability } from "../../apps/desktop/src/lib/sql/sqlAnalysis.ts"; import { resultSqlForGrid } from "../../apps/desktop/src/lib/tabs/tabPresentation.ts"; @@ -10,6 +10,7 @@ import { useExportTracker } from "../../apps/desktop/src/composables/useExportTr import { resolveHistorySqlRestoreTarget } from "../../apps/desktop/src/lib/history/historyRestoreTarget.ts"; import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts"; import { useQueryStore } from "../../apps/desktop/src/stores/queryStore.ts"; +import { useSavedSqlStore } from "../../apps/desktop/src/stores/savedSqlStore.ts"; import { useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts"; import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts"; import type { HistoryEntry } from "../../apps/desktop/src/lib/backend/tauri.ts"; @@ -291,6 +292,189 @@ test("clean saved SQL tabs persist without duplicating SQL text", async () => { } }); +test("saved SQL opens with its saved execution target by default", async () => { + const restoreStorage = installMemoryStorage(); + try { + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + connectionStore.connections = [conn("saved-connection"), conn("current-connection")]; + const store = useQueryStore(); + store.createTab("current-connection", "current_database", "current.sql", "query", "current_schema", undefined, "current_catalog"); + + const tabId = store.openSavedSql({ + id: "saved-target", + connectionId: "saved-connection", + name: "saved.sql", + database: "saved_database", + schema: "saved_schema", + sql: "SELECT 1;", + sqlLoaded: true, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + }); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.equal(tab?.connectionId, "saved-connection"); + assert.equal(tab?.database, "saved_database"); + assert.equal(tab?.schema, "saved_schema"); + assert.equal(tab?.catalog, undefined); + } finally { + await nextTick(); + restoreStorage(); + } +}); + +test("saved SQL can follow the current tab execution target", async () => { + const restoreStorage = installMemoryStorage(); + try { + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + connectionStore.connections = [conn("saved-connection"), conn("current-connection")]; + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ savedSqlOpenTargetMode: "current" }); + const store = useQueryStore(); + store.createTab("current-connection", "current_database", "current.sql", "query", "current_schema", undefined, "current_catalog"); + + const tabId = store.openSavedSql({ + id: "current-target", + connectionId: "saved-connection", + name: "saved.sql", + database: "saved_database", + schema: "saved_schema", + sql: "SELECT 1;", + sqlLoaded: true, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + }); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.equal(tab?.connectionId, "current-connection"); + assert.equal(tab?.database, "current_database"); + assert.equal(tab?.schema, "current_schema"); + assert.equal(tab?.catalog, "current_catalog"); + } finally { + await nextTick(); + restoreStorage(); + } +}); + +test("saved SQL supports a one-off current-tab target override", async () => { + const restoreStorage = installMemoryStorage(); + try { + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + connectionStore.connections = [conn("saved-connection"), conn("current-connection")]; + const store = useQueryStore(); + store.createTab("current-connection", "tenant_42", "tenant.sql", "query", "tenant", undefined, "analytics"); + + const tabId = store.openSavedSql( + { + id: "current-target-override", + connectionId: "saved-connection", + name: "saved.sql", + database: "saved_database", + sql: "SELECT 1;", + sqlLoaded: true, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + }, + { targetMode: "current" }, + ); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.deepEqual( + { + connectionId: tab?.connectionId, + database: tab?.database, + schema: tab?.schema, + catalog: tab?.catalog, + }, + { + connectionId: "current-connection", + database: "tenant_42", + schema: "tenant", + catalog: "analytics", + }, + ); + } finally { + await nextTick(); + restoreStorage(); + } +}); + +test("hydrating saved SQL content preserves its restored runtime target", async () => { + const restoreStorage = installMemoryStorage(); + try { + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + connectionStore.connections = [conn("saved-connection"), conn("current-connection")]; + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ savedSqlOpenTargetMode: "current" }); + const store = useQueryStore(); + const savedSqlStore = useSavedSqlStore(); + const file = { + id: "hydrate-current-target", + connectionId: "saved-connection", + name: "saved.sql", + database: "saved_database", + schema: "saved_schema", + sql: "SELECT 1;", + sqlLoaded: true, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + }; + savedSqlStore.files = [file]; + store.createTab("current-connection", "runtime_database", "current.sql", "query", "runtime_schema"); + const tabId = store.openSavedSql(file); + const tab = store.tabs.find((item) => item.id === tabId)!; + tab.sql = ""; + + await store.hydrateSavedSqlTabs(); + + assert.equal(tab.connectionId, "current-connection"); + assert.equal(tab.database, "runtime_database"); + assert.equal(tab.schema, "runtime_schema"); + assert.equal(tab.sql, "SELECT 1;"); + } finally { + await nextTick(); + restoreStorage(); + } +}); + +test("changing a saved SQL tab target does not rebind its saved default", async () => { + const restoreStorage = installMemoryStorage(); + try { + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + connectionStore.connections = [conn("saved-connection"), conn("runtime-connection")]; + const savedSqlStore = useSavedSqlStore(); + const file = { + id: "keep-saved-target", + connectionId: "saved-connection", + name: "saved.sql", + database: "saved_database", + schema: "saved_schema", + sql: "SELECT 1;", + sqlLoaded: true, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + }; + savedSqlStore.files = [file]; + const store = useQueryStore(); + const tabId = store.openSavedSql(file); + + store.updateConnection(tabId, "runtime-connection", "runtime_database"); + store.updateSchema(tabId, "runtime_schema"); + + assert.equal(savedSqlStore.getFile(file.id)?.connectionId, "saved-connection"); + assert.equal(savedSqlStore.getFile(file.id)?.database, "saved_database"); + assert.equal(savedSqlStore.getFile(file.id)?.schema, "saved_schema"); + } finally { + await nextTick(); + restoreStorage(); + } +}); + test("dirty saved SQL tabs keep unsaved edits in open tab persistence", async () => { const restoreStorage = installMemoryStorage(); try { diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index 929673e0a8..a08633a376 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -262,6 +262,21 @@ test("defaults unsaved SQL close confirmation to enabled", () => { assert.equal(normalizeEditorSettings({ confirmUnsavedSqlClose: false }).confirmUnsavedSqlClose, false); }); +test("defaults saved SQL to its saved target and normalizes persisted target modes", () => { + assert.equal(DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode, "saved"); + assert.equal(normalizeEditorSettings({}).savedSqlOpenTargetMode, "saved"); + assert.equal(normalizeEditorSettings({ savedSqlOpenTargetMode: "current" }).savedSqlOpenTargetMode, "current"); + assert.equal(normalizeEditorSettings({ savedSqlOpenTargetMode: "invalid" as any }).savedSqlOpenTargetMode, "saved"); +}); + +test("shows the saved SQL target selector in Editor settings", () => { + const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8"); + + assert.match(source, /id="editor-saved-sql-open-target"/); + assert.match(source, //); + assert.match(source, //); +}); + test("defaults Vim mode to off and preserves saved booleans", () => { assert.equal(DEFAULT_EDITOR_SETTINGS.vimModeEnabled, false); assert.equal(normalizeEditorSettings({}).vimModeEnabled, false); diff --git a/packages/app-tests/sqlLibraryImportEncoding.test.ts b/packages/app-tests/sqlLibraryImportEncoding.test.ts index bcd162df2c..ad52067c8a 100644 --- a/packages/app-tests/sqlLibraryImportEncoding.test.ts +++ b/packages/app-tests/sqlLibraryImportEncoding.test.ts @@ -12,3 +12,9 @@ test("SQL library imports use the charset-aware external SQL reader", () => { assert.match(handler, /await api\.readExternalSqlFile\(path\)/); assert.doesNotMatch(handler, /readTextFile/); }); + +test("SQL library file menu can open a file with the current tab target", () => { + assert.match(source, /label: t\("sqlLibrary\.openInCurrentDatabase"\)/); + assert.match(source, /action: \(\) => openFile\(target, "current"\)/); + assert.match(source, /disabled: !hasCurrentSavedSqlExecutionTarget\.value/); +});