feat(editor): configure SQL variable syntax
This commit is contained in:
parent
54632f4c16
commit
1a309b6b63
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<TableColumnTemplateGridRow[]>(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields));
|
||||
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");
|
||||
|
||||
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<SqlVariableSyntaxToggles> = {};
|
||||
for (const toggleKey of SQL_VARIABLE_SYNTAX_KEYS) {
|
||||
if (!merged[toggleKey]) partial[toggleKey] = false;
|
||||
}
|
||||
next[dbType] = partial;
|
||||
}
|
||||
editSqlVariableSyntaxOverrides.value = next;
|
||||
}
|
||||
const tableColumnTemplateSectionRef = ref<HTMLElement | null>(null);
|
||||
const draggedTableColumnTemplateRowId = ref<string | null>(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);
|
|||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium text-muted-foreground">{{ t("settings.sqlVariableSyntax") }}</div>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.sqlVariableSyntaxDescription") }}</p>
|
||||
</div>
|
||||
<Select v-model="editSqlVariableSyntaxDatabaseType">
|
||||
<SelectTrigger class="h-8 w-44 px-2 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent class="max-h-72">
|
||||
<SelectItem v-for="dbType in SQL_VARIABLE_SYNTAX_DATABASE_TYPES" :key="dbType" :value="dbType">
|
||||
{{ dbType }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</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 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>
|
||||
<span>{{ t(`settings.sqlVariableSyntax_${key}`) }}</span>
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t(`settings.sqlVariableSyntax_${key}Description`) }}</p>
|
||||
</div>
|
||||
<Switch :id="`sql-var-syntax-${key}`" :model-value="sqlVariableSyntaxToggle(key)" class="mt-0.5 shrink-0" @update:model-value="(value) => setSqlVariableSyntaxToggle(key, value as boolean)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.preview") }}</Label>
|
||||
|
|
|
|||
|
|
@ -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<SqlParameterSyntax, string> = {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
/>
|
||||
<SqlParameterDialog v-if="showSqlParameterDialog" :open="showSqlParameterDialog" :sql="sqlParameterSourceSql" :parameters="sqlParameterNames" :database-type="sqlParameterDatabaseType" @update:open="emit('update:showSqlParameterDialog', $event)" @execute="emit('sqlParametersConfirm', $event)" />
|
||||
<SqlParameterDialog
|
||||
v-if="showSqlParameterDialog"
|
||||
:open="showSqlParameterDialog"
|
||||
:sql="sqlParameterSourceSql"
|
||||
:parameters="sqlParameterNames"
|
||||
:database-type="sqlParameterDatabaseType"
|
||||
:enabled-syntaxes="sqlParameterEnabledSyntaxes"
|
||||
@update:open="emit('update:showSqlParameterDialog', $event)"
|
||||
@execute="emit('sqlParametersConfirm', $event)"
|
||||
/>
|
||||
<DataTransferDialog v-model:open="dialogs.showTransferDialog.value" :prefill-connection-id="dialogs.transferPrefillConnectionId.value" :prefill-database="dialogs.transferPrefillDatabase.value" />
|
||||
<SchemaDiffDialog v-if="dialogs.showSchemaDiffDialog.value" v-model:open="dialogs.showSchemaDiffDialog.value" :prefill-connection-id="dialogs.schemaDiffPrefillConnectionId.value" :prefill-database="dialogs.schemaDiffPrefillDatabase.value" :prefill-schema="dialogs.schemaDiffPrefillSchema.value" />
|
||||
<DataCompareDialog
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ import { classifySqlActivityKind } from "@/lib/history/historyActivityKind";
|
|||
import { sqlMetadataRefreshTarget } from "@/lib/sql/sqlMetadataRefresh";
|
||||
import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redis/redisCommandSafety";
|
||||
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { extractSqlParameterDescriptors, type SqlParameterDescriptor } from "@/lib/sql/sqlParameters";
|
||||
import { extractSqlParameterDescriptors, type SqlParameterDescriptor, type SqlParameterSyntax } from "@/lib/sql/sqlParameters";
|
||||
import { expandSqlVariables } from "@/lib/sql/sqlVariables";
|
||||
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
|
||||
import type { ConnectionConfig, DatabaseType, QueryTab } from "@/types/database";
|
||||
|
||||
const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
|
||||
|
|
@ -63,12 +64,15 @@ export function useSqlExecution(deps: {
|
|||
const sqlParameterSourceSql = ref("");
|
||||
const sqlParameterNames = ref<SqlParameterDescriptor[]>([]);
|
||||
const sqlParameterDatabaseType = ref<DatabaseType | undefined>();
|
||||
const sqlParameterEnabledSyntaxes = ref<SqlParameterSyntax[]>([]);
|
||||
|
||||
async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<string> {
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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スキャンカウント",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 扫描数量",
|
||||
|
|
|
|||
|
|
@ -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 掃描數量",
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<keyof SqlVariableSyntaxToggles, string> = {
|
||||
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<Record<DatabaseType, Partial<SqlVariableSyntaxToggles>>>;
|
||||
|
||||
// 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<string, unknown>)) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
||||
const partial: Partial<SqlVariableSyntaxToggles> = {};
|
||||
for (const key of SQL_VARIABLE_SYNTAX_KEYS) {
|
||||
if ((raw as Record<string, unknown>)[key] === false) partial[key] = false;
|
||||
}
|
||||
if (Object.keys(partial).length > 0) result[dbType as DatabaseType] = partial;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -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<EditorSettings>, 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue