mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-29 06:11:22 +08:00
🐛 fix(oracle): 修复普通查询重复列自动别名缺失
- 在 QueryEditor 查询计划阶段识别显式列与 alias.* 的重复列冲突 - Oracle 执行前自动为冲突显式列补充 _1 风格唯一别名 - 让 locator 与后续追加表达式复用改写后的可执行 SQL - 补充普通查询重复列自动别名的 Oracle 回归测试
This commit is contained in:
@@ -2878,6 +2878,62 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('auto aliases Oracle duplicate explicit columns before alias star expansion', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'APP';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{
|
||||
columns: ['EHR_USERID_1', 'USERID', 'EHR_USERID', 'USERNAME'],
|
||||
rows: [{
|
||||
EHR_USERID_1: 'emp-1',
|
||||
USERID: 7,
|
||||
EHR_USERID: 'emp-1',
|
||||
USERNAME: 'alice',
|
||||
}],
|
||||
}],
|
||||
});
|
||||
backendApp.DBGetColumns.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ name: 'USERID', key: 'PRI' },
|
||||
{ name: 'EHR_USERID', key: '' },
|
||||
{ name: 'USERNAME', key: '' },
|
||||
],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({
|
||||
dbName: 'APP',
|
||||
query: 'SELECT EHR_USERID, a.* FROM S_USER_BASE a',
|
||||
})} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(dataGridState.latestProps?.readOnly).toBe(false);
|
||||
expect(dataGridState.latestProps?.editLocator).toMatchObject({
|
||||
strategy: 'primary-key',
|
||||
columns: ['USERID'],
|
||||
valueColumns: ['USERID'],
|
||||
writableColumns: {
|
||||
USERID: 'USERID',
|
||||
EHR_USERID: 'EHR_USERID',
|
||||
USERNAME: 'USERNAME',
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('EHR_USERID AS EHR_USERID_1, a.*');
|
||||
expect(messageApi.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'mysql',
|
||||
'mariadb',
|
||||
|
||||
@@ -210,7 +210,13 @@ export const getLastIdentifierPart = (path: string): string => {
|
||||
return parts[parts.length - 1] || '';
|
||||
};
|
||||
|
||||
export const resolveSimpleSelectItemColumn = (item: string): { resultName: string; sourceName: string } | 'all' | undefined => {
|
||||
export type SelectItemInfo = {
|
||||
expression: string;
|
||||
resultName: string;
|
||||
sourceName?: string;
|
||||
};
|
||||
|
||||
export const resolveSelectItemInfo = (item: string): SelectItemInfo | 'all' | undefined => {
|
||||
const text = String(item || '').trim();
|
||||
if (!text) return undefined;
|
||||
if (text === '*' || /\.\s*\*$/.test(text)) return 'all';
|
||||
@@ -232,10 +238,16 @@ export const resolveSimpleSelectItemColumn = (item: string): { resultName: strin
|
||||
}
|
||||
}
|
||||
|
||||
if (!SIMPLE_IDENTIFIER_PATH_RE.test(expr)) return undefined;
|
||||
const sourceName = getLastIdentifierPart(expr);
|
||||
if (!alias && !SIMPLE_IDENTIFIER_PATH_RE.test(expr)) return undefined;
|
||||
const sourceName = SIMPLE_IDENTIFIER_PATH_RE.test(expr) ? getLastIdentifierPart(expr) : '';
|
||||
const resultName = alias || sourceName;
|
||||
return sourceName && resultName ? { resultName, sourceName } : undefined;
|
||||
return resultName ? { expression: expr, resultName, sourceName: sourceName || undefined } : undefined;
|
||||
};
|
||||
|
||||
export const resolveSimpleSelectItemColumn = (item: string): { resultName: string; sourceName: string } | 'all' | undefined => {
|
||||
const resolved = resolveSelectItemInfo(item);
|
||||
if (!resolved || resolved === 'all' || !resolved.sourceName) return resolved === 'all' ? 'all' : undefined;
|
||||
return { resultName: resolved.resultName, sourceName: resolved.sourceName };
|
||||
};
|
||||
|
||||
export const parseSimpleSelectInfo = (sql: string): SimpleSelectInfo | undefined => {
|
||||
@@ -354,6 +366,57 @@ export const rewriteOracleSelectAllWithExpressions = (sql: string, expressions:
|
||||
return `${prefix}${finalSelectItems.join(', ')}${fromKeyword}${tableText}${aliasClause}${parsedAlias.remainder}`;
|
||||
};
|
||||
|
||||
export const rewriteOracleDuplicateSelectColumns = (sql: string, tableColumnNames: string[]): string | undefined => {
|
||||
const metadataNames = new Set(
|
||||
tableColumnNames
|
||||
.map((name) => String(name || '').trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
if (metadataNames.size === 0) return undefined;
|
||||
|
||||
const match = String(sql || '').match(/^(\s*SELECT\s+)([\s\S]+?)(\s+FROM\s+[\s\S]*)$/i);
|
||||
if (!match) return undefined;
|
||||
|
||||
const prefix = match[1];
|
||||
const selectList = match[2].trim();
|
||||
const rest = match[3];
|
||||
const selectItems = splitTopLevelComma(selectList);
|
||||
if (selectItems.length === 0) return undefined;
|
||||
|
||||
const parsedItems = selectItems.map((item) => ({
|
||||
raw: String(item || '').trimEnd(),
|
||||
info: resolveSelectItemInfo(item),
|
||||
}));
|
||||
const hasWildcard = parsedItems.some(({ info }) => info === 'all');
|
||||
if (!hasWildcard) return undefined;
|
||||
|
||||
const usedResultNames = new Set<string>(metadataNames);
|
||||
parsedItems.forEach(({ info }) => {
|
||||
if (!info || info === 'all') return;
|
||||
const normalizedResult = String(info.resultName || '').trim().toLowerCase();
|
||||
if (normalizedResult) usedResultNames.add(normalizedResult);
|
||||
});
|
||||
|
||||
let changed = false;
|
||||
const rewrittenItems = parsedItems.map(({ raw, info }) => {
|
||||
if (!info || info === 'all') return raw;
|
||||
const normalizedResult = String(info.resultName || '').trim().toLowerCase();
|
||||
if (!metadataNames.has(normalizedResult)) return raw;
|
||||
|
||||
let nextIndex = 1;
|
||||
let alias = `${info.resultName}_${nextIndex}`;
|
||||
while (usedResultNames.has(alias.toLowerCase())) {
|
||||
nextIndex++;
|
||||
alias = `${info.resultName}_${nextIndex}`;
|
||||
}
|
||||
usedResultNames.add(alias.toLowerCase());
|
||||
changed = true;
|
||||
return `${info.expression} AS ${alias}`;
|
||||
});
|
||||
|
||||
return changed ? `${prefix}${rewrittenItems.join(', ')}${rest}` : undefined;
|
||||
};
|
||||
|
||||
export const findWritableResultColumnForSource = (writableColumns: Record<string, string>, target: string): string | undefined => {
|
||||
const normalizedTarget = String(target || '').trim().toLowerCase();
|
||||
return Object.entries(writableColumns || {}).find(([, sourceColumn]) => (
|
||||
@@ -1968,6 +2031,11 @@ export const resolveQueryLocatorPlan = async ({
|
||||
|
||||
const tableColumns = resCols.data as ColumnDefinition[];
|
||||
const tableColumnNames = tableColumns.map(getColumnDefinitionName).filter(Boolean);
|
||||
let executableStatement = statement;
|
||||
if (isOracleLikeDialect(dbType) && selectInfo.selectsAll) {
|
||||
const rewritten = rewriteOracleDuplicateSelectColumns(executableStatement, tableColumnNames);
|
||||
if (rewritten) executableStatement = rewritten;
|
||||
}
|
||||
const primaryKeys = tableColumns
|
||||
.filter((column: any) => getColumnDefinitionKey(column) === 'PRI')
|
||||
.map(getColumnDefinitionName)
|
||||
@@ -2058,7 +2126,7 @@ export const resolveQueryLocatorPlan = async ({
|
||||
];
|
||||
|
||||
if (executableAppendExpressions.length > 0 && isOracleLikeDialect(dbType) && selectInfo.selectsBareAll) {
|
||||
const rewritten = rewriteOracleSelectAllWithExpressions(statement, executableAppendExpressions);
|
||||
const rewritten = rewriteOracleSelectAllWithExpressions(executableStatement, executableAppendExpressions);
|
||||
if (rewritten) {
|
||||
plan.executedSql = rewritten;
|
||||
return plan;
|
||||
@@ -2070,7 +2138,7 @@ export const resolveQueryLocatorPlan = async ({
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.executedSql = appendQuerySelectExpressions(statement, executableAppendExpressions);
|
||||
plan.executedSql = appendQuerySelectExpressions(executableStatement, executableAppendExpressions);
|
||||
return plan;
|
||||
} catch {
|
||||
const reason = translate('query_editor.message.read_only_table_locator_metadata_unavailable', {
|
||||
|
||||
Reference in New Issue
Block a user