mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-15 03:11:30 +00:00
fix: dbml export renaming fields bug (#921)
This commit is contained in:
@@ -469,10 +469,16 @@ export const exportBaseSQL = ({
|
||||
.join(', ');
|
||||
|
||||
if (fieldNames) {
|
||||
const indexName =
|
||||
const rawIndexName =
|
||||
table.schema && !isDBMLFlow
|
||||
? `${table.schema}_${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`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { exportBaseSQL } from '@/lib/data/sql-export/export-sql-script';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
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 { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||
|
||||
@@ -502,24 +501,6 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
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
|
||||
const deduplicateRelationships = (diagram: Diagram): 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
|
||||
const normalizeCharTypeFormat = (dbml: string): string => {
|
||||
// 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)
|
||||
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
|
||||
|
||||
// --- Final sanitization and renaming pass ---
|
||||
// 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;
|
||||
}> = [];
|
||||
|
||||
// Simplified processing - just handle duplicate field names
|
||||
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 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
|
||||
const count = fieldNameCounts.get(field.name) || 0;
|
||||
if (count > 0) {
|
||||
const newName = `${field.name}_${count + 1}`;
|
||||
finalSafeName = /[^\w]/.test(newName)
|
||||
? `"${newName.replace(/"/g, '\\"')}"`
|
||||
: newName;
|
||||
return {
|
||||
...field,
|
||||
name: newName,
|
||||
};
|
||||
}
|
||||
fieldNameCounts.set(field.name, count + 1);
|
||||
|
||||
// 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 field;
|
||||
});
|
||||
|
||||
return {
|
||||
...table,
|
||||
name: safeTableName,
|
||||
fields: processedFields,
|
||||
indexes: (table.indexes || [])
|
||||
.filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
|
||||
.map((index) => ({
|
||||
...index,
|
||||
name: index.name
|
||||
? /[^\w]/.test(index.name)
|
||||
? `"${index.name.replace(/"/g, '\\"')}"`
|
||||
: index.name
|
||||
: `idx_${Math.random().toString(36).substring(2, 8)}`,
|
||||
name:
|
||||
index.name ||
|
||||
`idx_${Math.random().toString(36).substring(2, 8)}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -979,19 +846,6 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
||||
|
||||
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(
|
||||
normalizeCharTypeFormat(
|
||||
fixMultilineTableNames(
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
||||
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 = `
|
||||
Table "magic_items" {
|
||||
"id" bigint [pk]
|
||||
@@ -197,10 +197,9 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
||||
|
||||
const exported = generateDBMLFromDiagram(diagram);
|
||||
|
||||
// For PostgreSQL, keywords should be renamed in export
|
||||
expect(exported.standardDbml).toContain('Order_field');
|
||||
expect(exported.standardDbml).toContain('Yes_field');
|
||||
expect(exported.standardDbml).toContain('No_field');
|
||||
expect(exported.standardDbml).toContain('Order');
|
||||
expect(exported.standardDbml).toContain('Yes');
|
||||
expect(exported.standardDbml).toContain('No');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user