fix: dbml editor spaces + refs on schemas (#1023)

This commit is contained in:
Guy Ben-Aharon
2025-12-18 16:08:14 +02:00
committed by GitHub
parent 20208f6a51
commit a5d1f40b6b
9 changed files with 142 additions and 34 deletions

44
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@dbml/core": "^3.14.1",
"@dbml/parse": "^5.3.0",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
@@ -48,7 +48,7 @@
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.55.1",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
@@ -3626,6 +3626,13 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -5411,6 +5418,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -7795,6 +7811,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/matchmediaquery": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
@@ -8744,11 +8772,15 @@
}
},
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/motion": {
"version": "12.23.26",

View File

@@ -20,7 +20,7 @@
"@dbml/core": "^3.14.1",
"@dbml/parse": "^5.3.0",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
@@ -56,7 +56,7 @@
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.55.1",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",

View File

@@ -203,6 +203,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
theme={effectiveTheme}
{...editorProps}
options={{
editContext: false,
readOnly: true,
automaticLayout: true,
scrollBeyondLastLine: false,

View File

@@ -515,6 +515,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
: 'dbml-light'
}
options={{
editContext: false,
formatOnPaste: false, // Never format on paste - we handle it manually
minimap: { enabled: false },
scrollBeyondLastLine: false,

View File

@@ -0,0 +1,30 @@
Table "pokemon"."abilities" {
"abil_id" int [pk, not null]
"abil_name" varchar(500) [not null]
}
Table "pokemon"."pokemon_abilities" {
"pok_id" int [not null]
"abil_id" int [not null]
"is_hidden" int [not null]
"slot" int [not null]
"music" bigint
Indexes {
(pok_id, slot) [pk]
is_hidden [name: "pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden"]
abil_id [name: "pokemon_abilities_pokemon_abil_id"]
}
}
Table "pokemon"."pokemon" {
"pok_id" int [pk, not null]
"pok_name" varchar(500) [not null]
"pok_height" int
"pok_weight" int
"pok_base_experience" int
}
Ref "fk_0_fk_pokemon_abilities_pok_id_pokemon_pok_id":"pokemon"."pokemon"."pok_id" < "pokemon"."pokemon_abilities"."pok_id"
Ref "fk_1_fk_pokemon_abilities_abil_id_abilities_abil_id":"pokemon"."abilities"."abil_id" < "pokemon"."pokemon_abilities"."abil_id"

View File

@@ -0,0 +1,26 @@
Table "pokemon"."abilities" {
"abil_id" int [pk, not null]
"abil_name" varchar(500) [not null]
}
Table "pokemon"."pokemon_abilities" {
"pok_id" int [not null, ref: < "pokemon"."pokemon"."pok_id"]
"abil_id" int [not null, ref: < "pokemon"."abilities"."abil_id"]
"is_hidden" int [not null]
"slot" int [not null]
"music" bigint
Indexes {
(pok_id, slot) [pk]
is_hidden [name: "pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden"]
abil_id [name: "pokemon_abilities_pokemon_abil_id"]
}
}
Table "pokemon"."pokemon" {
"pok_id" int [pk, not null]
"pok_name" varchar(500) [not null]
"pok_height" int
"pok_weight" int
"pok_base_experience" int
}

View File

@@ -0,0 +1 @@
{"id":"0kfb46kt4fqz","name":"SQL Import (postgresql)","createdAt":"2025-12-18T12:58:52.104Z","updatedAt":"2025-12-18T13:08:00.838Z","databaseType":"postgresql","tables":[{"id":"meui8qxt4ui15zn0wbpn8kf4h","name":"abilities","schema":"pokemon","order":13,"fields":[{"id":"40alqf0d9uw4pkhc43yg0xbsy","name":"abil_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":true,"default":"","createdAt":1766062732104,"increment":false},{"id":"3iipmd7o8icmen0jn8124gl9n","name":"abil_name","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false,"characterMaximumLength":"500"}],"indexes":[],"x":-1330.1839360655863,"y":-318.9120909292717,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"ndqyi89iebp8w1my9t4su9nlo","name":"pokemon_abilities","schema":"pokemon","order":5,"fields":[{"id":"v99oydscnxxyqg6knbycm39aj","name":"pok_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"vrwyacj8mb0oj6fspen6vqpc1","name":"abil_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"1eg794qd6zw2i281os7ycw27f","name":"is_hidden","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"k6dj4hreohg8ufybojn6re6wz","name":"slot","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"a35s1d192eompkn7yvuxng62d","name":"music","type":{"name":"bigint","id":"bigint"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false}],"indexes":[{"id":"hluplg0pndapd1fk086cawlsl","name":"pokemon_abilities_pokemon_ix_pokemon_abilities_is_hidden","fieldIds":["1eg794qd6zw2i281os7ycw27f"],"unique":false,"createdAt":1766062732104},{"id":"7t8x8wl5hjrsi914w95hpn2r9","name":"pokemon_abilities_pokemon_abil_id","fieldIds":["vrwyacj8mb0oj6fspen6vqpc1"],"unique":false,"createdAt":1766062732104}],"x":-958.2017535132559,"y":-415.8808703335114,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"uf6nokjc3llz4dmfze0bjhzyo","name":"pokemon","schema":"pokemon","order":10,"fields":[{"id":"zqyqypyht1uaum3sce3bvwlhi","name":"pok_id","type":{"id":"int","name":"int"},"nullable":false,"primaryKey":true,"unique":true,"default":"","createdAt":1766062732104,"increment":false},{"id":"d7o7wjf1gowlsq5p3etnper8o","name":"pok_name","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false,"characterMaximumLength":"500"},{"id":"yvw4bu3bgozb8hz7z3t8nkmz8","name":"pok_height","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"snci66nxy39m97a7eouwybn3x","name":"pok_weight","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false},{"id":"ekqck9gw9z1ktciuh95ryhpy9","name":"pok_base_experience","type":{"id":"int","name":"int"},"nullable":true,"primaryKey":false,"unique":false,"default":"","createdAt":1766062732104,"increment":false}],"indexes":[],"x":-627.5363678111918,"y":-454.0032933604259,"color":"#8eb7ff","isView":false,"createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"}],"relationships":[{"id":"ylhfaz43z8l46l6zlwpsmjdql","name":"fk_pokemon_abilities_pok_id_pokemon_pok_id","sourceSchema":"pokemon","targetSchema":"pokemon","sourceTableId":"uf6nokjc3llz4dmfze0bjhzyo","targetTableId":"ndqyi89iebp8w1my9t4su9nlo","sourceFieldId":"zqyqypyht1uaum3sce3bvwlhi","targetFieldId":"v99oydscnxxyqg6knbycm39aj","sourceCardinality":"one","targetCardinality":"many","createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"},{"id":"pcw4h4jau7sbprolm8b1m4f6k","name":"fk_pokemon_abilities_abil_id_abilities_abil_id","sourceSchema":"pokemon","targetSchema":"pokemon","sourceTableId":"meui8qxt4ui15zn0wbpn8kf4h","targetTableId":"ndqyi89iebp8w1my9t4su9nlo","sourceFieldId":"40alqf0d9uw4pkhc43yg0xbsy","targetFieldId":"vrwyacj8mb0oj6fspen6vqpc1","sourceCardinality":"one","targetCardinality":"many","createdAt":1766062732104,"diagramId":"0kfb46kt4fqz"}],"dependencies":[],"areas":[],"customTypes":[],"notes":[]}

View File

@@ -78,4 +78,8 @@ describe('DBML Export cases', () => {
it('should handle case 7 diagram', { timeout: 30000 }, async () => {
testCase('7');
});
it('should handle case 8 diagram', { timeout: 30000 }, async () => {
testCase('8');
});
});

View File

@@ -819,39 +819,52 @@ const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
// Single table with this name - simple case
const table = tablesGroup[0].table;
if (table.schema) {
// Match table definition without schema (e.g., Table "users" {)
const tablePattern = new RegExp(
`Table\\s+"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s*{`,
'g'
);
const schemaTableName = `Table "${table.schema}"."${table.name}" {`;
result = result.replace(tablePattern, schemaTableName);
// Update references in Ref statements
const escapedTableName = table.name.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
// Pattern 1: In Ref definitions - :"tablename"."field"
const refDefPattern = new RegExp(
`(Ref\\s+"[^"]+")\\s*:\\s*"${escapedTableName}"\\."([^"]+)"`,
'g'
);
result = result.replace(
refDefPattern,
`$1:"${table.schema}"."${table.name}"."$2"`
const escapedSchema = table.schema.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
// Pattern 2: In Ref targets - [<>] "tablename"."field"
const refTargetPattern = new RegExp(
`([<>])\\s*"${escapedTableName}"\\."([^"]+)"`,
// Check if the schema is already present in the table definition
const schemaAlreadyPresent = new RegExp(
`Table\\s+"${escapedSchema}"\\."${escapedTableName}"\\s*{`,
'g'
);
result = result.replace(
refTargetPattern,
`$1 "${table.schema}"."${table.name}"."$2"`
);
).test(result);
// Only add schema if it's not already present
if (!schemaAlreadyPresent) {
// Match table definition without schema (e.g., Table "users" {)
const tablePattern = new RegExp(
`Table\\s+"${escapedTableName}"\\s*{`,
'g'
);
const schemaTableName = `Table "${table.schema}"."${table.name}" {`;
result = result.replace(tablePattern, schemaTableName);
// Update references in Ref statements
// Pattern 1: In Ref definitions - :"tablename"."field"
const refDefPattern = new RegExp(
`(Ref\\s+"[^"]+")\\s*:\\s*"${escapedTableName}"\\."([^"]+)"`,
'g'
);
result = result.replace(
refDefPattern,
`$1:"${table.schema}"."${table.name}"."$2"`
);
// Pattern 2: In Ref targets - [<>] "tablename"."field"
const refTargetPattern = new RegExp(
`([<>])\\s*"${escapedTableName}"\\."([^"]+)"`,
'g'
);
result = result.replace(
refTargetPattern,
`$1 "${table.schema}"."${table.name}"."$2"`
);
}
}
} else {
// Multiple tables with the same name - need to be more careful