fix(dbml-import): use defaultSchemas instead of hardcoded schema values (#1012)

This commit is contained in:
Guy Ben-Aharon
2025-12-16 14:29:58 +02:00
committed by GitHub
parent a237b525c3
commit d3205f778b
5 changed files with 104 additions and 27 deletions

View File

@@ -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'

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});

View File

@@ -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,

View File

@@ -123,3 +123,7 @@ export function mergeRefs<T>(
}
};
}
export const isStringEmpty = (str: string | undefined | null): boolean => {
return !str || str.trim().length === 0;
};