mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			jf/add_dup
			...
			jf/add_sup
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					21ba816a6d | ||
| 
						 | 
					ad8e34483f | ||
| 
						 | 
					215d57979d | 
							
								
								
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							@@ -7,7 +7,7 @@ on:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  actions: write
 | 
			
		||||
  contents: write # this can be 'read' if the signatures are in remote repository
 | 
			
		||||
  contents: read
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
  statuses: write
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,10 @@ import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
 | 
			
		||||
import { chartDBContext } from './chartdb-context';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
import {
 | 
			
		||||
    getTableIndexesWithPrimaryKey,
 | 
			
		||||
    type DBIndex,
 | 
			
		||||
} from '@/lib/domain/db-index';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
@@ -348,6 +351,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                order: tables.length,
 | 
			
		||||
                ...attributes,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            table.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                table,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            await addTable(table);
 | 
			
		||||
 | 
			
		||||
            return table;
 | 
			
		||||
@@ -639,17 +647,30 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const prevField = getField(tableId, fieldId);
 | 
			
		||||
 | 
			
		||||
            const updateTableFn = (table: DBTable) => {
 | 
			
		||||
                const updatedTable: DBTable = {
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    fields: table.fields.map((f) =>
 | 
			
		||||
                        f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                    ),
 | 
			
		||||
                } satisfies DBTable;
 | 
			
		||||
 | 
			
		||||
                updatedTable.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                    table: updatedTable,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return updatedTable;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? {
 | 
			
		||||
                              ...table,
 | 
			
		||||
                              fields: table.fields.map((f) =>
 | 
			
		||||
                                  f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                              ),
 | 
			
		||||
                          }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
                tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        return updateTableFn(table);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const table = await db.getTable({ diagramId, id: tableId });
 | 
			
		||||
@@ -664,10 +685,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: table.fields.map((f) =>
 | 
			
		||||
                            f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                        ),
 | 
			
		||||
                        ...updateTableFn(table),
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
@@ -694,19 +712,29 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            fieldId: string,
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const updateTableFn = (table: DBTable) => {
 | 
			
		||||
                const updatedTable: DBTable = {
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    fields: table.fields.filter((f) => f.id !== fieldId),
 | 
			
		||||
                } satisfies DBTable;
 | 
			
		||||
 | 
			
		||||
                updatedTable.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                    table: updatedTable,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return updatedTable;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const fields = getTable(tableId)?.fields ?? [];
 | 
			
		||||
            const prevField = getField(tableId, fieldId);
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? {
 | 
			
		||||
                              ...table,
 | 
			
		||||
                              fields: table.fields.filter(
 | 
			
		||||
                                  (f) => f.id !== fieldId
 | 
			
		||||
                              ),
 | 
			
		||||
                          }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
                tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        return updateTableFn(table);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
@@ -730,8 +758,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: table.fields.filter((f) => f.id !== fieldId),
 | 
			
		||||
                        ...updateTableFn(table),
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
 
 | 
			
		||||
@@ -143,6 +143,7 @@ export const en = {
 | 
			
		||||
                        title: 'Field Attributes',
 | 
			
		||||
                        unique: 'Unique',
 | 
			
		||||
                        auto_increment: 'Auto Increment',
 | 
			
		||||
                        array: 'Declare Array',
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Precision',
 | 
			
		||||
                        scale: 'Scale',
 | 
			
		||||
 
 | 
			
		||||
@@ -165,3 +165,21 @@ export const supportsAutoIncrementDataType = (
 | 
			
		||||
        'decimal',
 | 
			
		||||
    ].includes(dataTypeName.toLocaleLowerCase());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const supportsArrayDataType = (dataTypeName: string): boolean => {
 | 
			
		||||
    // Types that do NOT support arrays in PostgreSQL
 | 
			
		||||
    const unsupportedTypes = [
 | 
			
		||||
        'serial',
 | 
			
		||||
        'bigserial',
 | 
			
		||||
        'smallserial',
 | 
			
		||||
        'serial2',
 | 
			
		||||
        'serial4',
 | 
			
		||||
        'serial8',
 | 
			
		||||
        'xml',
 | 
			
		||||
        'money',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Check if the type is in the unsupported list
 | 
			
		||||
    const normalizedType = dataTypeName.toLowerCase();
 | 
			
		||||
    return !unsupportedTypes.includes(normalizedType);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -124,6 +124,96 @@ describe('DBML Export - SQL Generation Tests', () => {
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should not create duplicate index for composite primary key', () => {
 | 
			
		||||
            const tableId = testId();
 | 
			
		||||
            const field1Id = testId();
 | 
			
		||||
            const field2Id = testId();
 | 
			
		||||
            const field3Id = testId();
 | 
			
		||||
 | 
			
		||||
            const diagram: Diagram = createDiagram({
 | 
			
		||||
                id: testId(),
 | 
			
		||||
                name: 'Landlord System',
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: tableId,
 | 
			
		||||
                        name: 'users_master_table',
 | 
			
		||||
                        schema: 'landlord',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: field1Id,
 | 
			
		||||
                                name: 'master_user_id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: field2Id,
 | 
			
		||||
                                name: 'tenant_id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: field3Id,
 | 
			
		||||
                                name: 'tenant_user_id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'enabled',
 | 
			
		||||
                                type: { id: 'boolean', name: 'boolean' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: true,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [
 | 
			
		||||
                            {
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'idx_users_master_table_master_user_id_tenant_id_tenant_user_id',
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                                fieldIds: [field1Id, field2Id, field3Id],
 | 
			
		||||
                                createdAt: testTime,
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'index_1',
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                                fieldIds: [field2Id, field3Id],
 | 
			
		||||
                                createdAt: testTime,
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const sql = exportBaseSQL({
 | 
			
		||||
                diagram,
 | 
			
		||||
                targetDatabaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                isDBMLFlow: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should contain composite primary key constraint
 | 
			
		||||
            expect(sql).toContain(
 | 
			
		||||
                'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Should NOT contain the duplicate index for the primary key fields
 | 
			
		||||
            expect(sql).not.toContain(
 | 
			
		||||
                'CREATE INDEX idx_users_master_table_master_user_id_tenant_id_tenant_user_id'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Should still contain the unique index on subset of fields
 | 
			
		||||
            expect(sql).toContain('CREATE UNIQUE INDEX index_1');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle single primary keys inline', () => {
 | 
			
		||||
            const diagram: Diagram = createDiagram({
 | 
			
		||||
                id: testId(),
 | 
			
		||||
 
 | 
			
		||||
@@ -178,7 +178,15 @@ export function exportMSSQL({
 | 
			
		||||
                    })
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    table.fields.filter((f) => f.primaryKey).length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${table.fields
 | 
			
		||||
                        ? `,\n    ${(() => {
 | 
			
		||||
                              // Find PK index to get the constraint name
 | 
			
		||||
                              const pkIndex = table.indexes.find(
 | 
			
		||||
                                  (idx) => idx.isPrimaryKey
 | 
			
		||||
                              );
 | 
			
		||||
                              return pkIndex?.name
 | 
			
		||||
                                  ? `CONSTRAINT [${pkIndex.name}] `
 | 
			
		||||
                                  : '';
 | 
			
		||||
                          })()}PRIMARY KEY (${table.fields
 | 
			
		||||
                              .filter((f) => f.primaryKey)
 | 
			
		||||
                              .map((f) => `[${f.name}]`)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
 
 | 
			
		||||
@@ -313,7 +313,15 @@ export function exportMySQL({
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    // Add PRIMARY KEY as table constraint
 | 
			
		||||
                    primaryKeyFields.length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                        ? `,\n    ${(() => {
 | 
			
		||||
                              // Find PK index to get the constraint name
 | 
			
		||||
                              const pkIndex = table.indexes.find(
 | 
			
		||||
                                  (idx) => idx.isPrimaryKey
 | 
			
		||||
                              );
 | 
			
		||||
                              return pkIndex?.name
 | 
			
		||||
                                  ? `CONSTRAINT \`${pkIndex.name}\` `
 | 
			
		||||
                                  : '';
 | 
			
		||||
                          })()}PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                              .map((f) => `\`${f.name}\``)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
                        : ''
 | 
			
		||||
 
 | 
			
		||||
@@ -286,10 +286,14 @@ export function exportPostgreSQL({
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Handle array types (check if the type name ends with '[]')
 | 
			
		||||
                        if (typeName.endsWith('[]')) {
 | 
			
		||||
                            typeWithSize =
 | 
			
		||||
                                typeWithSize.replace('[]', '') + '[]';
 | 
			
		||||
                        // Handle array types (check if the field has array property or type name ends with '[]')
 | 
			
		||||
                        if (field.array || typeName.endsWith('[]')) {
 | 
			
		||||
                            if (!typeName.endsWith('[]')) {
 | 
			
		||||
                                typeWithSize = typeWithSize + '[]';
 | 
			
		||||
                            } else {
 | 
			
		||||
                                typeWithSize =
 | 
			
		||||
                                    typeWithSize.replace('[]', '') + '[]';
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
@@ -325,7 +329,15 @@ export function exportPostgreSQL({
 | 
			
		||||
                    })
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    primaryKeyFields.length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                        ? `,\n    ${(() => {
 | 
			
		||||
                              // Find PK index to get the constraint name
 | 
			
		||||
                              const pkIndex = table.indexes.find(
 | 
			
		||||
                                  (idx) => idx.isPrimaryKey
 | 
			
		||||
                              );
 | 
			
		||||
                              return pkIndex?.name
 | 
			
		||||
                                  ? `CONSTRAINT "${pkIndex.name}" `
 | 
			
		||||
                                  : '';
 | 
			
		||||
                          })()}PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                              .map((f) => `"${f.name}"`)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
                        : ''
 | 
			
		||||
 
 | 
			
		||||
@@ -313,21 +313,33 @@ export const exportBaseSQL = ({
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle PRIMARY KEY constraint - only add inline if not composite
 | 
			
		||||
            if (field.primaryKey && !hasCompositePrimaryKey) {
 | 
			
		||||
            // Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
 | 
			
		||||
            const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
            if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
 | 
			
		||||
                sqlScript += ' PRIMARY KEY';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add a comma after each field except the last one (or before composite primary key)
 | 
			
		||||
            if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
 | 
			
		||||
            // Add a comma after each field except the last one (or before PK constraint)
 | 
			
		||||
            const needsPKConstraint =
 | 
			
		||||
                hasCompositePrimaryKey ||
 | 
			
		||||
                (primaryKeyFields.length === 1 && pkIndex?.name);
 | 
			
		||||
            if (index < table.fields.length - 1 || needsPKConstraint) {
 | 
			
		||||
                sqlScript += ',\n';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add composite primary key constraint if needed
 | 
			
		||||
        if (hasCompositePrimaryKey) {
 | 
			
		||||
        // Add primary key constraint if needed (for composite PKs or single PK with custom name)
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        if (
 | 
			
		||||
            hasCompositePrimaryKey ||
 | 
			
		||||
            (primaryKeyFields.length === 1 && pkIndex?.name)
 | 
			
		||||
        ) {
 | 
			
		||||
            const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
 | 
			
		||||
            sqlScript += `\n  PRIMARY KEY (${pkFieldNames})`;
 | 
			
		||||
            if (pkIndex?.name) {
 | 
			
		||||
                sqlScript += `\n  CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
 | 
			
		||||
            } else {
 | 
			
		||||
                sqlScript += `\n  PRIMARY KEY (${pkFieldNames})`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sqlScript += '\n);\n';
 | 
			
		||||
@@ -349,12 +361,33 @@ export const exportBaseSQL = ({
 | 
			
		||||
 | 
			
		||||
        // Generate SQL for indexes
 | 
			
		||||
        table.indexes.forEach((index) => {
 | 
			
		||||
            const fieldNames = index.fieldIds
 | 
			
		||||
                .map(
 | 
			
		||||
                    (fieldId) =>
 | 
			
		||||
                        table.fields.find((field) => field.id === fieldId)?.name
 | 
			
		||||
            // Skip the primary key index (it's already handled as a constraint)
 | 
			
		||||
            if (index.isPrimaryKey) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get the fields for this index
 | 
			
		||||
            const indexFields = index.fieldIds
 | 
			
		||||
                .map((fieldId) => table.fields.find((f) => f.id === fieldId))
 | 
			
		||||
                .filter(
 | 
			
		||||
                    (field): field is NonNullable<typeof field> =>
 | 
			
		||||
                        field !== undefined
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            // Skip if this index exactly matches the primary key fields
 | 
			
		||||
            // This prevents creating redundant indexes for composite primary keys
 | 
			
		||||
            if (
 | 
			
		||||
                primaryKeyFields.length > 0 &&
 | 
			
		||||
                primaryKeyFields.length === indexFields.length &&
 | 
			
		||||
                primaryKeyFields.every((pk) =>
 | 
			
		||||
                    indexFields.some((field) => field.id === pk.id)
 | 
			
		||||
                )
 | 
			
		||||
                .filter(Boolean)
 | 
			
		||||
            ) {
 | 
			
		||||
                return; // Skip this index as it's redundant with the primary key
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const fieldNames = indexFields
 | 
			
		||||
                .map((field) => field.name)
 | 
			
		||||
                .join(', ');
 | 
			
		||||
 | 
			
		||||
            if (fieldNames) {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ export interface SQLColumn {
 | 
			
		||||
    comment?: string;
 | 
			
		||||
    default?: string;
 | 
			
		||||
    increment?: boolean;
 | 
			
		||||
    array?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SQLTable {
 | 
			
		||||
@@ -612,6 +613,7 @@ export function convertToChartDBDiagram(
 | 
			
		||||
                default: column.default || '',
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
                increment: column.increment,
 | 
			
		||||
                array: column.array,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Add type arguments if present
 | 
			
		||||
 
 | 
			
		||||
@@ -373,6 +373,13 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
 | 
			
		||||
                'SMALLSERIAL',
 | 
			
		||||
            ].includes(upperType.split('(')[0]);
 | 
			
		||||
 | 
			
		||||
            // Check if it's an array type
 | 
			
		||||
            let isArrayType = false;
 | 
			
		||||
            if (columnType.endsWith('[]')) {
 | 
			
		||||
                isArrayType = true;
 | 
			
		||||
                columnType = columnType.slice(0, -2);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Normalize the type
 | 
			
		||||
            columnType = normalizePostgreSQLType(columnType);
 | 
			
		||||
 | 
			
		||||
@@ -395,6 +402,7 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
 | 
			
		||||
                    trimmedLine.includes('uuid_generate_v4()') ||
 | 
			
		||||
                    trimmedLine.includes('GENERATED ALWAYS AS IDENTITY') ||
 | 
			
		||||
                    trimmedLine.includes('GENERATED BY DEFAULT AS IDENTITY'),
 | 
			
		||||
                array: isArrayType,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -782,6 +790,16 @@ export async function fromPostgres(
 | 
			
		||||
                                    normalizePostgreSQLType(rawDataType);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // Check if it's an array type
 | 
			
		||||
                            let isArrayType = false;
 | 
			
		||||
                            if (normalizedBaseType.endsWith('[]')) {
 | 
			
		||||
                                isArrayType = true;
 | 
			
		||||
                                normalizedBaseType = normalizedBaseType.slice(
 | 
			
		||||
                                    0,
 | 
			
		||||
                                    -2
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // Now handle parameters - but skip for integer types that shouldn't have them
 | 
			
		||||
                            let finalDataType = normalizedBaseType;
 | 
			
		||||
 | 
			
		||||
@@ -874,6 +892,7 @@ export async function fromPostgres(
 | 
			
		||||
                                            stmt.sql
 | 
			
		||||
                                                .toUpperCase()
 | 
			
		||||
                                                .includes('IDENTITY')),
 | 
			
		||||
                                    array: isArrayType,
 | 
			
		||||
                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (def.resource === 'constraint') {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										114
									
								
								src/lib/dbml/dbml-export/__tests__/composite-pk-export.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/lib/dbml/dbml-export/__tests__/composite-pk-export.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { generateDBMLFromDiagram } from '../dbml-export';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { generateId } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
describe('Composite Primary Key Name Export', () => {
 | 
			
		||||
    it('should export composite primary key with name in DBML', () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
            id: generateId(),
 | 
			
		||||
            name: 'Test',
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
            createdAt: new Date(),
 | 
			
		||||
            updatedAt: new Date(),
 | 
			
		||||
            tables: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: 'users_master_table',
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    color: '#FFF',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name: 'master_user_id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name: 'tenant_id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name: 'tenant_user_id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name: 'enabled',
 | 
			
		||||
                            type: { id: 'boolean', name: 'boolean' },
 | 
			
		||||
                            nullable: true,
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name: 'users_master_table_index_1',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['dummy1', 'dummy2'], // Will be replaced
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            relationships: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Fix field IDs in the index and add PK index
 | 
			
		||||
        const table = diagram.tables![0];
 | 
			
		||||
        const masterUserIdField = table.fields.find(
 | 
			
		||||
            (f) => f.name === 'master_user_id'
 | 
			
		||||
        );
 | 
			
		||||
        const tenantIdField = table.fields.find((f) => f.name === 'tenant_id');
 | 
			
		||||
        const tenantUserIdField = table.fields.find(
 | 
			
		||||
            (f) => f.name === 'tenant_user_id'
 | 
			
		||||
        );
 | 
			
		||||
        table.indexes[0].fieldIds = [tenantIdField!.id, tenantUserIdField!.id];
 | 
			
		||||
 | 
			
		||||
        // Add the PK index with name
 | 
			
		||||
        table.indexes.push({
 | 
			
		||||
            id: generateId(),
 | 
			
		||||
            name: 'moshe',
 | 
			
		||||
            unique: true,
 | 
			
		||||
            isPrimaryKey: true,
 | 
			
		||||
            fieldIds: [
 | 
			
		||||
                masterUserIdField!.id,
 | 
			
		||||
                tenantIdField!.id,
 | 
			
		||||
                tenantUserIdField!.id,
 | 
			
		||||
            ],
 | 
			
		||||
            createdAt: Date.now(),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        // Check that the DBML contains the composite PK with name
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            '(master_user_id, tenant_id, tenant_user_id) [pk, name: "moshe"]'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check that the unique index is also present
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            '(tenant_id, tenant_user_id) [unique, name: "users_master_table_index_1"]'
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1383,12 +1383,9 @@ Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        // Check that the inline DBML has proper indentation
 | 
			
		||||
        // Note: indexes on primary key fields should be filtered out
 | 
			
		||||
        expect(result.inlineDbml).toContain(`Table "table_1" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
 | 
			
		||||
  Indexes {
 | 
			
		||||
    id [name: "index_1"]
 | 
			
		||||
  }
 | 
			
		||||
}`);
 | 
			
		||||
 | 
			
		||||
        expect(result.inlineDbml).toContain(`Table "table_2" {
 | 
			
		||||
 
 | 
			
		||||
@@ -605,6 +605,45 @@ const fixTableBracketSyntax = (dbml: string): string => {
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Restore composite primary key names in the DBML
 | 
			
		||||
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
 | 
			
		||||
    if (!tables || tables.length === 0) return dbml;
 | 
			
		||||
 | 
			
		||||
    let result = dbml;
 | 
			
		||||
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        // Check if this table has a PK index with a name
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        if (pkIndex?.name) {
 | 
			
		||||
            const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
            if (primaryKeyFields.length >= 1) {
 | 
			
		||||
                // Build the column list for the composite PK
 | 
			
		||||
                const columnList = primaryKeyFields
 | 
			
		||||
                    .map((f) => f.name)
 | 
			
		||||
                    .join(', ');
 | 
			
		||||
 | 
			
		||||
                // Build the table identifier pattern
 | 
			
		||||
                const tableIdentifier = table.schema
 | 
			
		||||
                    ? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
 | 
			
		||||
                    : `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
 | 
			
		||||
 | 
			
		||||
                // Pattern to match the composite PK index line
 | 
			
		||||
                // Match patterns like: (col1, col2, col3) [pk]
 | 
			
		||||
                const pkPattern = new RegExp(
 | 
			
		||||
                    `(Table ${tableIdentifier} \\{[^}]*?Indexes \\{[^}]*?)(\\(${columnList.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\) \\[pk\\])`,
 | 
			
		||||
                    'gs'
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Replace with the named version
 | 
			
		||||
                const replacement = `$1(${columnList}) [pk, name: "${pkIndex.name}"]`;
 | 
			
		||||
                result = result.replace(pkPattern, replacement);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Restore schema information that may have been stripped by the DBML importer
 | 
			
		||||
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
 | 
			
		||||
    if (!tables || tables.length === 0) return dbml;
 | 
			
		||||
@@ -870,14 +909,16 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
            ...table,
 | 
			
		||||
            name: safeTableName,
 | 
			
		||||
            fields: processedFields,
 | 
			
		||||
            indexes: (table.indexes || []).map((index) => ({
 | 
			
		||||
                ...index,
 | 
			
		||||
                name: index.name
 | 
			
		||||
                    ? /[^\w]/.test(index.name)
 | 
			
		||||
                        ? `"${index.name.replace(/"/g, '\\"')}"`
 | 
			
		||||
                        : index.name
 | 
			
		||||
                    : `idx_${Math.random().toString(36).substring(2, 8)}`,
 | 
			
		||||
            })),
 | 
			
		||||
            indexes: (table.indexes || [])
 | 
			
		||||
                .filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
 | 
			
		||||
                .map((index) => ({
 | 
			
		||||
                    ...index,
 | 
			
		||||
                    name: index.name
 | 
			
		||||
                        ? /[^\w]/.test(index.name)
 | 
			
		||||
                            ? `"${index.name.replace(/"/g, '\\"')}"`
 | 
			
		||||
                            : index.name
 | 
			
		||||
                        : `idx_${Math.random().toString(36).substring(2, 8)}`,
 | 
			
		||||
                })),
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -939,6 +980,9 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
        // Restore schema information that may have been stripped by DBML importer
 | 
			
		||||
        standard = restoreTableSchemas(standard, uniqueTables);
 | 
			
		||||
 | 
			
		||||
        // Restore composite primary key names
 | 
			
		||||
        standard = restoreCompositePKNames(standard, uniqueTables);
 | 
			
		||||
 | 
			
		||||
        // Prepend Enum DBML to the standard output
 | 
			
		||||
        if (enumsDBML) {
 | 
			
		||||
            standard = enumsDBML + '\n\n' + standard;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										190
									
								
								src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { importDBMLToDiagram } from '../dbml-import';
 | 
			
		||||
import { exportPostgreSQL } from '@/lib/data/export-metadata/export-per-type/postgresql';
 | 
			
		||||
import { exportMySQL } from '@/lib/data/export-metadata/export-per-type/mysql';
 | 
			
		||||
import { exportMSSQL } from '@/lib/data/export-metadata/export-per-type/mssql';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
 | 
			
		||||
describe('Composite Primary Key with Name', () => {
 | 
			
		||||
    it('should preserve composite primary key name in DBML import and SQL export', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "landlord"."users_master_table" {
 | 
			
		||||
  "master_user_id" bigint [not null]
 | 
			
		||||
  "tenant_id" bigint [not null]
 | 
			
		||||
  "tenant_user_id" bigint [not null]
 | 
			
		||||
  "enabled" boolean
 | 
			
		||||
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (master_user_id, tenant_id, tenant_user_id) [pk, name: "idx_users_master_table_master_user_id_tenant_id_tenant_user_id"]
 | 
			
		||||
    (tenant_id, tenant_user_id) [unique, name: "index_1"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        // Import DBML
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Check that the composite PK name was captured
 | 
			
		||||
        expect(diagram.tables).toBeDefined();
 | 
			
		||||
        const table = diagram.tables![0];
 | 
			
		||||
 | 
			
		||||
        // Check for the PK index
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        expect(pkIndex).toBeDefined();
 | 
			
		||||
        expect(pkIndex!.name).toBe(
 | 
			
		||||
            'idx_users_master_table_master_user_id_tenant_id_tenant_user_id'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check that fields are marked as primary keys
 | 
			
		||||
        const pkFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
        expect(pkFields).toHaveLength(3);
 | 
			
		||||
        expect(pkFields.map((f) => f.name)).toEqual([
 | 
			
		||||
            'master_user_id',
 | 
			
		||||
            'tenant_id',
 | 
			
		||||
            'tenant_user_id',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Check that we have both the PK index and the unique index
 | 
			
		||||
        expect(table.indexes).toHaveLength(2);
 | 
			
		||||
        const uniqueIndex = table.indexes.find((idx) => !idx.isPrimaryKey);
 | 
			
		||||
        expect(uniqueIndex!.name).toBe('index_1');
 | 
			
		||||
        expect(uniqueIndex!.unique).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should export composite primary key with CONSTRAINT name in PostgreSQL', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "users" {
 | 
			
		||||
  "id" bigint [not null]
 | 
			
		||||
  "tenant_id" bigint [not null]
 | 
			
		||||
  
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (id, tenant_id) [pk, name: "pk_users_composite"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const sqlScript = exportPostgreSQL({ diagram });
 | 
			
		||||
 | 
			
		||||
        // Check that the SQL contains the named constraint
 | 
			
		||||
        expect(sqlScript).toContain(
 | 
			
		||||
            'CONSTRAINT "pk_users_composite" PRIMARY KEY ("id", "tenant_id")'
 | 
			
		||||
        );
 | 
			
		||||
        expect(sqlScript).not.toContain('PRIMARY KEY ("id", "tenant_id"),'); // Should not have unnamed PK
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should export composite primary key with CONSTRAINT name in MySQL', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "orders" {
 | 
			
		||||
  "order_id" int [not null]
 | 
			
		||||
  "product_id" int [not null]
 | 
			
		||||
  
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (order_id, product_id) [pk, name: "orders_order_product_pk"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.MYSQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const sqlScript = exportMySQL({ diagram });
 | 
			
		||||
 | 
			
		||||
        // Check that the SQL contains the named constraint
 | 
			
		||||
        expect(sqlScript).toContain(
 | 
			
		||||
            'CONSTRAINT `orders_order_product_pk` PRIMARY KEY (`order_id`, `product_id`)'
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should export composite primary key with CONSTRAINT name in MSSQL', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "products" {
 | 
			
		||||
  "category_id" int [not null]
 | 
			
		||||
  "product_id" int [not null]
 | 
			
		||||
  
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (category_id, product_id) [pk, name: "pk_products"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.SQL_SERVER,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const sqlScript = exportMSSQL({ diagram });
 | 
			
		||||
 | 
			
		||||
        // Check that the SQL contains the named constraint
 | 
			
		||||
        expect(sqlScript).toContain(
 | 
			
		||||
            'CONSTRAINT [pk_products] PRIMARY KEY ([category_id], [product_id])'
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should merge duplicate PK index with name', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "test" {
 | 
			
		||||
  "a" int [not null]
 | 
			
		||||
  "b" int [not null]
 | 
			
		||||
  
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (a, b) [pk]
 | 
			
		||||
    (a, b) [name: "test_pk_name"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(diagram.tables).toBeDefined();
 | 
			
		||||
        const table = diagram.tables![0];
 | 
			
		||||
 | 
			
		||||
        // Should capture the name from the duplicate index
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        expect(pkIndex).toBeDefined();
 | 
			
		||||
        expect(pkIndex!.name).toBe('test_pk_name');
 | 
			
		||||
 | 
			
		||||
        // Should only have the PK index
 | 
			
		||||
        expect(table.indexes).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        // Fields should be marked as primary keys
 | 
			
		||||
        expect(table.fields.filter((f) => f.primaryKey)).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle composite PK without name', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table "simple" {
 | 
			
		||||
  "x" int [not null]
 | 
			
		||||
  "y" int [not null]
 | 
			
		||||
  
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (x, y) [pk]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(diagram.tables).toBeDefined();
 | 
			
		||||
        const table = diagram.tables![0];
 | 
			
		||||
 | 
			
		||||
        // PK index should not exist for composite PK without name
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        expect(pkIndex).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        const sqlScript = exportPostgreSQL({ diagram });
 | 
			
		||||
 | 
			
		||||
        // Should have unnamed PRIMARY KEY
 | 
			
		||||
        expect(sqlScript).toContain('PRIMARY KEY ("x", "y")');
 | 
			
		||||
        expect(sqlScript).toContain('CONSTRAINT');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -177,7 +177,7 @@ Table ranks {
 | 
			
		||||
            expect(wizardsTable?.fields).toHaveLength(11);
 | 
			
		||||
 | 
			
		||||
            // Check indexes
 | 
			
		||||
            expect(wizardsTable?.indexes).toHaveLength(2);
 | 
			
		||||
            expect(wizardsTable?.indexes).toHaveLength(3);
 | 
			
		||||
            const emailIndex = wizardsTable?.indexes.find((idx) =>
 | 
			
		||||
                idx.name.includes('email')
 | 
			
		||||
            );
 | 
			
		||||
@@ -920,7 +920,7 @@ Note dragon_note {
 | 
			
		||||
            expect(hoardsTable).toBeDefined();
 | 
			
		||||
 | 
			
		||||
            // Verify all indexes are imported correctly
 | 
			
		||||
            expect(hoardsTable?.indexes).toHaveLength(3); // Should have 3 indexes as defined in DBML
 | 
			
		||||
            expect(hoardsTable?.indexes).toHaveLength(4); // 3 from DBML + 1 implicit PK index
 | 
			
		||||
 | 
			
		||||
            // Verify named indexes
 | 
			
		||||
            const uniqueDragonIndex = hoardsTable?.indexes.find(
 | 
			
		||||
@@ -1119,7 +1119,7 @@ Table "public_3"."comments" {
 | 
			
		||||
            ).toBe('timestamp');
 | 
			
		||||
 | 
			
		||||
            // Check posts indexes thoroughly
 | 
			
		||||
            expect(postsTable?.indexes).toHaveLength(2);
 | 
			
		||||
            expect(postsTable?.indexes).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
            // Index 1: Composite unique index on (content, user_id)
 | 
			
		||||
            const compositeIndex = postsTable?.indexes.find(
 | 
			
		||||
@@ -1154,7 +1154,7 @@ Table "public_3"."comments" {
 | 
			
		||||
 | 
			
		||||
            // Check comments table
 | 
			
		||||
            expect(commentsTable?.fields).toHaveLength(5);
 | 
			
		||||
            expect(commentsTable?.indexes).toHaveLength(1);
 | 
			
		||||
            expect(commentsTable?.indexes).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
            // Index: Unique index on id
 | 
			
		||||
            const idIndex = commentsTable?.indexes.find(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { defaultTableColor } from '@/lib/colors';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type Field from '@dbml/core/types/model_structure/field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain';
 | 
			
		||||
import { getTableIndexesWithPrimaryKey, type DBIndex } from '@/lib/domain';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
@@ -100,6 +100,7 @@ interface DBMLIndex {
 | 
			
		||||
    columns: (string | DBMLIndexColumn)[];
 | 
			
		||||
    unique?: boolean;
 | 
			
		||||
    name?: string;
 | 
			
		||||
    pk?: boolean; // Primary key index flag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLTable {
 | 
			
		||||
@@ -387,15 +388,19 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                // Generate a consistent index name
 | 
			
		||||
                                // For PK indexes, only use the name if explicitly provided
 | 
			
		||||
                                // For regular indexes, generate a default name if needed
 | 
			
		||||
                                const indexName =
 | 
			
		||||
                                    dbmlIndex.name ||
 | 
			
		||||
                                    `idx_${table.name}_${indexColumns.join('_')}`;
 | 
			
		||||
                                    (!dbmlIndex.pk
 | 
			
		||||
                                        ? `idx_${table.name}_${indexColumns.join('_')}`
 | 
			
		||||
                                        : undefined);
 | 
			
		||||
 | 
			
		||||
                                return {
 | 
			
		||||
                                    columns: indexColumns,
 | 
			
		||||
                                    unique: dbmlIndex.unique || false,
 | 
			
		||||
                                    name: indexName,
 | 
			
		||||
                                    pk: Boolean(dbmlIndex.pk) || false,
 | 
			
		||||
                                };
 | 
			
		||||
                            }) || [],
 | 
			
		||||
                    });
 | 
			
		||||
@@ -484,29 +489,126 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Convert DBML indexes to ChartDB indexes
 | 
			
		||||
            const indexes: DBIndex[] =
 | 
			
		||||
                table.indexes?.map((dbmlIndex) => {
 | 
			
		||||
                    const fieldIds = dbmlIndex.columns.map((columnName) => {
 | 
			
		||||
                        const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                        if (!field) {
 | 
			
		||||
                            throw new Error(
 | 
			
		||||
                                `Index references non-existent column: ${columnName}`
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                        return field.id;
 | 
			
		||||
                    });
 | 
			
		||||
            // Process composite primary keys from indexes with [pk] attribute
 | 
			
		||||
            let compositePKFields: string[] = [];
 | 
			
		||||
            let compositePKIndexName: string | undefined;
 | 
			
		||||
 | 
			
		||||
                    return {
 | 
			
		||||
                        id: generateId(),
 | 
			
		||||
                        name:
 | 
			
		||||
                            dbmlIndex.name ||
 | 
			
		||||
                            `idx_${table.name}_${(dbmlIndex.columns as string[]).join('_')}`,
 | 
			
		||||
                        fieldIds,
 | 
			
		||||
                        unique: dbmlIndex.unique || false,
 | 
			
		||||
                        createdAt: Date.now(),
 | 
			
		||||
                    };
 | 
			
		||||
                }) || [];
 | 
			
		||||
            // Find PK indexes and mark fields as primary keys
 | 
			
		||||
            table.indexes?.forEach((dbmlIndex) => {
 | 
			
		||||
                if (dbmlIndex.pk) {
 | 
			
		||||
                    // Extract column names from the columns array
 | 
			
		||||
                    compositePKFields = dbmlIndex.columns.map((col) =>
 | 
			
		||||
                        typeof col === 'string' ? col : col.value
 | 
			
		||||
                    );
 | 
			
		||||
                    // Only store the name if it was explicitly provided (not undefined)
 | 
			
		||||
                    if (dbmlIndex.name) {
 | 
			
		||||
                        compositePKIndexName = dbmlIndex.name;
 | 
			
		||||
                    }
 | 
			
		||||
                    // Mark fields as primary keys
 | 
			
		||||
                    dbmlIndex.columns.forEach((col) => {
 | 
			
		||||
                        const columnName =
 | 
			
		||||
                            typeof col === 'string' ? col : col.value;
 | 
			
		||||
                        const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                        if (field) {
 | 
			
		||||
                            field.primaryKey = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // If we found a PK without a name, look for a duplicate index with just a name
 | 
			
		||||
            if (compositePKFields.length > 0 && !compositePKIndexName) {
 | 
			
		||||
                table.indexes?.forEach((dbmlIndex) => {
 | 
			
		||||
                    if (
 | 
			
		||||
                        !dbmlIndex.pk &&
 | 
			
		||||
                        dbmlIndex.name &&
 | 
			
		||||
                        dbmlIndex.columns.length === compositePKFields.length
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // Check if columns match
 | 
			
		||||
                        const indexColumns = dbmlIndex.columns.map((col) =>
 | 
			
		||||
                            typeof col === 'string' ? col : col.value
 | 
			
		||||
                        );
 | 
			
		||||
                        if (
 | 
			
		||||
                            indexColumns.every(
 | 
			
		||||
                                (col, i) => col === compositePKFields[i]
 | 
			
		||||
                            )
 | 
			
		||||
                        ) {
 | 
			
		||||
                            compositePKIndexName = dbmlIndex.name;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Convert DBML indexes to ChartDB indexes (excluding PK indexes and their duplicates)
 | 
			
		||||
            const indexes: DBIndex[] =
 | 
			
		||||
                table.indexes
 | 
			
		||||
                    ?.filter((dbmlIndex) => {
 | 
			
		||||
                        // Skip PK indexes - we'll handle them separately
 | 
			
		||||
                        if (dbmlIndex.pk) return false;
 | 
			
		||||
 | 
			
		||||
                        // Skip duplicate indexes that match the composite PK
 | 
			
		||||
                        // (when user has both [pk] and [name: "..."] on same fields)
 | 
			
		||||
                        if (
 | 
			
		||||
                            compositePKFields.length > 0 &&
 | 
			
		||||
                            dbmlIndex.columns.length ===
 | 
			
		||||
                                compositePKFields.length &&
 | 
			
		||||
                            dbmlIndex.columns.every((col, i) => {
 | 
			
		||||
                                const colName =
 | 
			
		||||
                                    typeof col === 'string' ? col : col.value;
 | 
			
		||||
                                return colName === compositePKFields[i];
 | 
			
		||||
                            })
 | 
			
		||||
                        ) {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return true;
 | 
			
		||||
                    })
 | 
			
		||||
                    .map((dbmlIndex) => {
 | 
			
		||||
                        const fieldIds = dbmlIndex.columns.map((columnName) => {
 | 
			
		||||
                            const field = fields.find(
 | 
			
		||||
                                (f) => f.name === columnName
 | 
			
		||||
                            );
 | 
			
		||||
                            if (!field) {
 | 
			
		||||
                                throw new Error(
 | 
			
		||||
                                    `Index references non-existent column: ${columnName}`
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                            return field.id;
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        return {
 | 
			
		||||
                            id: generateId(),
 | 
			
		||||
                            name:
 | 
			
		||||
                                dbmlIndex.name ||
 | 
			
		||||
                                `idx_${table.name}_${(dbmlIndex.columns as string[]).join('_')}`,
 | 
			
		||||
                            fieldIds,
 | 
			
		||||
                            unique: dbmlIndex.unique || false,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        };
 | 
			
		||||
                    }) || [];
 | 
			
		||||
 | 
			
		||||
            // Add PK as an index if it exists and has a name
 | 
			
		||||
            // Only create the PK index if there's an explicit name for it
 | 
			
		||||
            if (compositePKFields.length >= 1 && compositePKIndexName) {
 | 
			
		||||
                const pkFieldIds = compositePKFields.map((columnName) => {
 | 
			
		||||
                    const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                    if (!field) {
 | 
			
		||||
                        throw new Error(
 | 
			
		||||
                            `PK references non-existent column: ${columnName}`
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                    return field.id;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                indexes.push({
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: compositePKIndexName,
 | 
			
		||||
                    fieldIds: pkFieldIds,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    isPrimaryKey: true,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Extract table note/comment
 | 
			
		||||
            let tableComment: string | undefined;
 | 
			
		||||
@@ -521,7 +623,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
            const tableToReturn: DBTable = {
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: table.name.replace(/['"]/g, ''),
 | 
			
		||||
                schema:
 | 
			
		||||
@@ -540,6 +642,13 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
                comments: tableComment,
 | 
			
		||||
            } satisfies DBTable;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...tableToReturn,
 | 
			
		||||
                indexes: getTableIndexesWithPrimaryKey({
 | 
			
		||||
                    table: tableToReturn,
 | 
			
		||||
                }),
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create relationships using the refs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										192
									
								
								src/lib/domain/__tests__/composite-pk-metadata-import.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/lib/domain/__tests__/composite-pk-metadata-import.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,192 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { createTablesFromMetadata } from '../db-table';
 | 
			
		||||
import { DatabaseType } from '../database-type';
 | 
			
		||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
 | 
			
		||||
describe('Composite Primary Key Name from Metadata Import', () => {
 | 
			
		||||
    it('should capture composite primary key name from metadata indexes', () => {
 | 
			
		||||
        const metadata: DatabaseMetadata = {
 | 
			
		||||
            database_name: 'test_db',
 | 
			
		||||
            version: '',
 | 
			
		||||
            fk_info: [],
 | 
			
		||||
            pk_info: [
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    column: 'master_user_id',
 | 
			
		||||
                    pk_def: 'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    column: 'tenant_id',
 | 
			
		||||
                    pk_def: 'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    column: 'tenant_user_id',
 | 
			
		||||
                    pk_def: 'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'master_user_id',
 | 
			
		||||
                    ordinal_position: 1,
 | 
			
		||||
                    type: 'bigint',
 | 
			
		||||
                    character_maximum_length: null,
 | 
			
		||||
                    precision: null,
 | 
			
		||||
                    nullable: false,
 | 
			
		||||
                    default: '',
 | 
			
		||||
                    collation: '',
 | 
			
		||||
                    comment: '',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'tenant_id',
 | 
			
		||||
                    ordinal_position: 2,
 | 
			
		||||
                    type: 'bigint',
 | 
			
		||||
                    character_maximum_length: null,
 | 
			
		||||
                    precision: null,
 | 
			
		||||
                    nullable: false,
 | 
			
		||||
                    default: '',
 | 
			
		||||
                    collation: '',
 | 
			
		||||
                    comment: '',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'tenant_user_id',
 | 
			
		||||
                    ordinal_position: 3,
 | 
			
		||||
                    type: 'bigint',
 | 
			
		||||
                    character_maximum_length: null,
 | 
			
		||||
                    precision: null,
 | 
			
		||||
                    nullable: false,
 | 
			
		||||
                    default: '',
 | 
			
		||||
                    collation: '',
 | 
			
		||||
                    comment: '',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'enabled',
 | 
			
		||||
                    ordinal_position: 4,
 | 
			
		||||
                    type: 'boolean',
 | 
			
		||||
                    character_maximum_length: null,
 | 
			
		||||
                    precision: null,
 | 
			
		||||
                    nullable: true,
 | 
			
		||||
                    default: '',
 | 
			
		||||
                    collation: '',
 | 
			
		||||
                    comment: '',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            indexes: [
 | 
			
		||||
                // The composite PK index named "moshe"
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'moshe',
 | 
			
		||||
                    column: 'master_user_id',
 | 
			
		||||
                    index_type: 'btree',
 | 
			
		||||
                    cardinality: 0,
 | 
			
		||||
                    size: 8192,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    column_position: 1,
 | 
			
		||||
                    direction: 'asc',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'moshe',
 | 
			
		||||
                    column: 'tenant_id',
 | 
			
		||||
                    index_type: 'btree',
 | 
			
		||||
                    cardinality: 0,
 | 
			
		||||
                    size: 8192,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    column_position: 2,
 | 
			
		||||
                    direction: 'asc',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'moshe',
 | 
			
		||||
                    column: 'tenant_user_id',
 | 
			
		||||
                    index_type: 'btree',
 | 
			
		||||
                    cardinality: 0,
 | 
			
		||||
                    size: 8192,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    column_position: 3,
 | 
			
		||||
                    direction: 'asc',
 | 
			
		||||
                },
 | 
			
		||||
                // Another unique index
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'users_master_table_index_1',
 | 
			
		||||
                    column: 'tenant_id',
 | 
			
		||||
                    index_type: 'btree',
 | 
			
		||||
                    cardinality: 0,
 | 
			
		||||
                    size: 8192,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    column_position: 1,
 | 
			
		||||
                    direction: 'asc',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    name: 'users_master_table_index_1',
 | 
			
		||||
                    column: 'tenant_user_id',
 | 
			
		||||
                    index_type: 'btree',
 | 
			
		||||
                    cardinality: 0,
 | 
			
		||||
                    size: 8192,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    column_position: 2,
 | 
			
		||||
                    direction: 'asc',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            tables: [
 | 
			
		||||
                {
 | 
			
		||||
                    schema: 'landlord',
 | 
			
		||||
                    table: 'users_master_table',
 | 
			
		||||
                    rows: 0,
 | 
			
		||||
                    type: 'BASE TABLE',
 | 
			
		||||
                    engine: '',
 | 
			
		||||
                    collation: '',
 | 
			
		||||
                    comment: '',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            views: [],
 | 
			
		||||
            custom_types: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const tables = createTablesFromMetadata({
 | 
			
		||||
            databaseMetadata: metadata,
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(tables).toHaveLength(1);
 | 
			
		||||
        const table = tables[0];
 | 
			
		||||
 | 
			
		||||
        // Check that the composite PK name was captured as "moshe" in the PK index
 | 
			
		||||
        const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
        expect(pkIndex).toBeDefined();
 | 
			
		||||
        expect(pkIndex!.name).toBe('moshe');
 | 
			
		||||
 | 
			
		||||
        // Check that primary key fields are marked correctly
 | 
			
		||||
        const pkFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
        expect(pkFields).toHaveLength(3);
 | 
			
		||||
        expect(pkFields.map((f) => f.name).sort()).toEqual([
 | 
			
		||||
            'master_user_id',
 | 
			
		||||
            'tenant_id',
 | 
			
		||||
            'tenant_user_id',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Check that we have both the PK index and the unique index
 | 
			
		||||
        expect(table.indexes).toHaveLength(2);
 | 
			
		||||
        const uniqueIndex = table.indexes.find((idx) => !idx.isPrimaryKey);
 | 
			
		||||
        expect(uniqueIndex!.name).toBe('users_master_table_index_1');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -19,6 +19,7 @@ export interface DBField {
 | 
			
		||||
    unique: boolean;
 | 
			
		||||
    nullable: boolean;
 | 
			
		||||
    increment?: boolean | null;
 | 
			
		||||
    array?: boolean | null;
 | 
			
		||||
    createdAt: number;
 | 
			
		||||
    characterMaximumLength?: string | null;
 | 
			
		||||
    precision?: number | null;
 | 
			
		||||
@@ -36,6 +37,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
 | 
			
		||||
    unique: z.boolean(),
 | 
			
		||||
    nullable: z.boolean(),
 | 
			
		||||
    increment: z.boolean().or(z.null()).optional(),
 | 
			
		||||
    array: z.boolean().or(z.null()).optional(),
 | 
			
		||||
    createdAt: z.number(),
 | 
			
		||||
    characterMaximumLength: z.string().or(z.null()).optional(),
 | 
			
		||||
    precision: z.number().or(z.null()).optional(),
 | 
			
		||||
@@ -71,13 +73,48 @@ export const createFieldsFromMetadata = ({
 | 
			
		||||
        pk.column.trim()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return sortedColumns.map(
 | 
			
		||||
        (col: ColumnInfo): DBField => ({
 | 
			
		||||
    return sortedColumns.map((col: ColumnInfo): DBField => {
 | 
			
		||||
        // Check if type is an array (ends with [])
 | 
			
		||||
        const isArrayType = col.type.endsWith('[]');
 | 
			
		||||
        let baseType = col.type;
 | 
			
		||||
 | 
			
		||||
        // Extract base type and any parameters if it's an array
 | 
			
		||||
        if (isArrayType) {
 | 
			
		||||
            baseType = col.type.slice(0, -2); // Remove the [] suffix
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Extract parameters from types like "character varying(100)" or "numeric(10,2)"
 | 
			
		||||
        let charMaxLength = col.character_maximum_length;
 | 
			
		||||
        let precision = col.precision?.precision;
 | 
			
		||||
        let scale = col.precision?.scale;
 | 
			
		||||
 | 
			
		||||
        // Handle types with single parameter like varchar(100)
 | 
			
		||||
        const singleParamMatch = baseType.match(/^(.+?)\((\d+)\)$/);
 | 
			
		||||
        if (singleParamMatch) {
 | 
			
		||||
            baseType = singleParamMatch[1];
 | 
			
		||||
            if (!charMaxLength || charMaxLength === 'null') {
 | 
			
		||||
                charMaxLength = singleParamMatch[2];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle types with two parameters like numeric(10,2)
 | 
			
		||||
        const twoParamMatch = baseType.match(/^(.+?)\((\d+),\s*(\d+)\)$/);
 | 
			
		||||
        if (twoParamMatch) {
 | 
			
		||||
            baseType = twoParamMatch[1];
 | 
			
		||||
            if (!precision) {
 | 
			
		||||
                precision = parseInt(twoParamMatch[2]);
 | 
			
		||||
            }
 | 
			
		||||
            if (!scale) {
 | 
			
		||||
                scale = parseInt(twoParamMatch[3]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            id: generateId(),
 | 
			
		||||
            name: col.name,
 | 
			
		||||
            type: {
 | 
			
		||||
                id: col.type.split(' ').join('_').toLowerCase(),
 | 
			
		||||
                name: col.type.toLowerCase(),
 | 
			
		||||
                id: baseType.split(' ').join('_').toLowerCase(),
 | 
			
		||||
                name: baseType.toLowerCase(),
 | 
			
		||||
            },
 | 
			
		||||
            primaryKey: tablePrimaryKeysColumns.includes(col.name),
 | 
			
		||||
            unique: Object.values(aggregatedIndexes).some(
 | 
			
		||||
@@ -87,20 +124,18 @@ export const createFieldsFromMetadata = ({
 | 
			
		||||
                    idx.columns[0].name === col.name
 | 
			
		||||
            ),
 | 
			
		||||
            nullable: Boolean(col.nullable),
 | 
			
		||||
            ...(col.character_maximum_length &&
 | 
			
		||||
            col.character_maximum_length !== 'null'
 | 
			
		||||
                ? { characterMaximumLength: col.character_maximum_length }
 | 
			
		||||
            ...(isArrayType ? { array: true } : {}),
 | 
			
		||||
            ...(charMaxLength && charMaxLength !== 'null'
 | 
			
		||||
                ? { characterMaximumLength: charMaxLength }
 | 
			
		||||
                : {}),
 | 
			
		||||
            ...(col.precision?.precision
 | 
			
		||||
                ? { precision: col.precision.precision }
 | 
			
		||||
                : {}),
 | 
			
		||||
            ...(col.precision?.scale ? { scale: col.precision.scale } : {}),
 | 
			
		||||
            ...(precision ? { precision } : {}),
 | 
			
		||||
            ...(scale ? { scale } : {}),
 | 
			
		||||
            ...(col.default ? { default: col.default } : {}),
 | 
			
		||||
            ...(col.collation ? { collation: col.collation } : {}),
 | 
			
		||||
            createdAt: Date.now(),
 | 
			
		||||
            comments: col.comment ? col.comment : undefined,
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const generateDBFieldSuffix = (
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types
 | 
			
		||||
import { generateId } from '../utils';
 | 
			
		||||
import type { DBField } from './db-field';
 | 
			
		||||
import { DatabaseType } from './database-type';
 | 
			
		||||
import type { DBTable } from './db-table';
 | 
			
		||||
 | 
			
		||||
export const INDEX_TYPES = [
 | 
			
		||||
    'btree',
 | 
			
		||||
@@ -29,6 +30,7 @@ export interface DBIndex {
 | 
			
		||||
    fieldIds: string[];
 | 
			
		||||
    createdAt: number;
 | 
			
		||||
    type?: IndexType | null;
 | 
			
		||||
    isPrimaryKey?: boolean | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
 | 
			
		||||
@@ -38,6 +40,7 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
 | 
			
		||||
    fieldIds: z.array(z.string()),
 | 
			
		||||
    createdAt: z.number(),
 | 
			
		||||
    type: z.enum(INDEX_TYPES).optional(),
 | 
			
		||||
    isPrimaryKey: z.boolean().or(z.null()).optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const createIndexesFromMetadata = ({
 | 
			
		||||
@@ -64,3 +67,51 @@ export const createIndexesFromMetadata = ({
 | 
			
		||||
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
 | 
			
		||||
    [DatabaseType.POSTGRESQL]: ['btree', 'hash'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getTablePrimaryKeyIndex = ({
 | 
			
		||||
    table,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
}): DBIndex | null => {
 | 
			
		||||
    const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
    const existingPKIndex = table.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
 | 
			
		||||
    if (primaryKeyFields.length === 0) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const pkFieldIds = primaryKeyFields.map((f) => f.id);
 | 
			
		||||
 | 
			
		||||
    if (existingPKIndex) {
 | 
			
		||||
        return {
 | 
			
		||||
            ...existingPKIndex,
 | 
			
		||||
            fieldIds: pkFieldIds,
 | 
			
		||||
        };
 | 
			
		||||
    } else {
 | 
			
		||||
        // Create new PK index for primary key(s)
 | 
			
		||||
        const pkIndex: DBIndex = {
 | 
			
		||||
            id: generateId(),
 | 
			
		||||
            name: `pk_${table.name}_${primaryKeyFields.map((f) => f.name).join('_')}`,
 | 
			
		||||
            fieldIds: pkFieldIds,
 | 
			
		||||
            unique: true,
 | 
			
		||||
            isPrimaryKey: true,
 | 
			
		||||
            createdAt: Date.now(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return pkIndex;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getTableIndexesWithPrimaryKey = ({
 | 
			
		||||
    table,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
}): DBIndex[] => {
 | 
			
		||||
    const primaryKeyIndex = getTablePrimaryKeyIndex({ table });
 | 
			
		||||
    const indexesWithoutPKIndex = table.indexes.filter(
 | 
			
		||||
        (idx) => !idx.isPrimaryKey
 | 
			
		||||
    );
 | 
			
		||||
    return primaryKeyIndex
 | 
			
		||||
        ? [primaryKeyIndex, ...indexesWithoutPKIndex]
 | 
			
		||||
        : indexesWithoutPKIndex;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -203,11 +203,57 @@ export const createTablesFromMetadata = ({
 | 
			
		||||
            tableSchema,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Check for composite primary key and find matching index name
 | 
			
		||||
        const primaryKeyFields = fields.filter((f) => f.primaryKey);
 | 
			
		||||
        let pkMatchingIndexName: string | undefined;
 | 
			
		||||
        let pkIndex: DBIndex | undefined;
 | 
			
		||||
 | 
			
		||||
        if (primaryKeyFields.length >= 1) {
 | 
			
		||||
            // We have a composite primary key, look for an index that matches all PK columns
 | 
			
		||||
            const pkFieldNames = primaryKeyFields.map((f) => f.name).sort();
 | 
			
		||||
 | 
			
		||||
            // Find an index that matches the primary key columns exactly
 | 
			
		||||
            const matchingIndex = aggregatedIndexes.find((index) => {
 | 
			
		||||
                const indexColumnNames = index.columns
 | 
			
		||||
                    .map((c) => c.name)
 | 
			
		||||
                    .sort();
 | 
			
		||||
                return (
 | 
			
		||||
                    indexColumnNames.length === pkFieldNames.length &&
 | 
			
		||||
                    indexColumnNames.every((col, i) => col === pkFieldNames[i])
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (matchingIndex) {
 | 
			
		||||
                pkMatchingIndexName = matchingIndex.name;
 | 
			
		||||
                // Create a special PK index
 | 
			
		||||
                pkIndex = {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: matchingIndex.name,
 | 
			
		||||
                    unique: true,
 | 
			
		||||
                    fieldIds: primaryKeyFields.map((f) => f.id),
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                    isPrimaryKey: true,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Filter out the index that matches the composite PK (to avoid duplication)
 | 
			
		||||
        const filteredAggregatedIndexes = pkMatchingIndexName
 | 
			
		||||
            ? aggregatedIndexes.filter(
 | 
			
		||||
                  (idx) => idx.name !== pkMatchingIndexName
 | 
			
		||||
              )
 | 
			
		||||
            : aggregatedIndexes;
 | 
			
		||||
 | 
			
		||||
        const dbIndexes = createIndexesFromMetadata({
 | 
			
		||||
            aggregatedIndexes,
 | 
			
		||||
            aggregatedIndexes: filteredAggregatedIndexes,
 | 
			
		||||
            fields,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add the PK index if it exists
 | 
			
		||||
        if (pkIndex) {
 | 
			
		||||
            dbIndexes.push(pkIndex);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Determine if the current table is a view by checking against pre-computed sets
 | 
			
		||||
        const viewKey = generateTableKey({
 | 
			
		||||
            schemaName: tableSchema,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    findDataTypeDataById,
 | 
			
		||||
    supportsAutoIncrementDataType,
 | 
			
		||||
    supportsArrayDataType,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    Popover,
 | 
			
		||||
@@ -87,6 +88,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
 | 
			
		||||
                unique: localField.unique,
 | 
			
		||||
                default: localField.default,
 | 
			
		||||
                increment: localField.increment,
 | 
			
		||||
                array: localField.array,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        prevFieldRef.current = localField;
 | 
			
		||||
@@ -102,6 +104,13 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
 | 
			
		||||
        [field.type.name]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const supportsArray = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            databaseType === 'postgresql' &&
 | 
			
		||||
            supportsArrayDataType(field.type.name),
 | 
			
		||||
        [field.type.name, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Popover
 | 
			
		||||
            open={isOpen}
 | 
			
		||||
@@ -168,6 +177,27 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                        {supportsArray ? (
 | 
			
		||||
                            <div className="flex items-center justify-between">
 | 
			
		||||
                                <Label
 | 
			
		||||
                                    htmlFor="array"
 | 
			
		||||
                                    className="text-subtitle"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        'side_panel.tables_section.table.field_actions.array'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                                <Checkbox
 | 
			
		||||
                                    checked={localField.array ?? false}
 | 
			
		||||
                                    onCheckedChange={(value) =>
 | 
			
		||||
                                        setLocalField((current) => ({
 | 
			
		||||
                                            ...current,
 | 
			
		||||
                                            array: !!value,
 | 
			
		||||
                                        }))
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                        <div className="flex flex-col gap-2">
 | 
			
		||||
                            <Label htmlFor="default" className="text-subtitle">
 | 
			
		||||
                                {t(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import type { DataTypeData } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    dataTypeDataToDataType,
 | 
			
		||||
    sortedDataTypeMap,
 | 
			
		||||
    supportsArrayDataType,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
@@ -175,6 +176,13 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if the new type supports arrays - if not, clear the array property
 | 
			
		||||
            const newTypeName = dataType?.name ?? (value as string);
 | 
			
		||||
            const shouldClearArray =
 | 
			
		||||
                databaseType === 'postgresql' &&
 | 
			
		||||
                !supportsArrayDataType(newTypeName) &&
 | 
			
		||||
                field.array;
 | 
			
		||||
 | 
			
		||||
            updateField({
 | 
			
		||||
                characterMaximumLength,
 | 
			
		||||
                precision,
 | 
			
		||||
@@ -185,6 +193,7 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                        name: value as string,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                ...(shouldClearArray ? { array: false } : {}),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
@@ -193,6 +202,7 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
            field.characterMaximumLength,
 | 
			
		||||
            field.precision,
 | 
			
		||||
            field.scale,
 | 
			
		||||
            field.array,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useCallback, useMemo } from 'react';
 | 
			
		||||
import { Ellipsis, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Ellipsis, Trash2, KeyRound } from 'lucide-react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
    databaseIndexTypes,
 | 
			
		||||
@@ -106,29 +106,45 @@ export const TableIndex: React.FC<TableIndexProps> = ({
 | 
			
		||||
                    'side_panel.tables_section.table.no_types_found'
 | 
			
		||||
                )}
 | 
			
		||||
                keepOrder
 | 
			
		||||
                disabled={index.isPrimaryKey ?? false}
 | 
			
		||||
            />
 | 
			
		||||
            <div className="flex shrink-0 gap-1">
 | 
			
		||||
                <Tooltip>
 | 
			
		||||
                    <TooltipTrigger asChild>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <TableIndexToggle
 | 
			
		||||
                                pressed={index.unique}
 | 
			
		||||
                                onPressedChange={(value) =>
 | 
			
		||||
                                    updateIndex({
 | 
			
		||||
                                        unique: !!value,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                U
 | 
			
		||||
                            </TableIndexToggle>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </TooltipTrigger>
 | 
			
		||||
                    <TooltipContent>
 | 
			
		||||
                        {t(
 | 
			
		||||
                            'side_panel.tables_section.table.index_actions.unique'
 | 
			
		||||
                        )}
 | 
			
		||||
                    </TooltipContent>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
                {index.isPrimaryKey ? (
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <TableIndexToggle pressed={true} disabled>
 | 
			
		||||
                                    <KeyRound className="h-3.5" />
 | 
			
		||||
                                </TableIndexToggle>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('side_panel.tables_section.table.primary_key')}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <TableIndexToggle
 | 
			
		||||
                                    pressed={index.unique}
 | 
			
		||||
                                    onPressedChange={(value) =>
 | 
			
		||||
                                        updateIndex({
 | 
			
		||||
                                            unique: !!value,
 | 
			
		||||
                                        })
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    U
 | 
			
		||||
                                </TableIndexToggle>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t(
 | 
			
		||||
                                'side_panel.tables_section.table.index_actions.unique'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                )}
 | 
			
		||||
                <Popover>
 | 
			
		||||
                    <PopoverTrigger asChild>
 | 
			
		||||
                        <Button
 | 
			
		||||
@@ -164,52 +180,56 @@ export const TableIndex: React.FC<TableIndexProps> = ({
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div className="mt-2 flex items-center justify-between">
 | 
			
		||||
                                <Label
 | 
			
		||||
                                    htmlFor="width"
 | 
			
		||||
                                    className="text-subtitle"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        'side_panel.tables_section.table.index_actions.unique'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                                <Checkbox
 | 
			
		||||
                                    checked={index.unique}
 | 
			
		||||
                                    onCheckedChange={(value) =>
 | 
			
		||||
                                        updateIndex({
 | 
			
		||||
                                            unique: !!value,
 | 
			
		||||
                                        })
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {indexTypeOptions.length > 0 ? (
 | 
			
		||||
                                <div className="mt-2 flex flex-col gap-2">
 | 
			
		||||
                                    <Label
 | 
			
		||||
                                        htmlFor="indexType"
 | 
			
		||||
                                        className="text-subtitle"
 | 
			
		||||
                            {!index.isPrimaryKey ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <div className="mt-2 flex items-center justify-between">
 | 
			
		||||
                                        <Label
 | 
			
		||||
                                            htmlFor="width"
 | 
			
		||||
                                            className="text-subtitle"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {t(
 | 
			
		||||
                                                'side_panel.tables_section.table.index_actions.unique'
 | 
			
		||||
                                            )}
 | 
			
		||||
                                        </Label>
 | 
			
		||||
                                        <Checkbox
 | 
			
		||||
                                            checked={index.unique}
 | 
			
		||||
                                            onCheckedChange={(value) =>
 | 
			
		||||
                                                updateIndex({
 | 
			
		||||
                                                    unique: !!value,
 | 
			
		||||
                                                })
 | 
			
		||||
                                            }
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    {indexTypeOptions.length > 0 ? (
 | 
			
		||||
                                        <div className="mt-2 flex flex-col gap-2">
 | 
			
		||||
                                            <Label
 | 
			
		||||
                                                htmlFor="indexType"
 | 
			
		||||
                                                className="text-subtitle"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {t(
 | 
			
		||||
                                                    'side_panel.tables_section.table.index_actions.index_type'
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </Label>
 | 
			
		||||
                                            <SelectBox
 | 
			
		||||
                                                options={indexTypeOptions}
 | 
			
		||||
                                                value={index.type || 'btree'}
 | 
			
		||||
                                                onChange={updateIndexType}
 | 
			
		||||
                                            />
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    ) : null}
 | 
			
		||||
                                    <Separator orientation="horizontal" />
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        className="flex gap-2 !text-red-700"
 | 
			
		||||
                                        onClick={removeIndex}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                                        {t(
 | 
			
		||||
                                            'side_panel.tables_section.table.index_actions.index_type'
 | 
			
		||||
                                            'side_panel.tables_section.table.index_actions.delete_index'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Label>
 | 
			
		||||
                                    <SelectBox
 | 
			
		||||
                                        options={indexTypeOptions}
 | 
			
		||||
                                        value={index.type || 'btree'}
 | 
			
		||||
                                        onChange={updateIndexType}
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                            <Separator orientation="horizontal" />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="outline"
 | 
			
		||||
                                className="flex gap-2 !text-red-700"
 | 
			
		||||
                                onClick={removeIndex}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'side_panel.tables_section.table.index_actions.delete_index'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </PopoverContent>
 | 
			
		||||
                </Popover>
 | 
			
		||||
 
 | 
			
		||||
@@ -224,19 +224,27 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </AccordionTrigger>
 | 
			
		||||
                    <AccordionContent className="pb-0 pt-1">
 | 
			
		||||
                        {table.indexes.map((index) => (
 | 
			
		||||
                            <TableIndex
 | 
			
		||||
                                key={index.id}
 | 
			
		||||
                                index={index}
 | 
			
		||||
                                removeIndex={() =>
 | 
			
		||||
                                    removeIndex(table.id, index.id)
 | 
			
		||||
                                }
 | 
			
		||||
                                updateIndex={(attrs: Partial<DBIndex>) =>
 | 
			
		||||
                                    updateIndex(table.id, index.id, attrs)
 | 
			
		||||
                                }
 | 
			
		||||
                                fields={table.fields}
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {[...table.indexes]
 | 
			
		||||
                            .sort((a, b) => {
 | 
			
		||||
                                // Sort PK indexes first
 | 
			
		||||
                                if (a.isPrimaryKey && !b.isPrimaryKey)
 | 
			
		||||
                                    return -1;
 | 
			
		||||
                                if (!a.isPrimaryKey && b.isPrimaryKey) return 1;
 | 
			
		||||
                                return 0;
 | 
			
		||||
                            })
 | 
			
		||||
                            .map((index) => (
 | 
			
		||||
                                <TableIndex
 | 
			
		||||
                                    key={index.id}
 | 
			
		||||
                                    index={index}
 | 
			
		||||
                                    removeIndex={() =>
 | 
			
		||||
                                        removeIndex(table.id, index.id)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    updateIndex={(attrs: Partial<DBIndex>) =>
 | 
			
		||||
                                        updateIndex(table.id, index.id, attrs)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    fields={table.fields}
 | 
			
		||||
                                />
 | 
			
		||||
                            ))}
 | 
			
		||||
                        <div className="flex justify-start p-1">
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user