From a5b27820cbe0baf096359807233bb9c5fe4e6280 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 5 Jun 2026 22:21:40 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ddl):=20=E4=B8=BA=20DDL=20?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E5=A2=9E=E5=8A=A0=E6=8C=89=E6=96=B9=E8=A8=80?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=B1=95=E7=A4=BA=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增通用 DDL 格式化工具 - DataGrid 查看 DDL 时按数据源方言输出可读 SQL - 覆盖 DuckDB DDL 展示与工具层测试 --- frontend/src/components/DataGrid.ddl.test.tsx | 40 +++++++++++++- frontend/src/components/DataGrid.tsx | 1 + frontend/src/components/useDataGridDdlView.ts | 7 ++- frontend/src/utils/ddlFormat.test.ts | 22 ++++++++ frontend/src/utils/ddlFormat.ts | 53 +++++++++++++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/ddlFormat.test.ts create mode 100644 frontend/src/utils/ddlFormat.ts diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index 7625aff..5f8ea08 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -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( + , + ); + }); + 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({ diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index b860102..21debb6 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -3214,6 +3214,7 @@ const DataGrid: React.FC = ({ canViewDdl, currentConnConfig, dbName, + dbType, tableName, isV2Ui, cellEditMode, diff --git a/frontend/src/components/useDataGridDdlView.ts b/frontend/src/components/useDataGridDdlView.ts index da1bbab..5ab694c 100644 --- a/frontend/src/components/useDataGridDdlView.ts +++ b/frontend/src/components/useDataGridDdlView.ts @@ -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('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; diff --git a/frontend/src/utils/ddlFormat.test.ts b/frontend/src/utils/ddlFormat.test.ts new file mode 100644 index 0000000..cbbaea5 --- /dev/null +++ b/frontend/src/utils/ddlFormat.test.ts @@ -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); + }); +}); diff --git a/frontend/src/utils/ddlFormat.ts b/frontend/src/utils/ddlFormat.ts new file mode 100644 index 0000000..a3e5c0c --- /dev/null +++ b/frontend/src/utils/ddlFormat.ts @@ -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; + } +};