fix: preserve index names when applying DBML changes (#997)

* fix: preserve index names when applying DBML changes

* fix
This commit is contained in:
Guy Ben-Aharon
2025-12-04 20:11:50 +02:00
committed by GitHub
parent fbd04e9d5e
commit be26154cc5
35 changed files with 835 additions and 720 deletions

View File

@@ -163,14 +163,6 @@ ALTER TABLE ONLY "wizard_resident" ADD CONSTRAINT "wizard_tower_fk2" FOREIGN KEY
const result = await fromPostgres(sql);
console.log('Relationships found:', result.relationships.length);
result.relationships.forEach((rel, i) => {
console.log(
`FK ${i + 1}: ${rel.sourceTable}.${rel.sourceColumn} -> ${rel.targetTable}.${rel.targetColumn}`
);
});
console.log('Warnings:', result.warnings);
expect(result.tables).toHaveLength(2);
// At least one relationship should be found (the regex fallback should catch at least one)
@@ -205,9 +197,6 @@ ALTER TABLE ONLY "public"."account_emailaddress" ADD CONSTRAINT "account_emailad
const result = await fromPostgres(sql);
console.log('Warnings:', result.warnings);
console.log('Relationships:', result.relationships);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);

View File

@@ -13,13 +13,6 @@ CREATE TABLE crystal_enchantments (
const result = await fromPostgres(sql);
console.log('\nDebug info:');
console.log('Tables found:', result.tables.length);
console.log(
'Table names:',
result.tables.map((t) => t.name)
);
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('crystal_enchantments');
expect(result.tables[0].columns).toHaveLength(2);

View File

@@ -180,36 +180,26 @@ CREATE TABLE rewards (
);`;
const result = await fromPostgres(sql);
console.log('\nParsing results:');
console.log(`- Tables found: ${result.tables.length}`);
console.log(`- Enums found: ${result.enums?.length || 0}`);
console.log(`- Warnings: ${result.warnings?.length || 0}`);
// List all table names
const tableNames = result.tables.map((t) => t.name).sort();
console.log('\nTable names:');
tableNames.forEach((name, i) => {
console.log(` ${i + 1}. ${name}`);
});
// Should have all 20 tables
expect(result.tables).toHaveLength(20);
// Verify parsing metadata
expect(result.enums).toHaveLength(5);
// Check for quest_sample_rewards specifically
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
expect(questSampleRewards).toBeDefined();
expect(questSampleRewards!.columns).toHaveLength(2);
if (questSampleRewards) {
console.log('\nquest_sample_rewards table details:');
console.log(`- Columns: ${questSampleRewards.columns.length}`);
questSampleRewards.columns.forEach((col) => {
console.log(
` - ${col.name}: ${col.type} (nullable: ${col.nullable})`
);
});
}
// Verify quest_sample_rewards columns
const qsrColumnNames = questSampleRewards!.columns.map((c) => c.name);
expect(qsrColumnNames).toContain('quest_template_id');
expect(qsrColumnNames).toContain('reward_id');
// Expected tables
const expectedTables = [

View File

@@ -62,15 +62,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
const result = await fromPostgres(sql);
// Check enum parsing
console.log('\n=== ENUMS FOUND ===');
console.log('Count:', result.enums?.length || 0);
if (result.enums) {
result.enums.forEach((e) => {
console.log(` - ${e.name}: ${e.values.length} values`);
});
}
// Should find all 7 enums
expect(result.enums).toHaveLength(7);
@@ -97,11 +88,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
'mythic',
]);
// Check table parsing
console.log('\n=== TABLES FOUND ===');
console.log('Count:', result.tables.length);
console.log('Names:', result.tables.map((t) => t.name).join(', '));
// Should find all 4 tables
expect(result.tables).toHaveLength(4);
expect(result.tables.map((t) => t.name).sort()).toEqual([
@@ -111,15 +97,6 @@ ALTER TABLE "spells" ADD CONSTRAINT "spells_wizard_id_wizard_id_fk"
'wizard_account',
]);
// Check warnings for syntax issues
console.log('\n=== WARNINGS ===');
console.log('Count:', result.warnings?.length || 0);
if (result.warnings) {
result.warnings.forEach((w) => {
console.log(` - ${w}`);
});
}
// Should have warnings about custom types and parsing failures
expect(result.warnings).toBeDefined();
expect(result.warnings!.length).toBeGreaterThan(0);
@@ -150,8 +127,7 @@ CREATE TABLE "dragons" (
expect(result.enums).toHaveLength(1);
expect(result.enums?.[0].name).toBe('dragon_element');
// Table might have issues due to missing space
console.log('Tables:', result.tables.length);
console.log('Warnings:', result.warnings);
// Table might succeed or fail due to missing space syntax
// The important thing is the enum was still parsed correctly
});
});

View File

@@ -11,19 +11,8 @@ CREATE TABLE dragon_bonds (
PRIMARY KEY (dragon_master_id, dragon_id)
);`;
console.log('Testing with SQL:', sql);
const result = await fromPostgres(sql);
console.log('Result:', {
tableCount: result.tables.length,
tables: result.tables.map((t) => ({
name: t.name,
columns: t.columns.length,
})),
warnings: result.warnings,
});
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('dragon_bonds');
});
@@ -60,11 +49,6 @@ CREATE TABLE dragon_bonds (
const result = await fromPostgres(sql);
console.log('With dependencies:', {
tableCount: result.tables.length,
tableNames: result.tables.map((t) => t.name),
});
expect(result.tables).toHaveLength(3);
const dragonBonds = result.tables.find(
(t) => t.name === 'dragon_bonds'

View File

@@ -49,11 +49,6 @@ CREATE TABLE dragons (
const result = await fromPostgres(sql);
console.log(
'Parsed enums:',
result.enums?.map((e) => e.name)
);
expect(result.enums).toHaveLength(3);
// Specifically check for dragon_status

View File

@@ -36,56 +36,6 @@ CREATE TABLE dragon_quests (
// Parse the SQL
const result = await fromPostgres(sql);
// Check enums
console.log('\nEnum parsing results:');
console.log(`Found ${result.enums?.length || 0} enum types`);
if (result.enums) {
result.enums.forEach((e) => {
console.log(` - ${e.name}: ${e.values.length} values`);
});
}
// Expected enums
const expectedEnums = [
'wizard_rank',
'spell_frequency',
'magic_school',
'quest_status',
'dragon_mood',
];
// Check which are missing
const foundEnumNames = result.enums?.map((e) => e.name) || [];
const missingEnums = expectedEnums.filter(
(e) => !foundEnumNames.includes(e)
);
if (missingEnums.length > 0) {
console.log('\nMissing enums:', missingEnums);
// Let's check if they're in the SQL at all
missingEnums.forEach((enumName) => {
const regex = new RegExp(`CREATE\\s+TYPE\\s+${enumName}`, 'i');
if (regex.test(sql)) {
console.log(
` ${enumName} exists in SQL but wasn't parsed`
);
// Find the line
const lines = sql.split('\n');
const lineIndex = lines.findIndex((line) =>
regex.test(line)
);
if (lineIndex !== -1) {
console.log(
` Line ${lineIndex + 1}: ${lines[lineIndex].trim()}`
);
}
}
});
}
// Convert to diagram
const diagram = convertToChartDBDiagram(
result,
@@ -93,68 +43,54 @@ CREATE TABLE dragon_quests (
DatabaseType.POSTGRESQL
);
// Check custom types in diagram
console.log(
'\nCustom types in diagram:',
diagram.customTypes?.length || 0
);
// Check wizards table
const wizardsTable = diagram.tables?.find((t) => t.name === 'wizards');
if (wizardsTable) {
console.log('\nWizards table:');
const rankField = wizardsTable.fields.find(
(f) => f.name === 'rank'
);
if (rankField) {
console.log(
` rank field type: ${rankField.type.name} (id: ${rankField.type.id})`
);
}
}
// Check spellbooks table
const spellbooksTable = diagram.tables?.find(
(t) => t.name === 'spellbooks'
);
if (spellbooksTable) {
console.log('\nSpellbooks table:');
const frequencyField = spellbooksTable.fields.find(
(f) => f.name === 'cast_frequency'
);
if (frequencyField) {
console.log(
` cast_frequency field type: ${frequencyField.type.name}`
);
}
const schoolField = spellbooksTable.fields.find(
(f) => f.name === 'primary_school'
);
if (schoolField) {
console.log(
` primary_school field type: ${schoolField.type.name}`
);
}
}
// Assertions
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
expect(diagram.customTypes).toHaveLength(5);
// Check that wizard_rank is present
// Verify all expected enums are present
const expectedEnums = [
'wizard_rank',
'spell_frequency',
'magic_school',
'quest_status',
'dragon_mood',
];
const foundEnumNames = result.enums!.map((e) => e.name);
expectedEnums.forEach((enumName) => {
expect(foundEnumNames).toContain(enumName);
});
// Check that wizard_rank is present with correct values
const wizardRankEnum = result.enums!.find(
(e) => e.name === 'wizard_rank'
);
expect(wizardRankEnum).toBeDefined();
expect(wizardRankEnum!.values).toHaveLength(5);
// Check that the rank field uses wizard_rank type
if (wizardsTable) {
const rankField = wizardsTable.fields.find(
(f) => f.name === 'rank'
);
expect(rankField?.type.name.toLowerCase()).toBe('wizard_rank');
}
const wizardsTable = diagram.tables?.find((t) => t.name === 'wizards');
expect(wizardsTable).toBeDefined();
const rankField = wizardsTable!.fields.find((f) => f.name === 'rank');
expect(rankField).toBeDefined();
expect(rankField!.type.name.toLowerCase()).toBe('wizard_rank');
// Check spellbooks table enum fields
const spellbooksTable = diagram.tables?.find(
(t) => t.name === 'spellbooks'
);
expect(spellbooksTable).toBeDefined();
const frequencyField = spellbooksTable!.fields.find(
(f) => f.name === 'cast_frequency'
);
expect(frequencyField).toBeDefined();
expect(frequencyField!.type.name.toLowerCase()).toBe('spell_frequency');
const schoolField = spellbooksTable!.fields.find(
(f) => f.name === 'primary_school'
);
expect(schoolField).toBeDefined();
expect(schoolField!.type.name.toLowerCase()).toBe('magic_school');
});
});

View File

@@ -12,25 +12,16 @@ CREATE TABLE spells (
const result = await fromPostgres(sql);
console.log('Spells table result:', {
tableCount: result.tables.length,
columns: result.tables[0]?.columns.map((c) => ({
name: c.name,
type: c.type,
})),
});
expect(result.tables).toHaveLength(1);
const spellsTable = result.tables[0];
expect(spellsTable.name).toBe('spells');
// Debug: list all columns found
console.log('Columns found:', spellsTable.columns.length);
spellsTable.columns.forEach((col, idx) => {
console.log(` ${idx + 1}. ${col.name}: ${col.type}`);
});
expect(spellsTable.columns).toHaveLength(3);
// Verify all columns are present
const columnNames = spellsTable.columns.map((c) => c.name);
expect(columnNames).toContain('id');
expect(columnNames).toContain('description');
expect(columnNames).toContain('category');
});
it('should handle magical enum types with mixed quotes', async () => {
@@ -38,11 +29,6 @@ CREATE TABLE spells (
const result = await fromPostgres(sql);
console.log('Enum result:', {
enumCount: result.enums?.length || 0,
values: result.enums?.[0]?.values,
});
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(1);
expect(result.enums![0].values).toEqual([

View File

@@ -22,14 +22,6 @@ CREATE TABLE spellbooks (
const result = await fromPostgres(sql);
// Debug output
console.log('Enums found:', result.enums?.length || 0);
if (result.enums) {
result.enums.forEach((e) => {
console.log(` - ${e.name}`);
});
}
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);

View File

@@ -32,48 +32,19 @@ CREATE TABLE creature_abilities (
);
`;
console.log(
'Testing PostgreSQL parser with CREATE EXTENSION and CREATE TYPE...\n'
);
const result = await fromPostgres(testSQL);
try {
const result = await fromPostgres(testSQL);
// Basic assertions
expect(result.tables.length).toBe(2);
expect(result.tables[0].name).toBe('mystical_creatures');
expect(result.tables[1].name).toBe('creature_abilities');
expect(result.relationships.length).toBe(1);
console.log('Parse successful!');
console.log('\nTables found:', result.tables.length);
result.tables.forEach((table) => {
console.log(`\n- Table: ${table.name}`);
console.log(' Columns:');
table.columns.forEach((col) => {
console.log(
` - ${col.name}: ${col.type}${col.nullable ? '' : ' NOT NULL'}${col.primaryKey ? ' PRIMARY KEY' : ''}`
);
});
});
console.log('\nRelationships found:', result.relationships.length);
result.relationships.forEach((rel) => {
console.log(
`- ${rel.sourceTable}.${rel.sourceColumn} -> ${rel.targetTable}.${rel.targetColumn}`
);
});
if (result.warnings && result.warnings.length > 0) {
console.log('\nWarnings:');
result.warnings.forEach((warning) => {
console.log(`- ${warning}`);
});
}
// Basic assertions
expect(result.tables.length).toBe(2);
expect(result.tables[0].name).toBe('mystical_creatures');
expect(result.tables[1].name).toBe('creature_abilities');
expect(result.relationships.length).toBe(1);
} catch (error) {
console.error('Error parsing SQL:', (error as Error).message);
console.error('\nStack trace:', (error as Error).stack);
throw error;
}
// Verify enums are parsed
expect(result.enums).toHaveLength(2);
expect(result.enums?.map((e) => e.name).sort()).toEqual([
'creature_alignment',
'magic_school',
]);
});
});

View File

@@ -140,64 +140,30 @@ CREATE TABLE guild_master_actions (
// First, verify the table exists in the SQL
const tableExists = sql.includes('CREATE TABLE quest_sample_rewards');
console.log('\nDebugging quest_sample_rewards:');
console.log('- Table exists in SQL:', tableExists);
// Extract the specific table definition
const tableMatch = sql.match(
/-- Junction table[\s\S]*?CREATE TABLE quest_sample_rewards[\s\S]*?;/
);
if (tableMatch) {
console.log('- Table definition found, first 200 chars:');
console.log(tableMatch[0].substring(0, 200) + '...');
}
// Now parse
const result = await fromPostgres(sql);
console.log('\nParsing results:');
console.log('- Total tables:', result.tables.length);
console.log(
'- Table names:',
result.tables.map((t) => t.name).join(', ')
);
// Look for quest_sample_rewards
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
console.log('- quest_sample_rewards found:', !!questSampleRewards);
if (!questSampleRewards) {
// Check warnings for clues
console.log('\nWarnings that might be relevant:');
result.warnings?.forEach((w, i) => {
if (
w.includes('quest_sample_rewards') ||
w.includes('Failed to parse')
) {
console.log(` ${i}: ${w}`);
}
});
// List all tables to see what's missing
console.log('\nAll parsed tables:');
result.tables.forEach((t, i) => {
console.log(
` ${i + 1}. ${t.name} (${t.columns.length} columns)`
);
});
} else {
console.log('\nquest_sample_rewards details:');
console.log('- Columns:', questSampleRewards.columns.length);
questSampleRewards.columns.forEach((c) => {
console.log(` - ${c.name}: ${c.type}`);
});
}
// The test expectation
expect(tableExists).toBe(true);
expect(result.tables.length).toBeGreaterThanOrEqual(19); // At least 19 tables
expect(questSampleRewards).toBeDefined();
// Verify quest_sample_rewards has correct columns
expect(questSampleRewards!.columns).toHaveLength(2);
const columnNames = questSampleRewards!.columns.map((c) => c.name);
expect(columnNames).toContain('quest_template_id');
expect(columnNames).toContain('reward_id');
// Verify no parsing warnings for this table
const questSampleRewardsWarnings = result.warnings?.filter((w) =>
w.includes('quest_sample_rewards')
);
expect(questSampleRewardsWarnings?.length ?? 0).toBe(0);
});
});

View File

@@ -15,12 +15,6 @@ CREATE TABLE towers (
const result = await fromPostgres(sql);
console.log(
'Tables:',
result.tables.map((t) => t.name)
);
console.log('Relationships:', result.relationships);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('towers');
@@ -43,13 +37,6 @@ CREATE TABLE quests (
const result = await fromPostgres(sql);
console.log(
'Tables:',
result.tables.map((t) => t.name)
);
console.log('Relationships:', result.relationships);
console.log('Warnings:', result.warnings);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
});

View File

@@ -40,19 +40,13 @@ CREATE TABLE plan_sample_spells (
const result = await fromPostgres(sql);
console.log('Parsing results:');
console.log(
'- Tables:',
result.tables.map((t) => t.name)
);
console.log('- Table count:', result.tables.length);
console.log('- Relationships:', result.relationships.length);
console.log('- Enums:', result.enums?.length || 0);
// Should have 3 tables
// Verify parsing results
expect(result.tables).toHaveLength(3);
expect(result.relationships).toHaveLength(2);
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
// Check table names
// Verify table names
const tableNames = result.tables.map((t) => t.name).sort();
expect(tableNames).toEqual([
'plan_sample_spells',
@@ -60,19 +54,12 @@ CREATE TABLE plan_sample_spells (
'spells',
]);
// Should have 2 relationships (both from plan_sample_spells)
expect(result.relationships).toHaveLength(2);
// Check plan_sample_spells specifically
const planSampleSpells = result.tables.find(
(t) => t.name === 'plan_sample_spells'
);
expect(planSampleSpells).toBeDefined();
expect(planSampleSpells!.columns).toHaveLength(2);
// Should have 5 enum types
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
});
it('should parse the exact junction table definition', async () => {

View File

@@ -15,11 +15,6 @@ CREATE TABLE test_table (
const result = await fromPostgres(sql);
// Even with syntax error, it should try to parse what it can
console.log('Result:', {
tables: result.tables.length,
warnings: result.warnings,
});
// Should attempt to parse the table even if parser fails
expect(result.tables.length).toBeGreaterThanOrEqual(0);
});
@@ -43,12 +38,6 @@ CREATE TABLE table3 (
const result = await fromPostgres(sql);
console.log('Multi-table result:', {
tableCount: result.tables.length,
tableNames: result.tables.map((t) => t.name),
warnings: result.warnings?.length || 0,
});
// Should parse at least table1 and table3
expect(result.tables.length).toBeGreaterThanOrEqual(2);

View File

@@ -14,12 +14,6 @@ CREATE TABLE wizard_spells (
const result = await fromPostgres(sql);
console.log('Test results:', {
tableCount: result.tables.length,
tableNames: result.tables.map((t) => t.name),
warnings: result.warnings,
});
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('wizard_spells');
});
@@ -201,46 +195,23 @@ CREATE TABLE guild_master_actions (
// Count CREATE TABLE statements
const createTableMatches = sql.match(/CREATE TABLE/gi) || [];
console.log(
`\nFound ${createTableMatches.length} CREATE TABLE statements in file`
);
// Find all table names
const tableNameMatches =
sql.match(
/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/gi
) || [];
const tableNames = tableNameMatches
.map((match) => {
const nameMatch = match.match(
/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
);
return nameMatch ? nameMatch[1] : null;
})
.filter(Boolean);
console.log('Table names found in SQL:', tableNames);
console.log(
'quest_sample_rewards in list?',
tableNames.includes('quest_sample_rewards')
);
// Parse the file
const result = await fromPostgres(sql);
console.log(`\nParsed ${result.tables.length} tables`);
console.log(
'Parsed table names:',
result.tables.map((t) => t.name).sort()
);
const junctionTable = result.tables.find(
(t) => t.name.includes('_') && t.columns.length >= 2
);
console.log('junction table found?', !!junctionTable);
// All CREATE TABLE statements should be parsed
expect(result.tables.length).toBe(createTableMatches.length);
expect(junctionTable).toBeDefined();
// Verify quest_sample_rewards is parsed
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
expect(questSampleRewards).toBeDefined();
expect(questSampleRewards!.columns).toHaveLength(2);
});
});

View File

@@ -241,49 +241,13 @@ CREATE TABLE audit_logs (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`;
console.log('Parsing SQL...');
const startTime = Date.now();
const result = await fromPostgres(sql);
const parseTime = Date.now() - startTime;
console.log(`Parse completed in ${parseTime}ms`);
// Expected counts
const expectedTables = 27;
const expectedEnums = 15;
const minExpectedRelationships = 36; // Adjusted based on actual relationships in the schema
console.log('\n=== PARSING RESULTS ===');
console.log(
`Tables parsed: ${result.tables.length} (expected: ${expectedTables})`
);
console.log(
`Enums parsed: ${result.enums?.length || 0} (expected: ${expectedEnums})`
);
console.log(
`Relationships parsed: ${result.relationships.length} (expected min: ${minExpectedRelationships})`
);
console.log(`Warnings: ${result.warnings?.length || 0}`);
// List parsed tables
console.log('\n=== TABLES PARSED ===');
const tableNames = result.tables.map((t) => t.name).sort();
tableNames.forEach((name) => console.log(`- ${name}`));
// List enums
if (result.enums && result.enums.length > 0) {
console.log('\n=== ENUMS PARSED ===');
result.enums.forEach((e) => {
console.log(`- ${e.name}: ${e.values.length} values`);
});
}
// Show warnings if any
if (result.warnings && result.warnings.length > 0) {
console.log('\n=== WARNINGS ===');
result.warnings.forEach((w) => console.log(`- ${w}`));
}
// Verify counts
expect(result.tables).toHaveLength(expectedTables);
expect(result.enums).toBeDefined();

View File

@@ -29,13 +29,6 @@ CREATE TABLE schema1.table_with_schema (id INTEGER PRIMARY KEY);`;
// Count CREATE TABLE statements in the SQL
const createTableCount = (sql.match(/CREATE TABLE/gi) || []).length;
console.log(`\nValidation:`);
console.log(`- CREATE TABLE statements in SQL: ${createTableCount}`);
console.log(`- Tables parsed: ${result.tables.length}`);
console.log(
`- Table names: ${result.tables.map((t) => t.name).join(', ')}`
);
// All CREATE TABLE statements should result in a parsed table
expect(result.tables).toHaveLength(createTableCount);
@@ -82,20 +75,14 @@ CREATE TABLE complex_constraints (
const createTableCount = (sql.match(/CREATE TABLE/gi) || []).length;
console.log(`\nEdge case validation:`);
console.log(`- CREATE TABLE statements: ${createTableCount}`);
console.log(`- Tables parsed: ${result.tables.length}`);
console.log(
`- Expected tables: only_fks, no_pk, empty_table, complex_constraints`
);
console.log(
`- Actual tables: ${result.tables.map((t) => t.name).join(', ')}`
);
result.tables.forEach((t) => {
console.log(`- ${t.name}: ${t.columns.length} columns`);
});
// Even edge cases should be parsed
expect(result.tables).toHaveLength(createTableCount);
// Verify the expected tables are present
const tableNames = result.tables.map((t) => t.name).sort();
expect(tableNames).toContain('only_fks');
expect(tableNames).toContain('no_pk');
expect(tableNames).toContain('empty_table');
expect(tableNames).toContain('complex_constraints');
});
});

View File

@@ -17,14 +17,8 @@ CREATE TYPE ritual_status AS ENUM ('pending', 'channeling', 'completed', 'failed
CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
`;
console.log('Testing with fromPostgres...');
const result = await fromPostgres(sql);
console.log(
'Enums found:',
result.enums?.map((e) => e.name)
);
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);

View File

@@ -55,13 +55,6 @@ CREATE INDEX "grimoires_category_idx" ON "grimoires" ("category");
const result = await fromPostgres(sql);
// Verify enum parsing
console.log('\n=== IMPORT RESULTS ===');
console.log(`Enums parsed: ${result.enums?.length || 0}`);
console.log(`Tables parsed: ${result.tables.length}`);
console.log(`Relationships found: ${result.relationships.length}`);
console.log(`Warnings: ${result.warnings?.length || 0}`);
// All enums should be parsed despite schema qualification
expect(result.enums).toHaveLength(3);
expect(result.enums?.map((e) => e.name).sort()).toEqual([
@@ -98,12 +91,6 @@ CREATE INDEX "grimoires_category_idx" ON "grimoires" ("category");
'master',
'archmage',
]);
// Log warnings for visibility
if (result.warnings && result.warnings.length > 0) {
console.log('\n=== WARNINGS ===');
result.warnings.forEach((w) => console.log(`- ${w}`));
}
});
it('should provide actionable feedback for common syntax issues', async () => {

View File

@@ -16,13 +16,6 @@ CREATE TABLE "wizards" (
const result = await fromPostgres(sql);
console.log('Enums found:', result.enums?.length || 0);
if (result.enums) {
result.enums.forEach((e) => {
console.log(` - ${e.name}: ${e.values.join(', ')}`);
});
}
// Should find both enums
expect(result.enums).toHaveLength(2);
@@ -64,8 +57,7 @@ CREATE TABLE "dragons" (
expect(result.enums).toHaveLength(1);
expect(result.enums?.[0].name).toBe('dragon_type');
// Table parsing might fail due to syntax error
console.log('Tables found:', result.tables.length);
console.log('Warnings:', result.warnings);
// Table parsing might succeed or fail due to missing space syntax
// The important thing is the enum was still parsed correctly
});
});

View File

@@ -14,13 +14,6 @@ CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
const result = await fromPostgres(sql);
console.log('Result enums:', result.enums?.length || 0);
if (result.enums) {
result.enums.forEach((e) => {
console.log(` - ${e.name}`);
});
}
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(5);
});
@@ -48,9 +41,6 @@ CREATE TYPE mana_status AS ENUM ('pending', 'charged', 'depleted');
for (const enumDef of enums) {
const result = await fromPostgres(enumDef.sql);
console.log(`\nTesting ${enumDef.name}:`);
console.log(` Found enums: ${result.enums?.length || 0}`);
expect(result.enums).toBeDefined();
expect(result.enums).toHaveLength(1);
expect(result.enums![0].name).toBe(enumDef.name);

View File

@@ -44,16 +44,8 @@ CREATE TABLE plan_sample_spells (
PRIMARY KEY (spell_plan_id, spell_id)
);`;
console.log('Testing exact SQL from forth example...');
const result = await fromPostgres(sql);
console.log('Results:', {
tables: result.tables.length,
tableNames: result.tables.map((t) => t.name),
warnings: result.warnings?.length || 0,
});
// Should have 3 tables
expect(result.tables).toHaveLength(3);

View File

@@ -11,15 +11,6 @@ CREATE TABLE spell_ingredients (
const result = await fromPostgres(sql);
console.log('String preservation result:', {
tableCount: result.tables.length,
columns: result.tables[0]?.columns.map((c) => ({
name: c.name,
type: c.type,
default: c.default,
})),
});
expect(result.tables).toHaveLength(1);
expect(result.tables[0].columns).toHaveLength(2);

View File

@@ -21,12 +21,6 @@ CREATE TABLE table3 (
const result = await fromPostgres(sql);
console.log('Test results:', {
tableCount: result.tables.length,
tableNames: result.tables.map((t) => t.name),
warnings: result.warnings,
});
// Should parse all 3 tables even though table2 has undefined reference
expect(result.tables).toHaveLength(3);

View File

@@ -62,14 +62,6 @@ CREATE TABLE rewards (
// Use the improved parser
const parserResult = await fromPostgres(sql);
console.log('\nParser Result:');
console.log('- Enums found:', parserResult.enums?.length || 0);
if (parserResult.enums) {
parserResult.enums.forEach((e) => {
console.log(` - ${e.name}: ${e.values.length} values`);
});
}
// Convert to diagram
const diagram = convertToChartDBDiagram(
parserResult,
@@ -77,33 +69,6 @@ CREATE TABLE rewards (
DatabaseType.POSTGRESQL
);
console.log('\nDiagram Result:');
console.log('- Custom types:', diagram.customTypes?.length || 0);
if (diagram.customTypes) {
diagram.customTypes.forEach((t) => {
console.log(` - ${t.name} (${t.kind})`);
});
}
// Check contracts table
const contractsTable = diagram.tables?.find(
(t) => t.name === 'contracts'
);
if (contractsTable) {
console.log('\nContracts table enum fields:');
const enumFields = ['status'];
enumFields.forEach((fieldName) => {
const field = contractsTable.fields.find(
(f) => f.name === fieldName
);
if (field) {
console.log(
` - ${field.name}: ${field.type.name} (id: ${field.type.id})`
);
}
});
}
// Assertions
expect(parserResult.enums).toHaveLength(5);
expect(diagram.customTypes).toHaveLength(5);

View File

@@ -203,39 +203,18 @@ CREATE TABLE guild_master_actions (
const result = await fromPostgres(sql);
console.log('\n=== PARSING RESULTS ===');
console.log(`Tables parsed: ${result.tables.length}`);
console.log(`Expected: ${expectedTables.length}`);
const parsedTableNames = result.tables.map((t) => t.name).sort();
console.log('\nParsed tables:');
parsedTableNames.forEach((name, i) => {
console.log(` ${i + 1}. ${name}`);
});
// Find missing tables
// Find missing tables and verify none are missing
const missingTables = expectedTables.filter(
(expected) => !parsedTableNames.includes(expected)
);
if (missingTables.length > 0) {
console.log('\nMissing tables:');
missingTables.forEach((name) => {
console.log(` - ${name}`);
});
}
expect(missingTables).toHaveLength(0);
// Check for quest_sample_rewards specifically
const questSampleRewards = result.tables.find(
(t) => t.name === 'quest_sample_rewards'
);
console.log(`\nquest_sample_rewards found: ${!!questSampleRewards}`);
if (questSampleRewards) {
console.log('quest_sample_rewards details:');
console.log(` - Columns: ${questSampleRewards.columns.length}`);
questSampleRewards.columns.forEach((col) => {
console.log(` - ${col.name}: ${col.type}`);
});
}
// Verify all tables were parsed
expect(result.tables).toHaveLength(expectedTables.length);
@@ -249,11 +228,5 @@ CREATE TABLE guild_master_actions (
.map((c) => c.name)
.sort();
expect(columnNames).toEqual(['quest_template_id', 'reward_id']);
// Check warnings if any
if (result.warnings && result.warnings.length > 0) {
console.log('\nWarnings:');
result.warnings.forEach((w) => console.log(` - ${w}`));
}
});
});

View File

@@ -20,32 +20,8 @@ CREATE TABLE [DBO].[SpellComponent](
[ITSSCHOOLMETA] [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
console.log('Testing complex fantasy SQL...');
console.log(
'Number of CREATE TABLE statements:',
(sql.match(/CREATE\s+TABLE/gi) || []).length
);
const result = await fromSQLServer(sql);
console.log(
'Result tables:',
result.tables.map((t) => t.name)
);
console.log('Result relationships:', result.relationships.length);
// Debug: Show actual relationships
if (result.relationships.length === 0) {
console.log('WARNING: No relationships found!');
} else {
console.log('Relationships found:');
result.relationships.forEach((r) => {
console.log(
` ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
);
});
}
// Should create TWO tables
expect(result.tables).toHaveLength(2);

View File

@@ -60,28 +60,6 @@ CREATE TABLE [DBO].[SpellComponent](
)
);
expect(fk2).toBeDefined();
console.log(
'Full flow test - Relationships created:',
diagram.relationships?.length
);
diagram.relationships?.forEach((r) => {
const sourceTable = diagram.tables?.find(
(t) => t.id === r.sourceTableId
);
const targetTable = diagram.tables?.find(
(t) => t.id === r.targetTableId
);
const sourceField = sourceTable?.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(f) => f.id === r.targetFieldId
);
console.log(
` ${sourceTable?.name}.${sourceField?.name} -> ${targetTable?.name}.${targetField?.name}`
);
});
});
it('should handle case-insensitive field matching', async () => {

View File

@@ -415,10 +415,8 @@ ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_P
(name) => !foundRelationshipNames.includes(name)
);
if (missingRelationships.length > 0) {
console.log('Missing relationships:', missingRelationships);
console.log('Found relationships:', foundRelationshipNames);
}
// Verify all expected relationships were found
expect(missingRelationships.length).toBe(0);
// Verify relationships count - we have 32 working relationships
expect(result.relationships.length).toBe(32);
@@ -509,16 +507,6 @@ ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_P
r.targetSchema === 'realm'
);
expect(citiesToKingdoms).toBeDefined();
console.log('Multi-schema test results:');
console.log('Total schemas:', schemas.size);
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'Cross-schema relationships:',
crossSchemaRelationships.length
);
console.log('Within-schema relationships:', withinSchemaRels.length);
});
it('should handle mixed schema notation formats', async () => {

View File

@@ -164,10 +164,6 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
const result = await fromSQLServer(sql);
// Debug output
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
// Check if we have the expected number of tables and relationships
expect(result.tables).toHaveLength(4);
expect(result.relationships).toHaveLength(4);
@@ -182,37 +178,22 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
expect(spellCastingRel).toBeDefined();
if (spellCastingRel) {
// Find the corresponding tables
const spellTable = result.tables.find(
(t) => t.name === 'Spell' && t.schema === 'spellcasting'
);
const spellCastingProcessTable = result.tables.find(
(t) =>
t.name === 'SpellCastingProcess' &&
t.schema === 'spellcasting'
);
// Find the corresponding tables
const spellTable = result.tables.find(
(t) => t.name === 'Spell' && t.schema === 'spellcasting'
);
const spellCastingProcessTable = result.tables.find(
(t) =>
t.name === 'SpellCastingProcess' && t.schema === 'spellcasting'
);
console.log('SpellCastingProcess relationship:', {
sourceTableId: spellCastingRel.sourceTableId,
targetTableId: spellCastingRel.targetTableId,
spellCastingProcessTableId: spellCastingProcessTable?.id,
spellTableId: spellTable?.id,
isSourceIdValid:
spellCastingRel.sourceTableId ===
spellCastingProcessTable?.id,
isTargetIdValid:
spellCastingRel.targetTableId === spellTable?.id,
});
// Verify the IDs are properly linked
expect(spellCastingRel.sourceTableId).toBeTruthy();
expect(spellCastingRel.targetTableId).toBeTruthy();
expect(spellCastingRel.sourceTableId).toBe(
spellCastingProcessTable!.id
);
expect(spellCastingRel.targetTableId).toBe(spellTable!.id);
}
// Verify the IDs are properly linked
expect(spellCastingRel!.sourceTableId).toBeTruthy();
expect(spellCastingRel!.targetTableId).toBeTruthy();
expect(spellCastingRel!.sourceTableId).toBe(
spellCastingProcessTable!.id
);
expect(spellCastingRel!.targetTableId).toBe(spellTable!.id);
// Check the apprentice self-referencing relationships
const apprenticeWizardRel = result.relationships.find(
@@ -241,13 +222,6 @@ describe('SQL Server Foreign Key Relationship Tests', () => {
r.targetTableId === ''
);
if (relationshipsWithMissingIds.length > 0) {
console.log(
'Relationships with missing IDs:',
relationshipsWithMissingIds.slice(0, 5)
);
}
expect(relationshipsWithMissingIds).toHaveLength(0);
});
});

View File

@@ -598,13 +598,6 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
const result = await fromSQLServer(sql);
// Debug: log table names to see what's parsed
console.log('Tables found:', result.tables.length);
console.log(
'Table names:',
result.tables.map((t) => t.name)
);
// Verify correct number of tables
expect(result.tables.length).toBe(37); // Actually 37 tables after counting
@@ -614,7 +607,6 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
expect(schemas.has('dbo')).toBe(true);
// Verify correct number of relationships
console.log('Relationships found:', result.relationships.length);
expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed
// Verify all relationships have valid source and target table IDs
@@ -682,23 +674,5 @@ ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
console.log('Single-schema test results:');
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'All relationships properly linked:',
validRelationships.length === result.relationships.length
);
// Sample of relationship names for verification
const sampleRelationships = result.relationships
.slice(0, 5)
.map((r) => ({
name: r.name,
source: `${r.sourceTable}.${r.sourceColumn}`,
target: `${r.targetTable}.${r.targetColumn}`,
}));
console.log('Sample relationships:', sampleRelationships);
});
});

View File

@@ -54,20 +54,6 @@ CREATE TABLE [DBO].[SpellComponent](
expect(fk2?.targetColumn).toBe('SPELLID');
expect(fk2?.sourceTableId).toBeTruthy();
expect(fk2?.targetTableId).toBeTruthy();
// Log for debugging
console.log('\n=== FK Verification Results ===');
console.log(
'Tables:',
result.tables.map((t) => `${t.schema}.${t.name}`)
);
console.log('Total FKs found:', result.relationships.length);
result.relationships.forEach((r, i) => {
console.log(
`FK ${i + 1}: ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
);
console.log(` IDs: ${r.sourceTableId} -> ${r.targetTableId}`);
});
});
it('should parse inline FOREIGN KEY syntax correctly', async () => {

View File

@@ -596,6 +596,666 @@ describe('Apply DBML Changes - single table', () => {
// Check that the new field is added correctly
expect(result).toEqual(expectedResult);
});
it('should preserve index name', () => {
const sourceDiagram: Diagram = {
id: 'mqqwkkod9jb8',
name: 'buckle_db',
createdAt: new Date('2025-12-04T16:00:06.463Z'),
updatedAt: new Date('2025-12-04T16:06:49.070Z'),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: 'r1rp4f64dtpifw7mub089bxy8',
name: 'buckle_diagrams_history',
schema: 'public',
x: 100,
y: 300,
fields: [
{
id: '0t0b4e7irmvw53w4qyz99zqm1',
name: 'rownum',
type: {
id: 'int',
name: 'int',
},
primaryKey: false,
unique: false,
nullable: false,
increment: true,
isArray: false,
createdAt: 1764864006454,
},
{
id: 'hugwwfirbk609ejryqqsvw3be',
name: 'id',
type: {
id: 'uuid',
name: 'uuid',
},
primaryKey: true,
unique: true,
nullable: false,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'd9sw9l864y0s6b9bc990y7ebi',
name: 'action_type',
type: {
id: 'varchar',
name: 'varchar',
},
primaryKey: false,
unique: false,
nullable: true,
characterMaximumLength: '50',
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'tref4xugo95u21hd02j3s5q67',
name: 'diagram_id',
type: {
id: 'uuid',
name: 'uuid',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'v3g2bkhtozhthlnhs3bhu6s4h',
name: 'diagram_name',
type: {
id: 'varchar',
name: 'varchar',
},
primaryKey: false,
unique: false,
nullable: true,
characterMaximumLength: '2500',
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: '79jq9oamvjw7j5i081pplb4ii',
name: 'database_type',
type: {
id: 'varchar',
name: 'varchar',
},
primaryKey: false,
unique: false,
nullable: true,
characterMaximumLength: '50',
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'dd05cmr4ntvwu5aaeir6s4bco',
name: 'database_edition',
type: {
id: 'varchar',
name: 'varchar',
},
primaryKey: false,
unique: false,
nullable: true,
characterMaximumLength: '50',
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: '7589goqn7n2xk3rg5gny817bb',
name: 'diagram_json',
type: {
id: 'json',
name: 'json',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'i5zaobzgwbyn8g9xdcskq4qc8',
name: 'changed_by_user_id',
type: {
id: 'uuid',
name: 'uuid',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'u9uana25zrknsc5g7t8sinegm',
name: 'account_id',
type: {
id: 'uuid',
name: 'uuid',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'jy29dbbfcr4zn5l585y75tob1',
name: 'created_at',
type: {
id: 'timestamp',
name: 'timestamp',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
{
id: 'k5jdbgind50qitigvmpuk00n3',
name: 'updated_at',
type: {
id: 'timestamp',
name: 'timestamp',
},
primaryKey: false,
unique: false,
nullable: true,
increment: false,
isArray: false,
createdAt: 1764864006455,
},
],
indexes: [
{
id: 'drzp2kp4a74ceiwh0jl8fvuy7',
name: 'index_diagrams_history_on_changed_by_user_id',
unique: false,
fieldIds: ['i5zaobzgwbyn8g9xdcskq4qc8'],
createdAt: 1764864006455,
type: 'btree',
},
{
id: 'r3gc7sxg5dv8o6ur2z2jen8vf',
name: 'index_diagrams_history_on_diagram_id',
unique: false,
fieldIds: ['tref4xugo95u21hd02j3s5q67'],
createdAt: 1764864006455,
type: 'btree',
},
{
id: '7v78ps9i99hkwt086wos3dywa',
name: 'index_diagrams_history_on_account_id',
unique: false,
fieldIds: ['u9uana25zrknsc5g7t8sinegm'],
createdAt: 1764864006455,
type: 'btree',
},
{
id: 'jzv2yaggsmorqfczz6g60s22y',
name: 'buckle_diagrams_history_new_pkey',
unique: true,
fieldIds: ['hugwwfirbk609ejryqqsvw3be'],
createdAt: 1764864006455,
isPrimaryKey: true,
},
],
color: '#8eb7ff',
isView: false,
isMaterializedView: false,
createdAt: 1764864006455,
},
],
relationships: [],
dependencies: [],
areas: [],
customTypes: [],
notes: [],
};
const targetDiagram: Diagram = {
id: 'mqqwkkod9jb8',
name: 'buckle_db',
createdAt: new Date('2025-12-04T16:00:06.463Z'),
updatedAt: new Date('2025-12-04T16:06:49.070Z'),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: 'lexrean4b8pm5lg15ppmkywsv',
name: 'buckle_diagrams_history',
schema: '',
order: 0,
fields: [
{
id: '45z2qw9van4yyowwnbs21m767',
name: 'rownum',
type: {
name: 'int',
id: 'int',
},
nullable: false,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
increment: true,
},
{
id: 'rdef7ta48s1wd1tjjoozaw1lv',
name: 'id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: false,
primaryKey: true,
unique: true,
createdAt: 1764864417835,
},
{
id: '2mthge4c8s5yt1qe6zp2slsop',
name: 'action_type',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
characterMaximumLength: '50',
},
{
id: '8zpgr6s9whcuf43klrbjhaamg',
name: 'diagram_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
{
id: '2rxqewro66448l3i7qo79f22d',
name: 'diagram_name',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
characterMaximumLength: '2500',
},
{
id: 'khvw72ifx0s1abkj70vn09gsy',
name: 'database_type',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
characterMaximumLength: '50',
},
{
id: 'vjle9bee169krs44qhr0xqnz8',
name: 'database_edition',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
characterMaximumLength: '50',
},
{
id: 'd6n910e7qnsc32lymsk02die3',
name: 'diagram_json',
type: {
name: 'json',
id: 'json',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
{
id: 'za4nagw0bx1824awleviede7b',
name: 'changed_by_user_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
{
id: 'cspop4m5whpw8ckq51aafe58a',
name: 'account_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
{
id: '2z2sixnpkv243xx7nuh2ki0fw',
name: 'created_at',
type: {
name: 'timestamp',
id: 'timestamp',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
{
id: '5jeve2jfv557ijuvwfv5j1t8m',
name: 'updated_at',
type: {
name: 'timestamp',
id: 'timestamp',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864417835,
},
],
indexes: [
{
id: '3fsk7qhy0xosg4gr6dcul5t01',
name: 'pk_buckle_diagrams_history_id',
fieldIds: ['rdef7ta48s1wd1tjjoozaw1lv'],
unique: true,
isPrimaryKey: true,
createdAt: 1764864417836,
},
{
id: 'iujj3p438ueo1vpbv08szxln3',
name: 'index_diagrams_history_on_changed_by_user_id',
fieldIds: ['za4nagw0bx1824awleviede7b'],
unique: false,
createdAt: 1764864417836,
},
{
id: 'mq0eh23rt98izx8zhwg5rl0tf',
name: 'index_diagrams_history_on_diagram_id',
fieldIds: ['8zpgr6s9whcuf43klrbjhaamg'],
unique: false,
createdAt: 1764864417836,
},
{
id: '57wcn3nl6nxeaq0uuptoolo48',
name: 'index_diagrams_history_on_account_id',
fieldIds: ['cspop4m5whpw8ckq51aafe58a'],
unique: false,
createdAt: 1764864417836,
},
],
x: 0,
y: 0,
color: '#8eb7ff',
isView: false,
createdAt: 1764864417836,
},
],
relationships: [],
dependencies: [],
areas: [],
customTypes: [],
notes: [],
};
const expectedResult: Diagram = {
id: 'mqqwkkod9jb8',
name: 'buckle_db',
createdAt: new Date('2025-12-04T16:00:06.463Z'),
updatedAt: new Date('2025-12-04T16:06:49.070Z'),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: 'r1rp4f64dtpifw7mub089bxy8',
name: 'buckle_diagrams_history',
schema: 'public',
x: 100,
y: 300,
fields: [
{
id: '0t0b4e7irmvw53w4qyz99zqm1',
name: 'rownum',
type: {
name: 'int',
id: 'int',
},
nullable: false,
primaryKey: false,
unique: false,
createdAt: 1764864006454,
increment: true,
},
{
id: 'hugwwfirbk609ejryqqsvw3be',
name: 'id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: false,
primaryKey: true,
unique: true,
createdAt: 1764864006455,
},
{
id: 'd9sw9l864y0s6b9bc990y7ebi',
name: 'action_type',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
characterMaximumLength: '50',
},
{
id: 'tref4xugo95u21hd02j3s5q67',
name: 'diagram_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
{
id: 'v3g2bkhtozhthlnhs3bhu6s4h',
name: 'diagram_name',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
characterMaximumLength: '2500',
},
{
id: '79jq9oamvjw7j5i081pplb4ii',
name: 'database_type',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
characterMaximumLength: '50',
},
{
id: 'dd05cmr4ntvwu5aaeir6s4bco',
name: 'database_edition',
type: {
name: 'varchar',
id: 'varchar',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
characterMaximumLength: '50',
},
{
id: '7589goqn7n2xk3rg5gny817bb',
name: 'diagram_json',
type: {
name: 'json',
id: 'json',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
{
id: 'i5zaobzgwbyn8g9xdcskq4qc8',
name: 'changed_by_user_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
{
id: 'u9uana25zrknsc5g7t8sinegm',
name: 'account_id',
type: {
name: 'uuid',
id: 'uuid',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
{
id: 'jy29dbbfcr4zn5l585y75tob1',
name: 'created_at',
type: {
name: 'timestamp',
id: 'timestamp',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
{
id: 'k5jdbgind50qitigvmpuk00n3',
name: 'updated_at',
type: {
name: 'timestamp',
id: 'timestamp',
},
nullable: true,
primaryKey: false,
unique: false,
createdAt: 1764864006455,
},
],
indexes: [
{
id: 'jzv2yaggsmorqfczz6g60s22y',
name: 'buckle_diagrams_history_new_pkey',
fieldIds: ['hugwwfirbk609ejryqqsvw3be'],
unique: true,
createdAt: 1764864006455,
isPrimaryKey: true,
},
{
id: 'drzp2kp4a74ceiwh0jl8fvuy7',
name: 'index_diagrams_history_on_changed_by_user_id',
fieldIds: ['i5zaobzgwbyn8g9xdcskq4qc8'],
unique: false,
createdAt: 1764864006455,
},
{
id: 'r3gc7sxg5dv8o6ur2z2jen8vf',
name: 'index_diagrams_history_on_diagram_id',
fieldIds: ['tref4xugo95u21hd02j3s5q67'],
unique: false,
createdAt: 1764864006455,
},
{
id: '7v78ps9i99hkwt086wos3dywa',
name: 'index_diagrams_history_on_account_id',
fieldIds: ['u9uana25zrknsc5g7t8sinegm'],
unique: false,
createdAt: 1764864006455,
},
],
color: '#8eb7ff',
isView: false,
isMaterializedView: false,
createdAt: 1764864006455,
},
],
relationships: [],
dependencies: [],
areas: [],
customTypes: [],
notes: [],
};
const result = applyDBMLChanges({
sourceDiagram,
targetDiagram,
});
// Check that the new field is added correctly
expect(result).toEqual(expectedResult);
});
});
describe('Apply DBML Changes - relationships', () => {

View File

@@ -279,19 +279,71 @@ const updateTables = ({
return targetField;
});
// Update indexes by matching on name within the table
// Update indexes - match by name first, then by semantic structure
// Build map of source indexes by name for quick lookup
const sourceIndexesByName = new Map<string, DBIndex>();
sourceTable.indexes?.forEach((index) => {
sourceIndexesByName.set(index.name, index);
});
const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
const sourceIndex = sourceIndexesByName.get(targetIndex.name);
if (sourceIndex) {
// First try to match by name
const sourceIndexByName = sourceIndexesByName.get(targetIndex.name);
if (sourceIndexByName) {
// Names match - preserve source's id, name, and createdAt
return {
...targetIndex,
id: sourceIndex.id,
createdAt: sourceIndex.createdAt,
id: sourceIndexByName.id,
name: sourceIndexByName.name,
createdAt: sourceIndexByName.createdAt,
};
}
// No name match - try semantic match by field IDs, unique, and isPrimaryKey
// Translate target field IDs to source field IDs for comparison
const targetFieldIdsAsSourceIds = targetIndex.fieldIds.map(
(fid) => idMappings.fields[fid] || fid
);
const sourceIndexBySemantic = sourceTable.indexes?.find(
(srcIndex) => {
// Skip if this source index was already matched by name
if (
sourceIndexesByName.has(srcIndex.name) &&
targetTable.indexes?.some(
(ti) => ti.name === srcIndex.name
)
) {
return false;
}
// Compare field IDs (order matters for indexes)
if (
srcIndex.fieldIds.length !==
targetFieldIdsAsSourceIds.length
) {
return false;
}
const fieldsMatch = srcIndex.fieldIds.every(
(fid, i) => fid === targetFieldIdsAsSourceIds[i]
);
if (!fieldsMatch) return false;
// Match unique and isPrimaryKey status
return (
srcIndex.unique === targetIndex.unique &&
!!srcIndex.isPrimaryKey === !!targetIndex.isPrimaryKey
);
}
);
if (sourceIndexBySemantic) {
// Semantic match - keep target's id and createdAt, use source's name
return {
...targetIndex,
name: sourceIndexBySemantic.name,
id: sourceIndexBySemantic.id,
createdAt: sourceIndexBySemantic.createdAt,
};
}
return targetIndex;

View File

@@ -43,9 +43,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
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
@@ -129,8 +126,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(resultField?.name).toBe(sourceField.name);
});
});
console.log('✓ All IDs preserved after DBML round-trip');
});
});
@@ -230,9 +225,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
// 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)
@@ -242,9 +234,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
pgDiagram.tables?.forEach((table) => {
expect(table.schema).toBe('');
console.log(
`✓ PostgreSQL: Table "${table.name}" has no schema (public is default)`
);
});
});
@@ -275,7 +264,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(magicTable?.schema).toBe('fantasy');
expect(questTable?.schema).toBe('adventure');
console.log('✓ Custom schemas preserved correctly');
});
});
@@ -427,8 +415,6 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
expect(currentTable?.id).toBe(original.id);
});
}
console.log('✓ Data integrity maintained through 3 cycles');
});
});
});