mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-30 11:33:58 +00:00
Compare commits
2 Commits
feat/table
...
fix/relati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfcd30269f | ||
|
|
a5f8e56b3c |
@@ -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:.*\]\}/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user