fix(hana): improve metadata fallback and SQLScript parameters

This commit is contained in:
LRcoding authored and GitHub committed 2026-07-08 17:25:34 +08:00
1 parent fd80464cb2
commit 430f097602
6 files changed
+57 -22

No files matched your search

+11 -9
View File
@@ -235,15 +235,16 @@ function promptActiveDatabaseSelection() {
toast(t("editor.selectDatabaseRequired"), 2500); toast(t("editor.selectDatabaseRequired"), 2500);
} }
const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, showSqlParameterDialog, sqlParameterSourceSql, sqlParameterNames, onSqlParametersConfirm, explainMode } = useSqlExecution({ const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, showSqlParameterDialog, sqlParameterSourceSql, sqlParameterNames, sqlParameterDatabaseType, onSqlParametersConfirm, explainMode } =
activeTab, useSqlExecution({
activeConnection, activeTab,
executableSql, activeConnection,
resolveExecutableSql: resolveActiveExecutableSql, executableSql,
activeOutputView, resolveExecutableSql: resolveActiveExecutableSql,
blockDangerousRedisCommands, activeOutputView,
onMissingDatabase: promptActiveDatabaseSelection, blockDangerousRedisCommands,
}); onMissingDatabase: promptActiveDatabaseSelection,
});
function requestActiveEditorExecute() { function requestActiveEditorExecute() {
if (contentAreaRef.value?.requestQueryEditorExecute?.()) return; if (contentAreaRef.value?.requestQueryEditorExecute?.()) return;
@@ -1951,6 +1952,7 @@ onUnmounted(() => {
:show-sql-parameter-dialog="showSqlParameterDialog" :show-sql-parameter-dialog="showSqlParameterDialog"
:sql-parameter-source-sql="sqlParameterSourceSql" :sql-parameter-source-sql="sqlParameterSourceSql"
:sql-parameter-names="sqlParameterNames" :sql-parameter-names="sqlParameterNames"
:sql-parameter-database-type="sqlParameterDatabaseType"
@update:show-connection-dialog="setConnectionDialogOpen" @update:show-connection-dialog="setConnectionDialogOpen"
@update:show-danger-dialog="showDangerDialog = $event" @update:show-danger-dialog="showDangerDialog = $event"
@update:suppress-danger-confirm="suppressDangerConfirm = $event" @update:suppress-danger-confirm="suppressDangerConfirm = $event"
@@ -11,6 +11,7 @@ import TruncatedTextTooltip from "@/components/ui/TruncatedTextTooltip.vue";
import { loadSqlParameterHistory, rememberSqlParameterValues } from "@/lib/sql/sqlParameterHistory"; import { loadSqlParameterHistory, rememberSqlParameterValues } from "@/lib/sql/sqlParameterHistory";
import { substituteSqlParameters, type SqlParameterDescriptor, type SqlParameterInput, type SqlParameterSyntax, type SqlParameterValueKind } from "@/lib/sql/sqlParameters"; import { substituteSqlParameters, type SqlParameterDescriptor, type SqlParameterInput, type SqlParameterSyntax, type SqlParameterValueKind } from "@/lib/sql/sqlParameters";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter"; import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import type { DatabaseType } from "@/types/database";
const { t } = useI18n(); const { t } = useI18n();
const { highlight } = useSqlHighlighter(); const { highlight } = useSqlHighlighter();
@@ -20,6 +21,7 @@ const open = defineModel<boolean>("open", { default: false });
const props = defineProps<{ const props = defineProps<{
sql: string; sql: string;
parameters: SqlParameterDescriptor[]; parameters: SqlParameterDescriptor[];
databaseType?: DatabaseType;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -41,7 +43,7 @@ const syntaxLabels: Record<SqlParameterSyntax, string> = {
sqlserver: "@name", sqlserver: "@name",
}; };
const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value)); const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value, { databaseType: props.databaseType }));
const highlightedSql = computed(() => highlight(resolvedSql.value)); const highlightedSql = computed(() => highlight(resolvedSql.value));
watch( watch(
@@ -22,6 +22,7 @@ import { useDialogSources } from "@/composables/useDialogSources";
import type { ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; import type { ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
import type { SqlParameterDescriptor } from "@/lib/sql/sqlParameters"; import type { SqlParameterDescriptor } from "@/lib/sql/sqlParameters";
import type { ConfigTab } from "@/components/connection/ConnectionDialog.vue"; import type { ConfigTab } from "@/components/connection/ConnectionDialog.vue";
import type { DatabaseType } from "@/types/database";
const props = defineProps<{ const props = defineProps<{
showConnectionDialog: boolean; showConnectionDialog: boolean;
@@ -33,6 +34,7 @@ const props = defineProps<{
showSqlParameterDialog: boolean; showSqlParameterDialog: boolean;
sqlParameterSourceSql: string; sqlParameterSourceSql: string;
sqlParameterNames: SqlParameterDescriptor[]; sqlParameterNames: SqlParameterDescriptor[];
sqlParameterDatabaseType?: DatabaseType;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -133,7 +135,7 @@ watch(
@update:suppress-future-prompts="emit('update:suppressDangerConfirm', $event)" @update:suppress-future-prompts="emit('update:suppressDangerConfirm', $event)"
@confirm="emit('dangerConfirm')" @confirm="emit('dangerConfirm')"
/> />
<SqlParameterDialog v-if="showSqlParameterDialog" :open="showSqlParameterDialog" :sql="sqlParameterSourceSql" :parameters="sqlParameterNames" @update:open="emit('update:showSqlParameterDialog', $event)" @execute="emit('sqlParametersConfirm', $event)" /> <SqlParameterDialog v-if="showSqlParameterDialog" :open="showSqlParameterDialog" :sql="sqlParameterSourceSql" :parameters="sqlParameterNames" :database-type="sqlParameterDatabaseType" @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" /> <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" /> <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 <DataCompareDialog
@@ -1,4 +1,4 @@
import { ref, type Ref, type ComputedRef } from "vue"; import { ref, watch, type Ref, type ComputedRef } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQueryStore } from "@/stores/queryStore"; import { useQueryStore } from "@/stores/queryStore";
import { useHistoryStore } from "@/stores/historyStore"; import { useHistoryStore } from "@/stores/historyStore";
@@ -12,7 +12,7 @@ import { sqlMetadataRefreshTarget } from "@/lib/sql/sqlMetadataRefresh";
import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redis/redisCommandSafety"; import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redis/redisCommandSafety";
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget"; import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
import { extractSqlParameterDescriptors, type SqlParameterDescriptor } from "@/lib/sql/sqlParameters"; import { extractSqlParameterDescriptors, type SqlParameterDescriptor } from "@/lib/sql/sqlParameters";
import type { ConnectionConfig, QueryTab } from "@/types/database"; import type { ConnectionConfig, DatabaseType, QueryTab } from "@/types/database";
const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
@@ -61,6 +61,7 @@ export function useSqlExecution(deps: {
const showSqlParameterDialog = ref(false); const showSqlParameterDialog = ref(false);
const sqlParameterSourceSql = ref(""); const sqlParameterSourceSql = ref("");
const sqlParameterNames = ref<SqlParameterDescriptor[]>([]); const sqlParameterNames = ref<SqlParameterDescriptor[]>([]);
const sqlParameterDatabaseType = ref<DatabaseType | undefined>();
async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<string> { async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<string> {
if (typeof source === "string") return source; if (typeof source === "string") return source;
@@ -107,10 +108,12 @@ export function useSqlExecution(deps: {
} }
function prepareSqlParameterDialog(sql: string): boolean { function prepareSqlParameterDialog(sql: string): boolean {
const parameters = extractSqlParameterDescriptors(sql); const databaseType = deps.activeConnection.value?.db_type;
const parameters = extractSqlParameterDescriptors(sql, { databaseType });
if (!parameters.length) return false; if (!parameters.length) return false;
sqlParameterSourceSql.value = sql; sqlParameterSourceSql.value = sql;
sqlParameterNames.value = parameters; sqlParameterNames.value = parameters;
sqlParameterDatabaseType.value = databaseType;
showSqlParameterDialog.value = true; showSqlParameterDialog.value = true;
return true; return true;
} }
@@ -201,9 +204,17 @@ export function useSqlExecution(deps: {
showSqlParameterDialog.value = false; showSqlParameterDialog.value = false;
sqlParameterSourceSql.value = ""; sqlParameterSourceSql.value = "";
sqlParameterNames.value = []; sqlParameterNames.value = [];
sqlParameterDatabaseType.value = undefined;
await continueExecute(sql); await continueExecute(sql);
} }
watch(showSqlParameterDialog, (open) => {
if (open) return;
sqlParameterSourceSql.value = "";
sqlParameterNames.value = [];
sqlParameterDatabaseType.value = undefined;
});
return { return {
dangerSql, dangerSql,
pendingDangerSql, pendingDangerSql,
@@ -217,6 +228,7 @@ export function useSqlExecution(deps: {
showSqlParameterDialog, showSqlParameterDialog,
sqlParameterSourceSql, sqlParameterSourceSql,
sqlParameterNames, sqlParameterNames,
sqlParameterDatabaseType,
onSqlParametersConfirm, onSqlParametersConfirm,
explainMode, explainMode,
}; };
@@ -176,6 +176,11 @@ describe("extractSqlParameters", () => {
const sql = "select value::int, value := 1, :actual_value"; const sql = "select value::int, value := 1, :actual_value";
expect(extractSqlParameters(sql)).toEqual(["actual_value"]); expect(extractSqlParameters(sql)).toEqual(["actual_value"]);
}); });
it("ignores HANA SQLScript variable references", () => {
const sql = "DO BEGIN Dummy1 = SELECT 1 FROM DUMMY; SELECT * FROM :Dummy1; END";
expect(extractSqlParameters(sql, { databaseType: "saphana" })).toEqual([]);
});
}); });
describe("substituteSqlParameters", () => { describe("substituteSqlParameters", () => {
@@ -264,6 +269,11 @@ DEALLOCATE PREPARE stmt;`;
}), }),
).toBe("exec dbo.search_orders @date_start = '2026-07-04', @status = 'paid', @tenant_id = 7"); ).toBe("exec dbo.search_orders @date_start = '2026-07-04', @status = 'paid', @tenant_id = 7");
}); });
it("keeps HANA SQLScript variable references while replacing template parameters", () => {
const sql = "DO BEGIN Dummy1 = SELECT * FROM ORDERS WHERE TENANT_ID = ${tenant_id}; SELECT * FROM :Dummy1; END";
expect(substituteSqlParameters(sql, { tenant_id: { kind: "number", value: "42" } }, { databaseType: "saphana" })).toBe("DO BEGIN Dummy1 = SELECT * FROM ORDERS WHERE TENANT_ID = 42; SELECT * FROM :Dummy1; END");
});
}); });
describe("sqlParameterLiteral", () => { describe("sqlParameterLiteral", () => {
+15 -8
View File
@@ -1,3 +1,5 @@
import type { DatabaseType } from "@/types/database";
export type SqlParameterValueKind = "string" | "number" | "boolean" | "null" | "raw"; export type SqlParameterValueKind = "string" | "number" | "boolean" | "null" | "raw";
export interface SqlParameterInput { export interface SqlParameterInput {
@@ -19,19 +21,23 @@ interface ParameterOccurrence extends SqlParameterDescriptor {
end: number; end: number;
} }
export interface SqlParameterOptions {
databaseType?: DatabaseType;
}
const PARAMETER_NAME_RE = /^[\p{L}_][\p{L}\p{N}_]*$/u; const PARAMETER_NAME_RE = /^[\p{L}_][\p{L}\p{N}_]*$/u;
const PARAMETER_NAME_START_RE = /[\p{L}_]/u; const PARAMETER_NAME_START_RE = /[\p{L}_]/u;
const PARAMETER_NAME_CHAR_RE = /[\p{L}\p{N}_]/u; const PARAMETER_NAME_CHAR_RE = /[\p{L}\p{N}_]/u;
const SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS = new Set(["table", "from", "join", "into", "update", "truncate"]); const SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS = new Set(["table", "from", "join", "into", "update", "truncate"]);
export function extractSqlParameters(sql: string): string[] { export function extractSqlParameters(sql: string, options?: SqlParameterOptions): string[] {
return extractSqlParameterDescriptors(sql).map((descriptor) => descriptor.key); return extractSqlParameterDescriptors(sql, options).map((descriptor) => descriptor.key);
} }
export function extractSqlParameterDescriptors(sql: string): SqlParameterDescriptor[] { export function extractSqlParameterDescriptors(sql: string, options?: SqlParameterOptions): SqlParameterDescriptor[] {
const names = new Set<string>(); const names = new Set<string>();
const descriptors: SqlParameterDescriptor[] = []; const descriptors: SqlParameterDescriptor[] = [];
for (const occurrence of findSqlParameterOccurrences(sql)) { for (const occurrence of findSqlParameterOccurrences(sql, options)) {
if (names.has(occurrence.key)) continue; if (names.has(occurrence.key)) continue;
names.add(occurrence.key); names.add(occurrence.key);
descriptors.push({ descriptors.push({
@@ -44,8 +50,8 @@ export function extractSqlParameterDescriptors(sql: string): SqlParameterDescrip
return descriptors; return descriptors;
} }
export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>): string { export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>, options?: SqlParameterOptions): string {
const occurrences = findSqlParameterOccurrences(sql); const occurrences = findSqlParameterOccurrences(sql, options);
if (!occurrences.length) return sql; if (!occurrences.length) return sql;
let result = ""; let result = "";
@@ -68,9 +74,10 @@ export function sqlParameterLiteral(input: SqlParameterInput): string {
return quoteSqlString(raw); return quoteSqlString(raw);
} }
function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] { function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions): ParameterOccurrence[] {
const occurrences: ParameterOccurrence[] = []; const occurrences: ParameterOccurrence[] = [];
const nativeSqlServerParameters = collectNativeSqlServerParameters(sql); const nativeSqlServerParameters = collectNativeSqlServerParameters(sql);
const supportsNamedParameters = options?.databaseType !== "saphana";
let i = 0; let i = 0;
let dollarQuoteEnd = ""; let dollarQuoteEnd = "";
let positionalIndex = 0; let positionalIndex = 0;
@@ -110,7 +117,7 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
i += 1; i += 1;
continue; continue;
} }
if (ch === ":") { if (ch === ":" && supportsNamedParameters) {
const name = readParameterName(sql, i + 1); const name = readParameterName(sql, i + 1);
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") { if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") {
occurrences.push({ occurrences.push({