mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-13 18:35:52 +00:00
437 lines
16 KiB
TypeScript
437 lines
16 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { importDBMLToDiagram } from '../dbml-import';
|
|
import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
|
|
import { applyDBMLChanges } from '../../apply-dbml/apply-dbml';
|
|
import { DatabaseType } from '@/lib/domain/database-type';
|
|
import type { Diagram } from '@/lib/domain/diagram';
|
|
|
|
describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
|
describe('MySQL - No Schema Support', () => {
|
|
it('should not add public schema for MySQL databases', async () => {
|
|
// Fantasy realm DBML with tables that would typically get 'public' schema
|
|
const dbmlContent = `
|
|
Table "wizards" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"magic_level" int
|
|
"Yes" varchar(10) // Reserved DBML keyword
|
|
"No" varchar(10) // Reserved DBML keyword
|
|
}
|
|
|
|
Table "dragons" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"treasure_count" int
|
|
"is_friendly" boolean
|
|
}
|
|
|
|
Table "spells" {
|
|
"id" bigint [pk]
|
|
"spell_name" varchar(200)
|
|
"wizard_id" bigint
|
|
"power_level" int
|
|
}
|
|
|
|
Ref: "spells"."wizard_id" > "wizards"."id"
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
// Verify no 'public' schema was added
|
|
expect(diagram.tables).toBeDefined();
|
|
diagram.tables?.forEach((table) => {
|
|
expect(table.schema).toBe('');
|
|
console.log(
|
|
`✓ Table "${table.name}" has no schema (MySQL behavior)`
|
|
);
|
|
});
|
|
|
|
// Check specific tables
|
|
const wizardsTable = diagram.tables?.find(
|
|
(t) => t.name === 'wizards'
|
|
);
|
|
expect(wizardsTable).toBeDefined();
|
|
expect(wizardsTable?.schema).toBe('');
|
|
|
|
// Check that reserved keywords are preserved as field names
|
|
const yesField = wizardsTable?.fields.find((f) => f.name === 'Yes');
|
|
const noField = wizardsTable?.fields.find((f) => f.name === 'No');
|
|
expect(yesField).toBeDefined();
|
|
expect(noField).toBeDefined();
|
|
});
|
|
|
|
it('should preserve IDs when re-importing DBML (no false changes)', async () => {
|
|
// Create initial diagram
|
|
const initialDBML = `
|
|
Table "kingdoms" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"ruler" varchar(100)
|
|
"Yes" varchar(10) // Acceptance status
|
|
"No" varchar(10) // Rejection status
|
|
}
|
|
|
|
Table "knights" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"kingdom_id" bigint
|
|
"honor_points" int
|
|
}
|
|
|
|
Ref: "knights"."kingdom_id" > "kingdoms"."id"
|
|
`;
|
|
|
|
// Import initial DBML
|
|
const sourceDiagram = await importDBMLToDiagram(initialDBML, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
// Export to DBML
|
|
const exported = generateDBMLFromDiagram(sourceDiagram);
|
|
|
|
// Re-import the exported DBML (simulating edit mode)
|
|
const reimportedDiagram = await importDBMLToDiagram(
|
|
exported.inlineDbml,
|
|
{
|
|
databaseType: DatabaseType.MYSQL,
|
|
}
|
|
);
|
|
|
|
// Apply DBML changes (should preserve IDs)
|
|
const targetDiagram: Diagram = {
|
|
...sourceDiagram,
|
|
tables: reimportedDiagram.tables,
|
|
relationships: reimportedDiagram.relationships,
|
|
customTypes: reimportedDiagram.customTypes,
|
|
};
|
|
|
|
const resultDiagram = applyDBMLChanges({
|
|
sourceDiagram,
|
|
targetDiagram,
|
|
});
|
|
|
|
// Verify IDs are preserved
|
|
expect(resultDiagram.tables?.length).toBe(
|
|
sourceDiagram.tables?.length
|
|
);
|
|
|
|
sourceDiagram.tables?.forEach((sourceTable, idx) => {
|
|
const resultTable = resultDiagram.tables?.[idx];
|
|
expect(resultTable?.id).toBe(sourceTable.id);
|
|
expect(resultTable?.name).toBe(sourceTable.name);
|
|
|
|
// Check field IDs are preserved
|
|
sourceTable.fields.forEach((sourceField, fieldIdx) => {
|
|
const resultField = resultTable?.fields[fieldIdx];
|
|
expect(resultField?.id).toBe(sourceField.id);
|
|
expect(resultField?.name).toBe(sourceField.name);
|
|
});
|
|
});
|
|
|
|
console.log('✓ All IDs preserved after DBML round-trip');
|
|
});
|
|
});
|
|
|
|
describe('PostgreSQL - Schema Support', () => {
|
|
it('should handle schemas correctly for PostgreSQL', async () => {
|
|
// Fantasy realm with multiple schemas
|
|
const dbmlContent = `
|
|
Table "public"."heroes" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"class" varchar(50)
|
|
}
|
|
|
|
Table "private"."secret_quests" {
|
|
"id" bigint [pk]
|
|
"quest_name" varchar(200)
|
|
"hero_id" bigint
|
|
}
|
|
|
|
Table "artifacts" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"power" int
|
|
}
|
|
|
|
Ref: "private"."secret_quests"."hero_id" > "public"."heroes"."id"
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
|
databaseType: DatabaseType.POSTGRESQL,
|
|
});
|
|
|
|
// Check schemas are preserved correctly
|
|
const heroesTable = diagram.tables?.find(
|
|
(t) => t.name === 'heroes'
|
|
);
|
|
expect(heroesTable?.schema).toBe(''); // 'public' should be converted to empty
|
|
|
|
const secretQuestsTable = diagram.tables?.find(
|
|
(t) => t.name === 'secret_quests'
|
|
);
|
|
expect(secretQuestsTable?.schema).toBe('private'); // Other schemas preserved
|
|
|
|
const artifactsTable = diagram.tables?.find(
|
|
(t) => t.name === 'artifacts'
|
|
);
|
|
expect(artifactsTable?.schema).toBe(''); // No schema = empty string
|
|
});
|
|
|
|
it('should handle reserved keywords for PostgreSQL', async () => {
|
|
const dbmlContent = `
|
|
Table "magic_items" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"Order" int // SQL keyword
|
|
"Yes" varchar(10) // DBML keyword
|
|
"No" varchar(10) // DBML keyword
|
|
}
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
|
databaseType: DatabaseType.POSTGRESQL,
|
|
});
|
|
|
|
const exported = generateDBMLFromDiagram(diagram);
|
|
|
|
expect(exported.standardDbml).toContain('Order');
|
|
expect(exported.standardDbml).toContain('Yes');
|
|
expect(exported.standardDbml).toContain('No');
|
|
});
|
|
});
|
|
|
|
describe('Public Schema Handling - The Core Fix', () => {
|
|
it('should strip public schema for MySQL to prevent ID mismatch', async () => {
|
|
// This test verifies the core fix - that 'public' schema is converted to empty string
|
|
const dbmlWithPublicSchema = `
|
|
Table "public"."enchanted_items" {
|
|
"id" bigint [pk]
|
|
"item_name" varchar(100)
|
|
"power" int
|
|
}
|
|
|
|
Table "public"."spell_books" {
|
|
"id" bigint [pk]
|
|
"title" varchar(200)
|
|
"author" varchar(100)
|
|
}
|
|
`;
|
|
|
|
const mysqlDiagram = await importDBMLToDiagram(
|
|
dbmlWithPublicSchema,
|
|
{
|
|
databaseType: DatabaseType.MYSQL,
|
|
}
|
|
);
|
|
|
|
// For MySQL, 'public' schema should be stripped
|
|
mysqlDiagram.tables?.forEach((table) => {
|
|
expect(table.schema).toBe('');
|
|
console.log(
|
|
`✓ MySQL: Table "${table.name}" has no schema (public was stripped)`
|
|
);
|
|
});
|
|
|
|
// Now test with PostgreSQL - public should also be stripped (it's the default)
|
|
const pgDiagram = await importDBMLToDiagram(dbmlWithPublicSchema, {
|
|
databaseType: DatabaseType.POSTGRESQL,
|
|
});
|
|
|
|
pgDiagram.tables?.forEach((table) => {
|
|
expect(table.schema).toBe('');
|
|
console.log(
|
|
`✓ PostgreSQL: Table "${table.name}" has no schema (public is default)`
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should preserve non-public schemas', async () => {
|
|
const dbmlWithCustomSchema = `
|
|
Table "fantasy"."magic_users" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"class" varchar(50)
|
|
}
|
|
|
|
Table "adventure"."quests" {
|
|
"id" bigint [pk]
|
|
"title" varchar(200)
|
|
"reward" int
|
|
}
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlWithCustomSchema, {
|
|
databaseType: DatabaseType.POSTGRESQL,
|
|
});
|
|
|
|
// Non-public schemas should be preserved
|
|
const magicTable = diagram.tables?.find(
|
|
(t) => t.name === 'magic_users'
|
|
);
|
|
const questTable = diagram.tables?.find((t) => t.name === 'quests');
|
|
|
|
expect(magicTable?.schema).toBe('fantasy');
|
|
expect(questTable?.schema).toBe('adventure');
|
|
console.log('✓ Custom schemas preserved correctly');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases - The Dungeon of Bugs', () => {
|
|
it('should handle tables with names that need quoting', async () => {
|
|
const dbmlContent = `
|
|
Table "dragons_lair" {
|
|
"id" bigint [pk]
|
|
"treasure_amount" decimal
|
|
}
|
|
|
|
Table "wizard_tower" {
|
|
"id" bigint [pk]
|
|
"floor_count" int
|
|
}
|
|
|
|
Table "quest_log" {
|
|
"id" bigint [pk]
|
|
"quest_name" varchar(200)
|
|
}
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
// Tables should be imported correctly
|
|
expect(diagram.tables?.length).toBe(3);
|
|
expect(
|
|
diagram.tables?.find((t) => t.name === 'dragons_lair')
|
|
).toBeDefined();
|
|
expect(
|
|
diagram.tables?.find((t) => t.name === 'wizard_tower')
|
|
).toBeDefined();
|
|
expect(
|
|
diagram.tables?.find((t) => t.name === 'quest_log')
|
|
).toBeDefined();
|
|
});
|
|
|
|
it('should handle the Work_Order_Page_Debug case with Yes/No fields', async () => {
|
|
// This is the exact case that was causing the original bug
|
|
const dbmlContent = `
|
|
Table "Work_Order_Page_Debug" {
|
|
"ID" bigint [pk, not null]
|
|
"Work_Order_For" varchar(255)
|
|
"Quan_to_Make" int
|
|
"Text_Gen" text
|
|
"Gen_Info" text
|
|
"Yes" varchar(255)
|
|
"No" varchar(255)
|
|
}
|
|
`;
|
|
|
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
const table = diagram.tables?.find(
|
|
(t) => t.name === 'Work_Order_Page_Debug'
|
|
);
|
|
expect(table).toBeDefined();
|
|
|
|
// Check Yes and No fields are preserved
|
|
const yesField = table?.fields.find((f) => f.name === 'Yes');
|
|
const noField = table?.fields.find((f) => f.name === 'No');
|
|
|
|
expect(yesField).toBeDefined();
|
|
expect(noField).toBeDefined();
|
|
expect(yesField?.name).toBe('Yes');
|
|
expect(noField?.name).toBe('No');
|
|
|
|
// Export and verify it doesn't cause errors
|
|
const exported = generateDBMLFromDiagram(diagram);
|
|
expect(exported.standardDbml).toContain('"Yes"');
|
|
expect(exported.standardDbml).toContain('"No"');
|
|
|
|
// Re-import should work without errors
|
|
const reimported = await importDBMLToDiagram(exported.inlineDbml, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
expect(reimported.tables?.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Round-trip Testing - The Eternal Cycle', () => {
|
|
it('should maintain data integrity through multiple import/export cycles', async () => {
|
|
const originalDBML = `
|
|
Table "guild_members" {
|
|
"id" bigint [pk]
|
|
"name" varchar(100)
|
|
"level" int
|
|
"Yes" varchar(10) // Active status
|
|
"No" varchar(10) // Inactive status
|
|
"Order" int // SQL keyword - rank order
|
|
}
|
|
|
|
Table "guild_quests" {
|
|
"id" bigint [pk]
|
|
"quest_name" varchar(200)
|
|
"assigned_to" bigint
|
|
"difficulty" int
|
|
}
|
|
|
|
Ref: "guild_quests"."assigned_to" > "guild_members"."id"
|
|
`;
|
|
|
|
let currentDiagram = await importDBMLToDiagram(originalDBML, {
|
|
databaseType: DatabaseType.MYSQL,
|
|
});
|
|
|
|
// Store original IDs
|
|
const originalTableIds = currentDiagram.tables?.map((t) => ({
|
|
name: t.name,
|
|
id: t.id,
|
|
}));
|
|
|
|
// Perform 3 round-trips
|
|
for (let cycle = 1; cycle <= 3; cycle++) {
|
|
console.log(`🔄 Round-trip cycle ${cycle}`);
|
|
|
|
// Export
|
|
const exported = generateDBMLFromDiagram(currentDiagram);
|
|
|
|
// Re-import
|
|
const reimported = await importDBMLToDiagram(
|
|
exported.inlineDbml,
|
|
{
|
|
databaseType: DatabaseType.MYSQL,
|
|
}
|
|
);
|
|
|
|
// Apply changes
|
|
const targetDiagram: Diagram = {
|
|
...currentDiagram,
|
|
tables: reimported.tables,
|
|
relationships: reimported.relationships,
|
|
customTypes: reimported.customTypes,
|
|
};
|
|
|
|
currentDiagram = applyDBMLChanges({
|
|
sourceDiagram: currentDiagram,
|
|
targetDiagram,
|
|
});
|
|
|
|
// Verify IDs are still the same as original
|
|
originalTableIds?.forEach((original) => {
|
|
const currentTable = currentDiagram.tables?.find(
|
|
(t) => t.name === original.name
|
|
);
|
|
expect(currentTable?.id).toBe(original.id);
|
|
});
|
|
}
|
|
|
|
console.log('✓ Data integrity maintained through 3 cycles');
|
|
});
|
|
});
|
|
});
|