mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
🐛 fix(ddl): 保留 Oracle 视图 DDL 原始排版
- Oracle 视图与物化视图跳过展示格式化 - 补充预览及对象编辑原始排版回归测试
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user