fix: treat PostgreSQL decimal as synonym for numeric (#1020)

* fix: treat PostgreSQL decimal as synonym for numeric

* fix
This commit is contained in:
Guy Ben-Aharon
2025-12-17 22:19:27 +02:00
committed by GitHub
parent e977ba5ef2
commit 4098524b90
5 changed files with 64 additions and 50 deletions

View File

@@ -21,23 +21,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
// { name: 'int8', id: 'int8', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
// {
// name: 'decimal',
// id: 'decimal',
// usageLevel: 2,
// fieldAttributes: {
// precision: {
// max: 131072,
// min: 0,
// default: 10,
// },
// scale: {
// max: 16383,
// min: 0,
// default: 2,
// },
// },
// },
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
@@ -53,6 +53,7 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{
name: 'numeric',
id: 'numeric',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
@@ -171,6 +172,9 @@ const synonymMap: Record<string, string> = {
// Bit types
'bit varying': 'varbit',
// Numeric types
decimal: 'numeric',
} as const;
/**

View File

@@ -142,7 +142,7 @@ describe('SQL Export - Array Fields (Fantasy RPG Theme)', () => {
{
id: generateId(),
name: 'skill_levels',
type: { id: 'decimal', name: 'decimal' },
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
unique: false,
nullable: true,
@@ -176,7 +176,7 @@ describe('SQL Export - Array Fields (Fantasy RPG Theme)', () => {
expect(sql).toContain('CREATE TABLE "game"."heroes"');
expect(sql).toContain('"abilities" varchar(100)[]');
expect(sql).toContain('"inventory_slots" integer[]');
expect(sql).toContain('"skill_levels" decimal(5, 2)[]');
expect(sql).toContain('"skill_levels" numeric(5, 2)[]');
});
it('should export non-array fields normally when isArray is false or undefined', () => {

View File

@@ -4,9 +4,12 @@ import { importDBMLToDiagram } from '../../dbml-import/dbml-import';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Self-Referencing Relationships', () => {
it('should preserve self-referencing relationships in DBML export', async () => {
// Create a DBML with self-referencing relationship (general_ledger example)
const inputDBML = `
it(
'should preserve self-referencing relationships in DBML export',
{ timeout: 30000 },
async () => {
// Create a DBML with self-referencing relationship (general_ledger example)
const inputDBML = `
Table "finance"."general_ledger" {
"ledger_id" bigint [pk]
"account_name" varchar(100)
@@ -16,35 +19,36 @@ Table "finance"."general_ledger" {
}
`;
// Import the DBML
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Import the DBML
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify the relationship was imported
expect(diagram.relationships).toBeDefined();
expect(diagram.relationships?.length).toBe(1);
// Verify the relationship was imported
expect(diagram.relationships).toBeDefined();
expect(diagram.relationships?.length).toBe(1);
// Verify it's a self-referencing relationship
const relationship = diagram.relationships![0];
expect(relationship.sourceTableId).toBe(relationship.targetTableId);
// Verify it's a self-referencing relationship
const relationship = diagram.relationships![0];
expect(relationship.sourceTableId).toBe(relationship.targetTableId);
// Export back to DBML
const exportResult = generateDBMLFromDiagram(diagram);
// Export back to DBML
const exportResult = generateDBMLFromDiagram(diagram);
// Check inline format
expect(exportResult.inlineDbml).toContain('reversal_id');
// The DBML parser correctly interprets FK as: target < source
expect(exportResult.inlineDbml).toMatch(
/ref:\s*<\s*"finance"\."general_ledger"\."ledger_id"/
);
// Check inline format
expect(exportResult.inlineDbml).toContain('reversal_id');
// The DBML parser correctly interprets FK as: target < source
expect(exportResult.inlineDbml).toMatch(
/ref:\s*<\s*"finance"\."general_ledger"\."ledger_id"/
);
// Check standard format
expect(exportResult.standardDbml).toContain('Ref ');
expect(exportResult.standardDbml).toMatch(
/"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
);
});
// Check standard format
expect(exportResult.standardDbml).toContain('Ref ');
expect(exportResult.standardDbml).toMatch(
/"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
);
}
);
it('should handle self-referencing relationships in employee hierarchy', async () => {
// Create an employee table with manager relationship

View File

@@ -88,7 +88,7 @@ Table "heroes" {
(f) => f.name === 'skill_levels'
);
expect(skillLevels?.isArray).toBe(true);
expect(skillLevels?.type.name).toBe('decimal');
expect(skillLevels?.type.name).toBe('numeric');
expect(skillLevels?.precision).toBe(5);
expect(skillLevels?.scale).toBe(2);
@@ -227,7 +227,7 @@ Table "guilds"."members" {
// Verify exported DBML has correct array syntax with types
expect(exportedDbml).toContain('varchar(50)[]');
expect(exportedDbml).toContain('int[]');
expect(exportedDbml).toContain('decimal(3,1)[]');
expect(exportedDbml).toContain('numeric(3,1)[]');
expect(exportedDbml).toContain('text[]');
// Re-import

View File

@@ -324,6 +324,12 @@ export const importDBMLToDiagram = async (
enums,
});
// Also check the preferred synonym for field attributes (e.g., decimal → numeric)
const preferredType = options.databaseType
? getPreferredSynonym(dataType.name, options.databaseType)
: null;
const effectiveType = preferredType ?? dataType;
// Check if this is a character type that should have a max length
const baseTypeName = typeName
.replace(/\(.*\)/, '')
@@ -340,8 +346,8 @@ export const importDBMLToDiagram = async (
characterMaximumLength: args[0],
};
} else if (
dataType.fieldAttributes?.precision &&
dataType.fieldAttributes?.scale
effectiveType.fieldAttributes?.precision &&
effectiveType.fieldAttributes?.scale
) {
const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;