Compare commits

...

2 Commits

3 changed files with 308 additions and 14 deletions

View File

@@ -957,4 +957,247 @@ describe('DBML Export - Issue Fixes', () => {
'(email, created_at) [name: "idx_email_created"]'
);
});
it('should handle tables with multiple relationships correctly', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Test',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
tables: [
{
id: 'users',
name: 'users',
x: 0,
y: 0,
fields: [
{
id: 'users_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'posts',
name: 'posts',
x: 0,
y: 0,
fields: [
{
id: 'posts_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'posts_user_id',
name: 'user_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'reviews',
name: 'reviews',
x: 0,
y: 0,
fields: [
{
id: 'reviews_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'reviews_user_id',
name: 'user_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'user_activities',
name: 'user_activities',
x: 0,
y: 0,
fields: [
{
id: 'activities_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'activities_entity_id',
name: 'entity_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'activities_type',
name: 'activity_type',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: true,
unique: false,
collation: null,
default: null,
characterMaximumLength: '50',
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'rel1',
name: 'fk_posts_user',
sourceTableId: 'posts',
sourceFieldId: 'posts_user_id',
targetTableId: 'users',
targetFieldId: 'users_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel2',
name: 'fk_reviews_user',
sourceTableId: 'reviews',
sourceFieldId: 'reviews_user_id',
targetTableId: 'users',
targetFieldId: 'users_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel3',
name: 'fk_activities_posts',
sourceTableId: 'user_activities',
sourceFieldId: 'activities_entity_id',
targetTableId: 'posts',
targetFieldId: 'posts_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel4',
name: 'fk_activities_reviews',
sourceTableId: 'user_activities',
sourceFieldId: 'activities_entity_id',
targetTableId: 'reviews',
targetFieldId: 'reviews_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
};
const result = generateDBMLFromDiagram(diagram);
// Debug output removed
// console.log('Inline DBML:', result.inlineDbml);
// Check standard DBML output
expect(result.standardDbml).toContain('Table "users" {');
expect(result.standardDbml).toContain('Table "posts" {');
expect(result.standardDbml).toContain('Table "reviews" {');
expect(result.standardDbml).toContain('Table "user_activities" {');
// Check that the entity_id field in user_activities has multiple relationships in inline DBML
// The field should have both references in a single bracket
expect(result.inlineDbml).toContain(
'"entity_id" int [not null, ref: < "posts"."id", ref: < "reviews"."id"]'
);
// Check that standard DBML has separate Ref entries for each relationship
expect(result.standardDbml).toContain(
'Ref "fk_0_fk_posts_user":"users"."id" < "posts"."user_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_1_fk_reviews_user":"users"."id" < "reviews"."user_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_2_fk_activities_posts":"posts"."id" < "user_activities"."entity_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_3_fk_activities_reviews":"reviews"."id" < "user_activities"."entity_id"'
);
// No automatic comment is added for fields with multiple relationships
// Check proper formatting - closing brace should be on a new line
expect(result.inlineDbml).toMatch(
/Table "user_activities" \{\s*\n\s*"id".*\n\s*"entity_id".*\]\s*\n\s*"activity_type".*\n\s*\}/
);
// Ensure no closing brace appears on the same line as a field with inline refs
expect(result.inlineDbml).not.toMatch(/\[.*ref:.*\]\}/);
});
});

View File

@@ -286,9 +286,14 @@ const convertToInlineRefs = (dbml: string): string => {
// Create a map for faster table lookup
const tableMap = new Map(Object.entries(tables));
// 1. Add inline refs to table contents
// 1. First, collect all refs per field
const fieldRefs = new Map<
string,
{ table: string; refs: string[]; relatedTables: string[] }
>();
refs.forEach((ref) => {
let targetTableName, fieldNameToModify, inlineRefSyntax;
let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
if (ref.direction === '<') {
targetTableName = ref.targetSchema
@@ -299,6 +304,7 @@ const convertToInlineRefs = (dbml: string): string => {
? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
: `"${ref.sourceTable}"."${ref.sourceField}"`;
inlineRefSyntax = `ref: < ${sourceRef}`;
relatedTable = ref.sourceTable;
} else {
targetTableName = ref.sourceSchema
? `${ref.sourceSchema}.${ref.sourceTable}`
@@ -308,13 +314,32 @@ const convertToInlineRefs = (dbml: string): string => {
? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
: `"${ref.targetTable}"."${ref.targetField}"`;
inlineRefSyntax = `ref: > ${targetRef}`;
relatedTable = ref.targetTable;
}
const tableData = tableMap.get(targetTableName);
const fieldKey = `${targetTableName}.${fieldNameToModify}`;
const existing = fieldRefs.get(fieldKey) || {
table: targetTableName,
refs: [],
relatedTables: [],
};
existing.refs.push(inlineRefSyntax);
existing.relatedTables.push(relatedTable);
fieldRefs.set(fieldKey, existing);
});
// 2. Apply all refs to fields
fieldRefs.forEach((fieldData, fieldKey) => {
// fieldKey might be "schema.table.field" or just "table.field"
const lastDotIndex = fieldKey.lastIndexOf('.');
const tableName = fieldKey.substring(0, lastDotIndex);
const fieldName = fieldKey.substring(lastDotIndex + 1);
const tableData = tableMap.get(tableName);
if (tableData) {
// Updated pattern to capture field definition and all existing attributes in brackets
const fieldPattern = new RegExp(
`^([ \t]*"${fieldNameToModify}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
`^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
'gm'
);
let newContent = tableData.content;
@@ -322,11 +347,6 @@ const convertToInlineRefs = (dbml: string): string => {
newContent = newContent.replace(
fieldPattern,
(lineMatch, fieldPart, existingBrackets, commentPart) => {
// Avoid adding duplicate refs
if (lineMatch.includes('ref:')) {
return lineMatch;
}
// Collect all attributes from existing brackets
const allAttributes: string[] = [];
if (existingBrackets) {
@@ -344,8 +364,8 @@ const convertToInlineRefs = (dbml: string): string => {
}
}
// Add the new ref
allAttributes.push(inlineRefSyntax);
// Add all refs for this field
allAttributes.push(...fieldData.refs);
// Combine all attributes into a single bracket
const combinedAttributes = allAttributes.join(', ');
@@ -353,6 +373,7 @@ const convertToInlineRefs = (dbml: string): string => {
// Preserve original spacing from fieldPart
const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
const fieldDefWithoutSpaces = fieldPart.trim();
return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
}
);
@@ -360,7 +381,7 @@ const convertToInlineRefs = (dbml: string): string => {
// Update the table content if modified
if (newContent !== tableData.content) {
tableData.content = newContent;
tableMap.set(targetTableName, tableData);
tableMap.set(tableName, tableData);
}
}
});
@@ -376,9 +397,18 @@ const convertToInlineRefs = (dbml: string): string => {
reconstructedDbml += dbml.substring(lastIndex, tableData.start);
// Preserve the original table definition format but with updated content
const originalTableDef = tableData.fullMatch;
// Ensure the content ends with proper whitespace before the closing brace
let content = tableData.content;
// Check if content ends with a field that has inline refs
if (content.match(/\[.*ref:.*\]\s*$/)) {
// Ensure there's a newline before the closing brace
content = content.trimEnd() + '\n';
}
const updatedTableDef = originalTableDef.replace(
/{[^}]*}/,
`{${tableData.content}}`
`{${content}}`
);
reconstructedDbml += updatedTableDef;
lastIndex = tableData.end;

View File

@@ -294,6 +294,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
setEdges,
showDependenciesOnCanvas,
databaseType,
tables, // Add tables to force edge recreation when table properties change
]);
useEffect(() => {
@@ -997,6 +998,19 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
width: event.data.table.width,
};
// Trigger a dimension change to force React Flow to update the node
onNodesChangeHandler([
{
id: event.data.id,
type: 'dimensions',
dimensions: {
width: event.data.table.width,
height: node.measured?.height || 0,
},
resizing: true, // Set resizing flag to ensure the change is processed
} as NodeDimensionChange,
]);
newOverlappingGraph = findTableOverlapping(
{
node: {
@@ -1051,7 +1065,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
setOverlapGraph(overlappingTablesInDiagram);
}
},
[overlapGraph, setOverlapGraph, getNode, nodes, filteredSchemas]
[
overlapGraph,
setOverlapGraph,
getNode,
nodes,
filteredSchemas,
onNodesChangeHandler,
]
);
events.useSubscription(eventConsumer);