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") }}
-
+
+
+
+
+
+
+
-
+
-
setSqlVariableSyntaxToggle(key, value as boolean)" />
+ setSqlVariableSyntaxToggle(key, value as boolean)" />
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);