🐛 fix(query-editor): 修复 OceanBase Oracle 查询默认 schema 误判

- 优先使用当前选中的 Oracle-like schema 解析未限定表名

- 为 OceanBase Oracle 只读账号查询补齐业务 schema 限定

- 增加回归用例覆盖登录用户与选中 schema 不一致场景
This commit is contained in:
Syngnat
2026-06-24 21:25:22 +08:00
parent 1a9d417c0a
commit 69b6072e37
3 changed files with 71 additions and 5 deletions

View File

@@ -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(<QueryEditor tab={createTab({ dbName: 'SBDEV', query: "select * from person_info where zjjhm=''" })} />);
});
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';

View File

@@ -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<string, CompletionTableMeta[]>();
const getOracleTablesForDb = async (dbName: string): Promise<CompletionTableMeta[]> => {
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;
}

View File

@@ -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;