fix: export dbml issues after upgrade version (#883)

* fix: dbml export

* fix

* fix

* fix

* fix

* fix
This commit is contained in:
Guy Ben-Aharon
2025-08-27 20:44:18 +03:00
committed by GitHub
parent d8e0bc7db8
commit 07937a2f51
10 changed files with 80943 additions and 20 deletions

2
.nvmrc
View File

@@ -1 +1 @@
v22.5.1 v22.18.0

View File

@@ -106,7 +106,7 @@ describe('DBML Export - SQL Generation Tests', () => {
}); });
// Should contain composite primary key syntax // Should contain composite primary key syntax
expect(sql).toContain('PRIMARY KEY (spell_id, component_id)'); expect(sql).toContain('PRIMARY KEY ("spell_id", "component_id")');
// Should NOT contain individual PRIMARY KEY constraints // Should NOT contain individual PRIMARY KEY constraints
expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
expect(sql).not.toMatch( expect(sql).not.toMatch(
@@ -192,7 +192,7 @@ describe('DBML Export - SQL Generation Tests', () => {
// Should contain composite primary key constraint // Should contain composite primary key constraint
expect(sql).toContain( expect(sql).toContain(
'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)' 'PRIMARY KEY ("master_user_id", "tenant_id", "tenant_user_id")'
); );
// Should NOT contain the duplicate index for the primary key fields // Should NOT contain the duplicate index for the primary key fields
@@ -245,7 +245,7 @@ describe('DBML Export - SQL Generation Tests', () => {
}); });
// Should contain inline PRIMARY KEY // Should contain inline PRIMARY KEY
expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); expect(sql).toMatch(/"id"\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
// Should NOT contain separate PRIMARY KEY constraint // Should NOT contain separate PRIMARY KEY constraint
expect(sql).not.toContain('PRIMARY KEY (id)'); expect(sql).not.toContain('PRIMARY KEY (id)');
}); });
@@ -306,8 +306,8 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).not.toContain('DEFAULT has default'); expect(sql).not.toContain('DEFAULT has default');
expect(sql).not.toContain('DEFAULT DEFAULT has default'); expect(sql).not.toContain('DEFAULT DEFAULT has default');
// The fields should still be in the table // The fields should still be in the table
expect(sql).toContain('is_active boolean'); expect(sql).toContain('"is_active" boolean');
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int expect(sql).toContain('"stock_count" integer NOT NULL'); // integer gets simplified to int
}); });
it('should handle valid default values correctly', () => { it('should handle valid default values correctly', () => {
@@ -429,8 +429,8 @@ describe('DBML Export - SQL Generation Tests', () => {
}); });
// Should convert NOW to NOW() and ('now') to now() // Should convert NOW to NOW() and ('now') to now()
expect(sql).toContain('created_at timestamp DEFAULT NOW'); expect(sql).toContain('"created_at" timestamp DEFAULT NOW');
expect(sql).toContain('updated_at timestamp DEFAULT now()'); expect(sql).toContain('"updated_at" timestamp DEFAULT now()');
}); });
}); });
@@ -485,9 +485,9 @@ describe('DBML Export - SQL Generation Tests', () => {
}); });
// Should handle char with explicit length // Should handle char with explicit length
expect(sql).toContain('element_code char(2)'); expect(sql).toContain('"element_code" char(2)');
// Should add default length for char without length // Should add default length for char without length
expect(sql).toContain('status char(1)'); expect(sql).toContain('"status" char(1)');
}); });
it('should not have spaces between char and parentheses', () => { it('should not have spaces between char and parentheses', () => {
@@ -715,7 +715,7 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).toContain('CREATE TABLE "guild_members"'); expect(sql).toContain('CREATE TABLE "guild_members"');
// Should create foreign key // Should create foreign key
expect(sql).toContain( expect(sql).toContain(
'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES "guilds" (id);' 'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY ("guild_id") REFERENCES "guilds" ("id");'
); );
}); });
}); });
@@ -939,9 +939,9 @@ describe('DBML Export - SQL Generation Tests', () => {
}); });
// Should include precision and scale // Should include precision and scale
expect(sql).toContain('amount numeric(15, 2)'); expect(sql).toContain('"amount" numeric(15, 2)');
// Should include precision only when scale is not provided // Should include precision only when scale is not provided
expect(sql).toContain('interest_rate numeric(5)'); expect(sql).toContain('"interest_rate" numeric(5)');
}); });
}); });
}); });

View File

@@ -424,7 +424,7 @@ describe('Quoted Identifiers - Special Characters Handling', () => {
}); });
expect(sql).toContain( expect(sql).toContain(
'ALTER TABLE "user-profiles" ADD CONSTRAINT fk_profiles_accounts FOREIGN KEY (account_id) REFERENCES "user-accounts" (id)' 'ALTER TABLE "user-profiles" ADD CONSTRAINT fk_profiles_accounts FOREIGN KEY ("account_id") REFERENCES "user-accounts" ("id")'
); );
}); });
@@ -493,7 +493,7 @@ describe('Quoted Identifiers - Special Characters Handling', () => {
}); });
expect(sql).toContain( expect(sql).toContain(
'ALTER TABLE "app-data"."user profiles" ADD CONSTRAINT fk_profiles_accounts FOREIGN KEY (account_id) REFERENCES "auth-db"."user accounts" (id)' 'ALTER TABLE "app-data"."user profiles" ADD CONSTRAINT fk_profiles_accounts FOREIGN KEY ("account_id") REFERENCES "auth-db"."user accounts" ("id")'
); );
}); });
}); });

View File

@@ -52,6 +52,29 @@ const getQuotedTableName = (
} }
}; };
const getQuotedFieldName = (
fieldName: string,
isDBMLFlow: boolean = false
): string => {
// Check if a name is already quoted
const isAlreadyQuoted = (name: string) => {
return (
(name.startsWith('"') && name.endsWith('"')) ||
(name.startsWith('`') && name.endsWith('`')) ||
(name.startsWith('[') && name.endsWith(']'))
);
};
if (isAlreadyQuoted(fieldName)) {
return fieldName;
}
// For DBML flow, always quote field names
// Otherwise, only quote if it contains special characters
const needsQuoting = /[^a-zA-Z0-9_]/.test(fieldName) || isDBMLFlow;
return needsQuoting ? `"${fieldName}"` : fieldName;
};
export const exportBaseSQL = ({ export const exportBaseSQL = ({
diagram, diagram,
targetDatabaseType, targetDatabaseType,
@@ -270,7 +293,8 @@ export const exportBaseSQL = ({
typeName = 'char'; typeName = 'char';
} }
sqlScript += ` ${field.name} ${typeName}`; const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow);
sqlScript += ` ${quotedFieldName} ${typeName}`;
// Add size for character types // Add size for character types
if ( if (
@@ -367,7 +391,9 @@ export const exportBaseSQL = ({
hasCompositePrimaryKey || hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name) (primaryKeyFields.length === 1 && pkIndex?.name)
) { ) {
const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', '); const pkFieldNames = primaryKeyFields
.map((f) => getQuotedFieldName(f.name, isDBMLFlow))
.join(', ');
if (pkIndex?.name) { if (pkIndex?.name) {
sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`; sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
} else { } else {
@@ -388,7 +414,11 @@ export const exportBaseSQL = ({
table.fields.forEach((field) => { table.fields.forEach((field) => {
// Add column comment (only for databases that support COMMENT ON syntax) // Add column comment (only for databases that support COMMENT ON syntax)
if (field.comments && supportsCommentOn) { if (field.comments && supportsCommentOn) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${escapeSQLComment(field.comments)}';\n`; const quotedFieldName = getQuotedFieldName(
field.name,
isDBMLFlow
);
sqlScript += `COMMENT ON COLUMN ${tableName}.${quotedFieldName} IS '${escapeSQLComment(field.comments)}';\n`;
} }
}); });
@@ -420,7 +450,7 @@ export const exportBaseSQL = ({
} }
const fieldNames = indexFields const fieldNames = indexFields
.map((field) => field.name) .map((field) => getQuotedFieldName(field.name, isDBMLFlow))
.join(', '); .join(', ');
if (fieldNames) { if (fieldNames) {
@@ -500,8 +530,16 @@ export const exportBaseSQL = ({
const fkTableName = getQuotedTableName(fkTable, isDBMLFlow); const fkTableName = getQuotedTableName(fkTable, isDBMLFlow);
const refTableName = getQuotedTableName(refTable, isDBMLFlow); const refTableName = getQuotedTableName(refTable, isDBMLFlow);
const quotedFkFieldName = getQuotedFieldName(
fkField.name,
isDBMLFlow
);
const quotedRefFieldName = getQuotedFieldName(
refField.name,
isDBMLFlow
);
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`; sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\n`;
} }
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
Table "bruit"."100-AAB-CABAS-Mdap" {
"qgs_fid" int [pk, not null]
"geom" geometry
"from" decimal(8,2)
"to" decimal(8,2)
"period" nvarchar(500)
"objectid" float
"insee" float
"nom" nvarchar(500)
"code_posta" int
"ut" nvarchar(500)
"territoire" nvarchar(500)
"surface" float
"perimetre" float
"ccodter" float
"numcom" nvarchar(500)
}

View File

@@ -0,0 +1 @@
{"id":"mqqwkkodtkfm","name":"NTP_CANPT-db","createdAt":"2025-08-27T17:03:48.994Z","updatedAt":"2025-08-27T17:12:54.617Z","databaseType":"sql_server","tables":[{"id":"e4qecug35j4b7q75u1j3sdca5","name":"100-AAB-CABAS-Mdap","schema":"bruit","x":100,"y":100,"fields":[{"id":"04liixxb8yenudc6gqjjbgm1r","name":"qgs_fid","type":{"id":"int","name":"int"},"primaryKey":true,"unique":true,"nullable":false,"createdAt":1739267036546},{"id":"hr29n1e1jgybuac3gcerk7jyi","name":"geom","type":{"id":"geometry","name":"geometry"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"jcqh683op52ovfwqwe0w0i2or","name":"from","type":{"id":"decimal","name":"decimal"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"precision":8,"scale":2},{"id":"xev33ok0oqqom2n1tabpp5eds","name":"to","type":{"id":"decimal","name":"decimal"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"precision":8,"scale":2},{"id":"pj36qhdpl0vice9tsyiaaef4l","name":"period","type":{"id":"nvarchar","name":"nvarchar"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"collation":"French_CI_AS"},{"id":"l4ce4a68j9h7l46p8dg5qi09u","name":"objectid","type":{"id":"float","name":"float"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"fi4s2aahfjdeelfkgnrk4q5mk","name":"insee","type":{"id":"float","name":"float"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"ujsajf0t5xg0td614lpxk32py","name":"nom","type":{"id":"nvarchar","name":"nvarchar"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"collation":"French_CI_AS"},{"id":"9j0c54ez2t5dgr0ybzd0ksbuz","name":"code_posta","type":{"id":"int","name":"int"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"gybxvu42odvvjyfoe9zdn7tul","name":"ut","type":{"id":"nvarchar","name":"nvarchar"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"collation":"French_CI_AS"},{"id":"qon7xs001v9q8frad6jr9lrho","name":"territoire","type":{"id":"nvarchar","name":"nvarchar"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"collation":"French_CI_AS"},{"id":"aeqrfvw5dvig7t8zyjfiri707","name":"surface","type":{"id":"float","name":"float"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"eqbcy7gfd49a3a6ds6ne6fmzd","name":"perimetre","type":{"id":"float","name":"float"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"cbxmodo9l3keqxapqnlfjnqy2","name":"ccodter","type":{"id":"float","name":"float"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546},{"id":"c3j131aycof5kgyiypva428l3","name":"numcom","type":{"id":"nvarchar","name":"nvarchar"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1739267036546,"collation":"French_CI_AS"}],"indexes":[],"color":"#8a61f5","isView":false,"isMaterializedView":false,"createdAt":1739267036546,"diagramId":"mqqwkkodtkfm","expanded":true}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}

View File

@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { diagramFromJSONInput } from '@/lib/export-import-utils';
import { generateDBMLFromDiagram } from '../dbml-export';
import * as fs from 'fs';
import * as path from 'path';
describe('DBML Export - Diagram Case 1 Tests', () => {
it('should handle case 1 diagram', { timeout: 30000 }, async () => {
// Read the JSON file
const jsonPath = path.join(__dirname, 'cases', '1.json');
const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
// Parse the JSON and convert to diagram
const diagram = diagramFromJSONInput(jsonContent);
// Generate DBML from the diagram
const result = generateDBMLFromDiagram(diagram);
const generatedDBML = result.standardDbml;
// Read the expected DBML file
const dbmlPath = path.join(__dirname, 'cases', '1.dbml');
const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
// Compare the generated DBML with the expected DBML
expect(generatedDBML).toBe(expectedDBML);
});
it('should handle case 2 diagram', { timeout: 30000 }, async () => {
// Read the JSON file
const jsonPath = path.join(__dirname, 'cases', '2.json');
const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
// Parse the JSON and convert to diagram
const diagram = diagramFromJSONInput(jsonContent);
// Generate DBML from the diagram
const result = generateDBMLFromDiagram(diagram);
const generatedDBML = result.standardDbml;
// Read the expected DBML file
const dbmlPath = path.join(__dirname, 'cases', '2.dbml');
const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
// Compare the generated DBML with the expected DBML
expect(generatedDBML).toBe(expectedDBML);
});
});

View File

@@ -32,6 +32,7 @@ export default defineConfig({
}, },
build: { build: {
rollupOptions: { rollupOptions: {
external: (id) => /__test__/.test(id),
output: { output: {
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if ( if (