fix(dbml-editor): for some cases that the dbml had issues (#739)

This commit is contained in:
Jonathan Fishner
2025-06-06 19:41:51 +03:00
committed by GitHub
parent 8b86e1c229
commit e0ff198c3f
2 changed files with 152 additions and 83 deletions

View File

@@ -217,7 +217,10 @@ export const exportBaseSQL = ({
sqlScript += ` ${field.name} ${typeName}`; sqlScript += ` ${field.name} ${typeName}`;
// Add size for character types // Add size for character types
if (field.characterMaximumLength) { if (
field.characterMaximumLength &&
parseInt(field.characterMaximumLength) > 0
) {
sqlScript += `(${field.characterMaximumLength})`; sqlScript += `(${field.characterMaximumLength})`;
} else if (field.type.name.toLowerCase().includes('varchar')) { } else if (field.type.name.toLowerCase().includes('varchar')) {
// Keep varchar sizing, but don't apply to TEXT (previously enum) // Keep varchar sizing, but don't apply to TEXT (previously enum)

View File

@@ -184,6 +184,22 @@ const sanitizeSQLforDBML = (sql: string): string => {
}); });
sanitized = processedLines.join('\n'); sanitized = processedLines.join('\n');
// Fix PostgreSQL type casting syntax that the DBML parser doesn't understand
sanitized = sanitized.replace(/::regclass/g, '');
sanitized = sanitized.replace(/: :regclass/g, ''); // Fix corrupted version
// Fix duplicate columns in index definitions
sanitized = sanitized.replace(
/CREATE\s+(?:UNIQUE\s+)?INDEX\s+\S+\s+ON\s+\S+\s*\(([^)]+)\)/gi,
(match, columnList) => {
const columns = columnList
.split(',')
.map((col: string) => col.trim());
const uniqueColumns = [...new Set(columns)]; // Remove duplicates
return match.replace(columnList, uniqueColumns.join(', '));
}
);
// Replace any remaining problematic characters // Replace any remaining problematic characters
sanitized = sanitized.replace(/\?\?/g, '__'); sanitized = sanitized.replace(/\?\?/g, '__');
@@ -316,6 +332,71 @@ const isSQLKeyword = (name: string): boolean => {
return keywords.has(name.toUpperCase()); return keywords.has(name.toUpperCase());
}; };
// Function to remove duplicate relationships from the diagram
const deduplicateRelationships = (diagram: Diagram): Diagram => {
if (!diagram.relationships) return diagram;
const seenRelationships = new Set<string>();
const uniqueRelationships = diagram.relationships.filter((rel) => {
// Create a unique key based on the relationship endpoints
const relationshipKey = `${rel.sourceTableId}-${rel.sourceFieldId}->${rel.targetTableId}-${rel.targetFieldId}`;
if (seenRelationships.has(relationshipKey)) {
return false; // Skip duplicate
}
seenRelationships.add(relationshipKey);
return true; // Keep unique relationship
});
return {
...diagram,
relationships: uniqueRelationships,
};
};
// Function to append comment statements for renamed tables and fields
const appendRenameComments = (
baseScript: string,
sqlRenamedTables: Map<string, string>,
fieldRenames: Array<{
table: string;
originalName: string;
newName: string;
}>,
finalDiagramForExport: Diagram
): string => {
let script = baseScript;
// Append COMMENTS for tables renamed due to SQL keywords
sqlRenamedTables.forEach((originalName, newName) => {
const escapedOriginal = originalName.replace(/'/g, "\\'");
// Find the table to get its schema
const table = finalDiagramForExport.tables?.find(
(t) => t.name === newName
);
const tableIdentifier = table?.schema
? `"${table.schema}"."${newName}"`
: `"${newName}"`;
script += `\nCOMMENT ON TABLE ${tableIdentifier} IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
// Append COMMENTS for fields renamed due to SQL keyword conflicts
fieldRenames.forEach(({ table, originalName, newName }) => {
const escapedOriginal = originalName.replace(/'/g, "\\'");
// Find the table to get its schema
const tableObj = finalDiagramForExport.tables?.find(
(t) => t.name === table
);
const tableIdentifier = tableObj?.schema
? `"${tableObj.schema}"."${table}"`
: `"${table}"`;
script += `\nCOMMENT ON COLUMN ${tableIdentifier}."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
return script;
};
// Fix DBML formatting to ensure consistent display of char and varchar types // Fix DBML formatting to ensure consistent display of char and varchar types
const normalizeCharTypeFormat = (dbml: string): string => { const normalizeCharTypeFormat = (dbml: string): string => {
// Replace "char (N)" with "char(N)" to match varchar's formatting // Replace "char (N)" with "char(N)" to match varchar's formatting
@@ -402,23 +483,22 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
const cleanDiagram = fixProblematicFieldNames(filteredDiagram); const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
// --- Final sanitization and renaming pass --- // --- Final sanitization and renaming pass ---
// Track tables renamed due to SQL keyword conflicts const shouldRenameKeywords =
currentDiagram.databaseType === DatabaseType.POSTGRESQL ||
currentDiagram.databaseType === DatabaseType.SQLITE;
const sqlRenamedTables = new Map<string, string>(); const sqlRenamedTables = new Map<string, string>();
// Track fields renamed due to SQL keyword conflicts
const fieldRenames: Array<{ const fieldRenames: Array<{
table: string; table: string;
originalName: string; originalName: string;
newName: string; newName: string;
}> = []; }> = [];
const finalDiagramForExport: Diagram = {
...cleanDiagram, const processTable = (table: DBTable) => {
tables:
cleanDiagram.tables?.map((table) => {
const originalName = table.name; const originalName = table.name;
// Sanitize table name
let safeTableName = originalName.replace(/[^\w]/g, '_'); let safeTableName = originalName.replace(/[^\w]/g, '_');
// Rename if SQL keyword
if (isSQLKeyword(safeTableName)) { // Rename table if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(safeTableName)) {
const newName = `${safeTableName}_table`; const newName = `${safeTableName}_table`;
sqlRenamedTables.set(newName, originalName); sqlRenamedTables.set(newName, originalName);
safeTableName = newName; safeTableName = newName;
@@ -426,28 +506,25 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
const fieldNameCounts = new Map<string, number>(); const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => { const processedFields = table.fields.map((field) => {
const originalSafeName = field.name.replace( const originalSafeName = field.name.replace(/[^\w]/g, '_');
/[^\w]/g,
'_'
);
let finalSafeName = originalSafeName; let finalSafeName = originalSafeName;
const count =
fieldNameCounts.get(originalSafeName) || 0;
// Handle duplicate field names
const count = fieldNameCounts.get(originalSafeName) || 0;
if (count > 0) { if (count > 0) {
finalSafeName = `${originalSafeName}_${count + 1}`; // Rename duplicate finalSafeName = `${originalSafeName}_${count + 1}`;
} }
fieldNameCounts.set(originalSafeName, count + 1); fieldNameCounts.set(originalSafeName, count + 1);
// Create a copy and remove comments // Create sanitized field
const sanitizedField: DBField = { const sanitizedField: DBField = {
...field, ...field,
name: finalSafeName, name: finalSafeName,
}; };
delete sanitizedField.comments; delete sanitizedField.comments;
// Rename if SQL keyword // Rename field if SQL keyword (PostgreSQL only)
if (isSQLKeyword(finalSafeName)) { if (shouldRenameKeywords && isSQLKeyword(finalSafeName)) {
const newFieldName = `${finalSafeName}_field`; const newFieldName = `${finalSafeName}_field`;
fieldRenames.push({ fieldRenames.push({
table: safeTableName, table: safeTableName,
@@ -456,13 +533,14 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
}); });
sanitizedField.name = newFieldName; sanitizedField.name = newFieldName;
} }
return sanitizedField; return sanitizedField;
}); });
return { return {
...table, ...table,
name: safeTableName, name: safeTableName,
fields: processedFields, // Use fields with renamed duplicates fields: processedFields,
indexes: (table.indexes || []).map((index) => ({ indexes: (table.indexes || []).map((index) => ({
...index, ...index,
name: index.name name: index.name
@@ -470,13 +548,17 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
: `idx_${Math.random().toString(36).substring(2, 8)}`, : `idx_${Math.random().toString(36).substring(2, 8)}`,
})), })),
}; };
}) ?? [], };
const finalDiagramForExport: Diagram = deduplicateRelationships({
...cleanDiagram,
tables: cleanDiagram.tables?.map(processTable) ?? [],
relationships: relationships:
cleanDiagram.relationships?.map((rel, index) => ({ cleanDiagram.relationships?.map((rel, index) => ({
...rel, ...rel,
name: `fk_${index}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`, name: `fk_${index}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`,
})) ?? [], })) ?? [],
} as Diagram; } as Diagram);
let standard = ''; let standard = '';
let inline = ''; let inline = '';
@@ -494,31 +576,15 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
baseScript = sanitizeSQLforDBML(baseScript); baseScript = sanitizeSQLforDBML(baseScript);
// Append COMMENTS for tables renamed due to SQL keywords // Append comments for renamed tables and fields (PostgreSQL only)
sqlRenamedTables.forEach((originalName, newName) => { if (shouldRenameKeywords) {
const escapedOriginal = originalName.replace(/'/g, "\\'"); baseScript = appendRenameComments(
// Find the table to get its schema baseScript,
const table = finalDiagramForExport.tables?.find( sqlRenamedTables,
(t) => t.name === newName fieldRenames,
finalDiagramForExport
); );
const tableIdentifier = table?.schema }
? `"${table.schema}"."${newName}"`
: `"${newName}"`;
baseScript += `\nCOMMENT ON TABLE ${tableIdentifier} IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
// Append COMMENTS for fields renamed due to SQL keyword conflicts
fieldRenames.forEach(({ table, originalName, newName }) => {
const escapedOriginal = originalName.replace(/'/g, "\\'");
// Find the table to get its schema
const tableObj = finalDiagramForExport.tables?.find(
(t) => t.name === table
);
const tableIdentifier = tableObj?.schema
? `"${tableObj.schema}"."${table}"`
: `"${table}"`;
baseScript += `\nCOMMENT ON COLUMN ${tableIdentifier}."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
standard = normalizeCharTypeFormat( standard = normalizeCharTypeFormat(
importer.import( importer.import(