From 1a309b6b63c0b28d1673128c403380d3e241bd8a Mon Sep 17 00:00:00 2001 From: miracle Date: Sat, 11 Jul 2026 20:45:07 +0800 Subject: [PATCH] feat(editor): configure SQL variable syntax --- apps/desktop/src/App.vue | 37 ++++++-- .../editor/EditorSettingsDialog.vue | 65 +++++++++++++ .../components/editor/SqlParameterDialog.vue | 3 +- .../src/components/layout/AppDialogs.vue | 14 ++- .../src/composables/useSqlExecution.ts | 22 +++-- apps/desktop/src/i18n/locales/en.ts | 14 +++ apps/desktop/src/i18n/locales/es.ts | 14 +++ apps/desktop/src/i18n/locales/it.ts | 14 +++ apps/desktop/src/i18n/locales/ja.ts | 14 +++ apps/desktop/src/i18n/locales/pt-BR.ts | 14 +++ apps/desktop/src/i18n/locales/zh-CN.ts | 14 +++ apps/desktop/src/i18n/locales/zh-TW.ts | 14 +++ .../lib/__tests__/sql/sqlParameters.spec.ts | 39 ++++++++ .../__tests__/sql/sqlVariableSyntax.spec.ts | 83 +++++++++++++++++ .../src/lib/settings/editorSettingsDraft.ts | 1 + apps/desktop/src/lib/sql/sqlParameters.ts | 14 ++- apps/desktop/src/lib/sql/sqlVariableSyntax.ts | 93 +++++++++++++++++++ apps/desktop/src/stores/settingsStore.ts | 5 + 18 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts create mode 100644 apps/desktop/src/lib/sql/sqlVariableSyntax.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index a70a9929d..9e98e8186 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -239,16 +239,32 @@ function promptActiveDatabaseSelection() { toast(t("editor.selectDatabaseRequired"), 2500); } -const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, showSqlParameterDialog, sqlParameterSourceSql, sqlParameterNames, sqlParameterDatabaseType, onSqlParametersConfirm, explainMode } = - useSqlExecution({ - activeTab, - activeConnection, - executableSql, - resolveExecutableSql: resolveActiveExecutableSql, - activeOutputView, - blockDangerousRedisCommands, - onMissingDatabase: promptActiveDatabaseSelection, - }); +const { + dangerSql, + pendingDangerSql, + showDangerDialog, + suppressDangerConfirm, + tryExecute, + doExecute, + cancelActiveExecution, + tryExplain, + onDangerConfirm, + showSqlParameterDialog, + sqlParameterSourceSql, + sqlParameterNames, + sqlParameterDatabaseType, + sqlParameterEnabledSyntaxes, + onSqlParametersConfirm, + explainMode, +} = useSqlExecution({ + activeTab, + activeConnection, + executableSql, + resolveExecutableSql: resolveActiveExecutableSql, + activeOutputView, + blockDangerousRedisCommands, + onMissingDatabase: promptActiveDatabaseSelection, +}); function requestActiveEditorExecute() { if (contentAreaRef.value?.requestQueryEditorExecute?.()) return; @@ -2007,6 +2023,7 @@ onUnmounted(() => { :sql-parameter-source-sql="sqlParameterSourceSql" :sql-parameter-names="sqlParameterNames" :sql-parameter-database-type="sqlParameterDatabaseType" + :sql-parameter-enabled-syntaxes="sqlParameterEnabledSyntaxes" @update:show-connection-dialog="setConnectionDialogOpen" @update:show-danger-dialog="showDangerDialog = $event" @update:suppress-danger-confirm="suppressDangerConfirm = $event" diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 9c911063a..3053dae5b 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -88,6 +88,7 @@ import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/ import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges"; import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache"; import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates"; +import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, normalizeSqlVariableSyntaxOverrides, SQL_VARIABLE_SYNTAX_DATABASE_TYPES, SQL_VARIABLE_SYNTAX_KEYS, SQL_VARIABLE_SYNTAX_TOKENS, type SqlVariableSyntaxOverrides, type SqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; import { buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, type McpEnvEntry, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; import { isWindows } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; @@ -284,6 +285,33 @@ const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll); const editInfiniteScrollMaxRows = ref(settingsStore.editorSettings.infiniteScrollMaxRows); const editTableColumnTemplateRows = ref(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields)); const editTableColumnTemplateDatabaseType = ref(TABLE_COLUMN_TEMPLATE_DATABASE_TYPES[0] ?? "mysql"); +const editSqlVariableSyntaxOverrides = ref(normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides)); +const editSqlVariableSyntaxDatabaseType = ref(SQL_VARIABLE_SYNTAX_DATABASE_TYPES[0] ?? "mysql"); + +function sqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles): boolean { + return editSqlVariableSyntaxOverrides.value[editSqlVariableSyntaxDatabaseType.value]?.[key] ?? true; +} + +function setSqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles, value: boolean) { + const dbType = editSqlVariableSyntaxDatabaseType.value; + const merged: SqlVariableSyntaxToggles = { + ...DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, + ...editSqlVariableSyntaxOverrides.value[dbType], + [key]: value, + }; + const next: SqlVariableSyntaxOverrides = { ...editSqlVariableSyntaxOverrides.value }; + // Keep storage sparse: an all-enabled type has no entry; otherwise persist only the disabled syntaxes. + if (SQL_VARIABLE_SYNTAX_KEYS.every((toggleKey) => merged[toggleKey])) { + delete next[dbType]; + } else { + const partial: Partial = {}; + for (const toggleKey of SQL_VARIABLE_SYNTAX_KEYS) { + if (!merged[toggleKey]) partial[toggleKey] = false; + } + next[dbType] = partial; + } + editSqlVariableSyntaxOverrides.value = next; +} const tableColumnTemplateSectionRef = ref(null); const draggedTableColumnTemplateRowId = ref(null); let tableColumnTemplatePointerDragCleanup: (() => void) | null = null; @@ -393,6 +421,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { updateDownloadSource: editUpdateDownloadSource.value, toolbarItems: { ...editToolbarItems.value }, snippets: editSnippets.value, + sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides.value, }; } @@ -664,6 +693,7 @@ function syncEditorSettingsDraftFromStore() { editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource; editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems }; editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet); + editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides); editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings); } @@ -818,6 +848,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled; editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; + editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); } else if (tab === "formatter") { editSqlFormatter.value = normalizeSqlFormatterSettings(DEFAULT_EDITOR_SETTINGS.sqlFormatter); sqlFormatterConfigValid.value = true; @@ -890,6 +921,7 @@ function resetAllDefaults() { editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled; editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution; editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; + editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon; editQuitOnClose.value = DEFAULT_DESKTOP_SETTINGS.quit_on_close; @@ -2747,6 +2779,39 @@ onUnmounted(cleanupPreviewEditor); +
+
+
+
{{ t("settings.sqlVariableSyntax") }}
+

{{ t("settings.sqlVariableSyntaxDescription") }}

+
+ +
+
+
+
+ +

{{ t(`settings.sqlVariableSyntax_${key}Description`) }}

+
+ +
+
+
+ + +
diff --git a/apps/desktop/src/components/editor/SqlParameterDialog.vue b/apps/desktop/src/components/editor/SqlParameterDialog.vue index e27280055..310796f84 100644 --- a/apps/desktop/src/components/editor/SqlParameterDialog.vue +++ b/apps/desktop/src/components/editor/SqlParameterDialog.vue @@ -25,6 +25,7 @@ const props = defineProps<{ sql: string; parameters: SqlParameterDescriptor[]; databaseType?: DatabaseType; + enabledSyntaxes?: SqlParameterSyntax[]; }>(); const emit = defineEmits<{ @@ -46,7 +47,7 @@ const syntaxLabels: Record = { sqlserver: "@name", }; -const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value, { databaseType: props.databaseType })); +const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value, { databaseType: props.databaseType, enabledSyntaxes: props.enabledSyntaxes })); const highlightedSql = computed(() => highlight(resolvedSql.value)); watch( diff --git a/apps/desktop/src/components/layout/AppDialogs.vue b/apps/desktop/src/components/layout/AppDialogs.vue index 74e32fee0..01bbf8c15 100644 --- a/apps/desktop/src/components/layout/AppDialogs.vue +++ b/apps/desktop/src/components/layout/AppDialogs.vue @@ -20,7 +20,7 @@ const DataGenerateDialog = defineAsyncComponent(() => import("@/components/gener import { useConnectionStore } from "@/stores/connectionStore"; import { useDialogSources } from "@/composables/useDialogSources"; import type { ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; -import type { SqlParameterDescriptor } from "@/lib/sql/sqlParameters"; +import type { SqlParameterDescriptor, SqlParameterSyntax } from "@/lib/sql/sqlParameters"; import type { ConfigTab } from "@/components/connection/ConnectionDialog.vue"; import type { DatabaseType } from "@/types/database"; @@ -35,6 +35,7 @@ const props = defineProps<{ sqlParameterSourceSql: string; sqlParameterNames: SqlParameterDescriptor[]; sqlParameterDatabaseType?: DatabaseType; + sqlParameterEnabledSyntaxes?: SqlParameterSyntax[]; }>(); const emit = defineEmits<{ @@ -135,7 +136,16 @@ watch( @update:suppress-future-prompts="emit('update:suppressDangerConfirm', $event)" @confirm="emit('dangerConfirm')" /> - + ([]); const sqlParameterDatabaseType = ref(); + const sqlParameterEnabledSyntaxes = ref([]); async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise { - if (typeof source === "string") return expandSqlVariables(source).sql; - if (deps.resolveExecutableSql) return expandSqlVariables(await deps.resolveExecutableSql(source)).sql; - if (isSqlExecutionSnapshot(source)) return expandSqlVariables(resolveExecutableSql(source.fullSql, source.selectedSql, { cursorPos: source.cursorPos })).sql; - return expandSqlVariables(deps.executableSql.value).sql; + const atSetEnabled = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, deps.activeConnection.value?.db_type).atSet; + const expand = (sql: string) => (atSetEnabled ? expandSqlVariables(sql).sql : sql); + if (typeof source === "string") return expand(source); + if (deps.resolveExecutableSql) return expand(await deps.resolveExecutableSql(source)); + if (isSqlExecutionSnapshot(source)) return expand(resolveExecutableSql(source.fullSql, source.selectedSql, { cursorPos: source.cursorPos })); + return expand(deps.executableSql.value); } async function tryExecute(sqlOverride?: SqlExecutionOverride) { @@ -110,11 +114,14 @@ export function useSqlExecution(deps: { function prepareSqlParameterDialog(sql: string): boolean { const databaseType = deps.activeConnection.value?.db_type; - const parameters = extractSqlParameterDescriptors(sql, { databaseType }); + const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, databaseType); + const enabledSyntaxes = enabledSqlParameterSyntaxes(toggles); + const parameters = extractSqlParameterDescriptors(sql, { databaseType, enabledSyntaxes }); if (!parameters.length) return false; sqlParameterSourceSql.value = sql; sqlParameterNames.value = parameters; sqlParameterDatabaseType.value = databaseType; + sqlParameterEnabledSyntaxes.value = enabledSyntaxes; showSqlParameterDialog.value = true; return true; } @@ -206,6 +213,7 @@ export function useSqlExecution(deps: { sqlParameterSourceSql.value = ""; sqlParameterNames.value = []; sqlParameterDatabaseType.value = undefined; + sqlParameterEnabledSyntaxes.value = []; await continueExecute(sql); } @@ -214,6 +222,7 @@ export function useSqlExecution(deps: { sqlParameterSourceSql.value = ""; sqlParameterNames.value = []; sqlParameterDatabaseType.value = undefined; + sqlParameterEnabledSyntaxes.value = []; }); return { @@ -230,6 +239,7 @@ export function useSqlExecution(deps: { sqlParameterSourceSql, sqlParameterNames, sqlParameterDatabaseType, + sqlParameterEnabledSyntaxes, onSqlParametersConfirm, explainMode, }; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 1477263de..c8ee18c84 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -3164,6 +3164,20 @@ export default { confirmDangerousSqlExecutionDescription: "When disabled, ALTER, DROP, DELETE, TRUNCATE, and other dangerous SQL run without the warning dialog.", confirmUnsavedSqlClose: "Confirm before closing unsaved SQL", confirmUnsavedSqlCloseDescription: "When disabled, SQL tabs with unsaved edits close or quit without the save confirmation dialog.", + 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.", + sqlVariableSyntax_positional: "Positional placeholder", + sqlVariableSyntax_positionalDescription: "Prompt for a value to substitute each positional placeholder in order.", + sqlVariableSyntax_named: "Named placeholder", + sqlVariableSyntax_namedDescription: "Prompt for a value to substitute named placeholders.", + sqlVariableSyntax_shell: "Shell-style placeholder", + sqlVariableSyntax_shellDescription: "Prompt for a value to substitute shell-style placeholders.", + sqlVariableSyntax_mybatis: "MyBatis-style placeholder", + sqlVariableSyntax_mybatisDescription: "Prompt for a value to substitute MyBatis-style placeholders.", + sqlVariableSyntax_sqlserver: "SQL Server-style placeholder", + sqlVariableSyntax_sqlserverDescription: "Prompt for a value to substitute SQL Server-style placeholders (may collide with session variables).", + sqlVariableSyntax_atSet: "Inline variable expansion", + sqlVariableSyntax_atSetDescription: "Expand inline variable declarations and substitute their values before running.", autoAliasTables: "Automatically add table aliases", autoAliasTablesDescription: "When selecting a table completion in FROM or JOIN, insert a generated alias such as order_items AS oi.", redisScanPageSize: "Redis scan count", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index bea728647..075da9bb0 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -3045,6 +3045,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "Cuando se desactiva, ALTER, DROP, DELETE, TRUNCATE y otras sentencias peligrosas se ejecutan sin el diálogo de advertencia.", 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.", + 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.", + sqlVariableSyntax_positional: "Marcador posicional", + sqlVariableSyntax_positionalDescription: "Solicita un valor para sustituir cada marcador posicional en orden.", + sqlVariableSyntax_named: "Marcador con nombre", + sqlVariableSyntax_namedDescription: "Solicita un valor para sustituir los marcadores con nombre.", + sqlVariableSyntax_shell: "Marcador estilo shell", + sqlVariableSyntax_shellDescription: "Solicita un valor para sustituir los marcadores de estilo shell.", + sqlVariableSyntax_mybatis: "Marcador estilo MyBatis", + sqlVariableSyntax_mybatisDescription: "Solicita un valor para sustituir los marcadores de estilo MyBatis.", + sqlVariableSyntax_sqlserver: "Marcador estilo SQL Server", + sqlVariableSyntax_sqlserverDescription: "Solicita un valor para sustituir los marcadores de estilo SQL Server (pueden entrar en conflicto con variables de sesión).", + sqlVariableSyntax_atSet: "Expansión de variables en línea", + sqlVariableSyntax_atSetDescription: "Expande las declaraciones de variables en línea y sustituye sus valores antes de ejecutar.", autoAliasTables: "Agregar alias de tabla automáticamente", autoAliasTablesDescription: "Al seleccionar una tabla en FROM o JOIN, inserta un alias generado como order_items AS oi.", redisScanPageSize: "Cantidad de escaneo Redis", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index a79c12ad6..57aa7c445 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -3043,6 +3043,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "Se disattivato, ALTER, DROP, DELETE, TRUNCATE e altri SQL pericolosi verranno eseguiti senza la finestra di avviso.", 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.", + 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.", + sqlVariableSyntax_positional: "Segnaposto posizionale", + sqlVariableSyntax_positionalDescription: "Richiede un valore per sostituire ogni segnaposto posizionale in ordine.", + sqlVariableSyntax_named: "Segnaposto con nome", + sqlVariableSyntax_namedDescription: "Richiede un valore per sostituire i segnaposto con nome.", + sqlVariableSyntax_shell: "Segnaposto stile shell", + sqlVariableSyntax_shellDescription: "Richiede un valore per sostituire i segnaposto in stile shell.", + sqlVariableSyntax_mybatis: "Segnaposto stile MyBatis", + sqlVariableSyntax_mybatisDescription: "Richiede un valore per sostituire i segnaposto in stile MyBatis.", + sqlVariableSyntax_sqlserver: "Segnaposto stile SQL Server", + sqlVariableSyntax_sqlserverDescription: "Richiede un valore per sostituire i segnaposto in stile SQL Server (possono entrare in conflitto con le variabili di sessione).", + sqlVariableSyntax_atSet: "Espansione di variabili inline", + sqlVariableSyntax_atSetDescription: "Espande le dichiarazioni di variabili inline e ne sostituisce i valori prima dell'esecuzione.", autoAliasTables: "Aggiungi alias tabella automaticamente", autoAliasTablesDescription: "Quando scegli una tabella in FROM o JOIN, inserisce un alias generato come order_items AS oi.", redisScanPageSize: "Conteggio scansione Redis", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 8d75557f8..8ea57f0e9 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -3035,6 +3035,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "無効時、ALTER、DROP、DELETE、TRUNCATEなどの危険なSQLが警告ダイアログなしで実行されます。", confirmUnsavedSqlClose: "未保存のSQLを閉じる前に確認", confirmUnsavedSqlCloseDescription: "無効時、未保存の編集があるSQLタブは保存確認ダイアログなしで閉じるか終了します。", + sqlVariableSyntax: "SQL 変数・プレースホルダー置換", + sqlVariableSyntaxDescription: "SQL 実行前に置換する変数・プレースホルダー構文をデータベース種別ごとに選択します。既定ではすべて有効です。", + sqlVariableSyntax_positional: "位置プレースホルダー", + sqlVariableSyntax_positionalDescription: "各位置プレースホルダーを順番に置き換える値の入力を求めます。", + sqlVariableSyntax_named: "名前付きプレースホルダー", + sqlVariableSyntax_namedDescription: "名前付きプレースホルダーを置き換える値の入力を求めます。", + sqlVariableSyntax_shell: "シェル形式プレースホルダー", + sqlVariableSyntax_shellDescription: "シェル形式プレースホルダーを置き換える値の入力を求めます。", + sqlVariableSyntax_mybatis: "MyBatis 形式プレースホルダー", + sqlVariableSyntax_mybatisDescription: "MyBatis 形式プレースホルダーを置き換える値の入力を求めます。", + sqlVariableSyntax_sqlserver: "SQL Server 形式プレースホルダー", + sqlVariableSyntax_sqlserverDescription: "SQL Server 形式プレースホルダーを置き換える値の入力を求めます(セッション変数と競合する場合があります)。", + sqlVariableSyntax_atSet: "インライン変数展開", + sqlVariableSyntax_atSetDescription: "実行前にインライン変数宣言を展開し、その値をインライン化します。", autoAliasTables: "テーブル別名を自動追加", autoAliasTablesDescription: "FROM または JOIN でテーブル補完を選択すると、order_items AS oi のような生成済み別名を挿入します。", redisScanPageSize: "Redisスキャンカウント", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index a100d1af3..ae5842862 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -3045,6 +3045,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "Quando desativado, ALTER, DROP, DELETE, TRUNCATE e outros SQL perigosos são executados sem a caixa de diálogo de aviso.", 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.", + 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.", + 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", + sqlVariableSyntax_namedDescription: "Solicita um valor para substituir os espaços reservados nomeados.", + sqlVariableSyntax_shell: "Espaço reservado estilo shell", + sqlVariableSyntax_shellDescription: "Solicita um valor para substituir os espaços reservados de estilo shell.", + sqlVariableSyntax_mybatis: "Espaço reservado estilo MyBatis", + sqlVariableSyntax_mybatisDescription: "Solicita um valor para substituir os espaços reservados de estilo MyBatis.", + sqlVariableSyntax_sqlserver: "Espaço reservado estilo SQL Server", + sqlVariableSyntax_sqlserverDescription: "Solicita um valor para substituir os espaços reservados de estilo SQL Server (podem colidir com variáveis de sessão).", + sqlVariableSyntax_atSet: "Expansão de variáveis inline", + sqlVariableSyntax_atSetDescription: "Expande as declarações de variáveis inline e substitui seus valores antes de executar.", autoAliasTables: "Adicionar alias de tabela automaticamente", autoAliasTablesDescription: "Ao escolher uma tabela em FROM ou JOIN, insere um alias gerado como order_items AS oi.", redisScanPageSize: "Contagem de scan do Redis", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 57fd02343..516b48350 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -3163,6 +3163,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "关闭后,ALTER、DROP、DELETE、TRUNCATE 等危险 SQL 将直接执行。", confirmUnsavedSqlClose: "关闭未保存 SQL 前弹出确认", confirmUnsavedSqlCloseDescription: "关闭后,有未保存内容的 SQL 标签页会直接关闭或退出,不再弹出保存确认。", + sqlVariableSyntax: "SQL 变量与占位符替换", + sqlVariableSyntaxDescription: "按数据库类型选择执行 SQL 前替换哪些变量与占位符语法,默认全部开启。", + sqlVariableSyntax_positional: "位置占位符", + sqlVariableSyntax_positionalDescription: "按顺序为每个位置占位符提示输入替换值。", + sqlVariableSyntax_named: "命名占位符", + sqlVariableSyntax_namedDescription: "为命名占位符提示输入替换值。", + sqlVariableSyntax_shell: "Shell 风格占位符", + sqlVariableSyntax_shellDescription: "为 Shell 风格占位符提示输入替换值。", + sqlVariableSyntax_mybatis: "MyBatis 风格占位符", + sqlVariableSyntax_mybatisDescription: "为 MyBatis 风格占位符提示输入替换值。", + sqlVariableSyntax_sqlserver: "SQL Server 风格占位符", + sqlVariableSyntax_sqlserverDescription: "为 SQL Server 风格占位符提示输入替换值(可能与会话变量冲突)。", + sqlVariableSyntax_atSet: "内联变量展开", + sqlVariableSyntax_atSetDescription: "执行前展开内联变量声明并就地代入其值。", autoAliasTables: "自动添加表别名", autoAliasTablesDescription: "在 FROM 或 JOIN 中选择表名补全时,自动插入类似 order_items AS oi 的表别名。", redisScanPageSize: "Redis 扫描数量", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index ce12cd8c1..a5788e7f4 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2894,6 +2894,20 @@ export default withEnglishFallback({ confirmDangerousSqlExecutionDescription: "關閉後,ALTER、DROP、DELETE、TRUNCATE 等危險 SQL 會直接執行。", confirmUnsavedSqlClose: "關閉未儲存 SQL 前彈出確認", confirmUnsavedSqlCloseDescription: "關閉後,有未儲存內容的 SQL 分頁會直接關閉或結束,不再彈出儲存確認。", + sqlVariableSyntax: "SQL 變數與佔位符替換", + sqlVariableSyntaxDescription: "依資料庫類型選擇執行 SQL 前替換哪些變數與佔位符語法,預設全部開啟。", + sqlVariableSyntax_positional: "位置佔位符", + sqlVariableSyntax_positionalDescription: "依序為每個位置佔位符提示輸入替換值。", + sqlVariableSyntax_named: "具名佔位符", + sqlVariableSyntax_namedDescription: "為具名佔位符提示輸入替換值。", + sqlVariableSyntax_shell: "Shell 風格佔位符", + sqlVariableSyntax_shellDescription: "為 Shell 風格佔位符提示輸入替換值。", + sqlVariableSyntax_mybatis: "MyBatis 風格佔位符", + sqlVariableSyntax_mybatisDescription: "為 MyBatis 風格佔位符提示輸入替換值。", + sqlVariableSyntax_sqlserver: "SQL Server 風格佔位符", + sqlVariableSyntax_sqlserverDescription: "為 SQL Server 風格佔位符提示輸入替換值(可能與工作階段變數衝突)。", + sqlVariableSyntax_atSet: "內聯變數展開", + sqlVariableSyntax_atSetDescription: "執行前展開內聯變數宣告並就地代入其值。", autoAliasTables: "自動加入資料表別名", autoAliasTablesDescription: "在 FROM 或 JOIN 中選擇資料表補全時,自動插入類似 order_items AS oi 的資料表別名。", redisScanPageSize: "Redis 掃描數量", diff --git a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts index 3d77ac16f..74366552f 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts @@ -276,6 +276,45 @@ DEALLOCATE PREPARE stmt;`; }); }); +describe("enabledSyntaxes option", () => { + const mixedSql = "select ? as a, :named as b, ${shell_name} as c, #{mybatis_name} as d, @sql_server_name as e"; + + it("extracts every syntax when the option is omitted (backward compatible)", () => { + expect(extractSqlParameters(mixedSql)).toEqual(["?1", "named", "shell_name", "mybatis_name", "sql_server_name"]); + }); + + it("only extracts the enabled syntaxes", () => { + expect(extractSqlParameters(mixedSql, { enabledSyntaxes: ["named"] })).toEqual(["named"]); + expect(extractSqlParameters(mixedSql, { enabledSyntaxes: ["shell", "mybatis"] })).toEqual(["shell_name", "mybatis_name"]); + }); + + it("extracts nothing when no syntax is enabled", () => { + expect(extractSqlParameters(mixedSql, { enabledSyntaxes: [] })).toEqual([]); + }); + + it("leaves disabled-syntax tokens untouched when substituting", () => { + // Only :named is enabled, so every other token survives verbatim. + expect(substituteSqlParameters(mixedSql, { named: { kind: "number", value: "2" } }, { enabledSyntaxes: ["named"] })).toBe("select ? as a, 2 as b, ${shell_name} as c, #{mybatis_name} as d, @sql_server_name as e"); + }); + + it("does not consume the positional counter for disabled positional placeholders", () => { + expect(substituteSqlParameters("select ?, ?", {}, { enabledSyntaxes: ["named"] })).toBe("select ?, ?"); + }); + + it("keeps #{name} out of hash-comment handling when mybatis is disabled", () => { + expect(extractSqlParameters("select #{mybatis_name} from t", { enabledSyntaxes: ["shell"] })).toEqual([]); + expect(substituteSqlParameters("select #{mybatis_name} from t", {}, { enabledSyntaxes: ["shell"] })).toBe("select #{mybatis_name} from t"); + }); + + it("intersects the enabled set with the saphana named-parameter rule", () => { + const sql = "select :named as a, ${shell_name} as b"; + // saphana already disables :name; enabling named cannot re-enable it. + expect(extractSqlParameters(sql, { databaseType: "saphana", enabledSyntaxes: ["named", "shell"] })).toEqual(["shell_name"]); + // A non-saphana database with named disabled also drops :name. + expect(extractSqlParameters(sql, { enabledSyntaxes: ["shell"] })).toEqual(["shell_name"]); + }); +}); + describe("sqlParameterLiteral", () => { it("falls back to quoted strings for invalid boolean input", () => { expect(sqlParameterLiteral({ kind: "boolean", value: "maybe" })).toBe("'maybe'"); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts new file mode 100644 index 000000000..4e25f881c --- /dev/null +++ b/apps/desktop/src/lib/__tests__/sql/sqlVariableSyntax.spec.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { reactive } from "vue"; +import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, enabledSqlParameterSyntaxes, normalizeSqlVariableSyntaxOverrides, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; + +describe("resolveSqlVariableSyntaxToggles", () => { + it("enables every syntax when there are no overrides", () => { + expect(resolveSqlVariableSyntaxToggles(undefined, "mysql")).toEqual(DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES); + expect(resolveSqlVariableSyntaxToggles({}, "mysql")).toEqual(DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES); + }); + + it("enables every syntax for a database type without an entry", () => { + expect(resolveSqlVariableSyntaxToggles({ mysql: { shell: false } }, "postgres")).toEqual(DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES); + }); + + it("enables every syntax when the database type is unknown", () => { + expect(resolveSqlVariableSyntaxToggles({ mysql: { shell: false } }, undefined)).toEqual(DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES); + }); + + it("applies only the disabled syntaxes for the matching database type", () => { + expect(resolveSqlVariableSyntaxToggles({ mysql: { sqlserver: false, atSet: false } }, "mysql")).toEqual({ + positional: true, + named: true, + shell: true, + mybatis: true, + sqlserver: false, + atSet: false, + }); + }); +}); + +describe("enabledSqlParameterSyntaxes", () => { + it("returns the five placeholder syntaxes and never atSet", () => { + expect(enabledSqlParameterSyntaxes(DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES)).toEqual(["positional", "named", "shell", "mybatis", "sqlserver"]); + }); + + it("filters out disabled placeholder syntaxes", () => { + expect(enabledSqlParameterSyntaxes({ positional: false, named: true, shell: false, mybatis: true, sqlserver: false, atSet: true })).toEqual(["named", "mybatis"]); + }); + + it("ignores the atSet toggle entirely", () => { + expect(enabledSqlParameterSyntaxes({ positional: true, named: true, shell: true, mybatis: true, sqlserver: true, atSet: false })).toEqual(["positional", "named", "shell", "mybatis", "sqlserver"]); + expect(enabledSqlParameterSyntaxes({ positional: false, named: false, shell: false, mybatis: false, sqlserver: false, atSet: true })).toEqual([]); + }); +}); + +describe("normalizeSqlVariableSyntaxOverrides", () => { + it("returns an empty object for non-object input", () => { + expect(normalizeSqlVariableSyntaxOverrides(undefined)).toEqual({}); + expect(normalizeSqlVariableSyntaxOverrides(null)).toEqual({}); + expect(normalizeSqlVariableSyntaxOverrides([])).toEqual({}); + expect(normalizeSqlVariableSyntaxOverrides("mysql")).toEqual({}); + }); + + it("keeps only syntaxes explicitly set to false", () => { + expect(normalizeSqlVariableSyntaxOverrides({ mysql: { positional: true, shell: false, atSet: false } })).toEqual({ mysql: { shell: false, atSet: false } }); + }); + + it("drops entries with no disabled syntaxes", () => { + expect(normalizeSqlVariableSyntaxOverrides({ mysql: { positional: true }, postgres: {} })).toEqual({}); + }); + + it("ignores non-boolean values and unknown keys", () => { + expect(normalizeSqlVariableSyntaxOverrides({ mysql: { shell: "false", named: 0, bogus: false, sqlserver: false } })).toEqual({ mysql: { sqlserver: false } }); + }); + + it("skips non-object database entries", () => { + expect(normalizeSqlVariableSyntaxOverrides({ mysql: null, postgres: [], sqlserver: { shell: false } })).toEqual({ sqlserver: { shell: false } }); + }); + + it("is stable across a round-trip", () => { + const normalized = normalizeSqlVariableSyntaxOverrides({ mysql: { shell: false }, oracle: { atSet: false, named: false } }); + expect(normalizeSqlVariableSyntaxOverrides(normalized)).toEqual(normalized); + }); + + it("reads through a Vue reactive proxy without throwing (settings store is reactive; structuredClone would throw DataCloneError here)", () => { + const overrides = reactive({ mysql: { shell: false, atSet: false } }); + const cloned = normalizeSqlVariableSyntaxOverrides(overrides); + expect(cloned).toEqual({ mysql: { shell: false, atSet: false } }); + // Detached from the reactive source: mutating the clone must not touch the proxy. + cloned.mysql = {}; + expect(overrides.mysql).toEqual({ shell: false, atSet: false }); + }); +}); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index ac145e1b1..8d85d0623 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -47,6 +47,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "updateDownloadSource", "toolbarItems", "snippets", + "sqlVariableSyntaxOverrides", ] as const satisfies readonly (keyof EditorSettings)[]; export type EditorSettingsDraftKey = (typeof EDITOR_SETTINGS_DRAFT_KEYS)[number]; diff --git a/apps/desktop/src/lib/sql/sqlParameters.ts b/apps/desktop/src/lib/sql/sqlParameters.ts index a3d6d9bdc..c180e62cf 100644 --- a/apps/desktop/src/lib/sql/sqlParameters.ts +++ b/apps/desktop/src/lib/sql/sqlParameters.ts @@ -23,6 +23,8 @@ interface ParameterOccurrence extends SqlParameterDescriptor { export interface SqlParameterOptions { databaseType?: DatabaseType; + // Which placeholder syntaxes are recognized. Undefined enables all of them. + enabledSyntaxes?: readonly SqlParameterSyntax[]; } const PARAMETER_NAME_RE = /^[\p{L}_][\p{L}\p{N}_]*$/u; @@ -78,6 +80,8 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) const occurrences: ParameterOccurrence[] = []; const nativeSqlServerParameters = collectNativeSqlServerParameters(sql); const supportsNamedParameters = options?.databaseType !== "saphana"; + const enabledSyntaxes = options?.enabledSyntaxes ? new Set(options.enabledSyntaxes) : null; + const isSyntaxEnabled = (syntax: SqlParameterSyntax) => !enabledSyntaxes || enabledSyntaxes.has(syntax); let i = 0; let dollarQuoteEnd = ""; let positionalIndex = 0; @@ -110,14 +114,14 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) i = skipBlockComment(sql, i + 2); continue; } - if (ch === "?") { + if (ch === "?" && isSyntaxEnabled("positional")) { positionalIndex += 1; const key = `?${positionalIndex}`; occurrences.push({ key, name: key, syntax: "positional", token: "?", start: i, end: i + 1 }); i += 1; continue; } - if (ch === ":" && supportsNamedParameters) { + if (ch === ":" && supportsNamedParameters && isSyntaxEnabled("named")) { const name = readParameterName(sql, i + 1); if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") { occurrences.push({ @@ -132,7 +136,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) continue; } } - if (ch === "$" && next === "{") { + if (ch === "$" && next === "{" && isSyntaxEnabled("shell")) { const end = sql.indexOf("}", i + 2); if (end !== -1) { const name = sql.slice(i + 2, end).trim(); @@ -143,7 +147,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) } } } - if (ch === "#" && next === "{") { + if (ch === "#" && next === "{" && isSyntaxEnabled("mybatis")) { const end = sql.indexOf("}", i + 2); if (end !== -1) { const name = sql.slice(i + 2, end).trim(); @@ -158,7 +162,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) i = skipLine(sql, i + 1); continue; } - if (ch === "@") { + if (ch === "@" && isSyntaxEnabled("sqlserver")) { const name = readParameterName(sql, i + 1); if (name && next !== "@" && sql[i - 1] !== "@" && !nativeSqlServerParameters.declared.has(name.toLowerCase()) && !nativeSqlServerParameters.ignoredStarts.has(i)) { occurrences.push({ diff --git a/apps/desktop/src/lib/sql/sqlVariableSyntax.ts b/apps/desktop/src/lib/sql/sqlVariableSyntax.ts new file mode 100644 index 000000000..4e83d3d38 --- /dev/null +++ b/apps/desktop/src/lib/sql/sqlVariableSyntax.ts @@ -0,0 +1,93 @@ +// Per-database-type configuration for SQL variable/placeholder substitution. +// +// DBX runs two client-side substitution systems before sending SQL to a backend: +// the placeholder parameter dialog (`sqlParameters.ts`, five syntaxes) and the +// `@set name = value;` expansion (`sqlVariables.ts`). This module lets users opt +// out of individual syntaxes per database type. Every toggle defaults to `true`, +// so an empty/absent config reproduces the historical "always substitute" behaviour. +// +// Storage is sparse: only syntaxes explicitly turned off (`false`) are persisted, +// keyed by database type. Anything not stored resolves to enabled. + +import type { DatabaseType } from "@/types/database"; +import type { SqlParameterSyntax } from "@/lib/sql/sqlParameters"; +import { manifestDatabaseTypes } from "@/lib/database/databaseDriverManifest"; + +export interface SqlVariableSyntaxToggles { + positional: boolean; // ? + named: boolean; // :name + shell: boolean; // ${name} + mybatis: boolean; // #{name} + sqlserver: boolean; // @name + atSet: boolean; // @set name = value; (expandSqlVariables) +} + +export const DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES: SqlVariableSyntaxToggles = { + positional: true, + named: true, + shell: true, + mybatis: true, + sqlserver: true, + atSet: true, +}; + +// Fixed order for iterating the toggles in the settings UI. +export const SQL_VARIABLE_SYNTAX_KEYS = ["positional", "named", "shell", "mybatis", "sqlserver", "atSet"] as const satisfies readonly (keyof SqlVariableSyntaxToggles)[]; + +// Display tokens (code symbols, not translated) shown next to each toggle. +export const SQL_VARIABLE_SYNTAX_TOKENS: Record = { + positional: "?", + named: ":name", + shell: "${name}", + mybatis: "#{name}", + sqlserver: "@name", + atSet: "@set …;", +}; + +// The first five toggles map one-to-one onto placeholder parameter syntaxes. +const PARAMETER_SYNTAX_KEYS = ["positional", "named", "shell", "mybatis", "sqlserver"] as const satisfies readonly SqlParameterSyntax[]; + +export type SqlVariableSyntaxOverrides = Partial>>; + +// Database types offered in the settings selector — every connectable type. +export const SQL_VARIABLE_SYNTAX_DATABASE_TYPES: DatabaseType[] = manifestDatabaseTypes(); + +/** + * Resolve the effective toggles for a database type. Any syntax not explicitly + * disabled in `overrides` is enabled. A pure function safe to call on every + * execution. + */ +export function resolveSqlVariableSyntaxToggles(overrides: SqlVariableSyntaxOverrides | undefined, dbType: DatabaseType | undefined): SqlVariableSyntaxToggles { + const partial = dbType ? overrides?.[dbType] : undefined; + return { + positional: partial?.positional ?? true, + named: partial?.named ?? true, + shell: partial?.shell ?? true, + mybatis: partial?.mybatis ?? true, + sqlserver: partial?.sqlserver ?? true, + atSet: partial?.atSet ?? true, + }; +} + +/** Derive the enabled placeholder parameter syntaxes (excludes `atSet`). */ +export function enabledSqlParameterSyntaxes(toggles: SqlVariableSyntaxToggles): SqlParameterSyntax[] { + return PARAMETER_SYNTAX_KEYS.filter((key) => toggles[key]); +} + +/** + * Normalize persisted overrides into a sparse structure: drop non-object entries + * and only keep syntaxes explicitly set to `false`. Absence resolves to enabled. + */ +export function normalizeSqlVariableSyntaxOverrides(value: unknown): SqlVariableSyntaxOverrides { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const result: SqlVariableSyntaxOverrides = {}; + for (const [dbType, raw] of Object.entries(value as Record)) { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue; + const partial: Partial = {}; + for (const key of SQL_VARIABLE_SYNTAX_KEYS) { + if ((raw as Record)[key] === false) partial[key] = false; + } + if (Object.keys(partial).length > 0) result[dbType as DatabaseType] = partial; + } + return result; +} diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index fd7ac363f..26de55f92 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -6,6 +6,7 @@ import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/s import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay"; import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; +import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax"; import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick"; import type { SqlSnippet } from "@/types/database"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; @@ -426,6 +427,7 @@ export interface EditorSettings { toolbarItems: ToolbarItems; objectBrowserShowCheckbox: boolean; objectBrowserViewMode: "list" | "grid"; + sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides; } export interface ToolbarItems { @@ -558,6 +560,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS }, objectBrowserShowCheckbox: false, objectBrowserViewMode: "list", + sqlVariableSyntaxOverrides: {}, }; export const STORAGE_KEY = "dbx-editor-settings"; @@ -790,6 +793,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, + sqlVariableSyntaxOverrides: normalizeSqlVariableSyntaxOverrides(settings.sqlVariableSyntaxOverrides), }; } @@ -1034,6 +1038,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.sqlVariableSyntaxOverrides !== undefined) editorSettings.value.sqlVariableSyntaxOverrides = normalizeSqlVariableSyntaxOverrides(partial.sqlVariableSyntaxOverrides); saveEditorSettings(editorSettings.value); }