Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ const {
sqlParameterNames,
sqlParameterDatabaseType,
sqlParameterEnabledSyntaxes,
sqlParameterReplaceInsideQuotes,
sqlParameterAnsiQuotes,
sqlParameterNoBackslashEscapes,
onSqlParametersConfirm,
explainMode,
} = useSqlExecution({
Expand Down Expand Up @@ -2202,6 +2205,9 @@ onUnmounted(() => {
:sql-parameter-names="sqlParameterNames"
:sql-parameter-database-type="sqlParameterDatabaseType"
:sql-parameter-enabled-syntaxes="sqlParameterEnabledSyntaxes"
:sql-parameter-replace-inside-quotes="sqlParameterReplaceInsideQuotes"
:sql-parameter-ansi-quotes="sqlParameterAnsiQuotes"
:sql-parameter-no-backslash-escapes="sqlParameterNoBackslashEscapes"
@update:show-connection-dialog="setConnectionDialogOpen"
@update:show-danger-dialog="showDangerDialog = $event"
@update:suppress-danger-confirm="suppressDangerConfirm = $event"
Expand Down
22 changes: 16 additions & 6 deletions apps/desktop/src/components/editor/EditorSettingsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,16 @@ import { validateConfigName, generateId, type AiConfigItem, type ConfigNameValid
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 {
DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES,
normalizeSqlVariableSyntaxOverrides,
SQL_VARIABLE_SYNTAX_DATABASE_TYPES,
SQL_VARIABLE_SYNTAX_KEYS,
SQL_VARIABLE_SYNTAX_TOKENS,
sqlVariableSyntaxKeysForDatabase,
type SqlVariableSyntaxOverrides,
type SqlVariableSyntaxToggles,
} from "@/lib/sql/sqlVariableSyntax";
import { buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, type McpEnvEntry, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates";
import { isMacOS } from "@/lib/backend/platform";
import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState";
Expand Down Expand Up @@ -302,9 +311,10 @@ const editTableColumnTemplateRows = ref<TableColumnTemplateGridRow[]>(tableColum
const editTableColumnTemplateDatabaseType = ref<DatabaseType>(TABLE_COLUMN_TEMPLATE_DATABASE_TYPES[0] ?? "mysql");
const editSqlVariableSyntaxOverrides = ref<SqlVariableSyntaxOverrides>(normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides));
const editSqlVariableSyntaxDatabaseType = ref<DatabaseType>(SQL_VARIABLE_SYNTAX_DATABASE_TYPES[0] ?? "mysql");
const visibleSqlVariableSyntaxKeys = computed(() => sqlVariableSyntaxKeysForDatabase(editSqlVariableSyntaxDatabaseType.value));

function sqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles): boolean {
return editSqlVariableSyntaxOverrides.value[editSqlVariableSyntaxDatabaseType.value]?.[key] ?? true;
return editSqlVariableSyntaxOverrides.value[editSqlVariableSyntaxDatabaseType.value]?.[key] ?? DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES[key];
}

function setSqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles, value: boolean) {
Expand All @@ -315,13 +325,13 @@ function setSqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles, value:
[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])) {
// Keep storage sparse while preserving the opt-in setting whose default is false.
if (SQL_VARIABLE_SYNTAX_KEYS.every((toggleKey) => merged[toggleKey] === DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES[toggleKey])) {
delete next[dbType];
} else {
const partial: Partial<SqlVariableSyntaxToggles> = {};
for (const toggleKey of SQL_VARIABLE_SYNTAX_KEYS) {
if (!merged[toggleKey]) partial[toggleKey] = false;
if (merged[toggleKey] !== DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES[toggleKey]) partial[toggleKey] = merged[toggleKey];
}
next[dbType] = partial;
}
Expand Down Expand Up @@ -3107,7 +3117,7 @@ onUnmounted(cleanupPreviewEditor);
</Select>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div v-for="key in SQL_VARIABLE_SYNTAX_KEYS" :key="key" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div v-for="key in visibleSqlVariableSyntaxKeys" :key="key" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="min-w-0 space-y-1">
<Label :for="`sql-var-syntax-${key}`" class="flex items-center gap-1.5">
<span class="font-mono text-xs text-primary">{{ SQL_VARIABLE_SYNTAX_TOKENS[key] }}</span>
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/components/editor/SqlParameterDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const props = defineProps<{
parameters: SqlParameterDescriptor[];
databaseType?: DatabaseType;
enabledSyntaxes?: SqlParameterSyntax[];
replaceInsideQuotes?: boolean;
ansiQuotes?: boolean;
noBackslashEscapes?: boolean;
}>();

const emit = defineEmits<{
Expand All @@ -47,7 +50,15 @@ const syntaxLabels: Record<SqlParameterSyntax, string> = {
sqlserver: "@name",
};

const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value, { databaseType: props.databaseType, enabledSyntaxes: props.enabledSyntaxes }));
const resolvedSql = computed(() =>
substituteSqlParameters(props.sql, values.value, {
databaseType: props.databaseType,
enabledSyntaxes: props.enabledSyntaxes,
replaceInsideQuotes: props.replaceInsideQuotes,
ansiQuotes: props.ansiQuotes,
noBackslashEscapes: props.noBackslashEscapes,
}),
);
const highlightedSql = computed(() => highlight(resolvedSql.value));

watch(
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/components/layout/AppDialogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const props = defineProps<{
sqlParameterNames: SqlParameterDescriptor[];
sqlParameterDatabaseType?: DatabaseType;
sqlParameterEnabledSyntaxes?: SqlParameterSyntax[];
sqlParameterReplaceInsideQuotes?: boolean;
sqlParameterAnsiQuotes?: boolean;
sqlParameterNoBackslashEscapes?: boolean;
}>();

const emit = defineEmits<{
Expand Down Expand Up @@ -169,6 +172,9 @@ watch(
:parameters="sqlParameterNames"
:database-type="sqlParameterDatabaseType"
:enabled-syntaxes="sqlParameterEnabledSyntaxes"
:replace-inside-quotes="sqlParameterReplaceInsideQuotes"
:ansi-quotes="sqlParameterAnsiQuotes"
:no-backslash-escapes="sqlParameterNoBackslashEscapes"
@update:open="emit('update:showSqlParameterDialog', $event)"
@execute="emit('sqlParametersConfirm', $event)"
/>
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,58 @@ describe("useSqlExecution", () => {
expect(executedSql).toContain("where fp.create_at < @date_start");
});

it("keeps quoted braced placeholders unchanged when interpolation is not configured", async () => {
const sql = "select 'C:\\', '${date_end}'";
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
if (activeTab.value) activeTab.value.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
});
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
useSettingsStore().editorSettings.sqlVariableSyntaxOverrides = { mysql: { noBackslashEscapes: true } };

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(executeCurrentSql).toHaveBeenCalledWith(sql, {});
});

it("opens the parameter dialog for quoted braced placeholders after per-database opt-in", async () => {
const sql = "select '${date_end}', '#{status}'";
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockResolvedValue(undefined);
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
useSettingsStore().editorSettings.sqlVariableSyntaxOverrides = { mysql: { replaceInsideQuotes: true, ansiQuotes: true, noBackslashEscapes: true } };

const execution = useSqlExecution({
activeTab: computed(() => activeTab.value),
activeConnection: computed(() => activeConnection.value),
executableSql: computed(() => sql),
activeOutputView,
});

await execution.tryExecute();

expect(execution.showSqlParameterDialog.value).toBe(true);
expect(execution.sqlParameterNames.value.map((parameter) => parameter.name)).toEqual(["date_end", "status"]);
expect(execution.sqlParameterReplaceInsideQuotes.value).toBe(true);
expect(execution.sqlParameterAnsiQuotes.value).toBe(true);
expect(execution.sqlParameterNoBackslashEscapes.value).toBe(true);
expect(executeCurrentSql).not.toHaveBeenCalled();
});

it("sends Doris STRUCT DDL unchanged without opening the parameter dialog", async () => {
const sql = `
create table \`events\` (
Expand Down
23 changes: 22 additions & 1 deletion apps/desktop/src/composables/useSqlExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export function useSqlExecution(deps: {
const sqlParameterNames = ref<SqlParameterDescriptor[]>([]);
const sqlParameterDatabaseType = ref<DatabaseType | undefined>();
const sqlParameterEnabledSyntaxes = ref<SqlParameterSyntax[]>([]);
const sqlParameterReplaceInsideQuotes = ref(false);
const sqlParameterAnsiQuotes = ref(false);
const sqlParameterNoBackslashEscapes = ref(false);
const pendingSourceOffset = ref<number | undefined>();

async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<{ sql: string; sourceOffset?: number }> {
Expand Down Expand Up @@ -166,12 +169,21 @@ export function useSqlExecution(deps: {
const databaseType = deps.activeConnection.value?.db_type;
const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, databaseType);
const enabledSyntaxes = enabledSqlParameterSyntaxes(toggles);
const parameters = extractSqlParameterDescriptors(sql, { databaseType, enabledSyntaxes });
const parameters = extractSqlParameterDescriptors(sql, {
databaseType,
enabledSyntaxes,
replaceInsideQuotes: toggles.replaceInsideQuotes,
ansiQuotes: toggles.ansiQuotes,
noBackslashEscapes: toggles.noBackslashEscapes,
});
if (!parameters.length) return false;
sqlParameterSourceSql.value = sql;
sqlParameterNames.value = parameters;
sqlParameterDatabaseType.value = databaseType;
sqlParameterEnabledSyntaxes.value = enabledSyntaxes;
sqlParameterReplaceInsideQuotes.value = toggles.replaceInsideQuotes;
sqlParameterAnsiQuotes.value = toggles.ansiQuotes;
sqlParameterNoBackslashEscapes.value = toggles.noBackslashEscapes;
pendingSourceOffset.value = sourceOffset;
showSqlParameterDialog.value = true;
return true;
Expand Down Expand Up @@ -272,6 +284,9 @@ export function useSqlExecution(deps: {
sqlParameterNames.value = [];
sqlParameterDatabaseType.value = undefined;
sqlParameterEnabledSyntaxes.value = [];
sqlParameterReplaceInsideQuotes.value = false;
sqlParameterAnsiQuotes.value = false;
sqlParameterNoBackslashEscapes.value = false;
const sourceOffset = pendingSourceOffset.value;
pendingSourceOffset.value = undefined;
await continueExecute(sql, sourceOffset);
Expand All @@ -283,6 +298,9 @@ export function useSqlExecution(deps: {
sqlParameterNames.value = [];
sqlParameterDatabaseType.value = undefined;
sqlParameterEnabledSyntaxes.value = [];
sqlParameterReplaceInsideQuotes.value = false;
sqlParameterAnsiQuotes.value = false;
sqlParameterNoBackslashEscapes.value = false;
pendingSourceOffset.value = undefined;
});

Expand All @@ -301,6 +319,9 @@ export function useSqlExecution(deps: {
sqlParameterNames,
sqlParameterDatabaseType,
sqlParameterEnabledSyntaxes,
sqlParameterReplaceInsideQuotes,
sqlParameterAnsiQuotes,
sqlParameterNoBackslashEscapes,
onSqlParametersConfirm,
explainMode,
};
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3663,7 +3663,7 @@ export default {
prefillNewQueryWithSelect: "Prefill new query with SELECT *",
prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM <table> based on the active table tab or the table selected in the sidebar.",
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.",
sqlVariableSyntaxDescription: "Choose how DBX substitutes variables and placeholders before running SQL, per database type. Placeholder syntaxes are enabled by default; quoted interpolation is opt-in.",
sqlVariableSyntax_positional: "Positional placeholder",
sqlVariableSyntax_positionalDescription: "Prompt for a value to substitute each positional placeholder in order.",
sqlVariableSyntax_named: "Named placeholder",
Expand All @@ -3676,6 +3676,12 @@ export default {
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.",
sqlVariableSyntax_replaceInsideQuotes: "Replace braced placeholders inside quotes",
sqlVariableSyntax_replaceInsideQuotesDescription: "Also detect ${name} and #{name} inside quoted strings and identifiers. Values are inserted as escaped text; disabled by default.",
sqlVariableSyntax_ansiQuotes: "ANSI_QUOTES mode",
sqlVariableSyntax_ansiQuotesDescription: "Enable when this MySQL, Doris, or StarRocks session uses ANSI_QUOTES so double-quoted placeholders are treated as identifiers instead of strings.",
sqlVariableSyntax_noBackslashEscapes: "NO_BACKSLASH_ESCAPES mode",
sqlVariableSyntax_noBackslashEscapesDescription: "Enable when this MySQL, Doris, or StarRocks session uses NO_BACKSLASH_ESCAPES so DBX keeps backslashes literal instead of doubling them.",
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",
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3423,7 +3423,7 @@ export default withEnglishFallback({
prefillNewQueryWithSelect: "Rellenar nueva consulta con SELECT *",
prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM <tabla> según la pestaña de tabla activa o la tabla seleccionada en la barra lateral.",
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.",
sqlVariableSyntaxDescription: "Elige cómo sustituye DBX las variables y los marcadores antes de ejecutar SQL, por tipo de base de datos. Las sintaxis están activadas por defecto; la interpolación entre comillas es opcional.",
sqlVariableSyntax_positional: "Marcador posicional",
sqlVariableSyntax_positionalDescription: "Solicita un valor para sustituir cada marcador posicional en orden.",
sqlVariableSyntax_named: "Marcador con nombre",
Expand All @@ -3436,6 +3436,12 @@ export default withEnglishFallback({
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.",
sqlVariableSyntax_replaceInsideQuotes: "Sustituir marcadores entre comillas",
sqlVariableSyntax_replaceInsideQuotesDescription: "También detecta ${name} y #{name} dentro de cadenas e identificadores entre comillas. Los valores se insertan como texto escapado; desactivado de forma predeterminada.",
sqlVariableSyntax_ansiQuotes: "Modo ANSI_QUOTES",
sqlVariableSyntax_ansiQuotesDescription: "Actívalo cuando la sesión de MySQL, Doris o StarRocks use ANSI_QUOTES para tratar los marcadores entre comillas dobles como identificadores en lugar de cadenas.",
sqlVariableSyntax_noBackslashEscapes: "Modo NO_BACKSLASH_ESCAPES",
sqlVariableSyntax_noBackslashEscapesDescription: "Actívalo cuando la sesión de MySQL, Doris o StarRocks use NO_BACKSLASH_ESCAPES para conservar las barras invertidas literales en lugar de duplicarlas.",
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",
Expand Down
Loading
Loading