diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 2570da8..c580dc4 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -5541,6 +5541,47 @@ describe('QueryEditor external SQL save', () => { renderer?.unmount(); }); + it('qualifies OceanBase Oracle read-only queries with the selected schema instead of the login user', 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 = 'ORCLPDB1'; + (storeState.connections[0].config as any).readOnly = true; + backendApp.DBGetTables.mockResolvedValueOnce({ + success: true, + data: [{ Table: 'SBDEV.PERSON_INFO' }], + }); + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['ZJJHM'], rows: [{ ZJJHM: '' }] }], + }); + + 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).toHaveBeenCalledWith(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'); + expect(dataGridState.latestProps?.readOnly).toBe(true); + expect(storeState.addSqlLog).toHaveBeenCalledWith(expect.objectContaining({ + sql: "select * from person_info where zjjhm=''", + status: 'success', + })); + 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 adb22c9..f34d737 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -109,6 +109,7 @@ import { resolveNextResultSetIndex, resolveOracleExactCaseTableReference, resolveOracleLikeDefaultSchemaName, + resolveOracleLikeExecutionSchemaName, resolveQueryEditorFormatterLanguage, resolveQueryEditorHoverTarget, resolveQueryEditorNavigationDecorations, @@ -3353,6 +3354,13 @@ 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(); @@ -3409,12 +3417,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const leadingSegments = splitQueryIdentifierPathSegments(leadingTable.tableText); const oracleLookupDbName = String( (leadingSegments.length >= 2 ? leadingSegments[0]?.value : '') - || defaultOracleSchema + || oracleExecutionSchema || currentDb || '', ).trim(); const oracleTables = oracleLookupDbName ? await getOracleTablesForDb(oracleLookupDbName) : []; - const exactQualifiedTable = resolveOracleExactCaseTableReference(statement, oracleLookupDbName, oracleTables); + const exactQualifiedTable = resolveOracleExactCaseTableReference(statement, oracleLookupDbName, oracleTables, { + qualifyUnqualified: shouldQualifyOracleUnqualifiedTables, + }); if (exactQualifiedTable) { executableStatement = rewriteLeadingSelectTableReference(statement, exactQualifiedTable) || statement; } diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index da72521..7f2770d 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -1147,6 +1147,7 @@ export const resolveOracleExactCaseTableReference = ( statement: string, currentDb: string, tables: CompletionTableMeta[], + options?: { qualifyUnqualified?: boolean }, ): string | undefined => { const leadingTable = matchLeadingSelectTableReference(statement); if (!leadingTable) return undefined; @@ -1155,7 +1156,8 @@ export const resolveOracleExactCaseTableReference = ( if (segments.length === 0 || segments.length > 2 || segments.some((segment) => segment.quoted)) { return undefined; } - if (!segments.some((segment) => /[a-z]/.test(segment.value))) { + const shouldQualifyUnqualified = Boolean(options?.qualifyUnqualified && segments.length === 1); + if (!segments.some((segment) => /[a-z]/.test(segment.value)) && !shouldQualifyUnqualified) { return undefined; } @@ -1170,7 +1172,7 @@ export const resolveOracleExactCaseTableReference = ( const parsed = splitSidebarQualifiedName(String(table.tableName || '')); const objectName = String(parsed.objectName || table.tableName || '').trim(); const schemaName = String(parsed.schemaName || table.dbName || '').trim(); - if (objectName !== rawObjectName) return false; + if (objectName !== rawObjectName && objectName.toLowerCase() !== rawObjectName.toLowerCase()) return false; if (!rawSchemaName) return true; return schemaName.toLowerCase() === rawSchemaName.toLowerCase(); }); @@ -1181,6 +1183,8 @@ export const resolveOracleExactCaseTableReference = ( const exactSchemaName = String(matchedParsed.schemaName || matched.dbName || rawSchemaName).trim(); const quotedParts = rawSchemaName ? [exactSchemaName, exactObjectName] + : shouldQualifyUnqualified + ? [exactSchemaName || targetDbName, exactObjectName] : [exactObjectName]; if (quotedParts.some((part) => !String(part || '').trim())) { return undefined; @@ -1195,6 +1199,15 @@ export const resolveOracleLikeDefaultSchemaName = (config: any): string => { return String(userPart || '').trim(); }; +export const resolveOracleLikeExecutionSchemaName = (config: any, currentDb: string): string => { + const selectedDb = String(currentDb || '').trim(); + const configuredDb = String(config?.database || '').trim(); + if (selectedDb && (!configuredDb || selectedDb.toLowerCase() !== configuredDb.toLowerCase())) { + return selectedDb; + } + return resolveOracleLikeDefaultSchemaName(config) || selectedDb; +}; + export const getQueryEditorModelTextIfWithinLimit = (model: any, maxTextLength: number): string | null => { const modelLength = getQueryEditorModelValueLength(model); if (modelLength !== null && modelLength > maxTextLength) { @@ -2053,7 +2066,9 @@ export const resolveQueryLocatorPlan = async ({ }; if (forceReadOnly) return plan; - const defaultSchema = isOracleLikeDialect(dbType) ? resolveOracleLikeDefaultSchemaName(config) : ''; + const defaultSchema = isOracleLikeDialect(dbType) + ? resolveOracleLikeExecutionSchemaName(config, currentDb) + : ''; let tableRef = extractQueryResultTableRef(statement, dbType, currentDb, defaultSchema); if (!tableRef) return plan; plan.tableRef = tableRef;