From fbaf556f3a841672c49ac93c481d72b3960900a6 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 14 Aug 2026 23:54:54 +0000 Subject: [PATCH] feat(editor): add SQL substitution master switch Closes #5759 --- .../editor/EditorSettingsDialog.vue | 37 +++++++++++++------ .../src/components/editor/QueryEditor.vue | 2 +- .../__tests__/useSqlExecution.spec.ts | 27 ++++++++++++++ .../src/composables/useSqlExecution.ts | 4 +- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../__tests__/sql/sqlVariableSyntax.spec.ts | 27 ++++++++++++++ .../__tests__/editorSettingsDraft.spec.ts | 26 +++++++++++++ .../src/lib/settings/editorSettingsDraft.ts | 1 + apps/desktop/src/lib/sql/sqlVariableSyntax.ts | 19 ++++++++-- .../stores/__tests__/settingsStore.spec.ts | 35 ++++++++++++++++++ apps/desktop/src/stores/settingsStore.ts | 4 ++ 18 files changed, 171 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 880b78d937..939729189f 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -332,6 +332,7 @@ const editAutoCalculateTotalRows = ref(settingsStore.editorSettings.autoCalculat const editFlatteningMultiLineText = ref(settingsStore.editorSettings.flatteningMultiLineText); const editTableColumnTemplateRows = ref(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields)); const editTableColumnTemplateDatabaseType = ref(TABLE_COLUMN_TEMPLATE_DATABASE_TYPES[0] ?? "mysql"); +const editSqlVariableSubstitutionEnabled = ref(settingsStore.editorSettings.sqlVariableSubstitutionEnabled); const editSqlVariableSyntaxOverrides = ref(normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides)); const editSqlVariableSyntaxDatabaseType = ref(SQL_VARIABLE_SYNTAX_DATABASE_TYPES[0] ?? "mysql"); @@ -534,6 +535,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { updateDownloadSource: editUpdateDownloadSource.value, toolbarItems: { ...editToolbarItems.value }, snippets: editSnippets.value, + sqlVariableSubstitutionEnabled: editSqlVariableSubstitutionEnabled.value, sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides.value, clickTableNavigationTarget: editClickTableNavigationTarget.value, }; @@ -813,6 +815,7 @@ function syncEditorSettingsDraftFromStore() { editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource; editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems }; editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet); + editSqlVariableSubstitutionEnabled.value = settingsStore.editorSettings.sqlVariableSubstitutionEnabled; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides); editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget; editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings); @@ -981,6 +984,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget; + editSqlVariableSubstitutionEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); } else if (tab === "formatter") { editSqlFormatter.value = normalizeSqlFormatterSettings(DEFAULT_EDITOR_SETTINGS.sqlFormatter); @@ -1079,6 +1083,7 @@ function resetAllDefaults() { editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; + editSqlVariableSubstitutionEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon; @@ -3938,19 +3943,27 @@ onUnmounted(() => { {{ t("settings.sqlVariableSyntaxDescription") }}

- +
+
+ + +
+ +
-
+
- +
diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 83fc320081..2a32c5833b 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -220,7 +220,7 @@ const connectionStore = useConnectionStore(); const settingsStore = useSettingsStore(); function sqlStatementParameterOptions() { - const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType); + const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType, settingsStore.editorSettings.sqlVariableSubstitutionEnabled); return { databaseType: props.databaseType, enabledSyntaxes: enabledSqlParameterSyntaxes(toggles) }; } const { isDark, themePalette } = useTheme(); diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts index 07b7ec649e..a816a48eda 100644 --- a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -128,6 +128,33 @@ describe("useSqlExecution", () => { setActivePinia(createPinia()); }); + it("sends every placeholder syntax and @set unchanged when substitution is disabled", async () => { + const sql = ["@set tenant = 42;", "SELECT ?, :named, ${shell}, #{mybatis}, @sqlserver, @tenant;"].join("\n"); + const activeTab = ref({ ...queryTab("app"), sql }); + const activeConnection = ref(connection("mysql")); + const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); + const queryStore = useQueryStore(); + const settingsStore = useSettingsStore(); + const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { + if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; + }); + vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); + settingsStore.editorSettings.sqlVariableSubstitutionEnabled = false; + + const execution = useSqlExecution({ + activeTab: computed(() => activeTab.value), + activeConnection: computed(() => activeConnection.value), + executableSql: computed(() => sql), + activeOutputView, + }); + + await execution.tryExecute(); + + expect(execution.showSqlParameterDialog.value).toBe(false); + expect(execution.sqlParameterNames.value).toEqual([]); + expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); + }); + it("passes the selected statement's editor offset to the query store", async () => { const fullSql = "SELECT * FROM users;\nSELECT * FROM users;"; const selectionFrom = fullSql.lastIndexOf("SELECT"); diff --git a/apps/desktop/src/composables/useSqlExecution.ts b/apps/desktop/src/composables/useSqlExecution.ts index bd75c2701f..8ccc92b275 100644 --- a/apps/desktop/src/composables/useSqlExecution.ts +++ b/apps/desktop/src/composables/useSqlExecution.ts @@ -139,7 +139,7 @@ export function useSqlExecution(deps: { let pendingSqlParameterContinuation: ((sql: string, sourceOffset?: number) => Promise | void) | undefined; async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<{ sql: string; sourceOffset?: number }> { - const atSetEnabled = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, deps.activeConnection.value?.db_type).atSet; + const atSetEnabled = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, deps.activeConnection.value?.db_type, settingsStore.editorSettings.sqlVariableSubstitutionEnabled).atSet; const expand = (sql: string) => (atSetEnabled ? expandSqlVariables(sql).sql : sql); if (typeof source === "string") return { sql: expand(source) }; @@ -228,7 +228,7 @@ export function useSqlExecution(deps: { function prepareSqlParameterDialog(sql: string, sourceOffset?: number, options: SqlExecutionOptions = {}, continuation?: (sql: string, sourceOffset?: number) => Promise | void): boolean { const databaseType = deps.activeConnection.value?.db_type; - const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, databaseType); + const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, databaseType, settingsStore.editorSettings.sqlVariableSubstitutionEnabled); const enabledSyntaxes = enabledSqlParameterSyntaxes(toggles); const parameters = extractSqlParameterDescriptors(sql, { databaseType, enabledSyntaxes }); if (!parameters.length) return false; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 66f96b0499..3fb292ce9a 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -5962,6 +5962,7 @@ export default { clickTableNavigationTargetDescription: "When enabled, Ctrl/Cmd+clicking a table name opens the table structure editor (DDL). When disabled, it opens the table data view.", sqlVariableSyntax: "SQL variable & placeholder substitution", sqlVariableSyntaxDescription: "Choose which variable and placeholder syntaxes DBX substitutes before running SQL, per database type. All are enabled by default.", + sqlVariableSubstitutionEnabled: "Enabled", sqlVariableSyntax_positional: "Positional placeholder", sqlVariableSyntax_positionalDescription: "Prompt for a value to substitute each positional placeholder in order.", sqlVariableSyntax_named: "Named placeholder", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index b87853d2cd..f6fca8601b 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -5677,6 +5677,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "Cuando está activado, Ctrl/Cmd+clic en el nombre de la tabla abre el editor de estructura (DDL). Cuando está desactivado, abre la vista de datos.", sqlVariableSyntax: "Sustitución de variables y marcadores SQL", sqlVariableSyntaxDescription: "Elige qué sintaxis de variables y marcadores sustituye DBX antes de ejecutar SQL, por tipo de base de datos. Todas están habilitadas de forma predeterminada.", + sqlVariableSubstitutionEnabled: "Activada", sqlVariableSyntax_positional: "Marcador posicional", sqlVariableSyntax_positionalDescription: "Solicita un valor para sustituir cada marcador posicional en orden.", sqlVariableSyntax_named: "Marcador con nombre", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index b6b24fd196..21d22896e2 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -5677,6 +5677,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "Quando attivato, Ctrl/Cmd+clic sul nome della tabella apre l'editor di struttura (DDL). Quando disattivato, apre la vista dati.", sqlVariableSyntax: "Sostituzione di variabili e segnaposto SQL", sqlVariableSyntaxDescription: "Scegli quali sintassi di variabili e segnaposto DBX sostituisce prima di eseguire SQL, per tipo di database. Tutte abilitate per impostazione predefinita.", + sqlVariableSubstitutionEnabled: "Attiva", sqlVariableSyntax_positional: "Segnaposto posizionale", sqlVariableSyntax_positionalDescription: "Richiede un valore per sostituire ogni segnaposto posizionale in ordine.", sqlVariableSyntax_named: "Segnaposto con nome", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 040843b8d7..3493bb0b7b 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -5699,6 +5699,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "有効にすると、Ctrl/Cmd+クリックでテーブル構造エディタ(DDL)を開きます。無効の場合はテーブルデータビューを開きます。", sqlVariableSyntax: "SQL 変数・プレースホルダー置換", sqlVariableSyntaxDescription: "SQL 実行前に置換する変数・プレースホルダー構文をデータベース種別ごとに選択します。既定ではすべて有効です。", + sqlVariableSubstitutionEnabled: "置換を有効化", sqlVariableSyntax_positional: "位置プレースホルダー", sqlVariableSyntax_positionalDescription: "各位置プレースホルダーを順番に置き換える値の入力を求めます。", sqlVariableSyntax_named: "名前付きプレースホルダー", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index a58b526c76..f21b22a90c 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5433,6 +5433,7 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM 을 미리 채웁니다.", sqlVariableSyntax: "SQL 변수 및 자리표시자 치환", sqlVariableSyntaxDescription: "데이터베이스 유형별로 SQL을 실행하기 전에 DBX가 치환할 변수와 자리표시자 문법을 선택하세요. 모두 기본적으로 활성화됩니다.", + sqlVariableSubstitutionEnabled: "치환 사용", sqlVariableSyntax_positional: "위치 자리표시자", sqlVariableSyntax_positionalDescription: "각 위치 자리표시자를 순서대로 치환할 값을 입력하라는 메시지를 표시합니다.", sqlVariableSyntax_named: "이름 있는 자리표시자", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 09249d5047..dd829666c8 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -5679,6 +5679,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "Quando ativado, Ctrl/Cmd+clicar no nome da tabela abre o editor de estrutura (DDL). Quando desativado, abre a visualização de dados.", sqlVariableSyntax: "Substituição de variáveis e espaços reservados SQL", sqlVariableSyntaxDescription: "Escolha quais sintaxes de variáveis e espaços reservados o DBX substitui antes de executar SQL, por tipo de banco de dados. Todas ativadas por padrão.", + sqlVariableSubstitutionEnabled: "Ativada", sqlVariableSyntax_positional: "Espaço reservado posicional", sqlVariableSyntax_positionalDescription: "Solicita um valor para substituir cada espaço reservado posicional em ordem.", sqlVariableSyntax_named: "Espaço reservado nomeado", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 67d3e8bae0..de0fe1ec10 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -5948,6 +5948,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "开启后,Ctrl/Cmd+点击表名将打开表结构编辑器(DDL);关闭则打开表数据视图。", sqlVariableSyntax: "SQL 变量与占位符替换", sqlVariableSyntaxDescription: "按数据库类型选择执行 SQL 前替换哪些变量与占位符语法,默认全部开启。", + sqlVariableSubstitutionEnabled: "启用替换", sqlVariableSyntax_positional: "位置占位符", sqlVariableSyntax_positionalDescription: "按顺序为每个位置占位符提示输入替换值。", sqlVariableSyntax_named: "命名占位符", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 8db2b2a536..a8e9feecd3 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5013,6 +5013,7 @@ export default withEnglishFallback({ clickTableNavigationTargetDescription: "開啟後,Ctrl/Cmd+點擊表名將開啟表結構編輯器(DDL);關閉則開啟表資料檢視。", sqlVariableSyntax: "SQL 變數與佔位符替換", sqlVariableSyntaxDescription: "依資料庫類型選擇執行 SQL 前替換哪些變數與佔位符語法,預設全部開啟。", + sqlVariableSubstitutionEnabled: "啟用替換", sqlVariableSyntax_positional: "位置佔位符", sqlVariableSyntax_positionalDescription: "依序為每個位置佔位符提示輸入替換值。", sqlVariableSyntax_named: "具名佔位符", diff --git a/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts index 4e25f881cf..03ce6c92cb 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts @@ -26,6 +26,33 @@ describe("resolveSqlVariableSyntaxToggles", () => { atSet: false, }); }); + + it("disables every syntax when substitution is globally disabled", () => { + expect(resolveSqlVariableSyntaxToggles({ mysql: { shell: false } }, "mysql", false)).toEqual({ + positional: false, + named: false, + shell: false, + mybatis: false, + sqlserver: false, + atSet: false, + }); + }); + + it("restores per-database overrides when substitution is re-enabled", () => { + const overrides = { mysql: { shell: false as const, atSet: false as const } }; + + resolveSqlVariableSyntaxToggles(overrides, "mysql", false); + + expect(resolveSqlVariableSyntaxToggles(overrides, "mysql", true)).toEqual({ + positional: true, + named: true, + shell: false, + mybatis: true, + sqlserver: true, + atSet: false, + }); + expect(overrides).toEqual({ mysql: { shell: false, atSet: false } }); + }); }); describe("enabledSqlParameterSyntaxes", () => { diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 673ced7003..8e07bca54b 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -25,6 +25,7 @@ function makeSettings(overrides: Partial = {}): EditorSettings { confirmUnsavedSqlClose: true, savedSqlOpenTargetMode: "saved", objectBrowserViewMode: "list", + sqlVariableSubstitutionEnabled: true, sqlVariableSyntaxOverrides: {}, tabLayout: "scroll", ...overrides, @@ -67,9 +68,34 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { it("includes completionTriggerMode", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode"); }); + + it("includes the SQL variable substitution master switch", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("sqlVariableSubstitutionEnabled"); + }); }); describe("editorSettingsDraftFromSettings", () => { + it("toggles substitution without discarding per-database overrides", () => { + const base = editorSettingsDraftFromSettings( + makeSettings({ + sqlVariableSubstitutionEnabled: true, + sqlVariableSyntaxOverrides: { mysql: { shell: false } }, + }), + ); + const draft = editorSettingsDraftFromSettings( + makeSettings({ + sqlVariableSubstitutionEnabled: true, + sqlVariableSyntaxOverrides: { mysql: { shell: false } }, + }), + ); + + draft.sqlVariableSubstitutionEnabled = false; + + expect(editorSettingsPatchFromDraft(draft, base)).toEqual({ sqlVariableSubstitutionEnabled: false }); + expect(draft.sqlVariableSyntaxOverrides).toEqual({ mysql: { shell: false } }); + expect(base.sqlVariableSyntaxOverrides).toEqual({ mysql: { shell: false } }); + }); + it("does not include persisted global timeout values in editor drafts", () => { const settings = makeSettings({ globalConnectTimeoutSecs: 17, globalQueryTimeoutSecs: 43 }); const draft = editorSettingsDraftFromSettings(settings); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 0326ba0974..5893aff519 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -73,6 +73,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "updateDownloadSource", "toolbarItems", "snippets", + "sqlVariableSubstitutionEnabled", "sqlVariableSyntaxOverrides", "continueOnErrorOnBatch", "clickTableNavigationTarget", diff --git a/apps/desktop/src/lib/sql/sqlVariableSyntax.ts b/apps/desktop/src/lib/sql/sqlVariableSyntax.ts index 4e83d3d387..4761aa6d9e 100644 --- a/apps/desktop/src/lib/sql/sqlVariableSyntax.ts +++ b/apps/desktop/src/lib/sql/sqlVariableSyntax.ts @@ -53,11 +53,22 @@ export type SqlVariableSyntaxOverrides = Partial { + it("enables SQL variable substitution by default and only preserves booleans", () => { + expect(normalizeEditorSettings({}).sqlVariableSubstitutionEnabled).toBe(true); + expect(normalizeEditorSettings({ sqlVariableSubstitutionEnabled: true }).sqlVariableSubstitutionEnabled).toBe(true); + expect(normalizeEditorSettings({ sqlVariableSubstitutionEnabled: false }).sqlVariableSubstitutionEnabled).toBe(false); + expect(normalizeEditorSettings({ sqlVariableSubstitutionEnabled: "false" } as any).sqlVariableSubstitutionEnabled).toBe(true); + expect(normalizeEditorSettings({ sqlVariableSubstitutionEnabled: null } as any).sqlVariableSubstitutionEnabled).toBe(true); + }); + it("keeps data type colors disabled by default and preserves an explicit opt-in", () => { expect(normalizeEditorSettings({}).colorizeDataGridCellTypes).toBe(false); expect(normalizeEditorSettings({ colorizeDataGridCellTypes: true }).colorizeDataGridCellTypes).toBe(true); @@ -628,6 +636,33 @@ describe("settingsStore persisted settings initialization", () => { expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ fontSize: 17, theme: "xcode-dark", appLayout: "separated" })); }); + it("loads and persists the substitution switch without discarding syntax overrides", async () => { + const loadEditorSettings = vi.fn().mockResolvedValue({ + sqlVariableSubstitutionEnabled: false, + sqlVariableSyntaxOverrides: { mysql: { shell: false } }, + }); + const saveEditorSettings = vi.fn().mockResolvedValue(undefined); + vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const store = useSettingsStore(); + await store.initEditorSettings(); + + expect(store.editorSettings.sqlVariableSubstitutionEnabled).toBe(false); + expect(store.editorSettings.sqlVariableSyntaxOverrides).toEqual({ mysql: { shell: false } }); + + await store.updateEditorSettingsAndPersist({ sqlVariableSubstitutionEnabled: true }); + + expect(store.editorSettings.sqlVariableSubstitutionEnabled).toBe(true); + expect(store.editorSettings.sqlVariableSyntaxOverrides).toEqual({ mysql: { shell: false } }); + expect(saveEditorSettings).toHaveBeenCalledWith( + expect.objectContaining({ + sqlVariableSubstitutionEnabled: true, + sqlVariableSyntaxOverrides: { mysql: { shell: false } }, + }), + ); + }); + it("shares concurrent initialization and applies startup changes after saved settings load", async () => { let resolveLoad!: (value: unknown) => void; const loadEditorSettings = vi.fn( diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index b4d6f1e3d6..150c7ed9e6 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -602,6 +602,7 @@ export interface EditorSettings { toolbarItems: ToolbarItems; objectBrowserShowCheckbox: boolean; objectBrowserViewMode: "list" | "grid"; + sqlVariableSubstitutionEnabled: boolean; sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides; continueOnErrorOnBatch: boolean; clickTableNavigationTarget: ClickTableNavigationTarget; @@ -799,6 +800,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS }, objectBrowserShowCheckbox: false, objectBrowserViewMode: "list", + sqlVariableSubstitutionEnabled: true, sqlVariableSyntaxOverrides: {}, continueOnErrorOnBatch: false, clickTableNavigationTarget: "data", @@ -1179,6 +1181,7 @@ export function normalizeEditorSettings(settings: Partial, exist toolbarItems: normalizeToolbarItems(settings.toolbarItems), objectBrowserShowCheckbox: typeof settings.objectBrowserShowCheckbox === "boolean" ? settings.objectBrowserShowCheckbox : DEFAULT_EDITOR_SETTINGS.objectBrowserShowCheckbox, objectBrowserViewMode: settings.objectBrowserViewMode === "grid" ? "grid" : DEFAULT_EDITOR_SETTINGS.objectBrowserViewMode, + sqlVariableSubstitutionEnabled: typeof settings.sqlVariableSubstitutionEnabled === "boolean" ? settings.sqlVariableSubstitutionEnabled : DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled, sqlVariableSyntaxOverrides: normalizeSqlVariableSyntaxOverrides(settings.sqlVariableSyntaxOverrides), continueOnErrorOnBatch: settings.continueOnErrorOnBatch === true, clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget), @@ -1721,6 +1724,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.toolbarItems !== undefined) editorSettings.value.toolbarItems = normalizeToolbarItems(partial.toolbarItems); if (partial.objectBrowserShowCheckbox !== undefined) editorSettings.value.objectBrowserShowCheckbox = partial.objectBrowserShowCheckbox === true; if (partial.objectBrowserViewMode !== undefined) editorSettings.value.objectBrowserViewMode = partial.objectBrowserViewMode === "grid" ? "grid" : "list"; + if (partial.sqlVariableSubstitutionEnabled !== undefined) editorSettings.value.sqlVariableSubstitutionEnabled = partial.sqlVariableSubstitutionEnabled === true; if (partial.sqlVariableSyntaxOverrides !== undefined) editorSettings.value.sqlVariableSyntaxOverrides = normalizeSqlVariableSyntaxOverrides(partial.sqlVariableSyntaxOverrides); if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true; if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);