fix(hana): improve metadata fallback and SQLScript parameters
This commit is contained in:
1 parent
fd80464cb2
commit
430f097602
6 files changed
+57
-22
No files matched your search
@@ -235,15 +235,16 @@ function promptActiveDatabaseSelection() {
|
||||
toast(t("editor.selectDatabaseRequired"), 2500);
|
||||
}
|
||||
|
||||
const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, showSqlParameterDialog, sqlParameterSourceSql, sqlParameterNames, 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, onSqlParametersConfirm, explainMode } =
|
||||
useSqlExecution({
|
||||
activeTab,
|
||||
activeConnection,
|
||||
executableSql,
|
||||
resolveExecutableSql: resolveActiveExecutableSql,
|
||||
activeOutputView,
|
||||
blockDangerousRedisCommands,
|
||||
onMissingDatabase: promptActiveDatabaseSelection,
|
||||
});
|
||||
|
||||
function requestActiveEditorExecute() {
|
||||
if (contentAreaRef.value?.requestQueryEditorExecute?.()) return;
|
||||
@@ -1951,6 +1952,7 @@ onUnmounted(() => {
|
||||
:show-sql-parameter-dialog="showSqlParameterDialog"
|
||||
:sql-parameter-source-sql="sqlParameterSourceSql"
|
||||
:sql-parameter-names="sqlParameterNames"
|
||||
:sql-parameter-database-type="sqlParameterDatabaseType"
|
||||
@update:show-connection-dialog="setConnectionDialogOpen"
|
||||
@update:show-danger-dialog="showDangerDialog = $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 { substituteSqlParameters, type SqlParameterDescriptor, type SqlParameterInput, type SqlParameterSyntax, type SqlParameterValueKind } from "@/lib/sql/sqlParameters";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
@@ -20,6 +21,7 @@ const open = defineModel<boolean>("open", { default: false });
|
||||
const props = defineProps<{
|
||||
sql: string;
|
||||
parameters: SqlParameterDescriptor[];
|
||||
databaseType?: DatabaseType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -41,7 +43,7 @@ const syntaxLabels: Record<SqlParameterSyntax, string> = {
|
||||
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));
|
||||
|
||||
watch(
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useDialogSources } from "@/composables/useDialogSources";
|
||||
import type { ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
import type { SqlParameterDescriptor } from "@/lib/sql/sqlParameters";
|
||||
import type { ConfigTab } from "@/components/connection/ConnectionDialog.vue";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
const props = defineProps<{
|
||||
showConnectionDialog: boolean;
|
||||
@@ -33,6 +34,7 @@ const props = defineProps<{
|
||||
showSqlParameterDialog: boolean;
|
||||
sqlParameterSourceSql: string;
|
||||
sqlParameterNames: SqlParameterDescriptor[];
|
||||
sqlParameterDatabaseType?: DatabaseType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -133,7 +135,7 @@ watch(
|
||||
@update:suppress-future-prompts="emit('update:suppressDangerConfirm', $event)"
|
||||
@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" />
|
||||
<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
|
||||
|
||||
@@ -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 { useQueryStore } from "@/stores/queryStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
@@ -12,7 +12,7 @@ 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 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;
|
||||
|
||||
@@ -61,6 +61,7 @@ export function useSqlExecution(deps: {
|
||||
const showSqlParameterDialog = ref(false);
|
||||
const sqlParameterSourceSql = ref("");
|
||||
const sqlParameterNames = ref<SqlParameterDescriptor[]>([]);
|
||||
const sqlParameterDatabaseType = ref<DatabaseType | undefined>();
|
||||
|
||||
async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<string> {
|
||||
if (typeof source === "string") return source;
|
||||
@@ -107,10 +108,12 @@ export function useSqlExecution(deps: {
|
||||
}
|
||||
|
||||
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;
|
||||
sqlParameterSourceSql.value = sql;
|
||||
sqlParameterNames.value = parameters;
|
||||
sqlParameterDatabaseType.value = databaseType;
|
||||
showSqlParameterDialog.value = true;
|
||||
return true;
|
||||
}
|
||||
@@ -201,9 +204,17 @@ export function useSqlExecution(deps: {
|
||||
showSqlParameterDialog.value = false;
|
||||
sqlParameterSourceSql.value = "";
|
||||
sqlParameterNames.value = [];
|
||||
sqlParameterDatabaseType.value = undefined;
|
||||
await continueExecute(sql);
|
||||
}
|
||||
|
||||
watch(showSqlParameterDialog, (open) => {
|
||||
if (open) return;
|
||||
sqlParameterSourceSql.value = "";
|
||||
sqlParameterNames.value = [];
|
||||
sqlParameterDatabaseType.value = undefined;
|
||||
});
|
||||
|
||||
return {
|
||||
dangerSql,
|
||||
pendingDangerSql,
|
||||
@@ -217,6 +228,7 @@ export function useSqlExecution(deps: {
|
||||
showSqlParameterDialog,
|
||||
sqlParameterSourceSql,
|
||||
sqlParameterNames,
|
||||
sqlParameterDatabaseType,
|
||||
onSqlParametersConfirm,
|
||||
explainMode,
|
||||
};
|
||||
|
||||
@@ -176,6 +176,11 @@ describe("extractSqlParameters", () => {
|
||||
const sql = "select value::int, value := 1, :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", () => {
|
||||
@@ -264,6 +269,11 @@ DEALLOCATE PREPARE stmt;`;
|
||||
}),
|
||||
).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", () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export type SqlParameterValueKind = "string" | "number" | "boolean" | "null" | "raw";
|
||||
|
||||
export interface SqlParameterInput {
|
||||
@@ -19,19 +21,23 @@ interface ParameterOccurrence extends SqlParameterDescriptor {
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface SqlParameterOptions {
|
||||
databaseType?: DatabaseType;
|
||||
}
|
||||
|
||||
const PARAMETER_NAME_RE = /^[\p{L}_][\p{L}\p{N}_]*$/u;
|
||||
const PARAMETER_NAME_START_RE = /[\p{L}_]/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"]);
|
||||
|
||||
export function extractSqlParameters(sql: string): string[] {
|
||||
return extractSqlParameterDescriptors(sql).map((descriptor) => descriptor.key);
|
||||
export function extractSqlParameters(sql: string, options?: SqlParameterOptions): string[] {
|
||||
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 descriptors: SqlParameterDescriptor[] = [];
|
||||
for (const occurrence of findSqlParameterOccurrences(sql)) {
|
||||
for (const occurrence of findSqlParameterOccurrences(sql, options)) {
|
||||
if (names.has(occurrence.key)) continue;
|
||||
names.add(occurrence.key);
|
||||
descriptors.push({
|
||||
@@ -44,8 +50,8 @@ export function extractSqlParameterDescriptors(sql: string): SqlParameterDescrip
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>): string {
|
||||
const occurrences = findSqlParameterOccurrences(sql);
|
||||
export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>, options?: SqlParameterOptions): string {
|
||||
const occurrences = findSqlParameterOccurrences(sql, options);
|
||||
if (!occurrences.length) return sql;
|
||||
|
||||
let result = "";
|
||||
@@ -68,9 +74,10 @@ export function sqlParameterLiteral(input: SqlParameterInput): string {
|
||||
return quoteSqlString(raw);
|
||||
}
|
||||
|
||||
function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
|
||||
function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions): ParameterOccurrence[] {
|
||||
const occurrences: ParameterOccurrence[] = [];
|
||||
const nativeSqlServerParameters = collectNativeSqlServerParameters(sql);
|
||||
const supportsNamedParameters = options?.databaseType !== "saphana";
|
||||
let i = 0;
|
||||
let dollarQuoteEnd = "";
|
||||
let positionalIndex = 0;
|
||||
@@ -110,7 +117,7 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === ":") {
|
||||
if (ch === ":" && supportsNamedParameters) {
|
||||
const name = readParameterName(sql, i + 1);
|
||||
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") {
|
||||
occurrences.push({
|
||||
|
||||
Reference in new issue
Block a user