diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 2183cb9c..69a1e6a2 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -8138,6 +8138,10 @@ describe('QueryEditor external SQL save', () => { storeState.connections[0].config.user = 'SBDEVREAD'; storeState.connections[0].config.database = 'ORCLPDB1'; (storeState.connections[0].config as any).readOnly = true; + backendApp.DBGetTables.mockResolvedValueOnce({ + success: true, + data: [], + }); backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Table: 'SBDEV.PERSON_INFO' }], @@ -8161,7 +8165,8 @@ describe('QueryEditor external SQL save', () => { }); const executedSql = String(backendApp.DBQueryMulti.mock.calls[0][2]); - expect(backendApp.DBGetTables).toHaveBeenCalledWith(expect.anything(), 'SBDEV'); + expect(backendApp.DBGetTables).toHaveBeenNthCalledWith(1, expect.anything(), 'SBDEVREAD'); + expect(backendApp.DBGetTables).toHaveBeenNthCalledWith(2, expect.anything(), 'SBDEV'); expect(backendApp.DBGetColumns).not.toHaveBeenCalled(); expect(executedSql).toMatch(/from\s+"SBDEV"\."PERSON_INFO"\s+where\s+zjjhm=''/i); expect(executedSql).not.toContain('SBDEVREAD.PERSON_INFO'); @@ -8173,6 +8178,46 @@ describe('QueryEditor external SQL save', () => { renderer?.unmount(); }); + it('keeps qualifying OceanBase Oracle read-only queries when config.database already equals the selected schema', async () => { + storeState.connections[0].config.type = 'oceanbase'; + (storeState.connections[0].config as any).oceanBaseProtocol = 'oracle'; + storeState.connections[0].config.user = 'SBDEVREAD'; + storeState.connections[0].config.database = 'SBDEV'; + (storeState.connections[0].config as any).readOnly = true; + backendApp.DBGetTables.mockResolvedValueOnce({ + success: true, + data: [], + }); + backendApp.DBGetTables.mockResolvedValueOnce({ + success: true, + data: [{ Table: 'SBDEV.SYSM_USER' }], + }); + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['USER_ID'], rows: [{ USER_ID: '0001477884' }] }], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const executedSql = String(backendApp.DBQueryMulti.mock.calls[0][2]); + expect(backendApp.DBGetTables).toHaveBeenNthCalledWith(1, expect.anything(), 'SBDEVREAD'); + expect(backendApp.DBGetTables).toHaveBeenNthCalledWith(2, expect.anything(), 'SBDEV'); + expect(executedSql).toMatch(/from\s+"SBDEV"\."SYSM_USER"\s+where\s+user_id='0001477884'/i); + expect(executedSql).not.toContain('SBDEVREAD.SYSM_USER'); + renderer?.unmount(); + }); + it('keeps Oracle anonymous PL/SQL blocks intact when running from the editor', async () => { storeState.connections[0].config.type = 'oracle'; storeState.connections[0].config.database = 'ORCLPDB1'; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 5cede146..de2a2c43 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -132,6 +132,7 @@ import { resolveOracleExactCaseTableReference, resolveOracleLikeDefaultSchemaName, resolveOracleLikeExecutionSchemaName, + resolveOracleLikeLookupSchemaCandidates, resolveQueryEditorFormatterLanguage, resolveQueryEditorHoverTarget, resolveQueryEditorNavigationDecorations, @@ -6012,13 +6013,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const defaultOracleSchema = isOracleLikeDialect(normalizedDbType) ? resolveOracleLikeDefaultSchemaName(config) : ''; - const oracleExecutionSchema = isOracleLikeDialect(normalizedDbType) - ? resolveOracleLikeExecutionSchemaName(config, currentDb) - : ''; - const shouldQualifyOracleUnqualifiedTables = Boolean( - oracleExecutionSchema - && oracleExecutionSchema.toLowerCase() !== String(defaultOracleSchema || '').trim().toLowerCase(), - ); const oracleTableCache = new Map(); const getOracleTablesForDb = async (dbName: string): Promise => { const normalizedDbName = String(dbName || '').trim(); @@ -6073,16 +6067,23 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const leadingTable = matchLeadingSelectTableReference(statement); if (leadingTable) { const leadingSegments = splitQueryIdentifierPathSegments(leadingTable.tableText); - const oracleLookupDbName = String( - (leadingSegments.length >= 2 ? leadingSegments[0]?.value : '') - || oracleExecutionSchema - || currentDb - || '', - ).trim(); - const oracleTables = oracleLookupDbName ? await getOracleTablesForDb(oracleLookupDbName) : []; - const exactQualifiedTable = resolveOracleExactCaseTableReference(statement, oracleLookupDbName, oracleTables, { - qualifyUnqualified: shouldQualifyOracleUnqualifiedTables, - }); + const oracleLookupDbCandidates = leadingSegments.length >= 2 + ? [String(leadingSegments[0]?.value || '').trim()].filter(Boolean) + : resolveOracleLikeLookupSchemaCandidates(config, currentDb); + let exactQualifiedTable: string | undefined; + for (const oracleLookupDbName of oracleLookupDbCandidates) { + const oracleTables = oracleLookupDbName ? await getOracleTablesForDb(oracleLookupDbName) : []; + exactQualifiedTable = resolveOracleExactCaseTableReference(statement, oracleLookupDbName, oracleTables, { + qualifyUnqualified: Boolean( + leadingSegments.length === 1 + && oracleLookupDbName + && oracleLookupDbName.toLowerCase() !== String(defaultOracleSchema || '').trim().toLowerCase(), + ), + }); + if (exactQualifiedTable) { + break; + } + } if (exactQualifiedTable) { executableStatement = rewriteLeadingSelectTableReference(statement, exactQualifiedTable) || statement; } diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts new file mode 100644 index 00000000..7634a44e --- /dev/null +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { + resolveOracleLikeDefaultSchemaName, + resolveOracleLikeExecutionSchemaName, + resolveOracleLikeLookupSchemaCandidates, +} from './QueryEditorHelpers'; + +describe('QueryEditorHelpers Oracle-like execution schema', () => { + it('uses the selected schema when it differs from the login user', () => { + const config = { + type: 'oceanbase', + oceanBaseProtocol: 'oracle', + user: 'SBDEVREAD', + database: 'SBDEV', + }; + + expect(resolveOracleLikeDefaultSchemaName(config)).toBe('SBDEVREAD'); + expect(resolveOracleLikeExecutionSchemaName(config, 'SBDEV')).toBe('SBDEVREAD'); + expect(resolveOracleLikeLookupSchemaCandidates(config, 'SBDEV')).toEqual(['SBDEVREAD', 'SBDEV']); + }); + + it('keeps the login user schema when the selected schema is the same owner', () => { + const config = { + type: 'oracle', + user: 'APP_OWNER', + database: 'ORCLPDB1', + }; + + expect(resolveOracleLikeExecutionSchemaName(config, 'APP_OWNER')).toBe('APP_OWNER'); + expect(resolveOracleLikeLookupSchemaCandidates(config, 'APP_OWNER')).toEqual(['APP_OWNER']); + }); +}); diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index c14c17ac..1e52453e 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -1305,6 +1305,27 @@ export const resolveOracleLikeExecutionSchemaName = (config: any, currentDb: str return resolveOracleLikeDefaultSchemaName(config) || selectedDb; }; +export const resolveOracleLikeLookupSchemaCandidates = (config: any, currentDb: string): string[] => { + const candidates: string[] = []; + const seen = new Set(); + const push = (value: string) => { + const normalized = String(value || '').trim(); + if (!normalized) return; + const key = normalized.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + candidates.push(normalized); + }; + + const defaultSchema = resolveOracleLikeDefaultSchemaName(config); + const selectedDb = String(currentDb || '').trim(); + push(defaultSchema); + if (selectedDb && selectedDb.toLowerCase() !== String(defaultSchema || '').trim().toLowerCase()) { + push(selectedDb); + } + return candidates; +}; + export const getQueryEditorModelTextIfWithinLimit = (model: any, maxTextLength: number): string | null => { const modelLength = getQueryEditorModelValueLength(model); if (modelLength !== null && modelLength > maxTextLength) {