🐛 fix(table-designer): 修复 DuckDB 表设计主键保存失效

- 为 DuckDB 表结构变更补充 ADD PRIMARY KEY 预览 SQL
- 保存前拦截已有主键表的主键替换与删除,避免假成功
- 补充 DuckDB 主键变更判定与 schema SQL 回归测试
This commit is contained in:
Syngnat
2026-06-05 11:25:44 +08:00
parent 6742495c6f
commit 805ab8b3d8
5 changed files with 148 additions and 0 deletions

View File

@@ -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,

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import type { EditableColumnSnapshot } from './tableDesignerSchemaSql';
import { summarizeDuckDbPrimaryKeyChange } from './tableDesignerDuckDbPrimaryKey';
const col = (overrides: Partial<EditableColumnSnapshot>): 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,
});
});
});

View File

@@ -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,
};
};

View File

@@ -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',

View File

@@ -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');
};