Files
chartdb/src/lib/dbml/dbml-import/__tests__/dbml-array-fields.test.ts
Jonathan Fishner 49328d8fbd fix: add support for arrays (#949)
* feat: add array field support with diff visualization

* some refactor

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-10-19 17:47:39 +03:00

318 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Array Fields - Fantasy RPG Theme', () => {
describe('Import - Spell and Magic Arrays', () => {
it('should import spell components as array fields', async () => {
const dbml = `
Table "magic"."spells" {
"id" uuid [pk, not null]
"name" varchar(200) [not null]
"level" integer [not null]
"components" text[] [note: 'Magical components: bat wing, dragon scale, phoenix feather']
"elemental_types" varchar(50)[] [note: 'Elements: fire, water, earth, air']
"mana_cost" integer [not null]
"created_at" timestamp [not null]
Indexes {
(name, level) [unique, name: "unique_spell"]
}
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(result.tables).toHaveLength(1);
const table = result.tables![0];
expect(table.name).toBe('spells');
expect(table.schema).toBe('magic');
// Find the array fields
const components = table.fields.find(
(f) => f.name === 'components'
);
const elementalTypes = table.fields.find(
(f) => f.name === 'elemental_types'
);
// Verify they are marked as arrays
expect(components).toBeDefined();
expect(components?.isArray).toBe(true);
expect(components?.type.name).toBe('text');
expect(elementalTypes).toBeDefined();
expect(elementalTypes?.isArray).toBe(true);
expect(elementalTypes?.type.name).toBe('varchar');
expect(elementalTypes?.characterMaximumLength).toBe('50');
// Verify non-array fields don't have isArray set
const idField = table.fields.find((f) => f.name === 'id');
expect(idField?.isArray).toBeUndefined();
});
it('should import hero inventory with various array types', async () => {
const dbml = `
Table "heroes" {
"id" bigint [pk]
"name" varchar(100) [not null]
"abilities" varchar(100)[]
"inventory_slots" integer[]
"skill_levels" decimal(5, 2)[]
"quest_log" text[]
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = result.tables![0];
const abilities = table.fields.find((f) => f.name === 'abilities');
expect(abilities?.isArray).toBe(true);
expect(abilities?.type.name).toBe('varchar');
expect(abilities?.characterMaximumLength).toBe('100');
const inventorySlots = table.fields.find(
(f) => f.name === 'inventory_slots'
);
expect(inventorySlots?.isArray).toBe(true);
expect(inventorySlots?.type.name).toBe('integer');
const skillLevels = table.fields.find(
(f) => f.name === 'skill_levels'
);
expect(skillLevels?.isArray).toBe(true);
expect(skillLevels?.type.name).toBe('decimal');
expect(skillLevels?.precision).toBe(5);
expect(skillLevels?.scale).toBe(2);
const questLog = table.fields.find((f) => f.name === 'quest_log');
expect(questLog?.isArray).toBe(true);
expect(questLog?.type.name).toBe('text');
});
it('should handle mixed array and non-array fields in creature table', async () => {
const dbml = `
Table "bestiary"."creatures" {
"id" uuid [pk]
"species_name" varchar(100) [not null]
"habitats" varchar(50)[]
"danger_level" integer [not null]
"resistances" varchar(50)[]
"is_tameable" boolean [not null]
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = result.tables![0];
// Non-array fields
const id = table.fields.find((f) => f.name === 'id');
expect(id?.isArray).toBeUndefined();
const speciesName = table.fields.find(
(f) => f.name === 'species_name'
);
expect(speciesName?.isArray).toBeUndefined();
const dangerLevel = table.fields.find(
(f) => f.name === 'danger_level'
);
expect(dangerLevel?.isArray).toBeUndefined();
// Array fields
const habitats = table.fields.find((f) => f.name === 'habitats');
expect(habitats?.isArray).toBe(true);
const resistances = table.fields.find(
(f) => f.name === 'resistances'
);
expect(resistances?.isArray).toBe(true);
});
});
describe('Round-trip - Quest and Adventure Arrays', () => {
it('should preserve quest rewards array through export and re-import', async () => {
const originalDbml = `
Table "adventures"."quests" {
"id" uuid [pk, not null]
"title" varchar(200) [not null]
"difficulty" varchar(20) [not null]
"reward_items" text[] [note: 'Legendary sword, enchanted armor, healing potion']
"required_skills" varchar(100)[]
"experience_points" integer [not null]
"gold_reward" decimal(10, 2) [not null]
"created_at" timestamp [not null]
Indexes {
(title, difficulty) [unique, name: "unique_quest"]
}
}
`;
// Import the DBML
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify array fields were imported correctly
const table = diagram.tables![0];
const rewardItems = table.fields.find(
(f) => f.name === 'reward_items'
);
const requiredSkills = table.fields.find(
(f) => f.name === 'required_skills'
);
expect(rewardItems?.isArray).toBe(true);
expect(requiredSkills?.isArray).toBe(true);
// Export back to DBML
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
// Verify the exported DBML contains array syntax
expect(exportedDbml).toContain('text[]');
expect(exportedDbml).toContain('"reward_items" text[]');
expect(exportedDbml).toContain('"required_skills" varchar(100)[]');
// Re-import the exported DBML
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify array fields are still marked as arrays
const reimportedTable = reimportedDiagram.tables![0];
const reimportedRewards = reimportedTable.fields.find(
(f) => f.name === 'reward_items'
);
const reimportedSkills = reimportedTable.fields.find(
(f) => f.name === 'required_skills'
);
expect(reimportedRewards?.isArray).toBe(true);
expect(reimportedSkills?.isArray).toBe(true);
});
it('should handle guild members with different array types in round-trip', async () => {
const originalDbml = `
Table "guilds"."members" {
"id" uuid [pk]
"name" varchar(100) [not null]
"class_specializations" varchar(50)[]
"completed_quest_ids" integer[]
"skill_ratings" decimal(3, 1)[]
"titles_earned" text[]
}
`;
// Import
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Export
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
// Verify exported DBML has correct array syntax with types
expect(exportedDbml).toContain('varchar(50)[]');
expect(exportedDbml).toContain('integer[]');
expect(exportedDbml).toContain('decimal(3,1)[]');
expect(exportedDbml).toContain('text[]');
// Re-import
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = reimportedDiagram.tables![0];
const classSpecs = table.fields.find(
(f) => f.name === 'class_specializations'
);
expect(classSpecs?.isArray).toBe(true);
expect(classSpecs?.characterMaximumLength).toBe('50');
const questIds = table.fields.find(
(f) => f.name === 'completed_quest_ids'
);
expect(questIds?.isArray).toBe(true);
const skillRatings = table.fields.find(
(f) => f.name === 'skill_ratings'
);
expect(skillRatings?.isArray).toBe(true);
expect(skillRatings?.precision).toBe(3);
expect(skillRatings?.scale).toBe(1);
const titles = table.fields.find((f) => f.name === 'titles_earned');
expect(titles?.isArray).toBe(true);
});
it('should preserve dungeon loot tables with mixed array and non-array fields', async () => {
const originalDbml = `
Table "dungeons"."loot_tables" {
"id" bigint [pk]
"dungeon_name" varchar(150) [not null]
"boss_name" varchar(100)
"common_drops" text[]
"rare_drops" text[]
"legendary_drops" text[]
"gold_range_min" integer [not null]
"gold_range_max" integer [not null]
"drop_rates" decimal(5, 2)[]
}
`;
// Import, export, and re-import
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = reimportedDiagram.tables![0];
// Verify non-array fields
expect(
table.fields.find((f) => f.name === 'id')?.isArray
).toBeUndefined();
expect(
table.fields.find((f) => f.name === 'dungeon_name')?.isArray
).toBeUndefined();
expect(
table.fields.find((f) => f.name === 'gold_range_min')?.isArray
).toBeUndefined();
// Verify array fields
expect(
table.fields.find((f) => f.name === 'common_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'rare_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'legendary_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'drop_rates')?.isArray
).toBe(true);
});
});
});