diff --git a/src/lib/data/sql-export/__tests__/export-sql.test.ts b/src/lib/data/sql-export/__tests__/export-sql.test.ts index 632e4ab2..e7077d7a 100644 --- a/src/lib/data/sql-export/__tests__/export-sql.test.ts +++ b/src/lib/data/sql-export/__tests__/export-sql.test.ts @@ -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({ diff --git a/src/lib/data/sql-export/export-per-type/postgresql.ts b/src/lib/data/sql-export/export-per-type/postgresql.ts index 7520e7f3..6b7829f9 100644 --- a/src/lib/data/sql-export/export-per-type/postgresql.ts +++ b/src/lib/data/sql-export/export-per-type/postgresql.ts @@ -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( diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts index cc54b3a0..22a1efa1 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-regression.test.ts @@ -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', + ]); + }); }); diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts index 480bd860..84040e3e 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts @@ -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