mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 06:52:18 +08:00
🐛 fix(query-editor): 修复 OceanBase 执行误用登录Schema
- 保持 OceanBase Oracle 默认登录 schema 解析不变 - 未限定表名改写时增加登录 schema 到所选 schema 的回退候选 - 补充 SBDEVREAD/SBDEV 与 ORCLPDB1 场景回归测试 Fixes #615
This commit is contained in:
@@ -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(<QueryEditor tab={createTab({ dbName: 'SBDEV', query: "select * from sysm_user where user_id='0001477884'" })} />);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
@@ -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<string, CompletionTableMeta[]>();
|
||||
const getOracleTablesForDb = async (dbName: string): Promise<CompletionTableMeta[]> => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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<string>();
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user