diff --git a/src/lib/data/sql-export/export-sql-script.ts b/src/lib/data/sql-export/export-sql-script.ts index fc4a043c..d7b5e21d 100644 --- a/src/lib/data/sql-export/export-sql-script.ts +++ b/src/lib/data/sql-export/export-sql-script.ts @@ -13,6 +13,55 @@ import { exportSQLite } from './export-per-type/sqlite'; import { exportMySQL } from './export-per-type/mysql'; import { escapeSQLComment } from './export-per-type/common'; +// Function to format default values with proper quoting +const formatDefaultValue = (value: string): string => { + const trimmed = value.trim(); + + // SQL keywords and function-like keywords that don't need quotes + const keywords = [ + 'TRUE', + 'FALSE', + 'NULL', + 'CURRENT_TIMESTAMP', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'NOW', + 'GETDATE', + 'NEWID', + 'UUID', + ]; + if (keywords.includes(trimmed.toUpperCase())) { + return trimmed; + } + + // Function calls (contain parentheses) don't need quotes + if (trimmed.includes('(') && trimmed.includes(')')) { + return trimmed; + } + + // Numbers don't need quotes + if (/^-?\d+(\.\d+)?$/.test(trimmed)) { + return trimmed; + } + + // Already quoted strings - keep as is + if ( + (trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"')) + ) { + return trimmed; + } + + // Check if it's a simple identifier (alphanumeric, no spaces) that might be a currency or enum + // These typically don't have spaces and are short (< 10 chars) + if (/^[A-Z][A-Z0-9_]*$/i.test(trimmed) && trimmed.length <= 10) { + return trimmed; // Treat as unquoted identifier (e.g., EUR, USD) + } + + // Everything else needs to be quoted and escaped + return `'${trimmed.replace(/'/g, "''")}'`; +}; + // Function to simplify verbose data type names const simplifyDataType = (typeName: string): string => { const typeMap: Record = {}; @@ -391,7 +440,9 @@ export const exportBaseSQL = ({ } } - sqlScript += ` DEFAULT ${fieldDefault}`; + // Format default value with proper quoting + const formattedDefault = formatDefaultValue(fieldDefault); + sqlScript += ` DEFAULT ${formattedDefault}`; } } diff --git a/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts b/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts index ef8f5dec..170fc02b 100644 --- a/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts +++ b/src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts @@ -295,4 +295,51 @@ describe('DBML Import cases', () => { it('should handle case 2 - tables with relationships', async () => { await testDBMLImportCase('2'); }); + + it('should handle table with default values', async () => { + const dbmlContent = `Table "public"."products" { + "id" bigint [pk, not null] + "name" varchar(255) [not null] + "price" decimal(10,2) [not null, default: 0] + "is_active" boolean [not null, default: true] + "status" varchar(50) [not null, default: "deprecated"] + "description" varchar(100) [default: \`complex "value" with quotes\`] + "created_at" timestamp [not null, default: "now()"] + + Indexes { + (name) [name: "idx_products_name"] + } +}`; + + const result = await importDBMLToDiagram(dbmlContent, { + databaseType: DatabaseType.POSTGRESQL, + }); + + expect(result.tables).toHaveLength(1); + const table = result.tables![0]; + expect(table.name).toBe('products'); + expect(table.fields).toHaveLength(7); + + // Check numeric default (0) + const priceField = table.fields.find((f) => f.name === 'price'); + expect(priceField?.default).toBe('0'); + + // Check boolean default (true) + const isActiveField = table.fields.find((f) => f.name === 'is_active'); + expect(isActiveField?.default).toBe('true'); + + // Check string default with all quotes removed + const statusField = table.fields.find((f) => f.name === 'status'); + expect(statusField?.default).toBe('deprecated'); + + // Check backtick string - all quotes removed + const descField = table.fields.find((f) => f.name === 'description'); + expect(descField?.default).toBe('complex value with quotes'); + + // Check function default with all quotes removed + const createdAtField = table.fields.find( + (f) => f.name === 'created_at' + ); + expect(createdAtField?.default).toBe('now()'); + }); }); diff --git a/src/lib/dbml/dbml-import/dbml-import.ts b/src/lib/dbml/dbml-import/dbml-import.ts index 9ac25d84..b9c7b33d 100644 --- a/src/lib/dbml/dbml-import/dbml-import.ts +++ b/src/lib/dbml/dbml-import/dbml-import.ts @@ -89,6 +89,7 @@ interface DBMLField { precision?: number | null; scale?: number | null; note?: string | { value: string } | null; + default?: string | null; } interface DBMLIndexColumn { @@ -334,6 +335,20 @@ export const importDBMLToDiagram = async ( schema: schemaName, note: table.note, fields: table.fields.map((field): DBMLField => { + // Extract default value and remove all quotes + let defaultValue: string | undefined; + if ( + field.dbdefault !== undefined && + field.dbdefault !== null + ) { + const rawDefault = String( + field.dbdefault.value + ); + // Remove ALL quotes (single, double, backticks) to clean the value + // The SQL export layer will handle adding proper quotes when needed + defaultValue = rawDefault.replace(/['"`]/g, ''); + } + return { name: field.name, type: field.type, @@ -342,6 +357,7 @@ export const importDBMLToDiagram = async ( not_null: field.not_null, increment: field.increment, note: field.note, + default: defaultValue, ...getFieldExtraAttributes(field, allEnums), } satisfies DBMLField; }), @@ -488,6 +504,7 @@ export const importDBMLToDiagram = async ( precision: field.precision, scale: field.scale, ...(fieldComment ? { comments: fieldComment } : {}), + ...(field.default ? { default: field.default } : {}), }; });