From 2220eed285caf93a8ec7ae0c6b95c806fd204c71 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 2 Aug 2026 16:03:41 +0800 Subject: [PATCH] fix(sql): preserve Oracle database link references --- .../__tests__/useSqlExecution.spec.ts | 25 +++++++++++++++++++ .../lib/__tests__/sql/sqlParameters.spec.ts | 8 ++++++ apps/desktop/src/lib/sql/sqlParameters.ts | 8 +++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts index 9310c37ae..74c0c6cb7 100644 --- a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -245,6 +245,31 @@ describe("useSqlExecution", () => { expect(executeCurrentSql).toHaveBeenCalledWith(resolvedSql, { openInNewResultTab: true }); }); + it("executes Oracle database-link queries without opening the parameter dialog", async () => { + const sql = "SELECT 1 FROM DUAL@WDHIS160;"; + const activeTab = ref(queryTab("ORCL")); + const activeConnection = ref(connection("oracle")); + 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: ["1"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; + }); + vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); + + 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(execution.sqlParameterNames.value).toEqual([]); + expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); + }); + it("sends native SET variables without client-side expansion", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts index 136d81af5..746c6354b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts @@ -53,6 +53,14 @@ describe("extractSqlParameters", () => { expect(extractSqlParameters("select @amount/2, @total / 4")).toEqual(["amount", "total"]); }); + it("ignores Oracle database links while preserving standalone at-sign placeholders", () => { + const sql = 'SELECT * FROM HR.EMPLOYEES@REMOTE_DB, "AUDIT_LOG"@ARCHIVE_DB WHERE tenant_id = @tenant_id'; + expect(extractSqlParameters("SELECT 1 FROM DUAL@WDHIS160;", { databaseType: "oracle" })).toEqual([]); + expect(extractSqlParameters(sql, { databaseType: "oracle" })).toEqual(["tenant_id"]); + expect(substituteSqlParameters(sql, { tenant_id: { kind: "number", value: "7" } }, { databaseType: "oracle" })).toBe('SELECT * FROM HR.EMPLOYEES@REMOTE_DB, "AUDIT_LOG"@ARCHIVE_DB WHERE tenant_id = 7'); + expect(extractSqlParameters("SELECT * FROM EMPLOYEES@REMOTE_DB", { databaseType: "postgres" })).toEqual(["REMOTE_DB"]); + }); + it("describes each placeholder syntax for the parameter dialog", () => { const sql = "select ? as a, :named as b, ${shell_name} as c, #{mybatis_name} as d, @sql_server_name as e"; expect(extractSqlParameterDescriptors(sql)).toEqual([ diff --git a/apps/desktop/src/lib/sql/sqlParameters.ts b/apps/desktop/src/lib/sql/sqlParameters.ts index e54d8005f..428588c6b 100644 --- a/apps/desktop/src/lib/sql/sqlParameters.ts +++ b/apps/desktop/src/lib/sql/sqlParameters.ts @@ -203,7 +203,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) } if (ch === "@" && isSyntaxEnabled("sqlserver")) { const name = readParameterName(sql, i + 1); - if (name && next !== "@" && sql[i - 1] !== "@" && !isJdbcxMcpScopedPackage(sql, i, i + 1 + name.length) && !nativeSqlServerParameters.declared.has(name.toLowerCase()) && !nativeSqlServerParameters.ignoredStarts.has(i)) { + if (name && next !== "@" && sql[i - 1] !== "@" && !isOracleDatabaseLinkMarker(sql, i, options?.databaseType) && !isJdbcxMcpScopedPackage(sql, i, i + 1 + name.length) && !nativeSqlServerParameters.declared.has(name.toLowerCase()) && !nativeSqlServerParameters.ignoredStarts.has(i)) { occurrences.push({ key: name, name, @@ -230,6 +230,12 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions) return occurrences; } +function isOracleDatabaseLinkMarker(sql: string, index: number, databaseType: DatabaseType | undefined): boolean { + if (databaseType !== "oracle" || index === 0) return false; + const previous = sql[index - 1]; + return PARAMETER_NAME_CHAR_RE.test(previous) || previous === "$" || previous === "#" || previous === '"'; +} + function collectDuckDbStructFieldSeparators(sql: string): Set { const separators = new Set(); const contexts: DuckDbStructLiteralContext[] = [];