mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 00:41:24 +08:00
🐛 fix(query-editor): 修复 OceanBase Oracle 查询默认 schema 误判
- 优先使用当前选中的 Oracle-like schema 解析未限定表名 - 为 OceanBase Oracle 只读账号查询补齐业务 schema 限定 - 增加回归用例覆盖登录用户与选中 schema 不一致场景
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user