From 805ab8b3d897adeeaee2efc309da44ba2d38b332 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 5 Jun 2026 11:25:44 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(table-designer):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20DuckDB=20=E8=A1=A8=E8=AE=BE=E8=AE=A1=E4=B8=BB?= =?UTF-8?q?=E9=94=AE=E4=BF=9D=E5=AD=98=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 DuckDB 表结构变更补充 ADD PRIMARY KEY 预览 SQL - 保存前拦截已有主键表的主键替换与删除,避免假成功 - 补充 DuckDB 主键变更判定与 schema SQL 回归测试 --- frontend/src/components/TableDesigner.tsx | 8 ++++ .../tableDesignerDuckDbPrimaryKey.test.ts | 44 +++++++++++++++++++ .../tableDesignerDuckDbPrimaryKey.ts | 38 ++++++++++++++++ .../components/tableDesignerSchemaSql.test.ts | 37 ++++++++++++++++ .../src/components/tableDesignerSchemaSql.ts | 21 +++++++++ 5 files changed, 148 insertions(+) create mode 100644 frontend/src/components/tableDesignerDuckDbPrimaryKey.test.ts create mode 100644 frontend/src/components/tableDesignerDuckDbPrimaryKey.ts diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx index d45ec5b..3b9a167 100644 --- a/frontend/src/components/TableDesigner.tsx +++ b/frontend/src/components/TableDesigner.tsx @@ -11,6 +11,7 @@ import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, D import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils'; import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql'; import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql'; +import { summarizeDuckDbPrimaryKeyChange } from './tableDesignerDuckDbPrimaryKey'; import { normalizeSchemaStatementForExecution, parseTableCommentFromDDL, splitSchemaExecutionStatements } from './tableDesignerExecutionSql'; import TableDesignerSqlPreview from './TableDesignerSqlPreview'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; @@ -2211,6 +2212,13 @@ END;`; setIsPreviewOpen(true); } else { const tableInfo = resolveTableInfo(); + if (tableInfo.dbType === 'duckdb') { + const pkChange = summarizeDuckDbPrimaryKeyChange(originalColumns, columns); + if (pkChange.isUnsupportedChange) { + message.warning('DuckDB 当前仅支持为无主键表新增主键;已有主键的修改或删除需要通过重建表完成。'); + return; + } + } const sql = buildAlterTablePreviewSql({ dbType: tableInfo.dbType, tableName: tableInfo.qualifiedName, diff --git a/frontend/src/components/tableDesignerDuckDbPrimaryKey.test.ts b/frontend/src/components/tableDesignerDuckDbPrimaryKey.test.ts new file mode 100644 index 0000000..c0da633 --- /dev/null +++ b/frontend/src/components/tableDesignerDuckDbPrimaryKey.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import type { EditableColumnSnapshot } from './tableDesignerSchemaSql'; +import { summarizeDuckDbPrimaryKeyChange } from './tableDesignerDuckDbPrimaryKey'; + +const col = (overrides: Partial): EditableColumnSnapshot => ({ + _key: overrides._key || 'id', + name: overrides.name || 'id', + type: overrides.type || 'BIGINT', + nullable: overrides.nullable || 'NO', + default: overrides.default || '', + extra: overrides.extra || '', + comment: overrides.comment || '', + key: overrides.key || '', + isAutoIncrement: overrides.isAutoIncrement || false, +}); + +describe('tableDesignerDuckDbPrimaryKey', () => { + it('treats first primary key addition as supported', () => { + const summary = summarizeDuckDbPrimaryKeyChange( + [col({ _key: 'id', key: '' })], + [col({ _key: 'id', key: 'PRI' })], + ); + + expect(summary).toEqual({ + hasChange: true, + isAddingPrimaryKey: true, + isUnsupportedChange: false, + }); + }); + + it('treats replacing an existing primary key as unsupported', () => { + const summary = summarizeDuckDbPrimaryKeyChange( + [col({ _key: 'id', key: 'PRI' }), col({ _key: 'name', name: 'name', key: '' })], + [col({ _key: 'id', key: '' }), col({ _key: 'name', name: 'name', key: 'PRI' })], + ); + + expect(summary).toEqual({ + hasChange: true, + isAddingPrimaryKey: false, + isUnsupportedChange: true, + }); + }); +}); diff --git a/frontend/src/components/tableDesignerDuckDbPrimaryKey.ts b/frontend/src/components/tableDesignerDuckDbPrimaryKey.ts new file mode 100644 index 0000000..4d93bbd --- /dev/null +++ b/frontend/src/components/tableDesignerDuckDbPrimaryKey.ts @@ -0,0 +1,38 @@ +import type { EditableColumnSnapshot } from './tableDesignerSchemaSql'; + +export interface DuckDbPrimaryKeyChangeSummary { + hasChange: boolean; + isAddingPrimaryKey: boolean; + isUnsupportedChange: boolean; +} + +const collectPrimaryKeys = (columns: EditableColumnSnapshot[]): string[] => ( + columns + .filter((column) => column.key === 'PRI') + .map((column) => String(column._key || '').trim()) + .filter(Boolean) + .sort() +); + +export const summarizeDuckDbPrimaryKeyChange = ( + originalColumns: EditableColumnSnapshot[], + columns: EditableColumnSnapshot[], +): DuckDbPrimaryKeyChangeSummary => { + const originalKeys = collectPrimaryKeys(originalColumns); + const nextKeys = collectPrimaryKeys(columns); + const hasChange = originalKeys.length !== nextKeys.length || originalKeys.some((key, index) => key !== nextKeys[index]); + if (!hasChange) { + return { + hasChange: false, + isAddingPrimaryKey: false, + isUnsupportedChange: false, + }; + } + + const isAddingPrimaryKey = originalKeys.length === 0 && nextKeys.length > 0; + return { + hasChange: true, + isAddingPrimaryKey, + isUnsupportedChange: !isAddingPrimaryKey, + }; +}; diff --git a/frontend/src/components/tableDesignerSchemaSql.test.ts b/frontend/src/components/tableDesignerSchemaSql.test.ts index c451d5e..a485eda 100644 --- a/frontend/src/components/tableDesignerSchemaSql.test.ts +++ b/frontend/src/components/tableDesignerSchemaSql.test.ts @@ -163,6 +163,43 @@ describe('tableDesignerSchemaSql', () => { expect(sql).not.toContain('MODIFY COLUMN'); }); + it('builds duckdb alter preview with add primary key when adding first primary key', () => { + const sql = buildAlterTablePreviewSql(buildInput({ + dbType: 'duckdb', + tableName: 'main.events', + originalColumns: [ + baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'YES', key: '' }), + baseColumn({ _key: 'name', name: 'name', type: 'VARCHAR', nullable: 'YES', key: '' }), + ], + columns: [ + baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'NO', key: 'PRI' }), + baseColumn({ _key: 'name', name: 'name', type: 'VARCHAR', nullable: 'YES', key: '' }), + ], + })); + + expect(sql).toContain('ALTER TABLE "main"."events"\nALTER COLUMN "id" SET NOT NULL;'); + expect(sql).toContain('ALTER TABLE "main"."events"\nADD PRIMARY KEY ("id");'); + }); + + it('marks unsupported duckdb primary key replacement with explicit warning comment', () => { + const sql = buildAlterTablePreviewSql(buildInput({ + dbType: 'duckdb', + tableName: 'main.events', + originalColumns: [ + baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'NO', key: 'PRI' }), + baseColumn({ _key: 'name', name: 'name', type: 'VARCHAR', nullable: 'YES', key: '' }), + ], + columns: [ + baseColumn({ _key: 'id', name: 'id', type: 'BIGINT', nullable: 'NO', key: '' }), + baseColumn({ _key: 'name', name: 'name', type: 'VARCHAR', nullable: 'NO', key: 'PRI' }), + ], + })); + + expect(sql).toContain('-- DuckDB 当前仅支持为无主键表新增 PRIMARY KEY;已有主键的修改或删除需要通过重建表完成。'); + expect(sql).not.toContain('DROP CONSTRAINT'); + expect(sql).not.toContain('DROP PRIMARY KEY'); + }); + it('builds doris alter preview without mysql-only syntax or metadata extra', () => { const sql = buildAlterTablePreviewSql(buildInput({ dbType: 'doris', diff --git a/frontend/src/components/tableDesignerSchemaSql.ts b/frontend/src/components/tableDesignerSchemaSql.ts index 0bf690a..c9f7802 100644 --- a/frontend/src/components/tableDesignerSchemaSql.ts +++ b/frontend/src/components/tableDesignerSchemaSql.ts @@ -79,6 +79,12 @@ export interface BuildStarRocksMaterializedViewPreviewInput { properties?: string; } +const collectPrimaryKeyColumnKeys = (columns: EditableColumnSnapshot[]): string[] => ( + columns + .filter((col) => col.key === 'PRI') + .map((col) => col._key) +); + const escapeSqlString = (value: string) => String(value || '').replace(/'/g, "''"); const stripIdentifierQuotes = unquoteSqlIdentifierPart; @@ -607,6 +613,21 @@ const buildDuckDbAlterPreviewSql = (input: BuildAlterTablePreviewInput): string } }); + const origPKKeys = collectPrimaryKeyColumnKeys(input.originalColumns); + const newPKKeys = collectPrimaryKeyColumnKeys(input.columns); + const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key)); + if (keysChanged) { + if (origPKKeys.length === 0 && newPKKeys.length > 0) { + const pkNames = input.columns + .filter((col) => col.key === 'PRI') + .map((col) => quoteIdentifierPart(col.name, dbType)) + .join(', '); + statements.push(`ALTER TABLE ${tableRef}\nADD PRIMARY KEY (${pkNames});`); + } else { + statements.push('-- DuckDB 当前仅支持为无主键表新增 PRIMARY KEY;已有主键的修改或删除需要通过重建表完成。'); + } + } + return statements.join('\n'); };