mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-10-31 12:03:51 +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) => ({ | ||||
|                 ...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 = ''; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user