mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 17:00:21 +08:00
🐛 fix(query-editor): 修复 Oracle 查询结果编辑提交失败
- 规范化 Oracle/Dameng 未加引号表名大小写 - 按元数据列名映射查询结果可写字段 - 补充查询结果编辑提交回归测试 Refs #464
This commit is contained in:
@@ -334,6 +334,47 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('normalizes unquoted lowercase Oracle identifiers before committing query result edits', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['NAME', '__gonavi_locator_1_ID'], rows: [{ NAME: 'old-name', __gonavi_locator_1_ID: 7 }] }],
|
||||
});
|
||||
backendApp.DBGetColumns.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'anonymous', query: 'select name from mycimled.edc_log' })} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBGetColumns).toHaveBeenCalledWith(expect.anything(), 'MYCIMLED', 'EDC_LOG');
|
||||
expect(dataGridState.latestProps?.tableName).toBe('MYCIMLED.EDC_LOG');
|
||||
expect(dataGridState.latestProps?.editLocator).toMatchObject({
|
||||
strategy: 'primary-key',
|
||||
columns: ['ID'],
|
||||
valueColumns: ['__gonavi_locator_1_ID'],
|
||||
hiddenColumns: ['__gonavi_locator_1_ID'],
|
||||
writableColumns: {
|
||||
name: 'NAME',
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
expect(dataGridState.latestProps?.readOnly).toBe(false);
|
||||
expect(messageApi.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses a unique index locator for query results without primary keys', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
@@ -579,7 +620,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(dataGridState.latestProps?.tableName).toBe(forceReadOnlyQueryResult ? undefined : 'users');
|
||||
const expectedTableName = dbType === 'oracle' || dbType === 'dameng' ? 'USERS' : 'users';
|
||||
expect(dataGridState.latestProps?.tableName).toBe(forceReadOnlyQueryResult ? undefined : expectedTableName);
|
||||
expect(dataGridState.latestProps?.editLocator).toBeUndefined();
|
||||
expect(dataGridState.latestProps?.readOnly).toBe(true);
|
||||
expect(backendApp.DBGetColumns).not.toHaveBeenCalled();
|
||||
|
||||
@@ -448,6 +448,13 @@ const findWritableResultColumnForSource = (writableColumns: Record<string, strin
|
||||
))?.[0];
|
||||
};
|
||||
|
||||
const resolveMetadataColumnName = (tableColumnNames: string[], sourceColumn: string): string => {
|
||||
const normalizedSource = String(sourceColumn || '').trim();
|
||||
if (!normalizedSource) return '';
|
||||
return tableColumnNames.find((column) => String(column || '').trim().toLowerCase() === normalizedSource.toLowerCase())
|
||||
|| normalizedSource;
|
||||
};
|
||||
|
||||
const buildQueryLocatorAlias = (column: string, index: number): string => {
|
||||
const normalized = String(column || '').trim().replace(/[^A-Za-z0-9_]/g, '_').slice(0, 48) || 'column';
|
||||
return `${QUERY_LOCATOR_ALIAS_PREFIX}${index}_${normalized}`;
|
||||
@@ -520,7 +527,8 @@ const resolveQueryLocatorPlan = async ({
|
||||
? Object.fromEntries(tableColumnNames.map((column) => [column, column]))
|
||||
: {};
|
||||
Object.entries(selectInfo.writableColumns).forEach(([resultColumn, sourceColumn]) => {
|
||||
writableColumns[resultColumn] = sourceColumn;
|
||||
const metadataColumn = resolveMetadataColumnName(tableColumnNames, sourceColumn);
|
||||
if (metadataColumn) writableColumns[resultColumn] = metadataColumn;
|
||||
});
|
||||
const appendExpressions: string[] = [];
|
||||
const hiddenColumns: string[] = [];
|
||||
|
||||
@@ -12,6 +12,24 @@ describe('extractQueryResultTableRef', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes unquoted Oracle identifiers to their folded uppercase names', () => {
|
||||
expect(extractQueryResultTableRef('select * from mycimled.edc_log fetch first 500 rows only', 'oracle', 'anonymous'))
|
||||
.toEqual({
|
||||
tableName: 'MYCIMLED.EDC_LOG',
|
||||
metadataDbName: 'MYCIMLED',
|
||||
metadataTableName: 'EDC_LOG',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves quoted Oracle identifier case', () => {
|
||||
expect(extractQueryResultTableRef('SELECT * FROM "mycimled"."edc_log"', 'oracle', 'ANONYMOUS'))
|
||||
.toEqual({
|
||||
tableName: 'mycimled.edc_log',
|
||||
metadataDbName: 'mycimled',
|
||||
metadataTableName: 'edc_log',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses current schema for unqualified Oracle tables', () => {
|
||||
expect(extractQueryResultTableRef('SELECT * FROM EDC_LOG', 'oracle', 'MYCIMLED'))
|
||||
.toEqual({
|
||||
|
||||
@@ -16,19 +16,42 @@ const stripIdentifierQuotes = (part: string): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
const normalizeQualifiedName = (raw: string): string => (
|
||||
String(raw || '')
|
||||
.split('.')
|
||||
.map((part) => stripIdentifierQuotes(part.trim()))
|
||||
.filter(Boolean)
|
||||
.join('.')
|
||||
);
|
||||
|
||||
const isOracleLikeDialect = (dialect: string): boolean => {
|
||||
const normalized = String(dialect || '').trim().toLowerCase();
|
||||
return normalized === 'oracle' || normalized === 'dameng' || normalized === 'dm' || normalized === 'dm8';
|
||||
};
|
||||
|
||||
const isQuotedIdentifier = (part: string): boolean => {
|
||||
const text = String(part || '').trim();
|
||||
if (!text) return false;
|
||||
return (text.startsWith('`') && text.endsWith('`'))
|
||||
|| (text.startsWith('"') && text.endsWith('"'))
|
||||
|| (text.startsWith('[') && text.endsWith(']'));
|
||||
};
|
||||
|
||||
const normalizeIdentifierPart = (part: string, dialect: string): string => {
|
||||
const text = String(part || '').trim();
|
||||
const value = stripIdentifierQuotes(text);
|
||||
if (!value) return '';
|
||||
if (isOracleLikeDialect(dialect) && !isQuotedIdentifier(text)) {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeCurrentDbName = (currentDb: string, dialect: string): string => {
|
||||
const value = String(currentDb || '').trim();
|
||||
if (!value) return '';
|
||||
return isOracleLikeDialect(dialect) ? value.toUpperCase() : value;
|
||||
};
|
||||
|
||||
const normalizeQualifiedNameParts = (raw: string, dialect: string): string[] => (
|
||||
String(raw || '')
|
||||
.split('.')
|
||||
.map((part) => normalizeIdentifierPart(part, dialect))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
export const extractQueryResultTableRef = (
|
||||
sql: string,
|
||||
dialect: string,
|
||||
@@ -43,15 +66,13 @@ export const extractQueryResultTableRef = (
|
||||
const tableMatch = text.match(/^\s*SELECT\s+.+?\s+FROM\s+((?:[`"\[]?\w+[`"\]]?)(?:\s*\.\s*(?:[`"\[]?\w+[`"\]]?)){0,2})\s*(?:$|[\s;])/im);
|
||||
if (!tableMatch) return undefined;
|
||||
|
||||
const qualifiedName = normalizeQualifiedName(tableMatch[1]);
|
||||
if (!qualifiedName) return undefined;
|
||||
|
||||
const parts = qualifiedName.split('.').filter(Boolean);
|
||||
const parts = normalizeQualifiedNameParts(tableMatch[1], dialect);
|
||||
if (parts.length === 0) return undefined;
|
||||
const metadataTableName = parts[parts.length - 1] || '';
|
||||
if (!metadataTableName) return undefined;
|
||||
|
||||
const owner = parts.length >= 2 ? parts[parts.length - 2] : '';
|
||||
const metadataDbName = owner || currentDb || '';
|
||||
const metadataDbName = owner || normalizeCurrentDbName(currentDb, dialect);
|
||||
const tableName = isOracleLikeDialect(dialect) && owner
|
||||
? `${owner}.${metadataTableName}`
|
||||
: metadataTableName;
|
||||
|
||||
Reference in New Issue
Block a user