mirror of
https://github.com/chartdb/chartdb.git
synced 2026-02-07 03:59:08 -06:00
fix: PostgreSQL unique/primary key import handling (#1062)
This commit is contained in:
@@ -198,6 +198,142 @@ describe('SQL Export Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unique Constraint Index Export', () => {
|
||||
it('should not generate CREATE UNIQUE INDEX for single-column unique fields in PostgreSQL', () => {
|
||||
const fieldId = testId();
|
||||
const diagram = createDiagram({
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
tables: [
|
||||
createTable({
|
||||
name: 'orders_copy',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
createField({
|
||||
id: fieldId,
|
||||
name: 'id',
|
||||
type: { id: 'bigserial', name: 'bigserial' },
|
||||
primaryKey: false,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
}),
|
||||
createField({
|
||||
name: 'created_at',
|
||||
type: {
|
||||
id: 'timestamptz',
|
||||
name: 'timestamptz',
|
||||
},
|
||||
nullable: false,
|
||||
}),
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
id: testId(),
|
||||
name: 'orders_copy_id_key',
|
||||
fieldIds: [fieldId],
|
||||
unique: true,
|
||||
isPrimaryKey: false,
|
||||
createdAt: testTime,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const sql = exportPostgreSQL({ diagram });
|
||||
|
||||
// Should have inline UNIQUE
|
||||
expect(sql).toContain('"id" bigserial NOT NULL UNIQUE');
|
||||
// Should NOT have separate CREATE UNIQUE INDEX for the unique field
|
||||
expect(sql).not.toContain('CREATE UNIQUE INDEX');
|
||||
});
|
||||
|
||||
it('should still generate CREATE UNIQUE INDEX for multi-column unique indexes in PostgreSQL', () => {
|
||||
const fieldId1 = testId();
|
||||
const fieldId2 = testId();
|
||||
const diagram = createDiagram({
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
tables: [
|
||||
createTable({
|
||||
name: 'test_table',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
createField({
|
||||
id: fieldId1,
|
||||
name: 'col_a',
|
||||
type: { id: 'text', name: 'text' },
|
||||
nullable: false,
|
||||
unique: false,
|
||||
}),
|
||||
createField({
|
||||
id: fieldId2,
|
||||
name: 'col_b',
|
||||
type: { id: 'text', name: 'text' },
|
||||
nullable: false,
|
||||
unique: false,
|
||||
}),
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
id: testId(),
|
||||
name: 'test_table_unique_idx',
|
||||
fieldIds: [fieldId1, fieldId2],
|
||||
unique: true,
|
||||
isPrimaryKey: false,
|
||||
createdAt: testTime,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const sql = exportPostgreSQL({ diagram });
|
||||
|
||||
// Should have CREATE UNIQUE INDEX for multi-column unique constraint
|
||||
expect(sql).toContain('CREATE UNIQUE INDEX');
|
||||
expect(sql).toContain('"col_a", "col_b"');
|
||||
});
|
||||
|
||||
it('should generate CREATE UNIQUE INDEX for single-column unique index when field is not marked unique', () => {
|
||||
const fieldId = testId();
|
||||
const diagram = createDiagram({
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
tables: [
|
||||
createTable({
|
||||
name: 'test_table',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
createField({
|
||||
id: fieldId,
|
||||
name: 'email',
|
||||
type: { id: 'text', name: 'text' },
|
||||
nullable: false,
|
||||
unique: false, // Field not marked as unique
|
||||
}),
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
id: testId(),
|
||||
name: 'test_table_email_key',
|
||||
fieldIds: [fieldId],
|
||||
unique: true,
|
||||
isPrimaryKey: false,
|
||||
createdAt: testTime,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const sql = exportPostgreSQL({ diagram });
|
||||
|
||||
// Should NOT have inline UNIQUE (field.unique is false)
|
||||
expect(sql).not.toContain('UNIQUE,');
|
||||
expect(sql).not.toContain('NOT NULL UNIQUE');
|
||||
// Should have CREATE UNIQUE INDEX since the field doesn't have inline UNIQUE
|
||||
expect(sql).toContain('CREATE UNIQUE INDEX');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportBaseSQL with foreign key relationships', () => {
|
||||
it('should export PostgreSQL diagram with two tables and a foreign key relationship', () => {
|
||||
const diagram = createDiagram({
|
||||
|
||||
@@ -402,6 +402,16 @@ export function exportPostgreSQL({
|
||||
return '';
|
||||
}
|
||||
|
||||
// Skip unique indexes on single columns that already have inline UNIQUE
|
||||
// PostgreSQL automatically creates an index for UNIQUE constraints
|
||||
if (
|
||||
index.unique &&
|
||||
indexFields.length === 1 &&
|
||||
indexFields[0]?.unique
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create unique index name using table name and index name
|
||||
// This ensures index names are unique across the database
|
||||
const safeTableName = table.name.replace(
|
||||
|
||||
@@ -196,4 +196,55 @@ CREATE TABLE patients(
|
||||
|
||||
expect(result.relationships).toHaveLength(14);
|
||||
});
|
||||
|
||||
it('should preserve UNIQUE constraint and correct PRIMARY KEY on import', async () => {
|
||||
// Regression test: When importing a table with:
|
||||
// 1. A column with inline UNIQUE (not part of PK)
|
||||
// 2. A composite PRIMARY KEY on different columns
|
||||
// The import should correctly identify which fields are PK vs just UNIQUE
|
||||
|
||||
const inputSql = `
|
||||
CREATE TABLE "public"."orders_copy" (
|
||||
"id" bigserial NOT NULL UNIQUE,
|
||||
"created_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"user_id" bigint NOT NULL,
|
||||
"customer_id" bigint,
|
||||
CONSTRAINT "orders_pkey" PRIMARY KEY ("user_id", "customer_id")
|
||||
);`;
|
||||
|
||||
const result = await fromPostgres(inputSql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
const table = result.tables[0];
|
||||
|
||||
expect(table.name).toBe('orders_copy');
|
||||
expect(table.columns).toBeDefined();
|
||||
|
||||
// Verify field properties
|
||||
const idField = table.columns.find((f) => f.name === 'id');
|
||||
const userIdField = table.columns.find((f) => f.name === 'user_id');
|
||||
const customerIdField = table.columns.find(
|
||||
(f) => f.name === 'customer_id'
|
||||
);
|
||||
|
||||
expect(idField).toBeDefined();
|
||||
expect(userIdField).toBeDefined();
|
||||
expect(customerIdField).toBeDefined();
|
||||
|
||||
// id should be UNIQUE but NOT a primary key
|
||||
expect(idField!.unique).toBe(true);
|
||||
expect(idField!.primaryKey).toBe(false);
|
||||
|
||||
// user_id and customer_id should be primary keys
|
||||
expect(userIdField!.primaryKey).toBe(true);
|
||||
expect(customerIdField!.primaryKey).toBe(true);
|
||||
|
||||
// Verify no field other than user_id and customer_id is marked as primary key
|
||||
const pkFields = table.columns.filter((f) => f.primaryKey);
|
||||
expect(pkFields).toHaveLength(2);
|
||||
expect(pkFields.map((f) => f.name).sort()).toEqual([
|
||||
'customer_id',
|
||||
'user_id',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1191,7 +1191,7 @@ export async function fromPostgres(
|
||||
? false
|
||||
: columnDef.nullable?.type !==
|
||||
'not null',
|
||||
primaryKey: isPrimaryKey || isSerialType,
|
||||
primaryKey: isPrimaryKey,
|
||||
unique: columnDef.unique === 'unique',
|
||||
typeArgs: getTypeArgs(columnDef.definition),
|
||||
default: isSerialType
|
||||
@@ -1652,8 +1652,7 @@ export async function fromPostgres(
|
||||
nullable: nullable,
|
||||
primaryKey:
|
||||
definition?.primary_key === 'primary key' ||
|
||||
definition?.constraint === 'primary key' ||
|
||||
isSerialType,
|
||||
definition?.constraint === 'primary key',
|
||||
unique: isUnique,
|
||||
default: defaultValue,
|
||||
increment:
|
||||
@@ -1769,8 +1768,7 @@ export async function fromPostgres(
|
||||
columnDef.primary_key ===
|
||||
'primary key' ||
|
||||
columnDef.definition?.constraint ===
|
||||
'primary key' ||
|
||||
isSerialType,
|
||||
'primary key',
|
||||
unique: columnDef.unique === 'unique',
|
||||
typeArgs: getTypeArgs(columnDef.definition),
|
||||
default: isSerialType
|
||||
|
||||
Reference in New Issue
Block a user