mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 14:03:15 +00:00 
			
		
		
		
	fix(dbml): support spaces in names (#794)
This commit is contained in:
		@@ -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"]'
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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) => ({
 | 
			
		||||
            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}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`,
 | 
			
		||||
            })) ?? [],
 | 
			
		||||
                    name: `fk_${index}_${safeName}`,
 | 
			
		||||
                };
 | 
			
		||||
            }) ?? [],
 | 
			
		||||
    } as Diagram);
 | 
			
		||||
 | 
			
		||||
    let standard = '';
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user