mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-15 11:21:41 +00:00
fix: dbml export renaming fields bug (#921)
This commit is contained in:
@@ -469,10 +469,16 @@ export const exportBaseSQL = ({
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
if (fieldNames) {
|
if (fieldNames) {
|
||||||
const indexName =
|
const rawIndexName =
|
||||||
table.schema && !isDBMLFlow
|
table.schema && !isDBMLFlow
|
||||||
? `${table.schema}_${index.name}`
|
? `${table.schema}_${index.name}`
|
||||||
: index.name;
|
: index.name;
|
||||||
|
// Quote index name if it contains special characters
|
||||||
|
// For DBML flow, also quote if contains special characters
|
||||||
|
const needsQuoting = /[^a-zA-Z0-9_]/.test(rawIndexName);
|
||||||
|
const indexName = needsQuoting
|
||||||
|
? `"${rawIndexName}"`
|
||||||
|
: rawIndexName;
|
||||||
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { exportBaseSQL } from '@/lib/data/sql-export/export-sql-script';
|
|||||||
import type { Diagram } from '@/lib/domain/diagram';
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { type DBField } from '@/lib/domain/db-field';
|
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
@@ -502,24 +501,6 @@ const convertToInlineRefs = (dbml: string): string => {
|
|||||||
return cleanedDbml;
|
return cleanedDbml;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to check for DBML reserved keywords
|
|
||||||
const isDBMLKeyword = (name: string): boolean => {
|
|
||||||
const keywords = new Set([
|
|
||||||
'YES',
|
|
||||||
'NO',
|
|
||||||
'TRUE',
|
|
||||||
'FALSE',
|
|
||||||
'NULL', // DBML reserved keywords (boolean literals)
|
|
||||||
]);
|
|
||||||
return keywords.has(name.toUpperCase());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to check for SQL keywords (add more if needed)
|
|
||||||
const isSQLKeyword = (name: string): boolean => {
|
|
||||||
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Common SQL keywords
|
|
||||||
return keywords.has(name.toUpperCase());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to remove duplicate relationships from the diagram
|
// Function to remove duplicate relationships from the diagram
|
||||||
const deduplicateRelationships = (diagram: Diagram): Diagram => {
|
const deduplicateRelationships = (diagram: Diagram): Diagram => {
|
||||||
if (!diagram.relationships) return diagram;
|
if (!diagram.relationships) return diagram;
|
||||||
@@ -543,48 +524,6 @@ const deduplicateRelationships = (diagram: Diagram): Diagram => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@@ -843,105 +782,33 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
// Sanitize field names ('from'/'to' in 'relation' table)
|
// Sanitize field names ('from'/'to' in 'relation' table)
|
||||||
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
|
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
|
||||||
|
|
||||||
// --- Final sanitization and renaming pass ---
|
// Simplified processing - just handle duplicate field names
|
||||||
// Only rename keywords for PostgreSQL/SQLite
|
|
||||||
// For other databases, we'll wrap problematic names in quotes instead
|
|
||||||
const shouldRenameKeywords =
|
|
||||||
diagram.databaseType === DatabaseType.POSTGRESQL ||
|
|
||||||
diagram.databaseType === DatabaseType.SQLITE;
|
|
||||||
const sqlRenamedTables = new Map<string, string>();
|
|
||||||
const fieldRenames: Array<{
|
|
||||||
table: string;
|
|
||||||
originalName: string;
|
|
||||||
newName: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
const processTable = (table: DBTable) => {
|
const processTable = (table: DBTable) => {
|
||||||
const originalName = table.name;
|
|
||||||
let safeTableName = originalName;
|
|
||||||
|
|
||||||
// If name contains spaces or special characters, wrap in quotes
|
|
||||||
if (/[^\w]/.test(originalName)) {
|
|
||||||
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename table if it's a keyword (PostgreSQL/SQLite only)
|
|
||||||
if (
|
|
||||||
shouldRenameKeywords &&
|
|
||||||
(isDBMLKeyword(originalName) || isSQLKeyword(originalName))
|
|
||||||
) {
|
|
||||||
const newName = `${originalName}_table`;
|
|
||||||
sqlRenamedTables.set(newName, originalName);
|
|
||||||
safeTableName = /[^\w]/.test(newName)
|
|
||||||
? `"${newName.replace(/"/g, '\\"')}"`
|
|
||||||
: newName;
|
|
||||||
}
|
|
||||||
// For other databases, just quote DBML keywords
|
|
||||||
else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
|
|
||||||
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldNameCounts = new Map<string, number>();
|
const fieldNameCounts = new Map<string, number>();
|
||||||
const processedFields = table.fields.map((field) => {
|
const processedFields = table.fields.map((field) => {
|
||||||
let finalSafeName = field.name;
|
|
||||||
|
|
||||||
// If field name contains spaces or special characters, wrap in quotes
|
|
||||||
if (/[^\w]/.test(field.name)) {
|
|
||||||
finalSafeName = `"${field.name.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle duplicate field names
|
// Handle duplicate field names
|
||||||
const count = fieldNameCounts.get(field.name) || 0;
|
const count = fieldNameCounts.get(field.name) || 0;
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const newName = `${field.name}_${count + 1}`;
|
const newName = `${field.name}_${count + 1}`;
|
||||||
finalSafeName = /[^\w]/.test(newName)
|
return {
|
||||||
? `"${newName.replace(/"/g, '\\"')}"`
|
...field,
|
||||||
: newName;
|
name: newName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
fieldNameCounts.set(field.name, count + 1);
|
fieldNameCounts.set(field.name, count + 1);
|
||||||
|
return field;
|
||||||
// Create sanitized field
|
|
||||||
const sanitizedField: DBField = {
|
|
||||||
...field,
|
|
||||||
name: finalSafeName,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rename field if it's a keyword (PostgreSQL/SQLite only)
|
|
||||||
if (
|
|
||||||
shouldRenameKeywords &&
|
|
||||||
(isDBMLKeyword(field.name) || isSQLKeyword(field.name))
|
|
||||||
) {
|
|
||||||
const newFieldName = `${field.name}_field`;
|
|
||||||
fieldRenames.push({
|
|
||||||
table: safeTableName,
|
|
||||||
originalName: field.name,
|
|
||||||
newName: newFieldName,
|
|
||||||
});
|
|
||||||
sanitizedField.name = /[^\w]/.test(newFieldName)
|
|
||||||
? `"${newFieldName.replace(/"/g, '\\"')}"`
|
|
||||||
: newFieldName;
|
|
||||||
}
|
|
||||||
// For other databases, just quote DBML keywords
|
|
||||||
else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
|
|
||||||
sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitizedField;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...table,
|
...table,
|
||||||
name: safeTableName,
|
|
||||||
fields: processedFields,
|
fields: processedFields,
|
||||||
indexes: (table.indexes || [])
|
indexes: (table.indexes || [])
|
||||||
.filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
|
.filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
|
||||||
.map((index) => ({
|
.map((index) => ({
|
||||||
...index,
|
...index,
|
||||||
name: index.name
|
name:
|
||||||
? /[^\w]/.test(index.name)
|
index.name ||
|
||||||
? `"${index.name.replace(/"/g, '\\"')}"`
|
`idx_${Math.random().toString(36).substring(2, 8)}`,
|
||||||
: index.name
|
|
||||||
: `idx_${Math.random().toString(36).substring(2, 8)}`,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -979,19 +846,6 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
|
|
||||||
baseScript = sanitizeSQLforDBML(baseScript);
|
baseScript = sanitizeSQLforDBML(baseScript);
|
||||||
|
|
||||||
// Append comments for renamed tables and fields (PostgreSQL/SQLite only)
|
|
||||||
if (
|
|
||||||
shouldRenameKeywords &&
|
|
||||||
(sqlRenamedTables.size > 0 || fieldRenames.length > 0)
|
|
||||||
) {
|
|
||||||
baseScript = appendRenameComments(
|
|
||||||
baseScript,
|
|
||||||
sqlRenamedTables,
|
|
||||||
fieldRenames,
|
|
||||||
finalDiagramForExport
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
standard = fixArrayTypes(
|
standard = fixArrayTypes(
|
||||||
normalizeCharTypeFormat(
|
normalizeCharTypeFormat(
|
||||||
fixMultilineTableNames(
|
fixMultilineTableNames(
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
|||||||
expect(artifactsTable?.schema).toBe(''); // No schema = empty string
|
expect(artifactsTable?.schema).toBe(''); // No schema = empty string
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename reserved keywords for PostgreSQL', async () => {
|
it('should handle reserved keywords for PostgreSQL', async () => {
|
||||||
const dbmlContent = `
|
const dbmlContent = `
|
||||||
Table "magic_items" {
|
Table "magic_items" {
|
||||||
"id" bigint [pk]
|
"id" bigint [pk]
|
||||||
@@ -197,10 +197,9 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
|||||||
|
|
||||||
const exported = generateDBMLFromDiagram(diagram);
|
const exported = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
// For PostgreSQL, keywords should be renamed in export
|
expect(exported.standardDbml).toContain('Order');
|
||||||
expect(exported.standardDbml).toContain('Order_field');
|
expect(exported.standardDbml).toContain('Yes');
|
||||||
expect(exported.standardDbml).toContain('Yes_field');
|
expect(exported.standardDbml).toContain('No');
|
||||||
expect(exported.standardDbml).toContain('No_field');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user