🐛 fix(ddl): 保留 Oracle 视图 DDL 原始排版

- Oracle 视图与物化视图跳过展示格式化
- 补充预览及对象编辑原始排版回归测试
This commit is contained in:
Syngnat
2026-07-31 20:06:25 +08:00
parent e117ec2c8c
commit e942f7dc5a
3 changed files with 14 additions and 117 deletions

View File

@@ -148,11 +148,12 @@ describe('DefinitionViewer object edit entry', () => {
expect(storeState.addTab.mock.calls[0][0].query).toContain('SELECT id, name FROM users;');
});
it('formats a one-line Oracle catalog view definition for display and editing', async () => {
it('preserves Oracle view DDL formatting for display and editing', async () => {
const rawDDL = `CREATE OR REPLACE VIEW APP.V_RISK AS SELECT a.id,NVL(b.org_name,'-') AS org_name FROM org a LEFT JOIN org_info b ON b.org_id=a.id WHERE a.deleted_flag=0`;
storeState.connections[0].config.type = 'oracle';
backendApp.DBShowCreateTable.mockResolvedValue({
success: true,
data: `CREATE OR REPLACE VIEW APP.V_RISK AS SELECT a.id,NVL(b.org_name,'-') AS org_name FROM org a LEFT JOIN org_info b ON b.org_id=a.id WHERE a.deleted_flag=0`,
data: rawDDL,
});
let renderer: any;
@@ -170,10 +171,7 @@ describe('DefinitionViewer object edit entry', () => {
expect(backendApp.DBShowCreateTable).toHaveBeenCalledWith(expect.anything(), 'APP', 'APP.V_RISK');
expect(backendApp.DBQuery).not.toHaveBeenCalled();
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('CREATE OR REPLACE VIEW APP.V_RISK AS');
expect(editorText).toContain('SELECT\n a.id,');
expect(editorText).toContain('\nFROM\n org a');
expect(editorText).toContain('\nWHERE\n a.deleted_flag = 0;');
expect(editorText).toBe(`${rawDDL};`);
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
await act(async () => {
@@ -181,8 +179,7 @@ describe('DefinitionViewer object edit entry', () => {
});
const query = String(storeState.addTab.mock.calls[0][0].query || '');
expect(query).toContain('SELECT\n a.id,');
expect(query).toContain('\nWHERE\n a.deleted_flag = 0;');
expect(query).toContain(`${rawDDL};`);
});
it('adds CREATE OR REPLACE without duplicating view fragments returned without ddl prefix', async () => {

View File

@@ -34,18 +34,9 @@ COMMENT ON COLUMN "H2"."S_BUSI"."ID" IS '主键';`;
expect(formatted).toContain(`COMMENT ON COLUMN "H2"."S_BUSI"."ID" IS '主键';`);
});
it('formats Oracle view columns and keeps the trailing read-only clause intact', () => {
it('preserves Oracle view DDL formatting', () => {
const raw = `CREATE OR REPLACE FORCE EDITIONABLE VIEW "APP"."V_RISK" ("ID", "ORG_NAME", "PARENT_NAME", "LEVEL_NO") DEFAULT COLLATION "USING_NLS_COMP" AS SELECT a.id,NVL(b.org_name,'-') org_name,DECODE(a.parent_id,NULL,'ROOT',c.org_name) parent_name,LEVEL level_no FROM org a,org_info b,org_info c WHERE a.id=b.org_id(+) AND a.parent_id=c.org_id(+) START WITH a.parent_id IS NULL CONNECT BY PRIOR a.id=a.parent_id WITH READ ONLY;`;
const formatted = formatDdlForDisplay(raw, 'oracle');
expect(formatted).toContain(`VIEW "APP"."V_RISK" (
"ID",
"ORG_NAME",
"PARENT_NAME",
"LEVEL_NO"
) DEFAULT COLLATION`);
expect(formatted).toContain('\nWITH READ ONLY;');
expect(formatted).not.toContain('\nWITH\n READ ONLY;');
expect(formatDdlForDisplay(raw, 'oracle')).toBe(raw);
});
});

View File

@@ -36,116 +36,25 @@ const resolveDdlFormatterLanguage = (dbType: string): SqlLanguage => {
}
};
const findOracleViewColumnList = (sql: string): { start: number; end: number } | null => {
const viewMatch = /^\s*CREATE\b[\s\S]*?\bVIEW\b/i.exec(sql);
if (!viewMatch) {
return null;
}
let inQuotedIdentifier = false;
let columnListStart = -1;
let parenthesisDepth = 0;
for (let index = viewMatch[0].length; index < sql.length; index += 1) {
const current = sql[index];
if (current === '"') {
if (inQuotedIdentifier && sql[index + 1] === '"') {
index += 1;
continue;
}
inQuotedIdentifier = !inQuotedIdentifier;
continue;
}
if (inQuotedIdentifier) {
continue;
}
if (parenthesisDepth === 0) {
const remaining = sql.slice(index);
if (/^AS\b/i.test(remaining)) {
return null;
}
if (current === '(') {
columnListStart = index;
parenthesisDepth = 1;
}
continue;
}
if (current === '(') {
parenthesisDepth += 1;
} else if (current === ')') {
parenthesisDepth -= 1;
if (parenthesisDepth === 0) {
return { start: columnListStart, end: index };
}
}
}
return null;
};
const splitOracleViewColumns = (columnList: string): string[] => {
const columns: string[] = [];
let inQuotedIdentifier = false;
let start = 0;
for (let index = 0; index < columnList.length; index += 1) {
const current = columnList[index];
if (current === '"') {
if (inQuotedIdentifier && columnList[index + 1] === '"') {
index += 1;
continue;
}
inQuotedIdentifier = !inQuotedIdentifier;
} else if (current === ',' && !inQuotedIdentifier) {
columns.push(columnList.slice(start, index).trim());
start = index + 1;
}
}
columns.push(columnList.slice(start).trim());
return columns.filter(Boolean);
};
const normalizeOracleViewDdlFormatting = (sql: string): string => {
const columnList = findOracleViewColumnList(sql);
let normalized = sql;
if (columnList) {
const columns = splitOracleViewColumns(sql.slice(columnList.start + 1, columnList.end));
if (columns.length > 1) {
normalized = [
sql.slice(0, columnList.start + 1),
'\n ',
columns.join(',\n '),
'\n',
sql.slice(columnList.end),
].join('');
}
}
return normalized.replace(
/\bWITH[ \t]*\r?\n[ \t]*(READ[ \t]+ONLY|CHECK[ \t]+OPTION)\b/gi,
'WITH $1',
);
};
const isOracleViewDdl = (sql: string): boolean => (
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NO\s*)?FORCE\s+)?(?:NONEDITIONABLE\s+|EDITIONABLE\s+)?(?:MATERIALIZED\s+)?VIEW\b/i.test(sql)
);
export const formatDdlForDisplay = (ddlText: unknown, dbType: string): string => {
const raw = String(ddlText ?? '').trim();
if (!raw) {
return '';
}
if (normalizeDbType(dbType) === 'oracle' && isOracleViewDdl(raw)) {
return raw;
}
const language = resolveDdlFormatterLanguage(dbType);
try {
const formatted = format(raw, {
return format(raw, {
language,
keywordCase: 'upper',
linesBetweenQueries: 1,
});
return normalizeDbType(dbType) === 'oracle'
? normalizeOracleViewDdlFormatting(formatted)
: formatted;
} catch {
return raw;
}