feat(ddl): 为 DDL 视图增加按方言格式化展示能力

- 新增通用 DDL 格式化工具
- DataGrid 查看 DDL 时按数据源方言输出可读 SQL
- 覆盖 DuckDB DDL 展示与工具层测试
This commit is contained in:
Syngnat
2026-06-05 22:21:40 +08:00
parent d2189e1442
commit a5b27820cb
5 changed files with 120 additions and 3 deletions

View File

@@ -1246,10 +1246,48 @@ describe('DataGrid DDL interactions', () => {
expect(editors).toHaveLength(1);
expect(editors[0].props['data-language']).toBe('sql');
expect(editors[0].props['data-read-only']).toBe('true');
expect(textContent(editors[0])).toContain('CREATE TABLE users');
expect(textContent(editors[0])).toContain('CREATE TABLE');
expect(textContent(editors[0])).toContain('users');
expect(renderer!.root.findAll((node) => node.type === 'pre' && textContent(node).includes('CREATE TABLE users'))).toHaveLength(0);
});
it('formats DuckDB DDL into readable multiline SQL in the v2 view', async () => {
storeState.appearance.uiVersion = 'v2';
storeState.connections[0].config.type = 'duckdb';
backendApp.DBShowCreateTable.mockResolvedValueOnce({
success: true,
data: 'CREATE TABLE customers(customer_id BIGINT, customer_code VARCHAR, city VARCHAR, tier VARCHAR, signup_date DATE, lifetime_value DECIMAL(12,2), PRIMARY KEY(customer_id));',
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(
<DataGrid
data={[{ __gonavi_row_key__: 'row-1', customer_id: 1 }]}
columnNames={['customer_id']}
loading={false}
tableName="example.main.customers"
dbName="main"
connectionId="conn-1"
/>,
);
});
await waitForEffects();
await act(async () => {
findButton(renderer!, '查看 DDL').props.onClick();
});
await waitForEffects();
const editors = renderer!.root.findAll((node) => node.props['data-monaco-editor'] === 'true');
expect(editors).toHaveLength(1);
const ddlText = textContent(editors[0]);
expect(ddlText).toContain('CREATE TABLE customers (');
expect(ddlText).toContain('customer_id BIGINT,');
expect(ddlText).toContain('PRIMARY KEY (customer_id)');
expect(ddlText).toContain('\n');
});
it('opens the v2 DDL view as a right sidebar while keeping the table visible', async () => {
storeState.appearance.uiVersion = 'v2';
backendApp.DBShowCreateTable.mockResolvedValueOnce({

View File

@@ -3214,6 +3214,7 @@ const DataGrid: React.FC<DataGridProps> = ({
canViewDdl,
currentConnConfig,
dbName,
dbType,
tableName,
isV2Ui,
cellEditMode,

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { DBShowCreateTable } from '../../wailsjs/go/app/App';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { formatDdlForDisplay } from '../utils/ddlFormat';
type GridViewMode = 'table' | 'json' | 'text' | 'fields' | 'ddl' | 'er';
type DdlViewLayoutMode = 'bottom' | 'side';
@@ -20,6 +21,7 @@ interface UseDataGridDdlViewParams {
messageApi: {
error: (content: string) => void;
};
dbType?: string;
}
export interface UseDataGridDdlViewResult {
@@ -54,6 +56,7 @@ export const useDataGridDdlView = ({
closeCellEditModeRef,
setTextRecordIndex,
messageApi,
dbType,
}: UseDataGridDdlViewParams): UseDataGridDdlViewResult => {
const [viewMode, setViewMode] = React.useState<GridViewMode>('table');
const [ddlModalOpen, setDdlModalOpen] = React.useState(false);
@@ -92,7 +95,7 @@ export const useDataGridDdlView = ({
const res = await DBShowCreateTable(buildRpcConnectionConfig(currentConnConfig as any) as any, dbName || '', tableName);
if (requestSeq !== ddlRequestSeqRef.current) return;
if (res.success) {
setDdlText(String(res.data ?? ''));
setDdlText(formatDdlForDisplay(res.data, dbType || String((currentConnConfig as any)?.type || '')));
return;
}
messageApi.error(res.message || '获取 DDL 失败');
@@ -104,7 +107,7 @@ export const useDataGridDdlView = ({
setDdlLoading(false);
}
}
}, [canViewDdl, currentConnConfig, dbName, isV2Ui, messageApi, tableName]);
}, [canViewDdl, currentConnConfig, dbName, dbType, isV2Ui, messageApi, tableName]);
React.useEffect(() => {
if (isV2Ui || (viewMode !== 'fields' && viewMode !== 'ddl' && viewMode !== 'er')) return;

View File

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { formatDdlForDisplay } from './ddlFormat';
describe('formatDdlForDisplay', () => {
it('formats DuckDB create table SQL into multiline output', () => {
const raw = 'CREATE TABLE customers(customer_id BIGINT, customer_code VARCHAR, city VARCHAR, tier VARCHAR, signup_date DATE, lifetime_value DECIMAL(12,2), PRIMARY KEY(customer_id));';
const formatted = formatDdlForDisplay(raw, 'duckdb');
expect(formatted).toContain('CREATE TABLE customers (');
expect(formatted).toContain('customer_id BIGINT,');
expect(formatted).toContain('PRIMARY KEY (customer_id)');
expect(formatted).toContain('\n');
});
it('returns original text when formatter cannot parse the statement', () => {
const raw = 'not valid ddl(';
expect(formatDdlForDisplay(raw, 'duckdb')).toBe(raw);
});
});

View File

@@ -0,0 +1,53 @@
import { format } from 'sql-formatter';
const resolveDdlFormatterLanguage = (dbType: string): string | null => {
const normalized = String(dbType || '').trim().toLowerCase();
switch (normalized) {
case 'duckdb':
return 'duckdb';
case 'sqlite':
return 'sqlite';
case 'postgres':
case 'postgresql':
case 'kingbase':
case 'highgo':
case 'opengauss':
case 'vastbase':
return 'postgresql';
case 'mariadb':
return 'mariadb';
case 'mysql':
case 'sphinx':
return 'mysql';
case 'sqlserver':
return 'transactsql';
case 'oracle':
case 'dameng':
case 'oceanbase':
return 'plsql';
case 'clickhouse':
return 'clickhouse';
default:
return 'sql';
}
};
export const formatDdlForDisplay = (ddlText: unknown, dbType: string): string => {
const raw = String(ddlText ?? '').trim();
if (!raw) {
return '';
}
const language = resolveDdlFormatterLanguage(dbType);
if (!language) {
return raw;
}
try {
return format(raw, {
language,
keywordCase: 'upper',
linesBetweenQueries: 1,
});
} catch {
return raw;
}
};