fix(dbml): support spaces in names (#794)

This commit is contained in:
Guy Ben-Aharon
2025-07-27 19:44:43 +03:00
committed by GitHub
parent a93ec2cab9
commit 8f27f10dec
2 changed files with 176 additions and 18 deletions

View File

@@ -213,4 +213,139 @@ describe('DBML Export - Issue Fixes', () => {
// The foreign key is on the users.tenant_id field, referencing service.tenant.id
expect(result.inlineDbml).toContain('ref: < "service"."tenant"."id"');
});
it('should wrap table and field names with spaces in quotes instead of replacing with underscores', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Test',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
tables: [
{
id: 'table1',
name: 'user profile',
x: 0,
y: 0,
fields: [
{
id: 'field1',
name: 'user id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'field2',
name: 'full name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: true,
unique: false,
collation: null,
default: null,
characterMaximumLength: '255',
createdAt: Date.now(),
},
],
indexes: [
{
id: 'idx1',
name: 'idx user name',
unique: false,
fieldIds: ['field2'],
createdAt: Date.now(),
},
],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'table2',
name: 'order details',
x: 0,
y: 0,
fields: [
{
id: 'field3',
name: 'order id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'field4',
name: 'user id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'rel1',
name: 'fk order user',
sourceTableId: 'table2',
sourceFieldId: 'field4',
targetTableId: 'table1',
targetFieldId: 'field1',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
};
const result = generateDBMLFromDiagram(diagram);
// Check that table names with spaces are wrapped in quotes
expect(result.standardDbml).toContain('Table "user profile"');
expect(result.standardDbml).toContain('Table "order details"');
// Check that field names with spaces are wrapped in quotes
expect(result.standardDbml).toContain('"user id" bigint');
expect(result.standardDbml).toContain('"full name" varchar(255)');
expect(result.standardDbml).toContain('"order id" bigint');
// Check that index names with spaces are wrapped in quotes (in DBML format)
expect(result.standardDbml).toContain('[name: "idx user name"]');
// Check that relationship names with spaces are replaced with underscores in constraint names
expect(result.standardDbml).toContain('Ref "fk_0_fk_order_user"');
// Verify that spaces are NOT replaced with underscores
expect(result.standardDbml).not.toContain('user_profile');
expect(result.standardDbml).not.toContain('user_id');
expect(result.standardDbml).not.toContain('full_name');
expect(result.standardDbml).not.toContain('order_details');
expect(result.standardDbml).not.toContain('order_id');
expect(result.standardDbml).not.toContain('idx_user_name');
// Check inline DBML as well - the ref is on the order details table
expect(result.inlineDbml).toContain(
'"user id" bigint [not null, ref: < "user profile"."user id"]'
);
});
});

View File

@@ -603,26 +603,40 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
const processTable = (table: DBTable) => {
const originalName = table.name;
let safeTableName = originalName.replace(/[^\w]/g, '_');
let safeTableName = originalName;
// If name contains spaces or special characters, wrap in quotes
if (/[^\w]/.test(originalName)) {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
// Rename table if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(safeTableName)) {
const newName = `${safeTableName}_table`;
if (shouldRenameKeywords && isSQLKeyword(originalName)) {
const newName = `${originalName}_table`;
sqlRenamedTables.set(newName, originalName);
safeTableName = newName;
safeTableName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
}
const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => {
const originalSafeName = field.name.replace(/[^\w]/g, '_');
let finalSafeName = originalSafeName;
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(originalSafeName) || 0;
const count = fieldNameCounts.get(field.name) || 0;
if (count > 0) {
finalSafeName = `${originalSafeName}_${count + 1}`;
const newName = `${field.name}_${count + 1}`;
finalSafeName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
}
fieldNameCounts.set(originalSafeName, count + 1);
fieldNameCounts.set(field.name, count + 1);
// Create sanitized field
const sanitizedField: DBField = {
@@ -632,14 +646,16 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
delete sanitizedField.comments;
// Rename field if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(finalSafeName)) {
const newFieldName = `${finalSafeName}_field`;
if (shouldRenameKeywords && isSQLKeyword(field.name)) {
const newFieldName = `${field.name}_field`;
fieldRenames.push({
table: safeTableName,
originalName: finalSafeName,
originalName: field.name,
newName: newFieldName,
});
sanitizedField.name = newFieldName;
sanitizedField.name = /[^\w]/.test(newFieldName)
? `"${newFieldName.replace(/"/g, '\\"')}"`
: newFieldName;
}
return sanitizedField;
@@ -652,7 +668,9 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
indexes: (table.indexes || []).map((index) => ({
...index,
name: index.name
? index.name.replace(/[^\w]/g, '_')
? /[^\w]/.test(index.name)
? `"${index.name.replace(/"/g, '\\"')}"`
: index.name
: `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
};
@@ -662,10 +680,15 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
...cleanDiagram,
tables: cleanDiagram.tables?.map(processTable) ?? [],
relationships:
cleanDiagram.relationships?.map((rel, index) => ({
...rel,
name: `fk_${index}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`,
})) ?? [],
cleanDiagram.relationships?.map((rel, index) => {
const safeName = rel.name
? rel.name.replace(/[^\w]/g, '_')
: Math.random().toString(36).substring(2, 8);
return {
...rel,
name: `fk_${index}_${safeName}`,
};
}) ?? [],
} as Diagram);
let standard = '';