mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-30 11:33:58 +00:00
fix: add support for parsing default values in DBML (#948)
This commit is contained in:
@@ -13,6 +13,55 @@ import { exportSQLite } from './export-per-type/sqlite';
|
|||||||
import { exportMySQL } from './export-per-type/mysql';
|
import { exportMySQL } from './export-per-type/mysql';
|
||||||
import { escapeSQLComment } from './export-per-type/common';
|
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
|
// Function to simplify verbose data type names
|
||||||
const simplifyDataType = (typeName: string): string => {
|
const simplifyDataType = (typeName: string): string => {
|
||||||
const typeMap: Record<string, string> = {};
|
const typeMap: Record<string, string> = {};
|
||||||
@@ -391,7 +440,9 @@ export const exportBaseSQL = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
// Format default value with proper quoting
|
||||||
|
const formattedDefault = formatDefaultValue(fieldDefault);
|
||||||
|
sqlScript += ` DEFAULT ${formattedDefault}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,4 +295,51 @@ describe('DBML Import cases', () => {
|
|||||||
it('should handle case 2 - tables with relationships', async () => {
|
it('should handle case 2 - tables with relationships', async () => {
|
||||||
await testDBMLImportCase('2');
|
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()');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ interface DBMLField {
|
|||||||
precision?: number | null;
|
precision?: number | null;
|
||||||
scale?: number | null;
|
scale?: number | null;
|
||||||
note?: string | { value: string } | null;
|
note?: string | { value: string } | null;
|
||||||
|
default?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DBMLIndexColumn {
|
interface DBMLIndexColumn {
|
||||||
@@ -334,6 +335,20 @@ export const importDBMLToDiagram = async (
|
|||||||
schema: schemaName,
|
schema: schemaName,
|
||||||
note: table.note,
|
note: table.note,
|
||||||
fields: table.fields.map((field): DBMLField => {
|
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 {
|
return {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
@@ -342,6 +357,7 @@ export const importDBMLToDiagram = async (
|
|||||||
not_null: field.not_null,
|
not_null: field.not_null,
|
||||||
increment: field.increment,
|
increment: field.increment,
|
||||||
note: field.note,
|
note: field.note,
|
||||||
|
default: defaultValue,
|
||||||
...getFieldExtraAttributes(field, allEnums),
|
...getFieldExtraAttributes(field, allEnums),
|
||||||
} satisfies DBMLField;
|
} satisfies DBMLField;
|
||||||
}),
|
}),
|
||||||
@@ -488,6 +504,7 @@ export const importDBMLToDiagram = async (
|
|||||||
precision: field.precision,
|
precision: field.precision,
|
||||||
scale: field.scale,
|
scale: field.scale,
|
||||||
...(fieldComment ? { comments: fieldComment } : {}),
|
...(fieldComment ? { comments: fieldComment } : {}),
|
||||||
|
...(field.default ? { default: field.default } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user