fix: mark primary key columns as NOT NULL when importing DBML (#1006)

* fix: mark primary key columns as NOT NULL when importing DBML

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2025-12-11 16:44:57 +02:00
committed by GitHub
parent 9ac99d928c
commit e5ae46c3d7
4 changed files with 71 additions and 4 deletions

View File

@@ -1 +1 @@
{"id":"mqqwkkodxt6p","name":"Diagram 3","createdAt":"2025-09-16T15:33:25.300Z","updatedAt":"2025-09-16T15:33:31.563Z","databaseType":"postgresql","tables":[{"id":"loyxg6mafzos5u971uirjs3zh","name":"table_3","schema":"","order":0,"fields":[{"id":"29e2p9bom0uxo1n0a9ze5auuy","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":true,"primaryKey":true,"unique":true,"createdAt":1758036805300}],"indexes":[{"id":"5gf0aeptch1uk1bxv0x89wxxe","name":"pk_table_3_id","fieldIds":["29e2p9bom0uxo1n0a9ze5auuy"],"unique":true,"isPrimaryKey":true,"createdAt":1758036811564}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758036805300,"diagramId":"mqqwkkodxt6p"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}
{"id":"mqqwkkodxt6p","name":"Diagram 3","createdAt":"2025-09-16T15:33:25.300Z","updatedAt":"2025-09-16T15:33:31.563Z","databaseType":"postgresql","tables":[{"id":"loyxg6mafzos5u971uirjs3zh","name":"table_3","schema":"","order":0,"fields":[{"id":"29e2p9bom0uxo1n0a9ze5auuy","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758036805300}],"indexes":[{"id":"5gf0aeptch1uk1bxv0x89wxxe","name":"pk_table_3_id","fieldIds":["29e2p9bom0uxo1n0a9ze5auuy"],"unique":true,"isPrimaryKey":true,"createdAt":1758036811564}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758036805300,"diagramId":"mqqwkkodxt6p"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}

View File

@@ -1 +1 @@
{"id":"mqqwkkod6r09","name":"Diagram 10","createdAt":"2025-09-16T15:47:40.655Z","updatedAt":"2025-09-16T15:47:50.179Z","databaseType":"postgresql","tables":[{"id":"6xbco4ihmuiyv2heuw9fggbgx","name":"table_3","schema":"","order":0,"fields":[{"id":"rxftaey7uxvq5qg6ix1hbak1c","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":true,"primaryKey":true,"unique":true,"createdAt":1758037660654}],"indexes":[{"id":"vsyjjaq2l58urkh9qm2g9hqhd","name":"pk_table_3_id","fieldIds":["rxftaey7uxvq5qg6ix1hbak1c"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660654}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660654,"diagramId":"mqqwkkod6r09"},{"id":"klu6k5ntddcxfdsu0fsfcwbiw","name":"table_2","schema":"","order":1,"fields":[{"id":"qq2415tivmtvun8vd727d9mr2","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660655}],"indexes":[{"id":"cvv7sgmq07i9y54lz9a97nah5","name":"pk_table_2_id","fieldIds":["qq2415tivmtvun8vd727d9mr2"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660655}],"x":300,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"relationships":[{"id":"yw2pbcumsabuncc6rjnp3n87t","name":"table_3_id_table_2_id","sourceSchema":"","targetSchema":"","sourceTableId":"6xbco4ihmuiyv2heuw9fggbgx","targetTableId":"klu6k5ntddcxfdsu0fsfcwbiw","sourceFieldId":"rxftaey7uxvq5qg6ix1hbak1c","targetFieldId":"qq2415tivmtvun8vd727d9mr2","sourceCardinality":"one","targetCardinality":"one","createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"dependencies":[],"areas":[],"customTypes":[]}
{"id":"mqqwkkod6r09","name":"Diagram 10","createdAt":"2025-09-16T15:47:40.655Z","updatedAt":"2025-09-16T15:47:50.179Z","databaseType":"postgresql","tables":[{"id":"6xbco4ihmuiyv2heuw9fggbgx","name":"table_3","schema":"","order":0,"fields":[{"id":"rxftaey7uxvq5qg6ix1hbak1c","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660654}],"indexes":[{"id":"vsyjjaq2l58urkh9qm2g9hqhd","name":"pk_table_3_id","fieldIds":["rxftaey7uxvq5qg6ix1hbak1c"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660654}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660654,"diagramId":"mqqwkkod6r09"},{"id":"klu6k5ntddcxfdsu0fsfcwbiw","name":"table_2","schema":"","order":1,"fields":[{"id":"qq2415tivmtvun8vd727d9mr2","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660655}],"indexes":[{"id":"cvv7sgmq07i9y54lz9a97nah5","name":"pk_table_2_id","fieldIds":["qq2415tivmtvun8vd727d9mr2"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660655}],"x":300,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"relationships":[{"id":"yw2pbcumsabuncc6rjnp3n87t","name":"table_3_id_table_2_id","sourceSchema":"","targetSchema":"","sourceTableId":"6xbco4ihmuiyv2heuw9fggbgx","targetTableId":"klu6k5ntddcxfdsu0fsfcwbiw","sourceFieldId":"rxftaey7uxvq5qg6ix1hbak1c","targetFieldId":"qq2415tivmtvun8vd727d9mr2","sourceCardinality":"one","targetCardinality":"one","createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"dependencies":[],"areas":[],"customTypes":[]}

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Import - Primary Key NOT NULL', () => {
it('should mark primary key columns as NOT NULL', async () => {
const dbml = `
Table users {
id int [pk]
name varchar(100)
}`;
const diagram = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const usersTable = diagram.tables?.find((t) => t.name === 'users');
expect(usersTable).toBeDefined();
const idField = usersTable?.fields.find((f) => f.name === 'id');
expect(idField?.primaryKey).toBe(true);
expect(idField?.nullable).toBe(false);
// Non-PK field should remain nullable by default
const nameField = usersTable?.fields.find((f) => f.name === 'name');
expect(nameField?.primaryKey).toBeFalsy();
expect(nameField?.nullable).toBe(true);
});
it('should mark composite primary key columns as NOT NULL', async () => {
const dbml = `
Table order_items {
order_id int
product_id int
quantity int
indexes {
(order_id, product_id) [pk]
}
}`;
const diagram = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = diagram.tables?.find((t) => t.name === 'order_items');
expect(table).toBeDefined();
const orderIdField = table?.fields.find((f) => f.name === 'order_id');
expect(orderIdField?.primaryKey).toBe(true);
expect(orderIdField?.nullable).toBe(false);
const productIdField = table?.fields.find(
(f) => f.name === 'product_id'
);
expect(productIdField?.primaryKey).toBe(true);
expect(productIdField?.nullable).toBe(false);
// Non-PK field should remain nullable
const quantityField = table?.fields.find((f) => f.name === 'quantity');
expect(quantityField?.primaryKey).toBeFalsy();
expect(quantityField?.nullable).toBe(true);
});
});

View File

@@ -569,7 +569,9 @@ export const importDBMLToDiagram = async (
name: field.name.replace(/['"]/g, ''),
type: finalType,
nullable:
field.increment || requiresNotNull(field.type.type_name)
field.increment ||
field.pk ||
requiresNotNull(field.type.type_name)
? false
: !field.not_null,
primaryKey: field.pk || false,
@@ -600,13 +602,14 @@ export const importDBMLToDiagram = async (
if (dbmlIndex.name) {
compositePKIndexName = dbmlIndex.name;
}
// Mark fields as primary keys
// Mark fields as primary keys and NOT NULL
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;
field.nullable = false;
}
});
}