From d3205f778b9ed78d60e9a90dcb4f41b952f2f66b Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Tue, 16 Dec 2025 14:29:58 +0200 Subject: [PATCH] fix(dbml-import): use defaultSchemas instead of hardcoded schema values (#1012) --- .../dbml-import-fantasy-examples.test.ts | 18 +++--- .../dbml-import/__tests__/dbml-import.test.ts | 58 +++++++++++++++++++ .../__tests__/dbml-schema-handling.test.ts | 18 +++--- src/lib/dbml/dbml-import/dbml-import.ts | 33 ++++++++--- src/lib/utils/utils.ts | 4 ++ 5 files changed, 104 insertions(+), 27 deletions(-) diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts index 929afa27..863cd048 100644 --- a/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts +++ b/src/lib/dbml/dbml-import/__tests__/dbml-import-fantasy-examples.test.ts @@ -672,9 +672,9 @@ Table projects { expect(diagram.customTypes).toBeDefined(); expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade - // Check job_status enum + // Check job_status enum (PostgreSQL default schema is 'public') const jobStatusEnum = diagram.customTypes?.find( - (ct) => ct.name === 'job_status' && !ct.schema + (ct) => ct.name === 'job_status' && ct.schema === 'public' ); expect(jobStatusEnum).toBeDefined(); expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum); @@ -698,9 +698,9 @@ Table projects { 'intern', ]); - // Check grade enum with quoted values + // Check grade enum with quoted values (PostgreSQL default schema is 'public') const gradeEnum = diagram.customTypes?.find( - (ct) => ct.name === 'grade' && !ct.schema + (ct) => ct.name === 'grade' && ct.schema === 'public' ); expect(gradeEnum).toBeDefined(); expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum); @@ -806,9 +806,9 @@ Table admin.users { // Verify both enums are created expect(diagram.customTypes).toHaveLength(2); - // Check public.status enum + // Check public.status enum (PostgreSQL default schema is 'public') const publicStatusEnum = diagram.customTypes?.find( - (ct) => ct.name === 'status' && !ct.schema + (ct) => ct.name === 'status' && ct.schema === 'public' ); expect(publicStatusEnum).toBeDefined(); expect(publicStatusEnum?.values).toEqual([ @@ -830,9 +830,9 @@ Table admin.users { ]); // Verify fields reference correct enums - // Note: 'public' schema is converted to empty string + // Note: 'public' schema is the default for PostgreSQL const publicUsersTable = diagram.tables?.find( - (t) => t.name === 'users' && t.schema === '' + (t) => t.name === 'users' && t.schema === 'public' ); const adminUsersTable = diagram.tables?.find( (t) => t.name === 'users' && t.schema === 'admin' @@ -1103,7 +1103,7 @@ Table "public_3"."comments" { // Note: 'public' schema is converted to empty string const usersTable = diagram.tables?.find( - (t) => t.name === 'users' && t.schema === '' + (t) => t.name === 'users' && t.schema === 'public' ); const postsTable = diagram.tables?.find( (t) => t.name === 'posts' && t.schema === 'public_2' diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts index 5b621064..220505b2 100644 --- a/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts +++ b/src/lib/dbml/dbml-import/__tests__/dbml-import.test.ts @@ -242,4 +242,62 @@ Note note_1750185617764 { getPreferredSynonymSpy.mockRestore(); }); }); + + describe('Schema Handling with defaultSchemas', () => { + it('should use defaultSchema when table schema is empty for PostgreSQL', async () => { + const dbml = ` + Table users { + id int [pk] + } + `; + + const diagram = await importDBMLToDiagram(dbml, { + databaseType: DatabaseType.POSTGRESQL, + }); + + expect(diagram.tables?.[0]?.schema).toBe('public'); + }); + + it('should use defaultSchema when table schema is empty for SQL Server', async () => { + const dbml = ` + Table users { + id int [pk] + } + `; + + const diagram = await importDBMLToDiagram(dbml, { + databaseType: DatabaseType.SQL_SERVER, + }); + + expect(diagram.tables?.[0]?.schema).toBe('dbo'); + }); + + it('should have undefined schema for database types without defaultSchema', async () => { + const dbml = ` + Table users { + id int [pk] + } + `; + + const diagram = await importDBMLToDiagram(dbml, { + databaseType: DatabaseType.SQLITE, + }); + + expect(diagram.tables?.[0]?.schema).toBeUndefined(); + }); + + it('should preserve explicit schema even when different from default', async () => { + const dbml = ` + Table "custom_schema"."users" { + id int [pk] + } + `; + + const diagram = await importDBMLToDiagram(dbml, { + databaseType: DatabaseType.POSTGRESQL, + }); + + expect(diagram.tables?.[0]?.schema).toBe('custom_schema'); + }); + }); }); diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts index 5dcb06b0..2c8e8b19 100644 --- a/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts +++ b/src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts @@ -39,10 +39,10 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => { databaseType: DatabaseType.MYSQL, }); - // Verify no 'public' schema was added + // Verify schema is undefined for MySQL (no default schema) expect(diagram.tables).toBeDefined(); diagram.tables?.forEach((table) => { - expect(table.schema).toBe(''); + expect(table.schema).toBeUndefined(); }); // Check specific tables @@ -50,7 +50,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => { (t) => t.name === 'wizards' ); expect(wizardsTable).toBeDefined(); - expect(wizardsTable?.schema).toBe(''); + expect(wizardsTable?.schema).toBeUndefined(); // Check that reserved keywords are preserved as field names const yesField = wizardsTable?.fields.find((f) => f.name === 'Yes'); @@ -162,7 +162,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => { const heroesTable = diagram.tables?.find( (t) => t.name === 'heroes' ); - expect(heroesTable?.schema).toBe(''); // 'public' should be converted to empty + expect(heroesTable?.schema).toBe('public'); // PostgreSQL default schema const secretQuestsTable = diagram.tables?.find( (t) => t.name === 'secret_quests' @@ -172,7 +172,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => { const artifactsTable = diagram.tables?.find( (t) => t.name === 'artifacts' ); - expect(artifactsTable?.schema).toBe(''); // No schema = empty string + expect(artifactsTable?.schema).toBe('public'); // No schema = default schema }); it('should handle reserved keywords for PostgreSQL', async () => { @@ -222,18 +222,18 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => { } ); - // For MySQL, 'public' schema should be stripped + // For MySQL, 'public' schema should become undefined (no default schema) mysqlDiagram.tables?.forEach((table) => { - expect(table.schema).toBe(''); + expect(table.schema).toBeUndefined(); }); - // Now test with PostgreSQL - public should also be stripped (it's the default) + // For PostgreSQL, 'public' is the default schema const pgDiagram = await importDBMLToDiagram(dbmlWithPublicSchema, { databaseType: DatabaseType.POSTGRESQL, }); pgDiagram.tables?.forEach((table) => { - expect(table.schema).toBe(''); + expect(table.schema).toBe('public'); }); }); diff --git a/src/lib/dbml/dbml-import/dbml-import.ts b/src/lib/dbml/dbml-import/dbml-import.ts index 3890ea22..ee6f2b6b 100644 --- a/src/lib/dbml/dbml-import/dbml-import.ts +++ b/src/lib/dbml/dbml-import/dbml-import.ts @@ -1,7 +1,8 @@ import { Parser } from '@dbml/core'; import type { Diagram } from '@/lib/domain/diagram'; -import { generateDiagramId, generateId } from '@/lib/utils'; +import { generateDiagramId, generateId, isStringEmpty } from '@/lib/utils'; import type { DBTable } from '@/lib/domain/db-table'; +import { defaultSchemas } from '@/lib/data/default-schemas'; import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship'; import type { DBField } from '@/lib/domain/db-field'; import type { DataTypeData } from '@/lib/data/data-types/data-types'; @@ -502,14 +503,22 @@ export const importDBMLToDiagram = async ( if (schema.enums) { schema.enums.forEach((enumDef) => { // Get schema name from enum or use schema's name - const enumSchema = + // DBML parser uses 'public' as its default - treat it as empty + const rawEnumSchema = typeof enumDef.schema === 'string' ? enumDef.schema : enumDef.schema?.name || schema.name; + const defaultSchema = defaultSchemas[options.databaseType]; + const isEnumSchemaEmpty = + isStringEmpty(rawEnumSchema) || + rawEnumSchema === 'public'; + const enumSchema = isEnumSchemaEmpty + ? defaultSchema + : rawEnumSchema; allEnums.push({ name: enumDef.name, - schema: enumSchema === 'public' ? '' : enumSchema, + schema: enumSchema, values: enumDef.values || [], note: enumDef.note, }); @@ -722,15 +731,21 @@ export const importDBMLToDiagram = async ( } } + // Get raw schema from DBML, then apply defaultSchema if empty + // DBML parser uses 'public' as its default - treat it as empty + const defaultSchema = defaultSchemas[options.databaseType]; + const rawSchema = + typeof table.schema === 'string' + ? table.schema + : table.schema?.name; + const isSchemaEmpty = + isStringEmpty(rawSchema) || rawSchema === 'public'; + const tableSchema = isSchemaEmpty ? defaultSchema : rawSchema; + const tableToReturn: DBTable = { id: generateId(), name: table.name.replace(/['"]/g, ''), - schema: - typeof table.schema === 'string' - ? table.schema === 'public' - ? '' - : table.schema - : table.schema?.name || '', + schema: tableSchema, order: index, fields, indexes, diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts index 229e51bc..d5701290 100644 --- a/src/lib/utils/utils.ts +++ b/src/lib/utils/utils.ts @@ -123,3 +123,7 @@ export function mergeRefs( } }; } + +export const isStringEmpty = (str: string | undefined | null): boolean => { + return !str || str.trim().length === 0; +};