fix(completion): resolve HANA session schema
This commit is contained in:
4 files changed
+123
-5
No files matched your search
@@ -37,6 +37,18 @@ function oracleConnection(): ConnectionConfig {
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function sapHanaConnection(): ConnectionConfig {
|
||||
return {
|
||||
...postgresConnection(),
|
||||
id: "hana-1",
|
||||
name: "SAP HANA",
|
||||
db_type: "saphana",
|
||||
port: 30015,
|
||||
username: "SYSTEM",
|
||||
database: "",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function sqlServerConnection(): ConnectionConfig {
|
||||
return {
|
||||
...postgresConnection(),
|
||||
@@ -296,6 +308,45 @@ describe("connectionStore completion assistant", () => {
|
||||
expect(completionAssistantSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("normalizes unquoted SAP HANA table and schema names before column lookup", async () => {
|
||||
const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null; parent_schema?: string | null }) => ({
|
||||
candidates: [
|
||||
{
|
||||
name: request.parent_name === "ACDOCA" ? "BELNR" : "MixedColumn",
|
||||
kind: "column",
|
||||
schema: request.parent_schema,
|
||||
parent_schema: request.parent_schema,
|
||||
parent_name: request.parent_name,
|
||||
data_type: "NVARCHAR",
|
||||
},
|
||||
],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
}));
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
getColumns: vi.fn(),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [sapHanaConnection()];
|
||||
store.connectedIds.add("hana-1");
|
||||
|
||||
const unquoted = await store.listCompletionColumns("hana-1", "", "acdoca", "saphanadb", { tableQuoted: false, schemaQuoted: false });
|
||||
const quoted = await store.listCompletionColumns("hana-1", "", "MixedTable", "MixedSchema", { tableQuoted: true, schemaQuoted: true });
|
||||
|
||||
expect(completionAssistantSearch.mock.calls.map(([request]) => ({ schema: request.schema, parent_schema: request.parent_schema, parent_name: request.parent_name }))).toEqual([
|
||||
{ schema: "SAPHANADB", parent_schema: "SAPHANADB", parent_name: "ACDOCA" },
|
||||
{ schema: "MixedSchema", parent_schema: "MixedSchema", parent_name: "MixedTable" },
|
||||
]);
|
||||
expect(unquoted).toEqual([expect.objectContaining({ name: "BELNR", table: "ACDOCA", schema: "SAPHANADB" })]);
|
||||
expect(quoted).toEqual([expect.objectContaining({ name: "MixedColumn", table: "MixedTable", schema: "MixedSchema" })]);
|
||||
});
|
||||
|
||||
it("keeps quoted and unquoted Oracle objects separate in the local column index", async () => {
|
||||
const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null }) => ({
|
||||
candidates: [
|
||||
|
||||
@@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({
|
||||
closeQuerySession: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
executeMulti: vi.fn(),
|
||||
executeQuery: vi.fn(),
|
||||
getConnectionConfig: vi.fn(),
|
||||
prepareQueryPaginationExecutionPlan: vi.fn(),
|
||||
saveOpenTabsState: vi.fn(),
|
||||
@@ -20,6 +21,7 @@ vi.mock("@/lib/backend/api", () => ({
|
||||
closeClientConnectionSession: mocks.closeClientConnectionSession,
|
||||
closeQuerySession: mocks.closeQuerySession,
|
||||
executeMulti: mocks.executeMulti,
|
||||
executeQuery: mocks.executeQuery,
|
||||
prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan,
|
||||
saveOpenTabsState: mocks.saveOpenTabsState,
|
||||
}));
|
||||
@@ -174,6 +176,40 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(mocks.executeMulti.mock.calls.map((call) => call[5]?.clientSessionId)).toEqual([tabA, tabA, tabA]);
|
||||
});
|
||||
|
||||
it("resolves the actual SAP HANA schema from the executing Agent session", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "hana-1",
|
||||
name: "SAP HANA",
|
||||
db_type: "saphana",
|
||||
database: "",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.executeMulti
|
||||
.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }])
|
||||
.mockResolvedValueOnce([{ columns: ["Error"], rows: [["schema missing"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }])
|
||||
.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
mocks.executeQuery.mockResolvedValueOnce({ columns: ["CURRENT_SCHEMA"], rows: [["APP_SCHEMA"]], affected_rows: 0, execution_time_ms: 1 }).mockResolvedValueOnce({ columns: ["CURRENT_SCHEMA"], rows: [["MixedTargetSchema"]], affected_rows: 0, execution_time_ms: 1 });
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabA = store.createTab("hana-1", "", "Tab A");
|
||||
const tabB = store.createTab("hana-1", "", "Tab B");
|
||||
|
||||
await store.executeTabSql(tabA, "SET SCHEMA app_schema_synonym");
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)).toMatchObject({ schema: "APP_SCHEMA", completionContextVersion: 1 });
|
||||
expect(store.tabs.find((tab) => tab.id === tabB)?.schema).toBeUndefined();
|
||||
|
||||
await store.executeTabSql(tabA, 'SET SCHEMA "MissingSchema"');
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)).toMatchObject({ schema: "APP_SCHEMA", completionContextVersion: 1 });
|
||||
|
||||
await store.executeTabSql(tabA, '/* switch */ SET SCHEMA "MixedSchema"');
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)).toMatchObject({ schema: "MixedTargetSchema", completionContextVersion: 2 });
|
||||
expect(mocks.executeMulti.mock.calls.map((call) => call[5]?.clientSessionId)).toEqual([tabA, tabA, tabA]);
|
||||
expect(mocks.executeQuery.mock.calls.map((call) => ({ sql: call[2], schema: call[3], clientSessionId: call[5]?.clientSessionId }))).toEqual([
|
||||
{ sql: "SELECT CURRENT_SCHEMA FROM DUMMY", schema: undefined, clientSessionId: tabA },
|
||||
{ sql: "SELECT CURRENT_SCHEMA FROM DUMMY", schema: undefined, clientSessionId: tabA },
|
||||
]);
|
||||
});
|
||||
|
||||
it("invalidates Oracle completion metadata when clearing a tab schema resets its session", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oracle-1",
|
||||
|
||||
@@ -5748,9 +5748,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number; tableQuoted?: boolean; schemaQuoted?: boolean }, catalog?: string): Promise<SqlCompletionColumn[]> {
|
||||
const config = getConfig(connectionId);
|
||||
const oracleIdentifier = config?.db_type === "oracle";
|
||||
const completionTable = oracleIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table;
|
||||
const uppercaseUnquotedIdentifier = oracleIdentifier || config?.db_type === "saphana";
|
||||
const completionTable = uppercaseUnquotedIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table;
|
||||
const rawCompletionSchema = schema?.trim() || undefined;
|
||||
const completionSchema = oracleIdentifier && rawCompletionSchema && context?.schemaQuoted === false ? rawCompletionSchema.toUpperCase() : rawCompletionSchema;
|
||||
const completionSchema = uppercaseUnquotedIdentifier && rawCompletionSchema && context?.schemaQuoted === false ? rawCompletionSchema.toUpperCase() : rawCompletionSchema;
|
||||
const usesOracleCurrentSchema = config?.db_type === "oracle" && !completionSchema;
|
||||
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesOracleCurrentSchema) {
|
||||
return [];
|
||||
|
||||
@@ -206,7 +206,7 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas
|
||||
return results;
|
||||
}
|
||||
|
||||
function isOracleCurrentSchemaStatement(statement: string | undefined): boolean {
|
||||
function sqlStatementWithoutLeadingComments(statement: string | undefined): string {
|
||||
let remaining = statement?.trimStart() ?? "";
|
||||
while (remaining) {
|
||||
if (remaining.startsWith("--")) {
|
||||
@@ -216,13 +216,26 @@ function isOracleCurrentSchemaStatement(statement: string | undefined): boolean
|
||||
}
|
||||
if (remaining.startsWith("/*")) {
|
||||
const end = remaining.indexOf("*/", 2);
|
||||
if (end < 0) return false;
|
||||
if (end < 0) return "";
|
||||
remaining = remaining.slice(end + 2).trimStart();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return /^ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\s*=/i.test(remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
function isOracleCurrentSchemaStatement(statement: string | undefined): boolean {
|
||||
return /^ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\s*=/i.test(sqlStatementWithoutLeadingComments(statement));
|
||||
}
|
||||
|
||||
function isSapHanaSetSchemaStatement(statement: string | undefined): boolean {
|
||||
return /^SET\s+SCHEMA\s+(?:"(?:[^"]|"")*"|[A-Za-z_][\w$#]*)\s*;?\s*$/i.test(sqlStatementWithoutLeadingComments(statement));
|
||||
}
|
||||
|
||||
function sapHanaCurrentSchemaFromResult(result: QueryResult): string | undefined {
|
||||
const schema = result.rows[0]?.[0];
|
||||
return typeof schema === "string" && schema.trim() ? schema.trim() : undefined;
|
||||
}
|
||||
|
||||
function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string, databaseType?: DatabaseType, sourceRange?: { from: number; to: number }): QueryResult {
|
||||
@@ -3689,6 +3702,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset);
|
||||
const successfulOracleSchemaChanges = effectiveDbType === "oracle" ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
|
||||
const successfulSapHanaSchemaChanges = effectiveDbType === "saphana" ? results.filter((result) => result.execution_error !== true && isSapHanaSetSchemaStatement(result.sourceStatement)).length : 0;
|
||||
if (hiddenPrimaryKeys.length > 0 && results.length === 1) {
|
||||
const hiddenIndexes = hiddenResultColumnIndexes(results[0]!.columns, hiddenPrimaryKeys);
|
||||
if (hiddenIndexes.length > 0) results[0]!.hidden_column_indexes = hiddenIndexes;
|
||||
@@ -3703,12 +3717,28 @@ export const useQueryStore = defineStore("query", () => {
|
||||
columnCounts: results.map((result) => result.columns.length),
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
let resolvedSapHanaSchema: string | undefined;
|
||||
if (successfulSapHanaSchemaChanges > 0 && tabs.value.find((item) => item.id === id)?.executionId === executionId) {
|
||||
try {
|
||||
const schemaResult = await api.executeQuery(tab.connectionId, executionDatabase, "SELECT CURRENT_SCHEMA FROM DUMMY", undefined, executionId, {
|
||||
clientSessionId: tabClientSessionId(tab),
|
||||
timeoutSecs: queryTimeoutSecs,
|
||||
});
|
||||
resolvedSapHanaSchema = sapHanaCurrentSchemaFromResult(schemaResult);
|
||||
} catch (error) {
|
||||
console.warn("[DBX] Failed to resolve SAP HANA CURRENT_SCHEMA", error);
|
||||
}
|
||||
}
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
if (current?.executionId === executionId) {
|
||||
if (openInNewResultTab && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
|
||||
if (successfulOracleSchemaChanges > 0) {
|
||||
current.completionContextVersion = (current.completionContextVersion ?? 0) + successfulOracleSchemaChanges;
|
||||
}
|
||||
if (resolvedSapHanaSchema) {
|
||||
current.schema = resolvedSapHanaSchema;
|
||||
current.completionContextVersion = (current.completionContextVersion ?? 0) + successfulSapHanaSchemaChanges;
|
||||
}
|
||||
const activeGroupIndex = current.activeResultIndex;
|
||||
const activeGroupResults = current.results;
|
||||
const shouldAppendResult = !!options?.appendResult && !!current.result;
|
||||
|
||||
Reference in new issue
Block a user