Compare commits

...

40 Commits

Author SHA1 Message Date
johnnyfish
80b23c3f4b feat: add right-click context menu with edit/delete options for areas on canvas 2025-09-15 18:58:49 +03:00
Guy Ben-Aharon
8954d893bb feat: add quick table mode on canvas (#915)
* fix: add quick table mode on canvas

* fix

* fix

* fix
2025-09-14 20:42:01 +03:00
Guy Ben-Aharon
1a6688e85e alignment (#912) 2025-09-11 12:32:58 +03:00
Guy Ben-Aharon
5e81c1848a fix(dbml): export array fields without quotes (#911) 2025-09-10 22:24:05 +03:00
Guy Ben-Aharon
2bd9ca25b2 fix: update deps vulns (#909) 2025-09-10 16:37:33 +03:00
Guy Ben-Aharon
b016a70691 fix: move auto arrange to toolbar (#904) 2025-09-07 12:02:33 +03:00
Jonathan Fishner
a0fb1ed08b feat: add zoom navigation buttons to canvas filter for tables and areas (#903)
* feat: add zoom navigation buttons to canvas filter for tables and areas

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-09-04 16:18:50 +03:00
Guy Ben-Aharon
ffddcdcc98 fix: export sql + import metadata lib (#902) 2025-09-04 12:10:56 +03:00
Jonathan Fishner
fe9ef275b8 fix: improve SQL default value parsing for PostgreSQL, MySQL, and SQL Server with proper type handling and casting support (#900) 2025-09-04 11:18:02 +03:00
Guy Ben-Aharon
df89f0b6b9 fix: remove general db creation (#901) 2025-09-03 20:57:12 +03:00
Guy Ben-Aharon
534d2858af readonly editor (#899) 2025-09-03 15:59:21 +03:00
Jonathan Fishner
2a64deebb8 fix(sql-import): handle SQL Server DDL with multiple tables, inline foreign keys, and case-insensitive field matching (#897) 2025-09-02 15:15:15 +03:00
Guy Ben-Aharon
e5e1d59327 fix: reset increment and default when change field (#896) 2025-09-01 18:48:00 +03:00
Guy Ben-Aharon
aa290615ca fix(sql-import): support ALTER TABLE ALTER COLUMN TYPE in PostgreSQL importer (#895) 2025-09-01 17:13:42 +03:00
Guy Ben-Aharon
ec6e46fe81 fix: add support for ALTER TABLE ADD COLUMN in PostgreSQL importer (#892) 2025-09-01 11:45:14 +03:00
Guy Ben-Aharon
ac128d67de align filter (#890) 2025-08-31 19:18:43 +03:00
Guy Ben-Aharon
07937a2f51 fix: export dbml issues after upgrade version (#883)
* fix: dbml export

* fix

* fix

* fix

* fix

* fix
2025-08-27 20:44:18 +03:00
Guy Ben-Aharon
d8e0bc7db8 fix: upgrade dbml lib (#880) 2025-08-27 14:42:02 +03:00
Guy Ben-Aharon
1ce265781b chore(main): release 1.15.1 (#878) 2025-08-27 12:53:00 +03:00
Aaron Dewes
60c5675cbf fix(custom-types): Make schema optional (#866)
* fix(custom-types): Make schema optional

The schema is optional in practice for custom types (as seen in the TS types above), and not always included in exports.

* add nullable

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-27 12:48:14 +03:00
Jonathan Fishner
66b086378c fix: handle quoted identifiers with special characters in SQL import/export and DBML generation (#877)
* fix: handle quoted identifiers with special characters in SQL import/export and DBML generation

* add tests and fix build

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-27 12:21:41 +03:00
Guy Ben-Aharon
abd2a6ccbe fix: add actions menu to diagram list + add duplicate diagram (#876) 2025-08-26 17:10:25 +03:00
Guy Ben-Aharon
459c5f1ce3 chore(main): release 1.15.0 (#835) 2025-08-26 15:15:41 +03:00
Guy Ben-Aharon
44be48ff3a fix: improve creating view to table dependency (#874) 2025-08-26 15:10:11 +03:00
Aaron Dewes
ad8e34483f fix(cla): Harden action (#867)
The CLA action does not need contents: write permission. Limit it to read for security.
2025-08-26 13:41:29 +03:00
Jonathan Fishner
215d57979d fix: preserve composite primary key constraint names across import/export workflows (#869)
* fix: composite primary key constraint names across import/export workflows

* fix: enhance PK index management with automatic lifecycle and improved UI

* fix build

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-26 12:48:24 +03:00
Guy Ben-Aharon
ec3719ebce fix: merge relationship & dependency sections to ref section (#870)
* fix: merge relationship & dependency sections to ref section

* fix

* fix

* fix
2025-08-25 20:14:32 +03:00
Guy Ben-Aharon
0a5874a69b feat: support create views (#868)
* feat: support create views

* fix

* fix

* fix

* fix

* fix
2025-08-25 16:14:28 +03:00
Guy Ben-Aharon
7e0fdd1595 fix: open filter by default (#863) 2025-08-21 17:55:45 +03:00
Guy Ben-Aharon
2531a7023f fix: move dbml into sections menu (#862) 2025-08-21 14:48:47 +03:00
Guy Ben-Aharon
73daf0df21 fix: area filter logic (#861) 2025-08-20 15:21:48 +03:00
Jonathan Fishner
c77c983989 feat: add auto increment support for fields with database-specific export (#851)
* feat: add auto increment support for fields with database-specific export

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-19 11:39:26 +03:00
Jonathan Fishner
0aaa451479 fix: prevent false change detection in DBML editor by stripping public schema on import (#858)
* fix: prevent false change detection in DBML editor by stripping public schema on import

* fix(dbml): preserve self-referencing relationships and character varying lengths in DBML import/export
2025-08-18 21:39:24 +03:00
Guy Ben-Aharon
b697e26170 fix(canvas): delete table + area together bug (#859) 2025-08-18 20:56:32 +03:00
Jonathan Fishner
04d91c67b1 fix(sql-import): fix SQL Server foreign key parsing for tables without schema prefix (#857) 2025-08-18 19:13:46 +03:00
Guy Ben-Aharon
d0dee84970 fix(filter): filter toggle issues with no schemas dbs (#856) 2025-08-17 12:54:56 +03:00
Guy Ben-Aharon
b4ccfcdcde fix: set default filter only if has more than 1 schemas (#855) 2025-08-14 11:37:24 +03:00
Guy Ben-Aharon
1759b0b9f2 fix: show default schema first (#854) 2025-08-13 21:08:53 +03:00
Guy Ben-Aharon
ab4845c772 fix: initially show filter when filter active (#853) 2025-08-13 20:20:59 +03:00
Jonathan Fishner
0545b41140 fix: DBML export error with multi-line table comments for SQL Server (#852)
* fix: DBML export error with multi-line table comments for SQL Server

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-13 18:15:38 +03:00
167 changed files with 94383 additions and 3814 deletions

View File

@@ -7,7 +7,7 @@ on:
permissions:
actions: write
contents: write # this can be 'read' if the signatures are in remote repository
contents: read
pull-requests: write
statuses: write

2
.nvmrc
View File

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

View File

@@ -1,5 +1,55 @@
# Changelog
## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
### Bug Fixes
* add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0))
* **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671))
* handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872))
## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26)
### Features
* add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103))
* **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4))
* include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992))
* **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006))
* support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c))
### Bug Fixes
* area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026))
* **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037))
* **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f))
* **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de))
* DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3))
* filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02))
* **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d))
* **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22))
* for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2))
* improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1))
* initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9))
* **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195))
* merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015))
* move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51))
* open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e))
* preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771))
* prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b))
* remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c))
* reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20))
* **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268))
* set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925))
* show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb))
* **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4))
* **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca))
* **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a))
* update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4))
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)

281
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "chartdb",
"version": "1.14.0",
"version": "1.15.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.14.0",
"version": "1.15.1",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
"@dbml/core": "^3.13.9",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.0",
@@ -586,15 +586,15 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
@@ -738,18 +738,18 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -766,26 +766,26 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7"
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.7"
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -827,27 +827,24 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -883,25 +880,25 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@dbml/core": {
"version": "3.9.5",
"resolved": "https://registry.npmjs.org/@dbml/core/-/core-3.9.5.tgz",
"integrity": "sha512-lX/G5qer42irufv5rvx6Y3ISV2ZLDRlxj8R+OZMdhC6wAw0VYPYIts23MdMFPY39Iay0TDtfmwsbOsVy/yjSIg==",
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/@dbml/core/-/core-3.13.9.tgz",
"integrity": "sha512-JgJ470yuTZU7tP64ZL5FpEh7zSXjSoKzkARmin8iVVhdsNM8Nq4e+FFhG6J6acPtGHtoLahOs9LqrC17B9MqYg==",
"license": "Apache-2.0",
"dependencies": {
"@dbml/parse": "^3.9.5",
"@dbml/parse": "^3.13.9",
"antlr4": "^4.13.1",
"lodash": "^4.17.15",
"parsimmon": "^1.13.0",
@@ -912,15 +909,15 @@
}
},
"node_modules/@dbml/parse": {
"version": "3.9.5",
"resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-3.9.5.tgz",
"integrity": "sha512-z8MjBYDFiYf7WtsagwGATEye81xQcO9VXFzttSjdJ+wgdSFzFSex9letJPIMIcYXBkm4Fg5qLDk+G9uq/413Dg==",
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-3.13.9.tgz",
"integrity": "sha512-JMfOxWquXMZpF/MTLy2xWLImx3z9D0t67T7x/BT892WvmhM+9cnJHFA2URT1NXu9jdajbTTFuoWSyzdsfNpaRw==",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.17.21"
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"lodash": "^4.17.21"
"engines": {
"node": ">=18"
}
},
"node_modules/@dnd-kit/accessibility": {
@@ -1370,9 +1367,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1417,9 +1414,9 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1431,10 +1428,20 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
"integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1445,9 +1452,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1482,13 +1489,16 @@
}
},
"node_modules/@eslint/js": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
"version": "9.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
"integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
@@ -1502,13 +1512,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
"integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.10.0",
"@eslint/core": "^0.15.2",
"levn": "^0.4.1"
},
"engines": {
@@ -1606,9 +1616,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -4276,12 +4286,6 @@
"@types/deep-eql": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
@@ -4553,9 +4557,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4961,9 +4965,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -5486,9 +5490,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6578,22 +6582,23 @@
}
},
"node_modules/eslint": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
"version": "9.35.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.10.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.19.0",
"@eslint/plugin-kit": "^0.2.5",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.35.0",
"@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
@@ -6601,9 +6606,9 @@
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.2.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -6812,9 +6817,9 @@
}
},
"node_modules/eslint-scope": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -6842,9 +6847,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -6862,15 +6867,15 @@
"peer": true
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6880,9 +6885,9 @@
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -7365,9 +7370,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -8440,6 +8445,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -9600,15 +9611,13 @@
}
},
"node_modules/react-router": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
"integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
"integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
@@ -9624,12 +9633,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.5.tgz",
"integrity": "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==",
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz",
"integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==",
"license": "MIT",
"dependencies": {
"react-router": "7.1.5"
"react-router": "7.8.2"
},
"engines": {
"node": ">=20.0.0"
@@ -9760,12 +9769,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -11026,12 +11029,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -11313,9 +11310,9 @@
}
},
"node_modules/vite": {
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"version": "5.4.20",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.14.0",
"version": "1.15.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,7 +17,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
"@dbml/core": "^3.13.9",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.0",

View File

@@ -0,0 +1,137 @@
import React from 'react';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { buttonVariants } from './button-variants';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface ButtonAlternative {
label: string;
onClick: () => void;
disabled?: boolean;
icon?: React.ReactNode;
className?: string;
tooltip?: string;
}
export interface ButtonWithAlternativesProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
alternatives: Array<ButtonAlternative>;
dropdownTriggerClassName?: string;
chevronDownIconClassName?: string;
}
const ButtonWithAlternatives = React.forwardRef<
HTMLButtonElement,
ButtonWithAlternativesProps
>(
(
{
className,
variant,
size,
asChild = false,
alternatives,
children,
onClick,
dropdownTriggerClassName,
chevronDownIconClassName,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const hasAlternatives = (alternatives?.length ?? 0) > 0;
return (
<div className="inline-flex items-stretch">
<Comp
className={cn(
buttonVariants({ variant, size }),
{ 'rounded-r-none': hasAlternatives },
className
)}
ref={ref}
onClick={onClick}
{...props}
>
{children}
</Comp>
{hasAlternatives ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={cn(
buttonVariants({ variant, size }),
'rounded-l-none border-l border-l-primary/5 px-2 min-w-0',
className?.includes('h-') &&
className.match(/h-\d+/)?.[0],
className?.includes('text-') &&
className.match(/text-\w+/)?.[0],
dropdownTriggerClassName
)}
type="button"
>
<ChevronDownIcon
className={cn(
'size-4 shrink-0',
chevronDownIconClassName
)}
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{alternatives.map((alternative, index) => {
const menuItem = (
<DropdownMenuItem
key={index}
onClick={alternative.onClick}
disabled={alternative.disabled}
className={cn(alternative.className)}
>
<span className="flex w-full items-center justify-between gap-2">
{alternative.label}
{alternative.icon}
</span>
</DropdownMenuItem>
);
if (alternative.tooltip) {
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
{menuItem}
</TooltipTrigger>
<TooltipContent side="left">
{alternative.tooltip}
</TooltipContent>
</Tooltip>
);
}
return menuItem;
})}
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
);
}
);
ButtonWithAlternatives.displayName = 'ButtonWithAlternatives';
export { ButtonWithAlternatives };

View File

@@ -5,27 +5,45 @@ import {
PopoverTrigger,
} from '@/components/popover/popover';
import { colorOptions } from '@/lib/colors';
import { cn } from '@/lib/utils';
export interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
disabled?: boolean;
popoverOnMouseDown?: (e: React.MouseEvent) => void;
popoverOnClick?: (e: React.MouseEvent) => void;
}
export const ColorPicker = React.forwardRef<
React.ElementRef<typeof PopoverTrigger>,
ColorPickerProps
>(({ color, onChange }, ref) => {
>(({ color, onChange, disabled, popoverOnMouseDown, popoverOnClick }, ref) => {
return (
<Popover>
<PopoverTrigger asChild ref={ref}>
<PopoverTrigger
asChild
ref={ref}
disabled={disabled}
{...(disabled ? { onClick: (e) => e.preventDefault() } : {})}
>
<div
className="h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
className={cn(
'h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md',
{
'hover:shadow-none cursor-default': disabled,
}
)}
style={{
backgroundColor: color,
}}
/>
</PopoverTrigger>
<PopoverContent className="w-fit">
<PopoverContent
className="w-fit"
onMouseDown={popoverOnMouseDown}
onClick={popoverOnClick}
>
<div className="grid grid-cols-4 gap-2">
{colorOptions.map((option) => (
<div

View File

@@ -27,6 +27,7 @@ export interface SelectBoxOption {
regex?: string;
extractRegex?: RegExp;
group?: string;
icon?: React.ReactNode;
}
export interface SelectBoxProps {
@@ -53,6 +54,10 @@ export interface SelectBoxProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
popoverClassName?: string;
readonly?: boolean;
footerButtons?: React.ReactNode;
commandOnMouseDown?: (e: React.MouseEvent) => void;
commandOnClick?: (e: React.MouseEvent) => void;
}
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
@@ -78,6 +83,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
open,
onOpenChange: setOpen,
popoverClassName,
readonly,
footerButtons,
commandOnMouseDown,
commandOnClick,
},
ref
) => {
@@ -152,18 +161,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
className={`inline-flex min-w-0 shrink-0 items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${oneLine ? 'mx-0.5' : ''}`}
>
<span>{option.label}</span>
<span
onClick={(e) => {
e.preventDefault();
handleSelect(option.value);
}}
className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
>
<Cross2Icon />
</span>
{!readonly ? (
<span
onClick={(e) => {
e.preventDefault();
handleSelect(option.value);
}}
className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
>
<Cross2Icon />
</span>
) : null}
</span>
)),
[options, value, handleSelect, oneLine, keepOrder]
[options, value, handleSelect, oneLine, keepOrder, readonly]
);
const isAllSelected = React.useMemo(
@@ -236,6 +247,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
matches?.map((match) => match?.toString())
)
}
onMouseDown={commandOnMouseDown}
onClick={commandOnClick}
>
{multiple && (
<div
@@ -250,6 +263,11 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
)}
<div className="flex flex-1 items-center truncate">
{option.icon ? (
<span className="mr-2 shrink-0">
{option.icon}
</span>
) : null}
<span>
{isRegexMatch ? searchTerm : option.label}
{!isRegexMatch && optionSuffix
@@ -276,7 +294,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</CommandItem>
);
},
[value, multiple, searchTerm, handleSelect, optionSuffix]
[
value,
multiple,
searchTerm,
handleSelect,
optionSuffix,
commandOnClick,
commandOnMouseDown,
]
);
return (
@@ -284,7 +310,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<div
className={cn(
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''} ${readonly ? 'pointer-events-none' : ''}`,
className
)}
>
@@ -354,6 +380,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
popoverClassName
)}
align="center"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<Command
filter={(value, search, keywords) => {
@@ -443,6 +471,9 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
</ScrollArea>
</Command>
{footerButtons ? (
<div className="border-t">{footerButtons}</div>
) : null}
</PopoverContent>
</Popover>
);

View File

@@ -14,6 +14,16 @@ export interface CanvasContext {
overlapGraph: Graph<string>;
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
showFilter: boolean;
editTableModeTable: {
tableId: string;
fieldId?: string;
} | null;
setEditTableModeTable: React.Dispatch<
React.SetStateAction<{
tableId: string;
fieldId?: string;
} | null>
>;
}
export const canvasContext = createContext<CanvasContext>({
@@ -23,4 +33,6 @@ export const canvasContext = createContext<CanvasContext>({
overlapGraph: createGraph(),
setShowFilter: emptyFn,
showFilter: false,
editTableModeTable: null,
setEditTableModeTable: emptyFn,
});

View File

@@ -1,4 +1,10 @@
import React, { type ReactNode, useCallback, useState } from 'react';
import React, {
type ReactNode,
useCallback,
useState,
useEffect,
useRef,
} from 'react';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import { adjustTablePositions } from '@/lib/domain/db-table';
@@ -15,14 +21,39 @@ interface CanvasProviderProps {
}
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, databaseType, areas } =
useChartDB();
const { filter } = useDiagramFilter();
const {
tables,
relationships,
updateTablesState,
databaseType,
areas,
diagramId,
} = useChartDB();
const { filter, loading: filterLoading } = useDiagramFilter();
const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
const [editTableModeTable, setEditTableModeTable] = useState<{
tableId: string;
fieldId?: string;
} | null>(null);
const [showFilter, setShowFilter] = useState(false);
const diagramIdActiveFilterRef = useRef<string>();
useEffect(() => {
if (filterLoading) {
return;
}
if (diagramIdActiveFilterRef.current === diagramId) {
return;
}
diagramIdActiveFilterRef.current = diagramId;
setShowFilter(true);
}, [filterLoading, diagramId]);
const reorderTables = useCallback(
(
@@ -100,6 +131,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
overlapGraph,
setShowFilter,
showFilter,
editTableModeTable,
setEditTableModeTable,
}}
>
{children}

View File

@@ -1,12 +1,15 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { deepCopy, generateId } from '@/lib/utils';
import { defaultTableColor, defaultAreaColor } from '@/lib/colors';
import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors';
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
import { chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import {
getTableIndexesWithPrimaryKey,
type DBIndex,
} from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -38,8 +41,7 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
const dbStorage = useStorage();
let db = dbStorage;
const storageDB = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
@@ -89,17 +91,16 @@ export const ChartDBProvider: React.FC<
diffEvents.useSubscription(diffCalculatedHandler);
const defaultSchemaName = defaultSchemas[databaseType];
const defaultSchemaName = useMemo(
() => defaultSchemas[databaseType],
[databaseType]
);
const readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false,
[readonlyProp, hasDiff]
);
if (readonly) {
db = storageInitialValue;
}
const schemas = useMemo(
() =>
databasesWithSchemas.includes(databaseType)
@@ -110,9 +111,11 @@ export const ChartDBProvider: React.FC<
.filter((schema) => !!schema) as string[]
),
]
.sort((a, b) =>
a === defaultSchemaName ? -1 : a.localeCompare(b)
)
.sort((a, b) => {
if (a === defaultSchemaName) return -1;
if (b === defaultSchemaName) return 1;
return a.localeCompare(b);
})
.map(
(schema): DBSchema => ({
id: schemaNameToSchemaId(schema),
@@ -126,6 +129,11 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const db = useMemo(
() => (readonly ? storageInitialValue : storageDB),
[storageDB, readonly]
);
const currentDiagram: Diagram = useMemo(
() => ({
id: diagramId,
@@ -337,12 +345,17 @@ export const ChartDBProvider: React.FC<
},
],
indexes: [],
color: defaultTableColor,
color: attributes?.isView ? viewColor : defaultTableColor,
createdAt: Date.now(),
isView: false,
order: tables.length,
...attributes,
};
table.indexes = getTableIndexesWithPrimaryKey({
table,
});
await addTable(table);
return table;
@@ -634,17 +647,30 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true }
) => {
const prevField = getField(tableId, fieldId);
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
const table = await db.getTable({ diagramId, id: tableId });
@@ -659,10 +685,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
...updateTableFn(table),
},
}),
]);
@@ -689,19 +712,29 @@ export const ChartDBProvider: React.FC<
fieldId: string,
options = { updateHistory: true }
) => {
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
const fields = getTable(tableId)?.fields ?? [];
const prevField = getField(tableId, fieldId);
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.filter(
(f) => f.id !== fieldId
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
events.emit({
@@ -725,8 +758,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
...updateTableFn(table),
},
}),
]);
@@ -1548,17 +1580,17 @@ export const ChartDBProvider: React.FC<
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? dbStorage : db;
const st = options?.forceUpdateStorage ? storageDB : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, dbStorage, loadDiagramFromData]
[db, storageDB, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
const diagram = await storageDB.getDiagram(diagramId, {
includeRelationships: true,
includeTables: true,
includeDependencies: true,
@@ -1572,7 +1604,7 @@ export const ChartDBProvider: React.FC<
return diagram;
},
[db, loadDiagramFromData]
[storageDB, loadDiagramFromData]
);
// Custom type operations

View File

@@ -74,7 +74,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
if (!filterToSet) {
// If no filter is stored, set default based on database type
filterToSet =
schemas.length > 0
schemas.length > 1
? { schemaIds: [schemas[0].id] }
: {};
}
@@ -205,7 +205,11 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
@@ -252,11 +256,15 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: undefined,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allTables]
[allTables, databaseType]
);
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
@@ -340,7 +348,11 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
@@ -450,11 +462,15 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allTables]
[allTables, databaseType]
);
const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
@@ -491,11 +507,15 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allTables]
[allTables, databaseType]
);
const eventConsumer = useCallback(

View File

@@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export type SidebarSection =
| 'dbml'
| 'tables'
| 'relationships'
| 'dependencies'
| 'refs'
| 'areas'
| 'customTypes';
@@ -13,14 +13,16 @@ export interface LayoutContext {
openTableFromSidebar: (tableId: string) => void;
closeAllTablesInSidebar: () => void;
openedRelationshipInSidebar: string | undefined;
openRelationshipFromSidebar: (relationshipId: string) => void;
closeAllRelationshipsInSidebar: () => void;
openedDependencyInSidebar: string | undefined;
openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void;
openedRefInSidebar: string | undefined;
openRefFromSidebar: (refId: string) => void;
closeAllRefsInSidebar: () => void;
openedAreaInSidebar: string | undefined;
openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void;
@@ -42,14 +44,16 @@ export const layoutContext = createContext<LayoutContext>({
openedTableInSidebar: undefined,
selectedSidebarSection: 'tables',
openedRelationshipInSidebar: undefined,
openRelationshipFromSidebar: emptyFn,
closeAllRelationshipsInSidebar: emptyFn,
openedDependencyInSidebar: undefined,
openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn,
openedRefInSidebar: undefined,
openRefFromSidebar: emptyFn,
closeAllRefsInSidebar: emptyFn,
openedAreaInSidebar: undefined,
openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn,

View File

@@ -10,10 +10,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
string | undefined
>();
const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] =
React.useState<string | undefined>();
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
React.useState<string | undefined>();
const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState<
string | undefined
>();
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined
>();
@@ -28,10 +27,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
() => setOpenedTableInSidebar('');
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
() => setOpenedRelationshipInSidebar('');
() => setOpenedRefInSidebar('');
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar('');
() => setOpenedRefInSidebar('');
const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () =>
setOpenedRefInSidebar('');
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar('');
@@ -60,17 +62,23 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
(relationshipId) => {
showSidePanel();
setSelectedSidebarSection('relationships');
setOpenedRelationshipInSidebar(relationshipId);
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(relationshipId);
};
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
(dependencyId) => {
showSidePanel();
setSelectedSidebarSection('dependencies');
setOpenedDependencyInSidebar(dependencyId);
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(dependencyId);
};
const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => {
showSidePanel();
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(refId);
};
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
areaId
) => {
@@ -93,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
selectedSidebarSection,
openTableFromSidebar,
selectSidebarSection: setSelectedSidebarSection,
openedRelationshipInSidebar,
openRelationshipFromSidebar,
closeAllTablesInSidebar,
closeAllRelationshipsInSidebar,
@@ -101,9 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
hideSidePanel,
showSidePanel,
toggleSidePanel,
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,
openedRefInSidebar,
openRefFromSidebar,
closeAllRefsInSidebar,
openedAreaInSidebar,
openAreaFromSidebar,
closeAllAreasInSidebar,

View File

@@ -11,6 +11,9 @@ export interface LocalConfigContext {
scrollAction: ScrollAction;
setScrollAction: (action: ScrollAction) => void;
showDBViews: boolean;
setShowDBViews: (showViews: boolean) => void;
showCardinality: boolean;
setShowCardinality: (showCardinality: boolean) => void;
@@ -23,9 +26,6 @@ export interface LocalConfigContext {
starUsDialogLastOpen: number;
setStarUsDialogLastOpen: (lastOpen: number) => void;
showDependenciesOnCanvas: boolean;
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
showMiniMapOnCanvas: boolean;
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
}
@@ -37,6 +37,9 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
scrollAction: 'pan',
setScrollAction: emptyFn,
showDBViews: false,
setShowDBViews: emptyFn,
showCardinality: true,
setShowCardinality: emptyFn,
@@ -49,9 +52,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
starUsDialogLastOpen: 0,
setStarUsDialogLastOpen: emptyFn,
showDependenciesOnCanvas: false,
setShowDependenciesOnCanvas: emptyFn,
showMiniMapOnCanvas: false,
setShowMiniMapOnCanvas: emptyFn,
});

View File

@@ -9,8 +9,8 @@ const showCardinalityKey = 'show_cardinality';
const showFieldAttributesKey = 'show_field_attributes';
const githubRepoOpenedKey = 'github_repo_opened';
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
const showDBViewsKey = 'show_db_views';
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -23,6 +23,10 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
);
const [showDBViews, setShowDBViews] = React.useState<boolean>(
(localStorage.getItem(showDBViewsKey) || 'false') === 'true'
);
const [showCardinality, setShowCardinality] = React.useState<boolean>(
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
);
@@ -41,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
);
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
'true'
);
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
@@ -72,15 +70,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
}, [scrollAction]);
useEffect(() => {
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
localStorage.setItem(showDBViewsKey, showDBViews.toString());
}, [showDBViews]);
useEffect(() => {
localStorage.setItem(
showDependenciesOnCanvasKey,
showDependenciesOnCanvas.toString()
);
}, [showDependenciesOnCanvas]);
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
useEffect(() => {
localStorage.setItem(
@@ -96,6 +91,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setTheme,
scrollAction,
setScrollAction,
showDBViews,
setShowDBViews,
showCardinality,
setShowCardinality,
showFieldAttributes,
@@ -104,8 +101,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
githubRepoOpened,
starUsDialogLastOpen,
setStarUsDialogLastOpen,
showDependenciesOnCanvas,
setShowDependenciesOnCanvas,
showMiniMapOnCanvas,
setShowMiniMapOnCanvas,
}}

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogContent } from '@/components/dialog/dialog';
import { DatabaseType } from '@/lib/domain/database-type';
import { useStorage } from '@/hooks/use-storage';
import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';

View File

@@ -69,6 +69,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
type="button"
variant="outline"
onClick={createNewDiagram}
disabled={databaseType === DatabaseType.GENERIC}
>
{t('new_diagram_dialog.empty_diagram')}
</Button>

View File

@@ -17,7 +17,7 @@ import { useDialog } from '@/hooks/use-dialog';
import {
exportBaseSQL,
exportSQL,
} from '@/lib/data/export-metadata/export-sql-script';
} from '@/lib/data/sql-export/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { Annoyed, Sparkles } from 'lucide-react';

View File

@@ -7,7 +7,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { loadDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Trans, useTranslation } from 'react-i18next';

View File

@@ -132,7 +132,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const preprocessedContent = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser();
parser.parse(sanitizedContent, 'dbml');
parser.parse(sanitizedContent, 'dbmlv2');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {

View File

@@ -0,0 +1,98 @@
import React, { useCallback } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { Button } from '@/components/button/button';
import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
import { useChartDB } from '@/hooks/use-chartdb';
import type { Diagram } from '@/lib/domain';
import { useStorage } from '@/hooks/use-storage';
import { cloneDiagram } from '@/lib/clone';
import { useTranslation } from 'react-i18next';
interface DiagramRowActionsMenuProps {
diagram: Diagram;
onOpen: () => void;
refetch: () => void;
numberOfDiagrams: number;
}
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
diagram,
onOpen,
refetch,
numberOfDiagrams,
}) => {
const { diagramId } = useChartDB();
const { deleteDiagram, addDiagram } = useStorage();
const { t } = useTranslation();
const onDelete = useCallback(async () => {
deleteDiagram(diagram.id);
refetch();
if (diagram.id === diagramId || numberOfDiagrams <= 1) {
window.location.href = '/';
}
}, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]);
const onDuplicate = useCallback(async () => {
const duplicatedDiagram = cloneDiagram(diagram);
const diagramToAdd = duplicatedDiagram.diagram;
if (!diagramToAdd) {
return;
}
diagramToAdd.name = `${diagram.name} (Copy)`;
addDiagram({ diagram: diagramToAdd });
refetch();
}, [addDiagram, refetch, diagram]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={onOpen}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.open')}
<SquareArrowOutUpRight className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDuplicate}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.duplicate')}
<Layers2 className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className="flex justify-between gap-4 text-red-700"
>
{t('open_diagram_dialog.diagram_actions.delete')}
<Trash2 className="size-3.5 text-red-700" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useDebounce } from '@/hooks/use-debounce';
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
export interface OpenDiagramDialogProps extends BaseDialogProps {
canClose?: boolean;
@@ -46,21 +47,22 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
string | undefined
>();
useEffect(() => {
setSelectedDiagramId(undefined);
}, [dialog.open]);
const fetchDiagrams = useCallback(async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
}, [listDiagrams]);
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
};
if (!dialog.open) {
return;
}
setSelectedDiagramId(undefined);
fetchDiagrams();
}, [listDiagrams, setDiagrams, dialog.open]);
}, [dialog.open, fetchDiagrams]);
const openDiagram = useCallback(
(diagramId: string) => {
@@ -166,6 +168,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
'open_diagram_dialog.table_columns.tables_count'
)}
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
@@ -221,6 +224,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<TableCell className="text-center">
{diagram.tables?.length}
</TableCell>
<TableCell className="items-center p-0 pr-1 text-right">
<DiagramRowActionsMenu
diagram={diagram}
onOpen={() => {
openDiagram(diagram.id);
closeOpenDiagramDialog();
}}
numberOfDiagrams={
diagrams.length
}
refetch={fetchDiagrams}
/>
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -0,0 +1,94 @@
import type { RefObject } from 'react';
import { useEffect } from 'react';
/**
* Hook that handles clicks outside of the passed ref elements
* @param refs - Array of refs to consider as "inside" (e.g., inputRef, buttonRef)
* @param handler - Function to call when clicking outside
* @param enabled - Whether the hook is active (default: true)
* @param delay - Delay before adding listeners to avoid immediate trigger (default: 100ms)
*/
export function useClickOutside(
refs: RefObject<HTMLElement>[],
handler: (() => void) | null,
enabled = true,
delay = 100
) {
useEffect(() => {
if (!enabled || !handler) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is on any of the refs
const isInsideAnyRef = refs.some((ref) => {
if (!ref.current) return false;
return ref.current.contains(target) || ref.current === target;
});
if (!isInsideAnyRef) {
handler();
}
};
// Small delay to avoid immediate trigger when entering edit mode
const timer = setTimeout(() => {
// Use capture phase to catch events before they might be stopped
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
}, delay);
return () => {
clearTimeout(timer);
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
};
}, [refs, handler, enabled, delay]);
}
/**
* Simplified version for edit mode scenarios
* Automatically saves and exits edit mode when clicking outside
*/
export function useEditClickOutside(
inputRef: RefObject<HTMLElement>,
editMode: boolean,
onSave: () => void,
delay = 100
) {
useEffect(() => {
if (!editMode) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is on the input
const isInput =
inputRef.current &&
(inputRef.current.contains(target) ||
inputRef.current === target);
// Check if click is on a button inside the same parent
const isRelatedButton =
target.closest('button') &&
inputRef.current?.parentElement?.contains(target);
if (!isInput && !isRelatedButton) {
onSave();
}
};
// Small delay to avoid immediate trigger when entering edit mode
const timer = setTimeout(() => {
// Use capture phase to catch events before they might be stopped
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
}, delay);
return () => {
clearTimeout(timer);
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
};
}, [editMode, inputRef, onSave, delay]);
}

142
src/hooks/use-focus-on.ts Normal file
View File

@@ -0,0 +1,142 @@
import { useCallback } from 'react';
import { useReactFlow } from '@xyflow/react';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
interface FocusOptions {
select?: boolean;
}
export const useFocusOn = () => {
const { fitView, setNodes, setEdges } = useReactFlow();
const { hideSidePanel } = useLayout();
const { isMd: isDesktop } = useBreakpoint('md');
const focusOnArea = useCallback(
(areaId: string, options: FocusOptions = {}) => {
const { select = true } = options;
if (select) {
setNodes((nodes) =>
nodes.map((node) =>
node.id === areaId
? {
...node,
selected: true,
}
: {
...node,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: areaId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setNodes, hideSidePanel, isDesktop]
);
const focusOnTable = useCallback(
(tableId: string, options: FocusOptions = {}) => {
const { select = true } = options;
if (select) {
setNodes((nodes) =>
nodes.map((node) =>
node.id === tableId
? {
...node,
selected: true,
}
: {
...node,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: tableId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setNodes, hideSidePanel, isDesktop]
);
const focusOnRelationship = useCallback(
(
relationshipId: string,
sourceTableId: string,
targetTableId: string,
options: FocusOptions = {}
) => {
const { select = true } = options;
if (select) {
setEdges((edges) =>
edges.map((edge) =>
edge.id === relationshipId
? {
...edge,
selected: true,
}
: {
...edge,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: sourceTableId,
},
{
id: targetTableId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setEdges, hideSidePanel, isDesktop]
);
return {
focusOnArea,
focusOnTable,
focusOnRelationship,
};
};

View File

@@ -0,0 +1,320 @@
import { useCallback, useMemo, useState, useEffect } from 'react';
import { useChartDB } from './use-chartdb';
import { useDebounce } from './use-debounce-v2';
import type { DBField, DBTable } from '@/lib/domain';
import type {
SelectBoxOption,
SelectBoxProps,
} from '@/components/select-box/select-box';
import {
dataTypeDataToDataType,
sortedDataTypeMap,
} from '@/lib/data/data-types/data-types';
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
const generateFieldRegexPatterns = (
dataType: DataTypeData
): {
regex?: string;
extractRegex?: RegExp;
} => {
if (!dataType.fieldAttributes) {
return { regex: undefined, extractRegex: undefined };
}
const typeName = dataType.name;
const fieldAttributes = dataType.fieldAttributes;
if (fieldAttributes.hasCharMaxLength) {
if (fieldAttributes.hasCharMaxLengthOption) {
return {
regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
extractRegex: /\((\d+|max)\)/i,
};
}
return {
regex: `^${typeName}\\(\\d+\\)$`,
extractRegex: /\((\d+)\)/,
};
}
if (fieldAttributes.precision && fieldAttributes.scale) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
extractRegex: new RegExp(
`${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
),
};
}
if (fieldAttributes.precision) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
extractRegex: /\((\d+)\)/,
};
}
return { regex: undefined, extractRegex: undefined };
};
export const useUpdateTableField = (
table: DBTable,
field: DBField,
customUpdateField?: (attrs: Partial<DBField>) => void
) => {
const {
databaseType,
customTypes,
updateField: chartDBUpdateField,
removeField: chartDBRemoveField,
} = useChartDB();
// Local state for responsive UI
const [localFieldName, setLocalFieldName] = useState(field.name);
const [localNullable, setLocalNullable] = useState(field.nullable);
const [localPrimaryKey, setLocalPrimaryKey] = useState(field.primaryKey);
// Update local state when field properties change externally
useEffect(() => {
setLocalFieldName(field.name);
setLocalNullable(field.nullable);
setLocalPrimaryKey(field.primaryKey);
}, [field.name, field.nullable, field.primaryKey]);
// Use custom updateField if provided, otherwise use the chartDB one
const updateField = useMemo(
() =>
customUpdateField
? (
_tableId: string,
_fieldId: string,
attrs: Partial<DBField>
) => customUpdateField(attrs)
: chartDBUpdateField,
[customUpdateField, chartDBUpdateField]
);
// Calculate primary key fields for validation
const primaryKeyFields = useMemo(() => {
return table.fields.filter((f) => f.primaryKey);
}, [table.fields]);
const primaryKeyCount = useMemo(
() => primaryKeyFields.length,
[primaryKeyFields.length]
);
// Generate data type options for select box
const dataFieldOptions = useMemo(() => {
const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
databaseType
].map((type) => {
const regexPatterns = generateFieldRegexPatterns(type);
return {
label: type.name,
value: type.id,
regex: regexPatterns.regex,
extractRegex: regexPatterns.extractRegex,
group: customTypes?.length ? 'Standard Types' : undefined,
};
});
if (!customTypes?.length) {
return standardTypes;
}
// Add custom types as options
const customTypeOptions: SelectBoxOption[] = customTypes.map(
(type) => ({
label: type.name,
value: type.name,
description:
type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
group: 'Custom Types',
})
);
return [...standardTypes, ...customTypeOptions];
}, [databaseType, customTypes]);
// Handle data type change
const handleDataTypeChange = useCallback<
NonNullable<SelectBoxProps['onChange']>
>(
(value, regexMatches) => {
const dataType = sortedDataTypeMap[databaseType].find(
(v) => v.id === value
) ?? {
id: value as string,
name: value as string,
};
let characterMaximumLength: string | undefined = undefined;
let precision: number | undefined = undefined;
let scale: number | undefined = undefined;
if (regexMatches?.length) {
if (dataType?.fieldAttributes?.hasCharMaxLength) {
characterMaximumLength = regexMatches[1]?.toLowerCase();
} else if (
dataType?.fieldAttributes?.precision &&
dataType?.fieldAttributes?.scale
) {
precision = parseInt(regexMatches[1]);
scale = regexMatches[2]
? parseInt(regexMatches[2])
: undefined;
} else if (dataType?.fieldAttributes?.precision) {
precision = parseInt(regexMatches[1]);
}
} else {
if (
dataType?.fieldAttributes?.hasCharMaxLength &&
field.characterMaximumLength
) {
characterMaximumLength = field.characterMaximumLength;
}
if (dataType?.fieldAttributes?.precision && field.precision) {
precision = field.precision;
}
if (dataType?.fieldAttributes?.scale && field.scale) {
scale = field.scale;
}
}
updateField(table.id, field.id, {
characterMaximumLength,
precision,
scale,
increment: undefined,
default: undefined,
type: dataTypeDataToDataType(
dataType ?? {
id: value as string,
name: value as string,
}
),
});
},
[
updateField,
databaseType,
field.characterMaximumLength,
field.precision,
field.scale,
field.id,
table.id,
]
);
// Debounced update for field name
const debouncedNameUpdate = useDebounce(
useCallback(
(value: string) => {
if (value.trim() !== field.name) {
updateField(table.id, field.id, { name: value });
}
},
[updateField, table.id, field.id, field.name]
),
300 // 300ms debounce for text input
);
// Debounced update for nullable toggle
const debouncedNullableUpdate = useDebounce(
useCallback(
(value: boolean) => {
updateField(table.id, field.id, { nullable: value });
},
[updateField, table.id, field.id]
),
100 // 100ms debounce for toggle
);
// Debounced update for primary key toggle
const debouncedPrimaryKeyUpdate = useDebounce(
useCallback(
(value: boolean, primaryKeyCount: number) => {
if (value) {
// When setting as primary key
const updates: Partial<DBField> = {
primaryKey: true,
};
// Only auto-set unique if this will be the only primary key
if (primaryKeyCount === 0) {
updates.unique = true;
}
updateField(table.id, field.id, updates);
} else {
// When removing primary key
updateField(table.id, field.id, {
primaryKey: false,
});
}
},
[updateField, table.id, field.id]
),
100 // 100ms debounce for toggle
);
// Handle primary key toggle with optimistic update
const handlePrimaryKeyToggle = useCallback(
(value: boolean) => {
setLocalPrimaryKey(value);
debouncedPrimaryKeyUpdate(value, primaryKeyCount);
},
[primaryKeyCount, debouncedPrimaryKeyUpdate]
);
// Handle nullable toggle with optimistic update
const handleNullableToggle = useCallback(
(value: boolean) => {
setLocalNullable(value);
debouncedNullableUpdate(value);
},
[debouncedNullableUpdate]
);
// Handle name change with optimistic update
const handleNameChange = useCallback(
(value: string) => {
setLocalFieldName(value);
debouncedNameUpdate(value);
},
[debouncedNameUpdate]
);
// Utility function to generate field suffix for display
const generateFieldSuffix = useCallback(
(typeId?: string) => {
return generateDBFieldSuffix(field, {
databaseType,
forceExtended: true,
typeId,
});
},
[field, databaseType]
);
const removeField = useCallback(() => {
chartDBRemoveField(table.id, field.id);
}, [chartDBRemoveField, table.id, field.id]);
return {
dataFieldOptions,
handleDataTypeChange,
handlePrimaryKeyToggle,
handleNullableToggle,
handleNameChange,
generateFieldSuffix,
primaryKeyCount,
fieldName: localFieldName,
nullable: localNullable,
primaryKey: localPrimaryKey,
removeField,
};
};

View File

@@ -0,0 +1,42 @@
import { useCallback, useState, useEffect } from 'react';
import { useChartDB } from './use-chartdb';
import { useDebounce } from './use-debounce-v2';
import type { DBTable } from '@/lib/domain';
// Hook for updating table properties with debouncing for performance
export const useUpdateTable = (table: DBTable) => {
const { updateTable: chartDBUpdateTable } = useChartDB();
const [localTableName, setLocalTableName] = useState(table.name);
// Debounced update function
const debouncedUpdate = useDebounce(
useCallback(
(value: string) => {
if (value.trim() && value.trim() !== table.name) {
chartDBUpdateTable(table.id, { name: value.trim() });
}
},
[chartDBUpdateTable, table.id, table.name]
),
1000 // 1000ms debounce
);
// Update local state immediately for responsive UI
const handleTableNameChange = useCallback(
(value: string) => {
setLocalTableName(value);
debouncedUpdate(value);
},
[debouncedUpdate]
);
// Update local state when table name changes externally
useEffect(() => {
setLocalTableName(table.name);
}, [table.name]);
return {
tableName: localTableName,
handleTableNameChange,
};
};

View File

@@ -6,21 +6,21 @@ export const ar: LanguageTranslation = {
new_diagram: 'جديد',
browse: 'تصفح',
tables: 'الجداول',
relationships: 'الروابط',
refs: 'المراجع',
areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
},
menu: {
databases: {
databases: 'قواعد البيانات',
new: 'مخطط جديد',
actions: {
actions: 'الإجراءات',
new: 'جديد...',
browse: 'تصفح...',
save: 'حفظ',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
delete_diagram: 'حذف',
},
edit: {
edit: 'تحرير',
@@ -37,6 +37,7 @@ export const ar: LanguageTranslation = {
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
show_views: 'عروض قاعدة البيانات',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
hide_dependencies: 'إخفاء الاعتمادات',
@@ -73,10 +74,10 @@ export const ar: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'إعادة ترتيب الرسم البياني',
title: 'ترتيب تلقائي للرسم البياني',
description:
'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟',
reorder: 'إعادة ترتيب',
reorder: 'ترتيب تلقائي',
cancel: 'إلغاء',
},
@@ -118,6 +119,7 @@ export const ar: LanguageTranslation = {
tables_section: {
tables: 'الجداول',
add_table: 'إضافة جدول',
add_view: 'إضافة عرض',
filter: 'تصفية',
collapse: 'طي الكل',
// TODO: Translate
@@ -143,6 +145,7 @@ export const ar: LanguageTranslation = {
field_actions: {
title: 'خصائص الحقل',
unique: 'فريد',
auto_increment: 'زيادة تلقائية',
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
@@ -174,12 +177,15 @@ export const ar: LanguageTranslation = {
description: 'أنشئ جدولاً للبدء',
},
},
relationships_section: {
relationships: 'العلاقات',
refs_section: {
refs: 'المراجع',
filter: 'تصفية',
add_relationship: 'إضافة علاقة',
collapse: 'طي الكل',
add_relationship: 'إضافة علاقة',
relationships: 'العلاقات',
dependencies: 'الاعتمادات',
relationship: {
relationship: 'العلاقة',
primary: 'الجدول الأساسي',
foreign: 'الجدول المرتبط',
cardinality: 'الكاردينالية',
@@ -189,16 +195,8 @@ export const ar: LanguageTranslation = {
delete_relationship: 'حذف',
},
},
empty_state: {
title: 'لا توجد علاقات',
description: 'إنشئ علاقة لربط الجداول',
},
},
dependencies_section: {
dependencies: 'الاعتمادات',
filter: 'تصفية',
collapse: 'طي الكل',
dependency: {
dependency: 'الاعتماد',
table: 'الجدول',
dependent_table: 'عرض الاعتمادات',
delete_dependency: 'حذف',
@@ -208,8 +206,8 @@ export const ar: LanguageTranslation = {
},
},
empty_state: {
title: 'لا توجد اعتمادات',
description: 'إنشاء اعتماد للبدء',
title: 'لا توجد علاقات',
description: 'إنشاء علاقة للبدء',
},
},
@@ -250,6 +248,7 @@ export const ar: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'لم يتم تحديد قيم التعداد',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const ar: LanguageTranslation = {
show_all: 'عرض الكل',
undo: 'تراجع',
redo: 'إعادة',
reorder_diagram: 'إعادة ترتيب الرسم البياني',
reorder_diagram: 'ترتيب تلقائي للرسم البياني',
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
// TODO: Translate
filter: 'Filter Tables',
@@ -315,7 +314,7 @@ export const ar: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'فتح مخطط',
title: 'فتح قاعدة بيانات',
description: 'اختر مخططًا لفتحه من القائمة ادناه',
table_columns: {
name: 'الإسم',
@@ -325,6 +324,12 @@ export const ar: LanguageTranslation = {
},
cancel: 'إلغاء',
open: 'فتح',
diagram_actions: {
open: 'فتح',
duplicate: 'تكرار',
delete: 'حذف',
},
},
export_sql_dialog: {
@@ -470,6 +475,7 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
@@ -491,6 +497,8 @@ export const ar: LanguageTranslation = {
language_select: {
change_language: 'اللغة',
},
on: 'تشغيل',
off: 'إيقاف',
},
};

View File

@@ -6,21 +6,21 @@ export const bn: LanguageTranslation = {
new_diagram: 'নতুন',
browse: 'ব্রাউজ',
tables: 'টেবিল',
relationships: 'সম্পর্ক',
refs: 'রেফস',
areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
},
menu: {
databases: {
databases: 'ডাটাবেস',
new: 'নতুন ডায়াগ্রাম',
actions: {
actions: 'কার্য',
new: 'নতুন...',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
delete_diagram: 'মুছুন',
},
edit: {
edit: 'সম্পাদনা',
@@ -37,6 +37,7 @@ export const bn: LanguageTranslation = {
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
show_views: 'ডাটাবেস ভিউ',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
hide_dependencies: 'নির্ভরতাগুলি লুকান',
@@ -74,10 +75,10 @@ export const bn: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
title: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান',
description:
'এই কাজটি ডায়াগ্রামের সমস্ত টেবিল পুনর্বিন্যাস করবে। আপনি কি চালিয়ে যেতে চান?',
reorder: 'পুনর্বিন্যাস করুন',
reorder: 'স্বয়ংক্রিয় সাজান',
cancel: 'বাতিল করুন',
},
@@ -119,6 +120,7 @@ export const bn: LanguageTranslation = {
tables_section: {
tables: 'টেবিল',
add_table: 'টেবিল যোগ করুন',
add_view: 'ভিউ যোগ করুন',
filter: 'ফিল্টার',
collapse: 'সব ভাঁজ করুন',
// TODO: Translate
@@ -144,6 +146,7 @@ export const bn: LanguageTranslation = {
field_actions: {
title: 'ফিল্ড কর্ম',
unique: 'অদ্বিতীয়',
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
@@ -176,14 +179,17 @@ export const bn: LanguageTranslation = {
description: 'শুরু করতে একটি টেবিল তৈরি করুন',
},
},
relationships_section: {
relationships: 'সম্পর্ক',
refs_section: {
refs: 'রেফস',
filter: 'ফিল্টার',
add_relationship: 'সম্পর্ক যোগ করুন',
collapse: 'সব ভাঁজ করুন',
add_relationship: 'সম্পর্ক যোগ করুন',
relationships: 'সম্পর্ক',
dependencies: 'নির্ভরতাগুলি',
relationship: {
relationship: 'সম্পর্ক',
primary: 'প্রাথমিক টেবিল',
foreign: 'বিদেশি টেবিল',
foreign: 'রেফারেন্স করা টেবিল',
cardinality: 'কার্ডিনালিটি',
delete_relationship: 'মুছুন',
relationship_actions: {
@@ -191,27 +197,19 @@ export const bn: LanguageTranslation = {
delete_relationship: 'মুছুন',
},
},
empty_state: {
title: 'কোনো সম্পর্ক নেই',
description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন',
},
},
dependencies_section: {
dependencies: 'নির্ভরতাগুলি',
filter: 'ফিল্টার',
collapse: 'ভাঁজ করুন',
dependency: {
dependency: 'নির্ভরতা',
table: 'টেবিল',
dependent_table: 'নির্ভরশীল টেবিল',
delete_dependency: 'নির্ভরতা মুছুন',
dependent_table: 'নির্ভরশীল ভিউ',
delete_dependency: 'মুছুন',
dependency_actions: {
title: 'কর্ম',
delete_dependency: 'নির্ভরতা মুছুন',
delete_dependency: 'মুছুন',
},
},
empty_state: {
title: 'কোনো নির্ভরতাগুলি নেই',
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
title: 'কোনো সম্পর্ক নেই',
description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
},
},
@@ -251,6 +249,7 @@ export const bn: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'কোন enum মান সংজ্ঞায়িত নেই',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -273,7 +272,7 @@ export const bn: LanguageTranslation = {
show_all: 'সব দেখান',
undo: 'পূর্বাবস্থায় ফিরুন',
redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
reorder_diagram: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান',
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
// TODO: Translate
@@ -317,7 +316,7 @@ export const bn: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'চিত্র খুলুন',
title: 'ডেটাবেস খুলুন',
description: 'নিচের তালিকা থেকে একটি চিত্র নির্বাচন করুন।',
table_columns: {
name: 'নাম',
@@ -327,6 +326,12 @@ export const bn: LanguageTranslation = {
},
cancel: 'বাতিল করুন',
open: 'খুলুন',
diagram_actions: {
open: 'খুলুন',
duplicate: 'ডুপ্লিকেট',
delete: 'মুছুন',
},
},
export_sql_dialog: {
@@ -475,6 +480,7 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
@@ -496,6 +502,9 @@ export const bn: LanguageTranslation = {
language_select: {
change_language: 'ভাষা পরিবর্তন করুন',
},
on: 'চালু',
off: 'বন্ধ',
},
};

View File

@@ -6,21 +6,21 @@ export const de: LanguageTranslation = {
new_diagram: 'Neu',
browse: 'Durchsuchen',
tables: 'Tabellen',
relationships: 'Beziehungen',
refs: 'Refs',
areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
},
menu: {
databases: {
databases: 'Datenbanken',
new: 'Neues Diagramm',
actions: {
actions: 'Aktionen',
new: 'Neu...',
browse: 'Durchsuchen...',
save: 'Speichern',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
delete_diagram: 'Löschen',
},
edit: {
edit: 'Bearbeiten',
@@ -37,6 +37,7 @@ export const de: LanguageTranslation = {
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
show_views: 'Datenbankansichten',
theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
@@ -74,10 +75,10 @@ export const de: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Diagramm neu anordnen',
title: 'Diagramm automatisch anordnen',
description:
'Diese Aktion wird alle Tabellen im Diagramm neu anordnen. Möchten Sie fortfahren?',
reorder: 'Neu anordnen',
reorder: 'Automatisch anordnen',
cancel: 'Abbrechen',
},
@@ -120,6 +121,7 @@ export const de: LanguageTranslation = {
tables_section: {
tables: 'Tabellen',
add_table: 'Tabelle hinzufügen',
add_view: 'Ansicht hinzufügen',
filter: 'Filter',
collapse: 'Alle einklappen',
// TODO: Translate
@@ -145,6 +147,7 @@ export const de: LanguageTranslation = {
field_actions: {
title: 'Feldattribute',
unique: 'Eindeutig',
auto_increment: 'Automatisch hochzählen',
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
@@ -177,32 +180,26 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Tabelle, um zu beginnen',
},
},
relationships_section: {
relationships: 'Beziehungen',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Beziehung hinzufügen',
collapse: 'Alle einklappen',
add_relationship: 'Beziehung hinzufügen',
relationships: 'Beziehungen',
dependencies: 'Abhängigkeiten',
relationship: {
relationship: 'Beziehung',
primary: 'Primäre Tabelle',
foreign: 'Referenzierte Tabelle',
cardinality: 'Kardinalität',
delete_relationship: 'Beziehung löschen',
delete_relationship: 'Löschen',
relationship_actions: {
title: 'Aktionen',
delete_relationship: 'Beziehung löschen',
delete_relationship: 'Löschen',
},
},
empty_state: {
title: 'Keine Beziehungen',
description:
'Erstellen Sie eine Beziehung, um Tabellen zu verbinden',
},
},
dependencies_section: {
dependencies: 'Abhängigkeiten',
filter: 'Filter',
collapse: 'Alle einklappen',
dependency: {
dependency: 'Abhängigkeit',
table: 'Tabelle',
dependent_table: 'Abhängige Ansicht',
delete_dependency: 'Löschen',
@@ -212,8 +209,8 @@ export const de: LanguageTranslation = {
},
},
empty_state: {
title: 'Keine Abhängigkeiten',
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
title: 'Keine Beziehungen',
description: 'Erstellen Sie eine Beziehung, um zu beginnen',
},
},
@@ -253,6 +250,7 @@ export const de: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Keine Enum-Werte definiert',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -275,7 +273,7 @@ export const de: LanguageTranslation = {
show_all: 'Alle anzeigen',
undo: 'Rückgängig',
redo: 'Wiederholen',
reorder_diagram: 'Diagramm neu anordnen',
reorder_diagram: 'Diagramm automatisch anordnen',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
@@ -321,7 +319,7 @@ export const de: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Diagramm öffnen',
title: 'Datenbank öffnen',
description: 'Wählen Sie ein Diagramm aus der Liste unten aus.',
table_columns: {
name: 'Name',
@@ -331,6 +329,12 @@ export const de: LanguageTranslation = {
},
cancel: 'Abbrechen',
open: 'Öffnen',
diagram_actions: {
open: 'Öffnen',
duplicate: 'Duplizieren',
delete: 'Löschen',
},
},
export_sql_dialog: {
@@ -479,6 +483,7 @@ export const de: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Neue Tabelle',
new_view: 'Neue Ansicht',
new_relationship: 'Neue Beziehung',
// TODO: Translate
new_area: 'New Area',
@@ -501,6 +506,9 @@ export const de: LanguageTranslation = {
language_select: {
change_language: 'Sprache',
},
on: 'Ein',
off: 'Aus',
},
};

View File

@@ -6,21 +6,21 @@ export const en = {
new_diagram: 'New',
browse: 'Browse',
tables: 'Tables',
relationships: 'Refs',
refs: 'Refs',
areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
},
menu: {
databases: {
databases: 'Databases',
new: 'New Diagram',
actions: {
actions: 'Actions',
new: 'New...',
browse: 'Browse...',
save: 'Save',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
delete_diagram: 'Delete',
},
edit: {
edit: 'Edit',
@@ -37,6 +37,7 @@ export const en = {
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll',
show_views: 'Database Views',
theme: 'Theme',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
@@ -72,10 +73,10 @@ export const en = {
},
reorder_diagram_alert: {
title: 'Reorder Diagram',
title: 'Auto Arrange Diagram',
description:
'This action will rearrange all tables in the diagram. Do you want to continue?',
reorder: 'Reorder',
reorder: 'Auto Arrange',
cancel: 'Cancel',
},
@@ -117,6 +118,7 @@ export const en = {
tables_section: {
tables: 'Tables',
add_table: 'Add Table',
add_view: 'Add View',
filter: 'Filter',
collapse: 'Collapse All',
clear: 'Clear Filter',
@@ -140,6 +142,7 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
auto_increment: 'Auto Increment',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
@@ -170,12 +173,15 @@ export const en = {
description: 'Create a table to get started',
},
},
relationships_section: {
relationships: 'Relationships',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Add Relationship',
collapse: 'Collapse All',
add_relationship: 'Add Relationship',
relationships: 'Relationships',
dependencies: 'Dependencies',
relationship: {
relationship: 'Relationship',
primary: 'Primary Table',
foreign: 'Referenced Table',
cardinality: 'Cardinality',
@@ -185,16 +191,8 @@ export const en = {
delete_relationship: 'Delete',
},
},
empty_state: {
title: 'No relationships',
description: 'Create a relationship to connect tables',
},
},
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
dependency: 'Dependency',
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
@@ -204,8 +202,8 @@ export const en = {
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'No relationships',
description: 'Create a relationship to get started',
},
},
@@ -244,6 +242,7 @@ export const en = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'No enum values defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -266,7 +265,7 @@ export const en = {
show_all: 'Show All',
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Reorder Diagram',
reorder_diagram: 'Auto Arrange Diagram',
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -308,7 +307,7 @@ export const en = {
},
open_diagram_dialog: {
title: 'Open Diagram',
title: 'Open Database',
description: 'Select a diagram to open from the list below.',
table_columns: {
name: 'Name',
@@ -318,6 +317,12 @@ export const en = {
},
cancel: 'Cancel',
open: 'Open',
diagram_actions: {
open: 'Open',
duplicate: 'Duplicate',
delete: 'Delete',
},
},
export_sql_dialog: {
@@ -465,6 +470,7 @@ export const en = {
canvas_context_menu: {
new_table: 'New Table',
new_view: 'New View',
new_relationship: 'New Relationship',
new_area: 'New Area',
},
@@ -485,6 +491,9 @@ export const en = {
language_select: {
change_language: 'Language',
},
on: 'On',
off: 'Off',
},
};

View File

@@ -6,21 +6,21 @@ export const es: LanguageTranslation = {
new_diagram: 'Nuevo',
browse: 'Examinar',
tables: 'Tablas',
relationships: 'Relaciones',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
},
menu: {
databases: {
databases: 'Bases de Datos',
new: 'Nuevo Diagrama',
actions: {
actions: 'Acciones',
new: 'Nuevo...',
browse: 'Examinar...',
save: 'Guardar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
delete_diagram: 'Eliminar',
},
edit: {
edit: 'Editar',
@@ -37,6 +37,7 @@ export const es: LanguageTranslation = {
show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
show_views: 'Vistas de Base de Datos',
theme: 'Tema',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
@@ -73,10 +74,10 @@ export const es: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Reordenar Diagrama',
title: 'Organizar Diagrama Automáticamente',
description:
'Esta acción reorganizará todas las tablas en el diagrama. ¿Deseas continuar?',
reorder: 'Reordenar',
reorder: 'Organizar Automáticamente',
cancel: 'Cancelar',
},
@@ -118,6 +119,7 @@ export const es: LanguageTranslation = {
tables_section: {
tables: 'Tablas',
add_table: 'Agregar Tabla',
add_view: 'Agregar Vista',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
// TODO: Translate
@@ -143,6 +145,7 @@ export const es: LanguageTranslation = {
field_actions: {
title: 'Atributos del Campo',
unique: 'Único',
auto_increment: 'Autoincremento',
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
@@ -175,14 +178,17 @@ export const es: LanguageTranslation = {
description: 'Crea una tabla para comenzar',
},
},
relationships_section: {
relationships: 'Relaciones',
add_relationship: 'Agregar Relación',
refs_section: {
refs: 'Refs',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
add_relationship: 'Agregar Relación',
relationships: 'Relaciones',
dependencies: 'Dependencias',
relationship: {
primary: 'Primaria',
foreign: 'Foránea',
relationship: 'Relación',
primary: 'Tabla Primaria',
foreign: 'Tabla Referenciada',
cardinality: 'Cardinalidad',
delete_relationship: 'Eliminar',
relationship_actions: {
@@ -190,18 +196,10 @@ export const es: LanguageTranslation = {
delete_relationship: 'Eliminar',
},
},
empty_state: {
title: 'No hay relaciones',
description: 'Crea una relación para conectar tablas',
},
},
dependencies_section: {
dependencies: 'Dependencias',
filter: 'Filtro',
collapse: 'Colapsar todo',
dependency: {
dependency: 'Dependencia',
table: 'Tabla',
dependent_table: 'Vista dependiente',
dependent_table: 'Vista Dependiente',
delete_dependency: 'Eliminar',
dependency_actions: {
title: 'Acciones',
@@ -209,8 +207,8 @@ export const es: LanguageTranslation = {
},
},
empty_state: {
title: 'Sin dependencias',
description: 'Crea una vista para comenzar',
title: 'Sin relaciones',
description: 'Crea una relación para comenzar',
},
},
@@ -250,6 +248,7 @@ export const es: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'No hay valores de enum definidos',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const es: LanguageTranslation = {
show_all: 'Mostrar Todo',
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
reorder_diagram: 'Organizar Diagrama Automáticamente',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -317,7 +316,7 @@ export const es: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Abrir Diagrama',
title: 'Abrir Base de Datos',
description:
'Selecciona un diagrama para abrir de la lista a continuación.',
table_columns: {
@@ -328,6 +327,12 @@ export const es: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Eliminar',
},
},
export_sql_dialog: {
@@ -477,6 +482,7 @@ export const es: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nueva Tabla',
new_view: 'Nueva Vista',
new_relationship: 'Nueva Relación',
// TODO: Translate
new_area: 'New Area',
@@ -499,6 +505,9 @@ export const es: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Encendido',
off: 'Apagado',
},
};

View File

@@ -6,21 +6,21 @@ export const fr: LanguageTranslation = {
new_diagram: 'Nouveau',
browse: 'Parcourir',
tables: 'Tables',
relationships: 'Relations',
refs: 'Refs',
areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
},
menu: {
databases: {
databases: 'Bases de Données',
new: 'Nouveau Diagramme',
actions: {
actions: 'Actions',
new: 'Nouveau...',
browse: 'Parcourir...',
save: 'Enregistrer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
delete_diagram: 'Supprimer',
},
edit: {
edit: 'Édition',
@@ -37,6 +37,7 @@ export const fr: LanguageTranslation = {
hide_field_attributes: 'Masquer les Attributs de Champ',
show_field_attributes: 'Afficher les Attributs de Champ',
zoom_on_scroll: 'Zoom sur le Défilement',
show_views: 'Vues de Base de Données',
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
@@ -72,10 +73,10 @@ export const fr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Réorganiser le Diagramme',
title: 'Organiser Automatiquement le Diagramme',
description:
'Cette action réorganisera toutes les tables dans le diagramme. Voulez-vous continuer ?',
reorder: 'Réorganiser',
reorder: 'Organiser Automatiquement',
cancel: 'Annuler',
},
@@ -117,6 +118,7 @@ export const fr: LanguageTranslation = {
tables_section: {
tables: 'Tables',
add_table: 'Ajouter une Table',
add_view: 'Ajouter une Vue',
filter: 'Filtrer',
collapse: 'Réduire Tout',
clear: 'Effacer le Filtre',
@@ -141,6 +143,7 @@ export const fr: LanguageTranslation = {
field_actions: {
title: 'Attributs du Champ',
unique: 'Unique',
auto_increment: 'Auto-incrément',
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
@@ -173,12 +176,15 @@ export const fr: LanguageTranslation = {
description: 'Créez une table pour commencer',
},
},
relationships_section: {
relationships: 'Relations',
refs_section: {
refs: 'Refs',
filter: 'Filtrer',
add_relationship: 'Ajouter une Relation',
collapse: 'Réduire Tout',
add_relationship: 'Ajouter une Relation',
relationships: 'Relations',
dependencies: 'Dépendances',
relationship: {
relationship: 'Relation',
primary: 'Table Principale',
foreign: 'Table Référencée',
cardinality: 'Cardinalité',
@@ -188,16 +194,8 @@ export const fr: LanguageTranslation = {
delete_relationship: 'Supprimer',
},
},
empty_state: {
title: 'Aucune relation',
description: 'Créez une relation pour connecter les tables',
},
},
dependencies_section: {
dependencies: 'Dépendances',
filter: 'Filtrer',
collapse: 'Réduire Tout',
dependency: {
dependency: 'Dépendance',
table: 'Table',
dependent_table: 'Vue Dépendante',
delete_dependency: 'Supprimer',
@@ -207,8 +205,8 @@ export const fr: LanguageTranslation = {
},
},
empty_state: {
title: 'Aucune dépendance',
description: 'Créez une vue pour commencer',
title: 'Aucune relation',
description: 'Créez une relation pour commencer',
},
},
@@ -248,6 +246,7 @@ export const fr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: "Aucune valeur d'énumération définie",
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -270,7 +269,7 @@ export const fr: LanguageTranslation = {
show_all: 'Afficher Tout',
undo: 'Annuler',
redo: 'Rétablir',
reorder_diagram: 'Réorganiser le Diagramme',
reorder_diagram: 'Organiser Automatiquement le Diagramme',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -314,7 +313,7 @@ export const fr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Ouvrir Diagramme',
title: 'Ouvrir Base de Données',
description:
'Sélectionnez un diagramme à ouvrir dans la liste ci-dessous.',
table_columns: {
@@ -325,6 +324,12 @@ export const fr: LanguageTranslation = {
},
cancel: 'Annuler',
open: 'Ouvrir',
diagram_actions: {
open: 'Ouvrir',
duplicate: 'Dupliquer',
delete: 'Supprimer',
},
},
export_sql_dialog: {
@@ -473,6 +478,7 @@ export const fr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nouvelle Table',
new_view: 'Nouvelle Vue',
new_relationship: 'Nouvelle Relation',
// TODO: Translate
new_area: 'New Area',
@@ -495,6 +501,9 @@ export const fr: LanguageTranslation = {
language_select: {
change_language: 'Langue',
},
on: 'Activé',
off: 'Désactivé',
},
};

View File

@@ -6,21 +6,21 @@ export const gu: LanguageTranslation = {
new_diagram: 'નવું',
browse: 'બ્રાઉજ',
tables: 'ટેબલો',
relationships: 'સંબંધો',
refs: 'રેફ્સ',
areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: {
databases: {
databases: 'ડેટાબેસેસ',
new: 'નવું ડાયાગ્રામ',
actions: {
actions: 'ક્રિયાઓ',
new: 'નવું...',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
delete_diagram: 'કાઢી નાખો',
},
edit: {
edit: 'ફેરફાર',
@@ -37,6 +37,7 @@ export const gu: LanguageTranslation = {
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
show_views: 'ડેટાબેઝ વ્યૂઝ',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
@@ -74,10 +75,10 @@ export const gu: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ડાયાગ્રામ ફરી વ્યવસ્થિત કરો',
title: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો',
description:
'આ ક્રિયા ડાયાગ્રામમાં બધી ટેબલ્સને ફરીથી વ્યવસ્થિત કરશે. શું તમે ચાલુ રાખવા માંગો છો?',
reorder: 'ફરી વ્યવસ્થિત કરો',
reorder: 'ઑટોમેટિક ગોઠવો',
cancel: 'રદ કરો',
},
@@ -119,6 +120,7 @@ export const gu: LanguageTranslation = {
tables_section: {
tables: 'ટેબલ્સ',
add_table: 'ટેબલ ઉમેરો',
add_view: 'વ્યૂ ઉમેરો',
filter: 'ફિલ્ટર',
collapse: 'બધાને સકુચિત કરો',
// TODO: Translate
@@ -145,6 +147,7 @@ export const gu: LanguageTranslation = {
field_actions: {
title: 'ફીલ્ડ લક્ષણો',
unique: 'અદ્વિતીય',
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
@@ -177,14 +180,17 @@ export const gu: LanguageTranslation = {
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
},
},
relationships_section: {
relationships: 'સંબંધો',
refs_section: {
refs: 'રેફ્સ',
filter: 'ફિલ્ટર',
add_relationship: 'સંબંધ ઉમેરો',
collapse: 'બધાને સકુચિત કરો',
add_relationship: 'સંબંધ ઉમેરો',
relationships: 'સંબંધો',
dependencies: 'નિર્ભરતાઓ',
relationship: {
relationship: 'સંબંધ',
primary: 'પ્રાથમિક ટેબલ',
foreign: 'સંદર્ભ ટેબલ',
foreign: 'સંદર્ભિત ટેબલ',
cardinality: 'કાર્ડિનાલિટી',
delete_relationship: 'કાઢી નાખો',
relationship_actions: {
@@ -192,27 +198,19 @@ export const gu: LanguageTranslation = {
delete_relationship: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ સંબંધો નથી',
description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો',
},
},
dependencies_section: {
dependencies: 'નિર્ભરતાઓ',
filter: 'ફિલ્ટર',
collapse: 'સિકોડો',
dependency: {
dependency: 'નિર્ભરતા',
table: 'ટેબલ',
dependent_table: 'આધાર રાખેલું ટેબલ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
dependent_table: 'નિર્ભરશીલ વ્યૂ',
delete_dependency: 'કાઢી નાખો',
dependency_actions: {
title: 'ક્રિયાઓ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
delete_dependency: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ નિર્ભરતાઓ નથી',
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
title: 'કોઈ સંબંધો નથી',
description: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
},
},
@@ -252,6 +250,7 @@ export const gu: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'કોઈ enum મૂલ્યો વ્યાખ્યાયિત નથી',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -274,7 +273,7 @@ export const gu: LanguageTranslation = {
show_all: 'બધું બતાવો',
undo: 'અનડુ',
redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
reorder_diagram: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -317,7 +316,7 @@ export const gu: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'ડાયાગ્રામ ખોલો',
title: 'ડેટાબેસ ખોલો',
description: 'નીચેની યાદીમાંથી એક ડાયાગ્રામ પસંદ કરો.',
table_columns: {
name: 'નામ',
@@ -327,6 +326,12 @@ export const gu: LanguageTranslation = {
},
cancel: 'રદ કરો',
open: 'ખોલો',
diagram_actions: {
open: 'ખોલો',
duplicate: 'ડુપ્લિકેટ',
delete: 'કાઢી નાખો',
},
},
export_sql_dialog: {
@@ -476,6 +481,7 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
@@ -497,6 +503,9 @@ export const gu: LanguageTranslation = {
language_select: {
change_language: 'ભાષા બદલો',
},
on: 'ચાલુ',
off: 'બંધ',
},
};

View File

@@ -6,21 +6,21 @@ export const hi: LanguageTranslation = {
new_diagram: 'नया',
browse: 'ब्राउज़',
tables: 'टेबल',
relationships: 'संबंध',
refs: 'रेफ्स',
areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
},
menu: {
databases: {
databases: 'डेटाबेस',
new: 'नया आरेख',
actions: {
actions: 'कार्य',
new: 'नया...',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
delete_diagram: 'हटाएँ',
},
edit: {
edit: 'संपादित करें',
@@ -37,6 +37,7 @@ export const hi: LanguageTranslation = {
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
show_views: 'डेटाबेस व्यू',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
@@ -73,10 +74,10 @@ export const hi: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'आरेख पुनः व्यवस्थित करें',
title: 'आरेख स्वचालित व्यवस्थित करें',
description:
'यह क्रिया आरेख में सभी तालिकाओं को पुनः व्यवस्थित कर देगी। क्या आप जारी रखना चाहते हैं?',
reorder: 'पुनः व्यवस्थित करें',
reorder: 'स्वचालित व्यवस्थित करें',
cancel: 'रद्द करें',
},
@@ -119,6 +120,7 @@ export const hi: LanguageTranslation = {
tables_section: {
tables: 'तालिकाएँ',
add_table: 'तालिका जोड़ें',
add_view: 'व्यू जोड़ें',
filter: 'फ़िल्टर',
collapse: 'सभी को संक्षिप्त करें',
// TODO: Translate
@@ -144,6 +146,7 @@ export const hi: LanguageTranslation = {
field_actions: {
title: 'फ़ील्ड विशेषताएँ',
unique: 'अद्वितीय',
auto_increment: 'ऑटो इंक्रीमेंट',
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
@@ -176,12 +179,15 @@ export const hi: LanguageTranslation = {
description: 'शुरू करने के लिए एक तालिका बनाएँ',
},
},
relationships_section: {
relationships: 'संबंध',
refs_section: {
refs: 'रेफ्स',
filter: 'फ़िल्टर',
add_relationship: 'संबंध जोड़ें',
collapse: 'सभी को संक्षिप्त करें',
add_relationship: 'संबंध जोड़ें',
relationships: 'संबंध',
dependencies: 'निर्भरताएँ',
relationship: {
relationship: 'संबंध',
primary: 'प्राथमिक तालिका',
foreign: 'संदर्भित तालिका',
cardinality: 'कार्डिनैलिटी',
@@ -191,28 +197,19 @@ export const hi: LanguageTranslation = {
delete_relationship: 'हटाएँ',
},
},
empty_state: {
title: 'कोई संबंध नहीं',
description:
'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ',
},
},
dependencies_section: {
dependencies: 'निर्भरताएँ',
filter: 'फ़िल्टर',
collapse: 'सिकोड़ें',
dependency: {
dependency: 'निर्भरता',
table: 'तालिका',
dependent_table: 'आश्रित तालिका',
delete_dependency: 'निर्भरता हटाएँ',
dependent_table: 'आश्रित दृश्य',
delete_dependency: 'हटाएँ',
dependency_actions: {
title: 'कार्रवाइयाँ',
delete_dependency: 'निर्भरता हटाएँ',
title: 'क्रियाँ',
delete_dependency: 'हटाएँ',
},
},
empty_state: {
title: 'कोई निर्भरता नहीं',
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
title: 'कोई संबंध नहीं',
description: 'शुरू करने के लिए एक संबंध बनाएँ',
},
},
@@ -252,6 +249,7 @@ export const hi: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कोई enum मान परिभाषित नहीं',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -274,7 +272,7 @@ export const hi: LanguageTranslation = {
show_all: 'सभी दिखाएँ',
undo: 'पूर्ववत करें',
redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
reorder_diagram: 'आरेख स्वचालित व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -320,7 +318,7 @@ export const hi: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'आरेख खोलें',
title: 'डेटाबेस खोलें',
description: 'नीचे दी गई सूची से एक आरेख चुनें।',
table_columns: {
name: 'नाम',
@@ -330,6 +328,12 @@ export const hi: LanguageTranslation = {
},
cancel: 'रद्द करें',
open: 'खोलें',
diagram_actions: {
open: 'खोलें',
duplicate: 'डुप्लिकेट',
delete: 'हटाएं',
},
},
export_sql_dialog: {
@@ -479,6 +483,7 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_view: 'नया व्यू',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
@@ -501,6 +506,9 @@ export const hi: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदलें',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -6,21 +6,21 @@ export const hr: LanguageTranslation = {
new_diagram: 'Novi',
browse: 'Pregledaj',
tables: 'Tablice',
relationships: 'Veze',
refs: 'Refs',
areas: 'Područja',
dependencies: 'Ovisnosti',
custom_types: 'Prilagođeni Tipovi',
},
menu: {
databases: {
databases: 'Baze Podataka',
new: 'Novi Dijagram',
actions: {
actions: 'Akcije',
new: 'Novi...',
browse: 'Pregledaj...',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
delete_diagram: 'Izbriši',
},
edit: {
edit: 'Uredi',
@@ -37,6 +37,7 @@ export const hr: LanguageTranslation = {
hide_field_attributes: 'Sakrij atribute polja',
show_field_attributes: 'Prikaži atribute polja',
zoom_on_scroll: 'Zumiranje pri skrolanju',
show_views: 'Pogledi Baze Podataka',
theme: 'Tema',
show_dependencies: 'Prikaži ovisnosti',
hide_dependencies: 'Sakrij ovisnosti',
@@ -72,10 +73,10 @@ export const hr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Preuredi dijagram',
title: 'Automatski preuredi dijagram',
description:
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
reorder: 'Preuredi',
reorder: 'Automatski preuredi',
cancel: 'Odustani',
},
@@ -117,6 +118,7 @@ export const hr: LanguageTranslation = {
tables_section: {
tables: 'Tablice',
add_table: 'Dodaj tablicu',
add_view: 'Dodaj Pogled',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
clear: 'Očisti filter',
@@ -141,6 +143,7 @@ export const hr: LanguageTranslation = {
field_actions: {
title: 'Atributi polja',
unique: 'Jedinstven',
auto_increment: 'Automatsko povećavanje',
character_length: 'Maksimalna dužina',
precision: 'Preciznost',
scale: 'Skala',
@@ -171,12 +174,15 @@ export const hr: LanguageTranslation = {
description: 'Stvorite tablicu za početak',
},
},
relationships_section: {
relationships: 'Veze',
refs_section: {
refs: 'Refs',
filter: 'Filtriraj',
add_relationship: 'Dodaj vezu',
collapse: 'Sažmi sve',
add_relationship: 'Dodaj vezu',
relationships: 'Veze',
dependencies: 'Ovisnosti',
relationship: {
relationship: 'Veza',
primary: 'Primarna tablica',
foreign: 'Referentna tablica',
cardinality: 'Kardinalnost',
@@ -186,16 +192,8 @@ export const hr: LanguageTranslation = {
delete_relationship: 'Izbriši',
},
},
empty_state: {
title: 'Nema veza',
description: 'Stvorite vezu za povezivanje tablica',
},
},
dependencies_section: {
dependencies: 'Ovisnosti',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
dependency: {
dependency: 'Ovisnost',
table: 'Tablica',
dependent_table: 'Ovisni pogled',
delete_dependency: 'Izbriši',
@@ -205,8 +203,8 @@ export const hr: LanguageTranslation = {
},
},
empty_state: {
title: 'Nema ovisnosti',
description: 'Stvorite pogled za početak',
title: 'Nema veze',
description: 'Stvorite vezu za početak',
},
},
@@ -247,6 +245,7 @@ export const hr: LanguageTranslation = {
enum_values: 'Enum vrijednosti',
composite_fields: 'Polja',
no_fields: 'Nema definiranih polja',
no_values: 'Nema definiranih enum vrijednosti',
field_name_placeholder: 'Naziv polja',
field_type_placeholder: 'Odaberi tip',
add_field: 'Dodaj polje',
@@ -270,7 +269,7 @@ export const hr: LanguageTranslation = {
show_all: 'Prikaži sve',
undo: 'Poništi',
redo: 'Ponovi',
reorder_diagram: 'Preuredi dijagram',
reorder_diagram: 'Automatski preuredi dijagram',
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -312,7 +311,7 @@ export const hr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Otvori dijagram',
title: 'Otvori bazu podataka',
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
table_columns: {
name: 'Naziv',
@@ -322,6 +321,12 @@ export const hr: LanguageTranslation = {
},
cancel: 'Odustani',
open: 'Otvori',
diagram_actions: {
open: 'Otvori',
duplicate: 'Dupliciraj',
delete: 'Obriši',
},
},
export_sql_dialog: {
@@ -470,6 +475,7 @@ export const hr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova tablica',
new_view: 'Novi Pogled',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
},
@@ -490,6 +496,9 @@ export const hr: LanguageTranslation = {
language_select: {
change_language: 'Jezik',
},
on: 'Uključeno',
off: 'Isključeno',
},
};

View File

@@ -6,21 +6,21 @@ export const id_ID: LanguageTranslation = {
new_diagram: 'Baru',
browse: 'Jelajahi',
tables: 'Tabel',
relationships: 'Relasi',
refs: 'Refs',
areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
},
menu: {
databases: {
databases: 'Basis Data',
new: 'Diagram Baru',
actions: {
actions: 'Aksi',
new: 'Baru...',
browse: 'Jelajahi...',
save: 'Simpan',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
delete_diagram: 'Hapus',
},
edit: {
edit: 'Ubah',
@@ -37,6 +37,7 @@ export const id_ID: LanguageTranslation = {
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll',
show_views: 'Tampilan Database',
theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi',
hide_dependencies: 'Sembunyikan Dependensi',
@@ -73,10 +74,10 @@ export const id_ID: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Atur Ulang Diagram',
title: 'Atur Otomatis Diagram',
description:
'Tindakan ini akan mengatur ulang semua tabel di diagram. Apakah Anda ingin melanjutkan?',
reorder: 'Atur Ulang',
reorder: 'Atur Otomatis',
cancel: 'Batal',
},
@@ -118,6 +119,7 @@ export const id_ID: LanguageTranslation = {
tables_section: {
tables: 'Tabel',
add_table: 'Tambah Tabel',
add_view: 'Tambah Tampilan',
filter: 'Saring',
collapse: 'Lipat Semua',
// TODO: Translate
@@ -143,6 +145,7 @@ export const id_ID: LanguageTranslation = {
field_actions: {
title: 'Atribut Kolom',
unique: 'Unik',
auto_increment: 'Kenaikan Otomatis',
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
@@ -175,12 +178,15 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tabel untuk memulai',
},
},
relationships_section: {
relationships: 'Hubungan',
refs_section: {
refs: 'Refs',
filter: 'Saring',
add_relationship: 'Tambah Hubungan',
collapse: 'Lipat Semua',
add_relationship: 'Tambah Hubungan',
relationships: 'Hubungan',
dependencies: 'Dependensi',
relationship: {
relationship: 'Hubungan',
primary: 'Tabel Primer',
foreign: 'Tabel Referensi',
cardinality: 'Kardinalitas',
@@ -190,16 +196,8 @@ export const id_ID: LanguageTranslation = {
delete_relationship: 'Hapus',
},
},
empty_state: {
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk menghubungkan tabel',
},
},
dependencies_section: {
dependencies: 'Dependensi',
filter: 'Saring',
collapse: 'Lipat Semua',
dependency: {
dependency: 'Dependensi',
table: 'Tabel',
dependent_table: 'Tampilan Dependen',
delete_dependency: 'Hapus',
@@ -209,8 +207,8 @@ export const id_ID: LanguageTranslation = {
},
},
empty_state: {
title: 'Tidak ada dependensi',
description: 'Buat tampilan untuk memulai',
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk memulai',
},
},
@@ -250,6 +248,7 @@ export const id_ID: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Tidak ada nilai enum yang ditentukan',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const id_ID: LanguageTranslation = {
show_all: 'Tampilkan Semua',
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Atur Ulang Diagram',
reorder_diagram: 'Atur Otomatis Diagram',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -316,7 +315,7 @@ export const id_ID: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Buka Diagram',
title: 'Buka Database',
description: 'Pilih diagram untuk dibuka dari daftar di bawah.',
table_columns: {
name: 'Name',
@@ -326,6 +325,12 @@ export const id_ID: LanguageTranslation = {
},
cancel: 'Batal',
open: 'Buka',
diagram_actions: {
open: 'Buka',
duplicate: 'Duplikat',
delete: 'Hapus',
},
},
export_sql_dialog: {
@@ -475,6 +480,7 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tabel Baru',
new_view: 'Tampilan Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
new_area: 'New Area',
@@ -496,6 +502,9 @@ export const id_ID: LanguageTranslation = {
language_select: {
change_language: 'Bahasa',
},
on: 'Aktif',
off: 'Nonaktif',
},
};

View File

@@ -6,21 +6,21 @@ export const ja: LanguageTranslation = {
new_diagram: '新規',
browse: '参照',
tables: 'テーブル',
relationships: 'リレーション',
refs: '参照',
areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
},
menu: {
databases: {
databases: 'データベース',
new: '新しいダイアグラム',
actions: {
actions: 'アクション',
new: '新規...',
browse: '参照...',
save: '保存',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
delete_diagram: '削除',
},
edit: {
edit: '編集',
@@ -37,6 +37,7 @@ export const ja: LanguageTranslation = {
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
show_views: 'データベースビュー',
theme: 'テーマ',
// TODO: Translate
show_dependencies: 'Show Dependencies',
@@ -75,10 +76,10 @@ export const ja: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ダイアグラムを並べ替え',
title: 'ダイアグラムを自動配置',
description:
'この操作によりダイアグラム内のすべてのテーブルが再配置されます。続行しますか?',
reorder: '並べ替え',
reorder: '自動配置',
cancel: 'キャンセル',
},
@@ -122,6 +123,7 @@ export const ja: LanguageTranslation = {
tables_section: {
tables: 'テーブル',
add_table: 'テーブルを追加',
add_view: 'ビューを追加',
filter: 'フィルタ',
collapse: 'すべて折りたたむ',
// TODO: Translate
@@ -147,6 +149,7 @@ export const ja: LanguageTranslation = {
field_actions: {
title: 'フィールド属性',
unique: 'ユニーク',
auto_increment: 'オートインクリメント',
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
@@ -179,12 +182,15 @@ export const ja: LanguageTranslation = {
description: 'テーブルを作成して開始してください',
},
},
relationships_section: {
relationships: 'リレーションシップ',
refs_section: {
refs: '参照',
filter: 'フィルタ',
add_relationship: 'リレーションシップを追加',
collapse: 'すべて折りたたむ',
add_relationship: 'リレーションシップを追加',
relationships: 'リレーションシップ',
dependencies: '依存関係',
relationship: {
relationship: 'リレーションシップ',
primary: '主テーブル',
foreign: '参照テーブル',
cardinality: 'カーディナリティ',
@@ -194,29 +200,20 @@ export const ja: LanguageTranslation = {
delete_relationship: '削除',
},
},
empty_state: {
title: 'リレーションシップがありません',
description:
'テーブルを接続するためにリレーションシップを作成してください',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency: '依存関係',
table: 'テーブル',
dependent_table: '依存ビュー',
delete_dependency: '削除',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
title: '操作',
delete_dependency: '削除',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'リレーションシップがありません',
description:
'開始するためにリレーションシップを作成してください',
},
},
@@ -256,6 +253,7 @@ export const ja: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '列挙値が定義されていません',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -278,7 +276,7 @@ export const ja: LanguageTranslation = {
show_all: 'すべて表示',
undo: '元に戻す',
redo: 'やり直し',
reorder_diagram: 'ダイアグラムを並べ替え',
reorder_diagram: 'ダイアグラムを自動配置',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
@@ -322,7 +320,7 @@ export const ja: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'ダイアグラムを開く',
title: 'データベースを開く',
description: '以下のリストからダイアグラムを選択してください。',
table_columns: {
name: '名前',
@@ -332,6 +330,12 @@ export const ja: LanguageTranslation = {
},
cancel: 'キャンセル',
open: '開く',
diagram_actions: {
open: '開く',
duplicate: '複製',
delete: '削除',
},
},
export_sql_dialog: {
@@ -481,6 +485,7 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
@@ -503,6 +508,9 @@ export const ja: LanguageTranslation = {
language_select: {
change_language: '言語',
},
on: 'オン',
off: 'オフ',
},
};

View File

@@ -6,21 +6,21 @@ export const ko_KR: LanguageTranslation = {
new_diagram: '새로 만들기',
browse: '찾아보기',
tables: '테이블',
relationships: '관계',
refs: 'Refs',
areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
},
menu: {
databases: {
databases: '데이터베이스',
new: '새 다이어그램',
actions: {
actions: '작업',
new: '새로 만들기...',
browse: '찾아보기...',
save: '저장',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
delete_diagram: '삭제',
},
edit: {
edit: '편집',
@@ -37,6 +37,7 @@ export const ko_KR: LanguageTranslation = {
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
show_views: '데이터베이스 뷰',
theme: '테마',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
@@ -73,10 +74,10 @@ export const ko_KR: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '다이어그램 정렬',
title: '다이어그램 자동 정렬',
description:
'이 작업은 모든 다이어그램이 재정렬됩니다. 계속하시겠습니까?',
reorder: '정렬',
reorder: '자동 정렬',
cancel: '취소',
},
@@ -118,6 +119,7 @@ export const ko_KR: LanguageTranslation = {
tables_section: {
tables: '테이블',
add_table: '테이블 추가',
add_view: '뷰 추가',
filter: '필터',
collapse: '모두 접기',
// TODO: Translate
@@ -143,6 +145,7 @@ export const ko_KR: LanguageTranslation = {
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
auto_increment: '자동 증가',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
@@ -175,12 +178,15 @@ export const ko_KR: LanguageTranslation = {
description: '테이블을 만들어 시작하세요.',
},
},
relationships_section: {
relationships: '연관 관계',
refs_section: {
refs: 'Refs',
filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기',
add_relationship: '연관 관계 추가',
relationships: '연관 관계',
dependencies: '종속성',
relationship: {
relationship: '연관 관계',
primary: '주 테이블',
foreign: '참조 테이블',
cardinality: '카디널리티',
@@ -190,16 +196,8 @@ export const ko_KR: LanguageTranslation = {
delete_relationship: '연관 관계 삭제',
},
},
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: {
dependency: '종속성',
table: '테이블',
dependent_table: '뷰 테이블',
delete_dependency: '삭제',
@@ -209,8 +207,8 @@ export const ko_KR: LanguageTranslation = {
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
title: '연관 관계 없음',
description: '연관 관계를 만들어 시작하세요.',
},
},
@@ -250,6 +248,7 @@ export const ko_KR: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '정의된 열거형 값이 없습니다',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const ko_KR: LanguageTranslation = {
show_all: '전체 저장',
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 정렬',
reorder_diagram: '다이어그램 자동 정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -316,7 +315,7 @@ export const ko_KR: LanguageTranslation = {
},
open_diagram_dialog: {
title: '다이어그램 열기',
title: '데이터베이스 열기',
description: '아래의 목록에서 다이어그램을 선택하세요.',
table_columns: {
name: '이름',
@@ -326,6 +325,12 @@ export const ko_KR: LanguageTranslation = {
},
cancel: '취소',
open: '열기',
diagram_actions: {
open: '열기',
duplicate: '복제',
delete: '삭제',
},
},
export_sql_dialog: {
@@ -472,6 +477,7 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +499,9 @@ export const ko_KR: LanguageTranslation = {
language_select: {
change_language: '언어',
},
on: '켜기',
off: '끄기',
},
};

View File

@@ -6,21 +6,21 @@ export const mr: LanguageTranslation = {
new_diagram: 'नवीन',
browse: 'ब्राउज',
tables: 'टेबल',
relationships: 'संबंध',
refs: 'Refs',
areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
},
menu: {
databases: {
databases: 'डेटाबेस',
new: 'नवीन आरेख',
actions: {
actions: 'क्रिया',
new: 'नवीन...',
browse: 'ब्राउज करा...',
save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
delete_diagram: 'हटवा',
},
edit: {
edit: 'संपादन करा',
@@ -37,6 +37,7 @@ export const mr: LanguageTranslation = {
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
show_views: 'डेटाबेस व्ह्यूज',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा',
@@ -74,10 +75,10 @@ export const mr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'आरेख पुनःक्रमित करा',
title: 'आरेख स्वयंचलित व्यवस्थित करा',
description:
'ही क्रिया आरेखातील सर्व टेबल्सची पुनर्रचना करेल. तुम्हाला पुढे जायचे आहे का?',
reorder: 'पुनःक्रमित करा',
reorder: 'स्वयंचलित व्यवस्थित करा',
cancel: 'रद्द करा',
},
@@ -121,6 +122,7 @@ export const mr: LanguageTranslation = {
tables_section: {
tables: 'टेबल्स',
add_table: 'टेबल जोडा',
add_view: 'व्ह्यू जोडा',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
// TODO: Translate
@@ -146,6 +148,7 @@ export const mr: LanguageTranslation = {
field_actions: {
title: 'फील्ड गुणधर्म',
unique: 'युनिक',
auto_increment: 'ऑटो इंक्रिमेंट',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
@@ -179,12 +182,15 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
},
},
relationships_section: {
relationships: 'रिलेशनशिप',
refs_section: {
refs: 'Refs',
filter: 'फिल्टर',
add_relationship: 'रिलेशनशिप जोडा',
collapse: 'सर्व संकुचित करा',
add_relationship: 'रिलेशनशिप जोडा',
relationships: 'रिलेशनशिप',
dependencies: 'डिपेंडेन्सि',
relationship: {
relationship: 'रिलेशनशिप',
primary: 'प्राथमिक टेबल',
foreign: 'रेफरंस टेबल',
cardinality: 'कार्डिनॅलिटी',
@@ -194,17 +200,8 @@ export const mr: LanguageTranslation = {
delete_relationship: 'हटवा',
},
},
empty_state: {
title: 'कोणतेही रिलेशनशिप नाहीत',
description:
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
dependencies_section: {
dependencies: 'डिपेंडेन्सि',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
dependency: {
dependency: 'डिपेंडेन्सि',
table: 'टेबल',
dependent_table: 'डिपेंडेन्सि दृश्य',
delete_dependency: 'हटवा',
@@ -214,8 +211,8 @@ export const mr: LanguageTranslation = {
},
},
empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
title: 'कोणतेही रिलेशनशिप नाहीत',
description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
@@ -255,6 +252,7 @@ export const mr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कोणतीही enum मूल्ये परिभाषित नाहीत',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -277,7 +275,7 @@ export const mr: LanguageTranslation = {
show_all: 'सर्व दाखवा',
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
reorder_diagram: 'आरेख स्वयंचलित व्यवस्थित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -323,7 +321,7 @@ export const mr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'आरेख उघडा',
title: 'डेटाबेस उघडा',
description: 'खालील यादीतून उघडण्यासाठी एक आरेख निवडा.',
table_columns: {
name: 'नाव',
@@ -333,6 +331,12 @@ export const mr: LanguageTranslation = {
},
cancel: 'रद्द करा',
open: 'उघडा',
diagram_actions: {
open: 'उघडा',
duplicate: 'डुप्लिकेट',
delete: 'हटवा',
},
},
export_sql_dialog: {
@@ -485,6 +489,7 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
@@ -508,6 +513,9 @@ export const mr: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदला',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -6,21 +6,21 @@ export const ne: LanguageTranslation = {
new_diagram: 'नयाँ',
browse: 'ब्राउज',
tables: 'टेबलहरू',
relationships: 'सम्बन्धहरू',
refs: 'Refs',
areas: 'क्षेत्रहरू',
dependencies: 'निर्भरताहरू',
custom_types: 'कस्टम प्रकारहरू',
},
menu: {
databases: {
databases: 'डाटाबेसहरू',
new: 'नयाँ डायाग्राम',
actions: {
actions: 'कार्यहरू',
new: 'नयाँ...',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
delete_diagram: 'हटाउनुहोस्',
},
edit: {
edit: 'सम्पादन',
@@ -37,6 +37,7 @@ export const ne: LanguageTranslation = {
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
show_views: 'डाटाबेस भ्यूहरू',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
@@ -74,10 +75,10 @@ export const ne: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'डायाग्राम पुनः क्रमबद्ध गर्नुहोस्',
title: 'डायाग्राम स्वचालित मिलाउनुहोस्',
description:
'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।',
reorder: 'पुनः क्रमबद्ध गर्नुहोस्',
reorder: 'स्वचालित मिलाउनुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
@@ -119,6 +120,7 @@ export const ne: LanguageTranslation = {
tables_section: {
tables: 'तालिकाहरू',
add_table: 'तालिका थप्नुहोस्',
add_view: 'भ्यू थप्नुहोस्',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
// TODO: Translate
@@ -144,6 +146,7 @@ export const ne: LanguageTranslation = {
field_actions: {
title: 'क्षेत्र विशेषताहरू',
unique: 'अनन्य',
auto_increment: 'स्वचालित वृद्धि',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
@@ -176,12 +179,15 @@ export const ne: LanguageTranslation = {
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
},
},
relationships_section: {
relationships: 'सम्बन्धहरू',
refs_section: {
refs: 'Refs',
filter: 'फिल्टर',
add_relationship: 'सम्बन्ध थप्नुहोस्',
collapse: 'सबै लुकाउनुहोस्',
add_relationship: 'सम्बन्ध थप्नुहोस्',
relationships: 'सम्बन्धहरू',
dependencies: 'डिपेन्डेन्सीहरू',
relationship: {
relationship: 'सम्बन्ध',
primary: 'मुख्य तालिका',
foreign: 'परिचित तालिका',
cardinality: 'कार्डिन्यालिटी',
@@ -191,16 +197,8 @@ export const ne: LanguageTranslation = {
delete_relationship: 'हटाउनुहोस्',
},
},
empty_state: {
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
dependencies_section: {
dependencies: 'डिपेन्डेन्सीहरू',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
dependency: {
dependency: 'डिपेन्डेन्सी',
table: 'तालिका',
dependent_table: 'विचलित तालिका',
delete_dependency: 'हटाउनुहोस्',
@@ -210,9 +208,8 @@ export const ne: LanguageTranslation = {
},
},
empty_state: {
title: 'कुनै डिपेन्डेन्सीहरू छैनन्',
description:
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
@@ -252,6 +249,7 @@ export const ne: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कुनै enum मानहरू परिभाषित छैनन्',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -274,7 +272,7 @@ export const ne: LanguageTranslation = {
show_all: 'सबै देखाउनुहोस्',
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
reorder_diagram: 'डायाग्राम स्वचालित मिलाउनुहोस्',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -319,7 +317,7 @@ export const ne: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'डायाग्राम खोल्नुहोस्',
title: 'डाटाबेस खोल्नुहोस्',
description:
'तलको सूचीबाट खोल्नका लागि एक डायाग्राम चयन गर्नुहोस्।',
table_columns: {
@@ -330,6 +328,12 @@ export const ne: LanguageTranslation = {
},
cancel: 'रद्द गर्नुहोस्',
open: 'खोल्नुहोस्',
diagram_actions: {
open: 'खोल्नुहोस्',
duplicate: 'डुप्लिकेट',
delete: 'मेटाउनुहोस्',
},
},
export_sql_dialog: {
@@ -479,6 +483,7 @@ export const ne: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_view: 'नयाँ भ्यू',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
new_area: 'New Area',
@@ -500,6 +505,9 @@ export const ne: LanguageTranslation = {
language_select: {
change_language: 'भाषा परिवर्तन गर्नुहोस्',
},
on: 'सक्रिय',
off: 'निष्क्रिय',
},
};

View File

@@ -6,21 +6,21 @@ export const pt_BR: LanguageTranslation = {
new_diagram: 'Novo',
browse: 'Navegar',
tables: 'Tabelas',
relationships: 'Relacionamentos',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependências',
custom_types: 'Tipos Personalizados',
},
menu: {
databases: {
databases: 'Bancos de Dados',
new: 'Novo Diagrama',
actions: {
actions: 'Ações',
new: 'Novo...',
browse: 'Navegar...',
save: 'Salvar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama',
delete_diagram: 'Excluir',
},
edit: {
edit: 'Editar',
@@ -37,6 +37,7 @@ export const pt_BR: LanguageTranslation = {
hide_field_attributes: 'Ocultar Atributos de Campo',
show_field_attributes: 'Mostrar Atributos de Campo',
zoom_on_scroll: 'Zoom ao Rolar',
show_views: 'Visualizações do Banco de Dados',
theme: 'Tema',
show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências',
@@ -74,10 +75,10 @@ export const pt_BR: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Reordenar Diagrama',
title: 'Organizar Diagrama Automaticamente',
description:
'Esta ação reorganizará todas as tabelas no diagrama. Deseja continuar?',
reorder: 'Reordenar',
reorder: 'Organizar Automaticamente',
cancel: 'Cancelar',
},
@@ -119,6 +120,7 @@ export const pt_BR: LanguageTranslation = {
tables_section: {
tables: 'Tabelas',
add_table: 'Adicionar Tabela',
add_view: 'Adicionar Visualização',
filter: 'Filtrar',
collapse: 'Colapsar Todas',
// TODO: Translate
@@ -144,6 +146,7 @@ export const pt_BR: LanguageTranslation = {
field_actions: {
title: 'Atributos do Campo',
unique: 'Único',
auto_increment: 'Incremento Automático',
comments: 'Comentários',
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
@@ -176,12 +179,15 @@ export const pt_BR: LanguageTranslation = {
description: 'Crie uma tabela para começar',
},
},
relationships_section: {
relationships: 'Relacionamentos',
refs_section: {
refs: 'Refs',
filter: 'Filtrar',
add_relationship: 'Adicionar Relacionamento',
collapse: 'Colapsar Todas',
add_relationship: 'Adicionar Relacionamento',
relationships: 'Relacionamentos',
dependencies: 'Dependências',
relationship: {
relationship: 'Relacionamento',
primary: 'Tabela Primária',
foreign: 'Tabela Referenciada',
cardinality: 'Cardinalidade',
@@ -191,16 +197,8 @@ export const pt_BR: LanguageTranslation = {
delete_relationship: 'Excluir',
},
},
empty_state: {
title: 'Sem relacionamentos',
description: 'Crie um relacionamento para conectar tabelas',
},
},
dependencies_section: {
dependencies: 'Dependências',
filter: 'Filtrar',
collapse: 'Colapsar Todas',
dependency: {
dependency: 'Dependência',
table: 'Tabela',
dependent_table: 'Visualização Dependente',
delete_dependency: 'Excluir',
@@ -210,8 +208,8 @@ export const pt_BR: LanguageTranslation = {
},
},
empty_state: {
title: 'Sem dependências',
description: 'Crie uma visualização para começar',
title: 'Sem relacionamentos',
description: 'Crie um relacionamento para começar',
},
},
@@ -251,6 +249,7 @@ export const pt_BR: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Nenhum valor de enum definido',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -273,7 +272,7 @@ export const pt_BR: LanguageTranslation = {
show_all: 'Mostrar Tudo',
undo: 'Desfazer',
redo: 'Refazer',
reorder_diagram: 'Reordenar Diagrama',
reorder_diagram: 'Organizar Diagrama Automaticamente',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -318,7 +317,7 @@ export const pt_BR: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Abrir Diagrama',
title: 'Abrir Banco de Dados',
description: 'Selecione um diagrama para abrir da lista abaixo.',
table_columns: {
name: 'Nome',
@@ -328,6 +327,12 @@ export const pt_BR: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Excluir',
},
},
export_sql_dialog: {
@@ -477,6 +482,7 @@ export const pt_BR: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova Tabela',
new_view: 'Nova Visualização',
new_relationship: 'Novo Relacionamento',
// TODO: Translate
new_area: 'New Area',
@@ -499,6 +505,9 @@ export const pt_BR: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Ligado',
off: 'Desligado',
},
};

View File

@@ -6,21 +6,21 @@ export const ru: LanguageTranslation = {
new_diagram: 'Новая',
browse: 'Обзор',
tables: 'Таблицы',
relationships: 'Связи',
refs: 'Ссылки',
areas: 'Области',
dependencies: 'Зависимости',
custom_types: 'Пользовательские типы',
},
menu: {
databases: {
databases: 'Базы данных',
new: 'Новая диаграмма',
actions: {
actions: 'Действия',
new: 'Новая...',
browse: 'Обзор...',
save: 'Сохранить',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
delete_diagram: 'Удалить',
},
edit: {
edit: 'Изменение',
@@ -37,6 +37,7 @@ export const ru: LanguageTranslation = {
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке',
show_views: 'Представления базы данных',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
@@ -72,10 +73,10 @@ export const ru: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Переупорядочить диаграмму',
title: 'Автоматическая расстановка диаграммы',
description:
'Это действие переставит все таблицы на диаграмме. Хотите продолжить?',
reorder: 'Изменить порядок',
reorder: 'Автоматическая расстановка',
cancel: 'Отменить',
},
@@ -116,6 +117,7 @@ export const ru: LanguageTranslation = {
tables_section: {
tables: 'Таблицы',
add_table: 'Добавить таблицу',
add_view: 'Добавить представление',
filter: 'Фильтр',
collapse: 'Свернуть все',
clear: 'Очистить фильтр',
@@ -141,6 +143,7 @@ export const ru: LanguageTranslation = {
field_actions: {
title: 'Атрибуты поля',
unique: 'Уникальный',
auto_increment: 'Автоинкремент',
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
@@ -172,12 +175,15 @@ export const ru: LanguageTranslation = {
description: 'Создайте таблицу, чтобы начать',
},
},
relationships_section: {
relationships: 'Отношения',
refs_section: {
refs: 'Ссылки',
filter: 'Фильтр',
add_relationship: 'Добавить отношение',
collapse: 'Свернуть все',
add_relationship: 'Добавить отношение',
relationships: 'Отношения',
dependencies: 'Зависимости',
relationship: {
relationship: 'Отношение',
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественной связи',
@@ -187,18 +193,10 @@ export const ru: LanguageTranslation = {
delete_relationship: 'Удалить',
},
},
empty_state: {
title: 'Нет отношений',
description: 'Создайте связь для соединения таблиц',
},
},
dependencies_section: {
dependencies: 'Зависимости',
filter: 'Фильтр',
collapse: 'Свернуть все',
dependency: {
table: 'Стол',
dependent_table: 'Зависимый вид',
dependency: 'Зависимость',
table: 'Таблица',
dependent_table: 'Зависимое представление',
delete_dependency: 'Удалить',
dependency_actions: {
title: 'Действия',
@@ -206,8 +204,8 @@ export const ru: LanguageTranslation = {
},
},
empty_state: {
title: 'Нет зависимостей',
description: 'Создайте представление, чтобы начать',
title: 'Нет отношений',
description: 'Создайте отношение, чтобы начать',
},
},
@@ -248,6 +246,7 @@ export const ru: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Значения перечисления не определены',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -270,7 +269,7 @@ export const ru: LanguageTranslation = {
show_all: 'Показать все',
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
reorder_diagram: 'Автоматическая расстановка диаграммы',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -314,7 +313,7 @@ export const ru: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Открыть диаграмму',
title: 'Открыть базу данных',
description:
'Выберите диаграмму, которую нужно открыть, из списка ниже.',
table_columns: {
@@ -325,6 +324,12 @@ export const ru: LanguageTranslation = {
},
cancel: 'Отмена',
open: 'Открыть',
diagram_actions: {
open: 'Открыть',
duplicate: 'Дублировать',
delete: 'Удалить',
},
},
export_sql_dialog: {
@@ -473,6 +478,7 @@ export const ru: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Создать таблицу',
new_view: 'Новое представление',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
},
@@ -494,6 +500,9 @@ export const ru: LanguageTranslation = {
language_select: {
change_language: 'Сменить язык',
},
on: 'Вкл',
off: 'Выкл',
},
};

View File

@@ -6,21 +6,21 @@ export const te: LanguageTranslation = {
new_diagram: 'కొత్తది',
browse: 'బ్రాఉజ్',
tables: 'టేబల్లు',
relationships: 'సంబంధాలు',
refs: 'సంబంధాలు',
areas: 'ప్రదేశాలు',
dependencies: 'ఆధారతలు',
custom_types: 'కస్టమ్ టైప్స్',
},
menu: {
databases: {
databases: 'డేటాబేస్లు',
new: 'కొత్త డైగ్రాం',
actions: {
actions: 'చర్యలు',
new: 'కొత్తది...',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి',
delete_diagram: 'తొలగించండి',
},
edit: {
edit: 'సవరించు',
@@ -37,6 +37,7 @@ export const te: LanguageTranslation = {
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
show_views: 'డేటాబేస్ వ్యూలు',
theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి',
hide_dependencies: 'ఆధారాలను దాచండి',
@@ -74,10 +75,10 @@ export const te: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'చిత్రాన్ని పునఃసరిచేయండి',
title: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి',
description:
'ఈ చర్య చిత్రంలోని అన్ని పట్టికలను పునఃస్థాపిస్తుంది. మీరు కొనసాగించాలనుకుంటున్నారా?',
reorder: 'పునఃసరిచేయండి',
reorder: 'స్వయంచాలకంగా అమర్చండి',
cancel: 'రద్దు',
},
@@ -119,6 +120,7 @@ export const te: LanguageTranslation = {
tables_section: {
tables: 'పట్టికలు',
add_table: 'పట్టికను జోడించు',
add_view: 'వ్యూ జోడించండి',
filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి',
// TODO: Translate
@@ -144,6 +146,7 @@ export const te: LanguageTranslation = {
field_actions: {
title: 'ఫీల్డ్ గుణాలు',
unique: 'అద్వితీయ',
auto_increment: 'ఆటో ఇంక్రిమెంట్',
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
@@ -177,12 +180,15 @@ export const te: LanguageTranslation = {
description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి',
},
},
relationships_section: {
relationships: 'సంబంధాలు',
refs_section: {
refs: 'Refs',
filter: 'ఫిల్టర్',
add_relationship: 'సంబంధం జోడించు',
collapse: 'అన్ని కూల్ చేయి',
add_relationship: 'సంబంధం జోడించు',
relationships: 'సంబంధాలు',
dependencies: 'ఆధారాలు',
relationship: {
relationship: 'సంబంధం',
primary: 'ప్రాథమిక పట్టిక',
foreign: 'సూచించబడిన పట్టిక',
cardinality: 'కార్డినాలిటీ',
@@ -192,16 +198,8 @@ export const te: LanguageTranslation = {
delete_relationship: 'సంబంధం తొలగించు',
},
},
empty_state: {
title: 'సంబంధాలు లేవు',
description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి',
},
},
dependencies_section: {
dependencies: 'ఆధారాలు',
filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి',
dependency: {
dependency: 'ఆధారం',
table: 'పట్టిక',
dependent_table: 'ఆధారిత వీక్షణ',
delete_dependency: 'ఆధారాన్ని తొలగించు',
@@ -211,8 +209,8 @@ export const te: LanguageTranslation = {
},
},
empty_state: {
title: 'ఆధారాలు లేవు',
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
title: 'సంబంధాలు లేవు',
description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి',
},
},
@@ -252,6 +250,7 @@ export const te: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'ఏ enum విలువలు నిర్వచించబడలేదు',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -274,7 +273,7 @@ export const te: LanguageTranslation = {
show_all: 'అన్ని చూపించు',
undo: 'తిరిగి చేయు',
redo: 'మరలా చేయు',
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
reorder_diagram: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -319,7 +318,7 @@ export const te: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'చిత్రం తెరవండి',
title: 'డేటాబేస్ తెరవండి',
description: 'కింద ఉన్న జాబితా నుండి చిత్రాన్ని ఎంచుకోండి.',
table_columns: {
name: 'పేరు',
@@ -329,6 +328,12 @@ export const te: LanguageTranslation = {
},
cancel: 'రద్దు',
open: 'తెరవు',
diagram_actions: {
open: 'తెరవు',
duplicate: 'నకలు',
delete: 'తొలగించు',
},
},
export_sql_dialog: {
@@ -481,6 +486,7 @@ export const te: LanguageTranslation = {
canvas_context_menu: {
new_table: 'కొత్త పట్టిక',
new_view: 'కొత్త వ్యూ',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
new_area: 'New Area',
@@ -504,6 +510,9 @@ export const te: LanguageTranslation = {
language_select: {
change_language: 'భాష మార్చు',
},
on: 'ఆన్',
off: 'ఆఫ్',
},
};

View File

@@ -6,21 +6,21 @@ export const tr: LanguageTranslation = {
new_diagram: 'Yeni',
browse: 'Gözat',
tables: 'Tablolar',
relationships: 'İlişkiler',
refs: 'Refs',
areas: 'Alanlar',
dependencies: 'Bağımlılıklar',
custom_types: 'Özel Tipler',
},
menu: {
databases: {
databases: 'Veritabanları',
new: 'Yeni Diyagram',
actions: {
actions: 'Eylemler',
new: 'Yeni...',
browse: 'Gözat...',
save: 'Kaydet',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil',
delete_diagram: 'Sil',
},
edit: {
edit: 'Düzenle',
@@ -37,6 +37,7 @@ export const tr: LanguageTranslation = {
show_field_attributes: 'Alan Özelliklerini Göster',
hide_field_attributes: 'Alan Özelliklerini Gizle',
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
show_views: 'Veritabanı Görünümleri',
theme: 'Tema',
show_dependencies: 'Bağımlılıkları Göster',
hide_dependencies: 'Bağımlılıkları Gizle',
@@ -74,10 +75,10 @@ export const tr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Diyagramı Yeniden Sırala',
title: 'Diyagramı Otomatik Düzenle',
description:
'Bu işlem tüm tabloları yeniden düzenleyecektir. Devam etmek istiyor musunuz?',
reorder: 'Yeniden Sırala',
reorder: 'Otomatik Düzenle',
cancel: 'İptal',
},
@@ -118,6 +119,7 @@ export const tr: LanguageTranslation = {
tables_section: {
tables: 'Tablolar',
add_table: 'Tablo Ekle',
add_view: 'Görünüm Ekle',
filter: 'Filtrele',
collapse: 'Hepsini Daralt',
// TODO: Translate
@@ -143,6 +145,7 @@ export const tr: LanguageTranslation = {
field_actions: {
title: 'Alan Özellikleri',
unique: 'Tekil',
auto_increment: 'Otomatik Artış',
comments: 'Yorumlar',
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
@@ -176,12 +179,15 @@ export const tr: LanguageTranslation = {
description: 'Başlamak için bir tablo oluşturun',
},
},
relationships_section: {
relationships: 'İlişkiler',
refs_section: {
refs: 'Refs',
filter: 'Filtrele',
add_relationship: 'İlişki Ekle',
collapse: 'Hepsini Daralt',
add_relationship: 'İlişki Ekle',
relationships: 'İlişkiler',
dependencies: 'Bağımlılıklar',
relationship: {
relationship: 'İlişki',
primary: 'Birincil Tablo',
foreign: 'Referans Tablo',
cardinality: 'Kardinalite',
@@ -191,16 +197,8 @@ export const tr: LanguageTranslation = {
delete_relationship: 'Sil',
},
},
empty_state: {
title: 'İlişki yok',
description: 'Tabloları bağlamak için bir ilişki oluşturun',
},
},
dependencies_section: {
dependencies: 'Bağımlılıklar',
filter: 'Filtrele',
collapse: 'Hepsini Daralt',
dependency: {
dependency: 'Bağımlılık',
table: 'Tablo',
dependent_table: 'Bağımlı Görünüm',
delete_dependency: 'Sil',
@@ -210,8 +208,8 @@ export const tr: LanguageTranslation = {
},
},
empty_state: {
title: 'Bağımlılık yok',
description: 'Başlamak için bir görünüm oluşturun',
title: 'İlişki yok',
description: 'Başlamak için bir ilişki oluşturun',
},
},
@@ -251,6 +249,7 @@ export const tr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Tanımlanmış enum değeri yok',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const tr: LanguageTranslation = {
show_all: 'Hepsini Gör',
undo: 'Geri Al',
redo: 'Yinele',
reorder_diagram: 'Diyagramı Yeniden Sırala',
reorder_diagram: 'Diyagramı Otomatik Düzenle',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -314,7 +313,7 @@ export const tr: LanguageTranslation = {
import: 'İçe Aktar',
},
open_diagram_dialog: {
title: 'Diyagramı Aç',
title: 'Veritabanı Aç',
description: 'Aşağıdaki listeden açmak için bir diyagram seçin.',
table_columns: {
name: 'Ad',
@@ -324,6 +323,12 @@ export const tr: LanguageTranslation = {
},
cancel: 'İptal',
open: 'Aç',
diagram_actions: {
open: 'Aç',
duplicate: 'Kopyala',
delete: 'Sil',
},
},
export_sql_dialog: {
@@ -466,6 +471,7 @@ export const tr: LanguageTranslation = {
},
canvas_context_menu: {
new_table: 'Yeni Tablo',
new_view: 'Yeni Görünüm',
new_relationship: 'Yeni İlişki',
// TODO: Translate
new_area: 'New Area',
@@ -488,6 +494,9 @@ export const tr: LanguageTranslation = {
language_select: {
change_language: 'Dil',
},
on: 'Açık',
off: 'Kapalı',
},
};

View File

@@ -6,21 +6,21 @@ export const uk: LanguageTranslation = {
new_diagram: 'Нова',
browse: 'Огляд',
tables: 'Таблиці',
relationships: 'Зв’язки',
refs: 'Зв’язки',
areas: 'Області',
dependencies: 'Залежності',
custom_types: 'Користувацькі типи',
},
menu: {
databases: {
databases: 'Бази даних',
new: 'Нова діаграма',
actions: {
actions: 'Дії',
new: 'Нова...',
browse: 'Огляд...',
save: 'Зберегти',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
delete_diagram: 'Видалити',
},
edit: {
edit: 'Редагувати',
@@ -37,6 +37,7 @@ export const uk: LanguageTranslation = {
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням',
show_views: 'Представлення бази даних',
theme: 'Тема',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
@@ -72,10 +73,10 @@ export const uk: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Перевпорядкувати діаграму',
title: 'Автоматичне розміщення діаграми',
description:
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
reorder: 'Перевпорядкувати',
reorder: 'Автоматичне розміщення',
cancel: 'Скасувати',
},
@@ -117,6 +118,7 @@ export const uk: LanguageTranslation = {
tables_section: {
tables: 'Таблиці',
add_table: 'Додати таблицю',
add_view: 'Додати представлення',
filter: 'Фільтр',
collapse: 'Згорнути все',
// TODO: Translate
@@ -142,6 +144,7 @@ export const uk: LanguageTranslation = {
field_actions: {
title: 'Атрибути полів',
unique: 'Унікальне',
auto_increment: 'Автоінкремент',
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
@@ -174,12 +177,15 @@ export const uk: LanguageTranslation = {
description: 'Щоб почати, створіть таблицю',
},
},
relationships_section: {
relationships: 'Звʼязки',
refs_section: {
refs: 'Refs',
filter: 'Фільтр',
add_relationship: 'Додати звʼязок',
collapse: 'Згорнути все',
add_relationship: 'Додати звʼязок',
relationships: 'Звʼязки',
dependencies: 'Залежності',
relationship: {
relationship: 'Звʼязок',
primary: 'Первинна таблиця',
foreign: 'Посилання на таблицю',
cardinality: 'Звʼязок',
@@ -189,16 +195,8 @@ export const uk: LanguageTranslation = {
delete_relationship: 'Видалити',
},
},
empty_state: {
title: 'Звʼязків немає',
description: 'Створіть звʼязок для зʼєднання таблиць',
},
},
dependencies_section: {
dependencies: 'Залежності',
filter: 'Фільтр',
collapse: 'Згорнути все',
dependency: {
dependency: 'Залежність',
table: 'Таблиця',
dependent_table: 'Залежне подання',
delete_dependency: 'Видалити',
@@ -208,8 +206,8 @@ export const uk: LanguageTranslation = {
},
},
empty_state: {
title: 'Жодних залежностей',
description: 'Створіть подання, щоб почати',
title: 'Жодних зв’язків',
description: 'Створіть зв’язок, щоб почати',
},
},
@@ -249,6 +247,7 @@ export const uk: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Значення переліку не визначені',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -271,7 +270,7 @@ export const uk: LanguageTranslation = {
show_all: 'Показати все',
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Перевпорядкувати діаграму',
reorder_diagram: 'Автоматичне розміщення діаграми',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -315,7 +314,7 @@ export const uk: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Відкрити діаграму',
title: 'Відкрити базу даних',
description:
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
table_columns: {
@@ -326,6 +325,12 @@ export const uk: LanguageTranslation = {
},
cancel: 'Скасувати',
open: 'Відкрити',
diagram_actions: {
open: 'Відкрити',
duplicate: 'Дублювати',
delete: 'Видалити',
},
},
export_sql_dialog: {
@@ -472,6 +477,7 @@ export const uk: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Нова таблиця',
new_view: 'Нове представлення',
new_relationship: 'Новий звʼязок',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +499,9 @@ export const uk: LanguageTranslation = {
language_select: {
change_language: 'Мова',
},
on: 'Увімк',
off: 'Вимк',
},
};

View File

@@ -6,21 +6,21 @@ export const vi: LanguageTranslation = {
new_diagram: 'Mới',
browse: 'Duyệt',
tables: 'Bảng',
relationships: 'Mối quan hệ',
refs: 'Refs',
areas: 'Khu vực',
dependencies: 'Phụ thuộc',
custom_types: 'Kiểu tùy chỉnh',
},
menu: {
databases: {
databases: 'Cơ sở dữ liệu',
new: 'Sơ đồ mới',
actions: {
actions: 'Hành động',
new: 'Mới...',
browse: 'Duyệt...',
save: 'Lưu',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
export_as: 'Xuất thành',
delete_diagram: 'Xóa sơ đồ',
delete_diagram: 'Xóa',
},
edit: {
edit: 'Sửa',
@@ -37,6 +37,7 @@ export const vi: LanguageTranslation = {
show_field_attributes: 'Hiển thị thuộc tính trường',
hide_field_attributes: 'Ẩn thuộc tính trường',
zoom_on_scroll: 'Thu phóng khi cuộn',
show_views: 'Chế độ xem Cơ sở dữ liệu',
theme: 'Chủ đề',
show_dependencies: 'Hiển thị các phụ thuộc',
hide_dependencies: 'Ẩn các phụ thuộc',
@@ -73,10 +74,10 @@ export const vi: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Sắp xếp lại sơ đồ',
title: 'Tự động sắp xếp sơ đồ',
description:
'Hành động này sẽ sắp xếp lại tất cả các bảng trong sơ đồ. Bạn có muốn tiếp tục không?',
reorder: 'Sắp xếp',
reorder: 'Tự động sắp xếp',
cancel: 'Hủy',
},
@@ -118,6 +119,7 @@ export const vi: LanguageTranslation = {
tables_section: {
tables: 'Bảng',
add_table: 'Thêm bảng',
add_view: 'Thêm Chế độ xem',
filter: 'Lọc',
collapse: 'Thu gọn tất cả',
// TODO: Translate
@@ -143,6 +145,7 @@ export const vi: LanguageTranslation = {
field_actions: {
title: 'Thuộc tính trường',
unique: 'Giá trị duy nhất',
auto_increment: 'Tự động tăng',
comments: 'Bình luận',
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
@@ -175,12 +178,15 @@ export const vi: LanguageTranslation = {
description: 'Tạo một bảng để bắt đầu',
},
},
relationships_section: {
relationships: 'Quan hệ',
refs_section: {
refs: 'Refs',
filter: 'Lọc',
add_relationship: 'Thêm quan hệ',
collapse: 'Thu gọn tất cả',
add_relationship: 'Thêm quan hệ',
relationships: 'Quan hệ',
dependencies: 'Phụ thuộc',
relationship: {
relationship: 'Quan hệ',
primary: 'Bảng khóa chính',
foreign: 'Bảng khóa ngoại',
cardinality: 'Quan hệ',
@@ -190,16 +196,8 @@ export const vi: LanguageTranslation = {
delete_relationship: 'Xóa',
},
},
empty_state: {
title: 'Không có quan hệ',
description: 'Tạo quan hệ để kết nối các bảng',
},
},
dependencies_section: {
dependencies: 'Phụ thuộc',
filter: 'Lọc',
collapse: 'Thu gọn tất cả',
dependency: {
dependency: 'Phụ thuộc',
table: 'Bảng',
dependent_table: 'Bảng xem phụ thuộc',
delete_dependency: 'Xóa',
@@ -209,8 +207,8 @@ export const vi: LanguageTranslation = {
},
},
empty_state: {
title: 'Không có phụ thuộc',
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
title: 'Không có quan hệ',
description: 'Tạo một quan hệ để bắt đầu',
},
},
@@ -250,6 +248,7 @@ export const vi: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Không có giá trị enum được định nghĩa',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -272,7 +271,7 @@ export const vi: LanguageTranslation = {
show_all: 'Hiển thị tất cả',
undo: 'Hoàn tác',
redo: 'Làm lại',
reorder_diagram: 'Sắp xếp lại sơ đồ',
reorder_diagram: 'Tự động sắp xếp sơ đồ',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -316,7 +315,7 @@ export const vi: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Mở sơ đồ',
title: 'Mở cơ sở dữ liệu',
description: 'Chọn sơ đồ để mở từ danh sách bên dưới.',
table_columns: {
name: 'Tên',
@@ -326,6 +325,12 @@ export const vi: LanguageTranslation = {
},
cancel: 'Hủy',
open: 'Mở',
diagram_actions: {
open: 'Mở',
duplicate: 'Nhân bản',
delete: 'Xóa',
},
},
export_sql_dialog: {
@@ -473,6 +478,7 @@ export const vi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tạo bảng mới',
new_view: 'Chế độ xem Mới',
new_relationship: 'Tạo quan hệ mới',
// TODO: Translate
new_area: 'New Area',
@@ -494,6 +500,9 @@ export const vi: LanguageTranslation = {
language_select: {
change_language: 'Ngôn ngữ',
},
on: 'Bật',
off: 'Tắt',
},
};

View File

@@ -6,21 +6,21 @@ export const zh_CN: LanguageTranslation = {
new_diagram: '新建',
browse: '浏览',
tables: '表',
relationships: '关系',
refs: '引用',
areas: '区域',
dependencies: '依赖关系',
custom_types: '自定义类型',
},
menu: {
databases: {
databases: '数据库',
new: '新建关系图',
actions: {
actions: '操作',
new: '新建...',
browse: '浏览...',
save: '保存',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
delete_diagram: '删除',
},
edit: {
edit: '编辑',
@@ -37,6 +37,7 @@ export const zh_CN: LanguageTranslation = {
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放',
show_views: '数据库视图',
theme: '主题',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
@@ -71,9 +72,9 @@ export const zh_CN: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '重新排列关系图',
title: '自动排列关系图',
description: '此操作将重新排列关系图中的所有表。是否要继续?',
reorder: '重新排列',
reorder: '自动排列',
cancel: '取消',
},
@@ -115,6 +116,7 @@ export const zh_CN: LanguageTranslation = {
tables_section: {
tables: '表',
add_table: '添加表',
add_view: '添加视图',
filter: '筛选',
collapse: '全部折叠',
// TODO: Translate
@@ -140,6 +142,7 @@ export const zh_CN: LanguageTranslation = {
field_actions: {
title: '字段属性',
unique: '唯一',
auto_increment: '自动递增',
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
@@ -172,12 +175,15 @@ export const zh_CN: LanguageTranslation = {
description: '新建表以开始',
},
},
relationships_section: {
relationships: '关系',
refs_section: {
refs: '引用',
filter: '筛选',
add_relationship: '添加关系',
collapse: '全部折叠',
add_relationship: '添加关系',
relationships: '关系',
dependencies: '依赖关系',
relationship: {
relationship: '关系',
primary: '主表',
foreign: '被引用表',
cardinality: '基数',
@@ -187,16 +193,8 @@ export const zh_CN: LanguageTranslation = {
delete_relationship: '删除',
},
},
empty_state: {
title: '无关系',
description: '创建关系以连接表',
},
},
dependencies_section: {
dependencies: '依赖关系',
filter: '筛选',
collapse: '全部折叠',
dependency: {
dependency: '依赖',
table: '表',
dependent_table: '依赖视图',
delete_dependency: '删除',
@@ -206,8 +204,8 @@ export const zh_CN: LanguageTranslation = {
},
},
empty_state: {
title: '无依赖',
description: '创建视图以开始',
title: '无关系',
description: '创建关系以开始',
},
},
@@ -247,6 +245,7 @@ export const zh_CN: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '没有定义枚举值',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -269,7 +268,7 @@ export const zh_CN: LanguageTranslation = {
show_all: '展示全部',
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
reorder_diagram: '自动排列关系图',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -313,7 +312,7 @@ export const zh_CN: LanguageTranslation = {
},
open_diagram_dialog: {
title: '打开关系图',
title: '打开数据库',
description: '从下面的列表中选择一个图表打开。',
table_columns: {
name: '名称',
@@ -323,6 +322,12 @@ export const zh_CN: LanguageTranslation = {
},
cancel: '取消',
open: '打开',
diagram_actions: {
open: '打开',
duplicate: '复制',
delete: '删除',
},
},
export_sql_dialog: {
@@ -468,6 +473,7 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表',
new_view: '新建视图',
new_relationship: '新建关系',
// TODO: Translate
new_area: 'New Area',
@@ -489,6 +495,9 @@ export const zh_CN: LanguageTranslation = {
language_select: {
change_language: '语言',
},
on: '开启',
off: '关闭',
},
};

View File

@@ -6,21 +6,21 @@ export const zh_TW: LanguageTranslation = {
new_diagram: '新建',
browse: '瀏覽',
tables: '表格',
relationships: '關係',
refs: 'Refs',
areas: '區域',
dependencies: '相依性',
custom_types: '自定義類型',
},
menu: {
databases: {
databases: '資料庫',
new: '新增圖表',
actions: {
actions: '操作',
new: '新增...',
browse: '瀏覽...',
save: '儲存',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
delete_diagram: '刪除',
},
edit: {
edit: '編輯',
@@ -37,6 +37,7 @@ export const zh_TW: LanguageTranslation = {
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放',
show_views: '資料庫檢視',
theme: '主題',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
@@ -71,9 +72,9 @@ export const zh_TW: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '重新排列圖表',
title: '自動排列圖表',
description: '此操作將重新排列圖表中的所有表格。是否繼續?',
reorder: '重新排列',
reorder: '自動排列',
cancel: '取消',
},
@@ -115,6 +116,7 @@ export const zh_TW: LanguageTranslation = {
tables_section: {
tables: '表格',
add_table: '新增表格',
add_view: '新增檢視',
filter: '篩選',
collapse: '全部摺疊',
// TODO: Translate
@@ -140,6 +142,7 @@ export const zh_TW: LanguageTranslation = {
field_actions: {
title: '欄位屬性',
unique: '唯一',
auto_increment: '自動遞增',
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
@@ -172,12 +175,15 @@ export const zh_TW: LanguageTranslation = {
description: '請新增表格以開始',
},
},
relationships_section: {
relationships: '關聯',
refs_section: {
refs: 'Refs',
filter: '篩選',
add_relationship: '新增關聯',
collapse: '全部摺疊',
add_relationship: '新增關聯',
relationships: '關聯',
dependencies: '相依性',
relationship: {
relationship: '關聯',
primary: '主表格',
foreign: '參照表格',
cardinality: '基數',
@@ -187,16 +193,8 @@ export const zh_TW: LanguageTranslation = {
delete_relationship: '刪除',
},
},
empty_state: {
title: '尚無關聯',
description: '請新增關聯以連接表格',
},
},
dependencies_section: {
dependencies: '相依性',
filter: '篩選',
collapse: '全部摺疊',
dependency: {
dependency: '相依性',
table: '表格',
dependent_table: '相依檢視',
delete_dependency: '刪除',
@@ -206,8 +204,8 @@ export const zh_TW: LanguageTranslation = {
},
},
empty_state: {
title: '尚無相依性',
description: '請建立檢視以開始',
title: '尚無關聯',
description: '請建立關聯以開始',
},
},
@@ -247,6 +245,7 @@ export const zh_TW: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '沒有定義列舉值',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -269,7 +268,7 @@ export const zh_TW: LanguageTranslation = {
show_all: '顯示全部',
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
reorder_diagram: '自動排列圖表',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -312,7 +311,7 @@ export const zh_TW: LanguageTranslation = {
},
open_diagram_dialog: {
title: '開啟圖表',
title: '開啟資料庫',
description: '請從以下列表中選擇一個圖表。',
table_columns: {
name: '名稱',
@@ -322,6 +321,12 @@ export const zh_TW: LanguageTranslation = {
},
cancel: '取消',
open: '開啟',
diagram_actions: {
open: '開啟',
duplicate: '複製',
delete: '刪除',
},
},
export_sql_dialog: {
@@ -468,6 +473,7 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表格',
new_view: '新檢視',
new_relationship: '新建關聯',
// TODO: Translate
new_area: 'New Area',
@@ -489,6 +495,9 @@ export const zh_TW: LanguageTranslation = {
language_select: {
change_language: '變更語言',
},
on: '開啟',
off: '關閉',
},
};

View File

@@ -18,4 +18,7 @@
.marker-definitions {
}
.nodrag {
}
}

View File

@@ -146,3 +146,22 @@ export const findDataTypeDataById = (
return dataTypesOptions.find((dataType) => dataType.id === id);
};
export const supportsAutoIncrementDataType = (
dataTypeName: string
): boolean => {
return [
'integer',
'int',
'bigint',
'smallint',
'tinyint',
'mediumint',
'serial',
'bigserial',
'smallserial',
'number',
'numeric',
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};

View File

@@ -0,0 +1,21 @@
import type { DBCustomType, DBCustomTypeKind } from '@/lib/domain';
import { schemaNameToDomainSchemaName } from '@/lib/domain';
import type { DBCustomTypeInfo } from '../metadata-types/custom-type-info';
import { generateId } from '@/lib/utils';
export const createCustomTypesFromMetadata = ({
customTypes,
}: {
customTypes: DBCustomTypeInfo[];
}): DBCustomType[] => {
return customTypes.map((customType) => {
return {
id: generateId(),
schema: schemaNameToDomainSchemaName(customType.schema),
name: customType.type,
kind: customType.kind as DBCustomTypeKind,
values: customType.values,
fields: customType.fields,
};
});
};

View File

@@ -0,0 +1,351 @@
import { generateId } from '@/lib/utils';
import type { AST } from 'node-sql-parser';
import type { DBDependency, DBTable } from '@/lib/domain';
import { DatabaseType, schemaNameToDomainSchemaName } from '@/lib/domain';
import type { ViewInfo } from '../metadata-types/view-info';
import { decodeViewDefinition } from './tables';
const astDatabaseTypes: Record<DatabaseType, string> = {
[DatabaseType.POSTGRESQL]: 'postgresql',
[DatabaseType.MYSQL]: 'postgresql',
[DatabaseType.MARIADB]: 'postgresql',
[DatabaseType.GENERIC]: 'postgresql',
[DatabaseType.SQLITE]: 'postgresql',
[DatabaseType.SQL_SERVER]: 'postgresql',
[DatabaseType.CLICKHOUSE]: 'postgresql',
[DatabaseType.COCKROACHDB]: 'postgresql',
[DatabaseType.ORACLE]: 'postgresql',
};
export const createDependenciesFromMetadata = async ({
views,
tables,
databaseType,
}: {
views: ViewInfo[];
tables: DBTable[];
databaseType: DatabaseType;
}): Promise<DBDependency[]> => {
if (!views || views.length === 0) {
return [];
}
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
const dependencies = views
.flatMap((view) => {
const viewSchema = schemaNameToDomainSchemaName(view.schema);
const viewTable = tables.find(
(table) =>
table.name === view.view_name && viewSchema === table.schema
);
if (!viewTable) {
console.warn(
`Source table for view ${view.view_name} not found (schema: ${viewSchema})`
);
return []; // Skip this view and proceed to the next
}
if (view.view_definition) {
try {
const decodedViewDefinition = decodeViewDefinition(
databaseType,
view.view_definition
);
let modifiedViewDefinition = '';
if (
databaseType === DatabaseType.MYSQL ||
databaseType === DatabaseType.MARIADB
) {
modifiedViewDefinition = preprocessViewDefinitionMySQL(
decodedViewDefinition
);
} else if (databaseType === DatabaseType.SQL_SERVER) {
modifiedViewDefinition =
preprocessViewDefinitionSQLServer(
decodedViewDefinition
);
} else {
modifiedViewDefinition = preprocessViewDefinition(
decodedViewDefinition
);
}
// Parse using the appropriate dialect
const ast = parser.astify(modifiedViewDefinition, {
database: astDatabaseTypes[databaseType],
type: 'select', // Parsing a SELECT statement
});
let relatedTables = extractTablesFromAST(ast);
// Filter out duplicate tables without schema
relatedTables = filterDuplicateTables(relatedTables);
return relatedTables.map((relTable) => {
const relSchema = relTable.schema || view.schema; // Use view's schema if relSchema is undefined
const relTableName = relTable.tableName;
const table = tables.find(
(table) =>
table.name === relTableName &&
(table.schema || '') === relSchema
);
if (table) {
const dependency: DBDependency = {
id: generateId(),
schema: view.schema,
tableId: table.id, // related table
dependentSchema: table.schema,
dependentTableId: viewTable.id, // dependent view
createdAt: Date.now(),
};
return dependency;
} else {
console.warn(
`Dependent table ${relSchema}.${relTableName} not found for view ${view.schema}.${view.view_name}`
);
return null;
}
});
} catch (error) {
console.error(
`Error parsing view ${view.schema}.${view.view_name}:`,
error
);
return [];
}
} else {
console.warn(
`View definition missing for ${view.schema}.${view.view_name}`
);
return [];
}
})
.filter((dependency) => dependency !== null);
return dependencies;
};
// Add this new function to filter out duplicate tables
function filterDuplicateTables(
tables: { schema?: string; tableName: string }[]
): { schema?: string; tableName: string }[] {
const tableMap = new Map<string, { schema?: string; tableName: string }>();
for (const table of tables) {
const key = table.tableName;
const existingTable = tableMap.get(key);
if (!existingTable || (table.schema && !existingTable.schema)) {
tableMap.set(key, table);
}
}
return Array.from(tableMap.values());
}
// Preprocess the view_definition to remove schema from CREATE VIEW
function preprocessViewDefinition(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove leading and trailing whitespace
viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim();
// Replace escaped double quotes with regular ones
viewDefinition = viewDefinition.replace(/\\"/g, '"');
// Replace 'CREATE MATERIALIZED VIEW' with 'CREATE VIEW'
viewDefinition = viewDefinition.replace(
/CREATE\s+MATERIALIZED\s+VIEW/i,
'CREATE VIEW'
);
// Regular expression to match 'CREATE VIEW [schema.]view_name [ (column definitions) ] AS'
// This regex captures the view name and skips any content between the view name and 'AS'
const regex =
/CREATE\s+VIEW\s+(?:(?:`[^`]+`|"[^"]+"|\w+)\.)?(?:`([^`]+)`|"([^"]+)"|(\w+))[\s\S]*?\bAS\b\s+/i;
const match = viewDefinition.match(regex);
let modifiedDefinition: string;
if (match) {
const viewName = match[1] || match[2] || match[3];
// Extract the SQL after the 'AS' keyword
const restOfDefinition = viewDefinition.substring(
match.index! + match[0].length
);
// Replace double-quoted identifiers with unquoted ones
let modifiedSQL = restOfDefinition.replace(/"(\w+)"/g, '$1');
// Replace '::' type casts with 'CAST' expressions
modifiedSQL = modifiedSQL.replace(
/\(([^()]+)\)::(\w+)/g,
'CAST($1 AS $2)'
);
// Remove ClickHouse-specific syntax that may still be present
// For example, remove SETTINGS clauses inside the SELECT statement
modifiedSQL = modifiedSQL.replace(/\bSETTINGS\b[\s\S]*$/i, '');
modifiedDefinition = `CREATE VIEW ${viewName} AS ${modifiedSQL}`;
} else {
console.warn('Could not preprocess view definition:', viewDefinition);
modifiedDefinition = viewDefinition;
}
return modifiedDefinition;
}
// Preprocess the view_definition for SQL Server
function preprocessViewDefinitionSQLServer(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove BOM if present
viewDefinition = viewDefinition.replace(/^\uFEFF/, '');
// Normalize whitespace
viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim();
// Remove square brackets and replace with double quotes
viewDefinition = viewDefinition.replace(/\[([^\]]+)\]/g, '"$1"');
// Remove database names from fully qualified identifiers
viewDefinition = viewDefinition.replace(
/"([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"/g,
'"$2"."$3"'
);
// Replace SQL Server functions with PostgreSQL equivalents
viewDefinition = viewDefinition.replace(/\bGETDATE\(\)/gi, 'NOW()');
viewDefinition = viewDefinition.replace(/\bISNULL\(/gi, 'COALESCE(');
// Replace 'TOP N' with 'LIMIT N' at the end of the query
const topMatch = viewDefinition.match(/SELECT\s+TOP\s+(\d+)/i);
if (topMatch) {
const topN = topMatch[1];
viewDefinition = viewDefinition.replace(
/SELECT\s+TOP\s+\d+/i,
'SELECT'
);
viewDefinition = viewDefinition.replace(/;+\s*$/, ''); // Remove semicolons at the end
viewDefinition += ` LIMIT ${topN}`;
}
viewDefinition = viewDefinition.replace(/\n/g, ''); // Remove newlines
// Adjust CREATE VIEW syntax
const regex =
/CREATE\s+VIEW\s+(?:"?([^".\s]+)"?\.)?"?([^".\s]+)"?\s+AS\s+/i;
const match = viewDefinition.match(regex);
let modifiedDefinition: string;
if (match) {
const viewName = match[2];
const modifiedSQL = viewDefinition.substring(
match.index! + match[0].length
);
// Remove semicolons at the end
const finalSQL = modifiedSQL.replace(/;+\s*$/, '');
modifiedDefinition = `CREATE VIEW "${viewName}" AS ${finalSQL}`;
} else {
console.warn('Could not preprocess view definition:', viewDefinition);
modifiedDefinition = viewDefinition;
}
return modifiedDefinition;
}
// Preprocess the view_definition to remove schema from CREATE VIEW
function preprocessViewDefinitionMySQL(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove any trailing semicolons
viewDefinition = viewDefinition.replace(/;\s*$/, '');
// Remove backticks from identifiers
viewDefinition = viewDefinition.replace(/`/g, '');
// Remove unnecessary parentheses around joins and ON clauses
viewDefinition = removeRedundantParentheses(viewDefinition);
return viewDefinition;
}
function removeRedundantParentheses(sql: string): string {
// Regular expressions to match unnecessary parentheses
const patterns = [
/\(\s*(JOIN\s+[^()]+?)\s*\)/gi,
/\(\s*(ON\s+[^()]+?)\s*\)/gi,
// Additional patterns if necessary
];
let prevSql;
do {
prevSql = sql;
patterns.forEach((pattern) => {
sql = sql.replace(pattern, '$1');
});
} while (sql !== prevSql);
return sql;
}
function extractTablesFromAST(
ast: AST | AST[]
): { schema?: string; tableName: string }[] {
const tablesMap = new Map<string, { schema: string; tableName: string }>();
const visitedNodes = new Set();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function traverse(node: any) {
if (!node || visitedNodes.has(node)) return;
visitedNodes.add(node);
if (Array.isArray(node)) {
node.forEach(traverse);
} else if (typeof node === 'object') {
// Check if node represents a table
if (
Object.hasOwnProperty.call(node, 'table') &&
typeof node.table === 'string'
) {
let schema = node.db || node.schema;
const tableName = node.table;
if (tableName) {
// Assign default schema if undefined
schema = schemaNameToDomainSchemaName(schema) || '';
const key = `${schema}.${tableName}`;
if (!tablesMap.has(key)) {
tablesMap.set(key, { schema, tableName });
}
}
}
// Recursively traverse all properties
for (const key in node) {
if (Object.hasOwnProperty.call(node, key)) {
traverse(node[key]);
}
}
}
}
traverse(ast);
return Array.from(tablesMap.values());
}

View File

@@ -0,0 +1,64 @@
import type { DBField } from '@/lib/domain';
import type { ColumnInfo } from '../metadata-types/column-info';
import type { AggregatedIndexInfo } from '../metadata-types/index-info';
import type { PrimaryKeyInfo } from '../metadata-types/primary-key-info';
import type { TableInfo } from '../metadata-types/table-info';
import { generateId } from '@/lib/utils';
export const createFieldsFromMetadata = ({
tableColumns,
tablePrimaryKeys,
aggregatedIndexes,
}: {
tableColumns: ColumnInfo[];
tableSchema?: string;
tableInfo: TableInfo;
tablePrimaryKeys: PrimaryKeyInfo[];
aggregatedIndexes: AggregatedIndexInfo[];
}) => {
const uniqueColumns = tableColumns.reduce((acc, col) => {
if (!acc.has(col.name)) {
acc.set(col.name, col);
}
return acc;
}, new Map<string, ColumnInfo>());
const sortedColumns = Array.from(uniqueColumns.values()).sort(
(a, b) => a.ordinal_position - b.ordinal_position
);
const tablePrimaryKeysColumns = tablePrimaryKeys.map((pk) =>
pk.column.trim()
);
return sortedColumns.map(
(col: ColumnInfo): DBField => ({
id: generateId(),
name: col.name,
type: {
id: col.type.split(' ').join('_').toLowerCase(),
name: col.type.toLowerCase(),
},
primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some(
(idx) =>
idx.unique &&
idx.columns.length === 1 &&
idx.columns[0].name === col.name
),
nullable: Boolean(col.nullable),
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { characterMaximumLength: col.character_maximum_length }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }
: {}),
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}),
createdAt: Date.now(),
comments: col.comment ? col.comment : undefined,
})
);
};

View File

@@ -0,0 +1,82 @@
import type { DatabaseEdition, Diagram } from '@/lib/domain';
import { adjustTablePositions, DatabaseType } from '@/lib/domain';
import { generateDiagramId } from '@/lib/utils';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import { createCustomTypesFromMetadata } from './custom-types';
import { createRelationshipsFromMetadata } from './relationships';
import { createTablesFromMetadata } from './tables';
import { createDependenciesFromMetadata } from './dependencies';
export const loadFromDatabaseMetadata = async ({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition,
}: {
databaseType: DatabaseType;
databaseMetadata: DatabaseMetadata;
diagramNumber?: number;
databaseEdition?: DatabaseEdition;
}): Promise<Diagram> => {
const {
fk_info: foreignKeys,
views: views,
custom_types: customTypes,
} = databaseMetadata;
const tables = createTablesFromMetadata({
databaseMetadata,
databaseType,
});
const relationships = createRelationshipsFromMetadata({
foreignKeys,
tables,
});
const dependencies = await createDependenciesFromMetadata({
views,
tables,
databaseType,
});
const dbCustomTypes = customTypes
? createCustomTypesFromMetadata({
customTypes,
})
: [];
const adjustedTables = adjustTablePositions({
tables,
relationships,
mode: 'perSchema',
});
const sortedTables = adjustedTables.sort((a, b) => {
if (a.isView === b.isView) {
// Both are either tables or views, so sort alphabetically by name
return a.name.localeCompare(b.name);
}
// If one is a view and the other is not, put tables first
return a.isView ? 1 : -1;
});
const diagram: Diagram = {
id: generateDiagramId(),
name: databaseMetadata.database_name
? `${databaseMetadata.database_name}-db`
: diagramNumber
? `Diagram ${diagramNumber}`
: 'New Diagram',
databaseType: databaseType ?? DatabaseType.GENERIC,
databaseEdition,
tables: sortedTables,
relationships,
dependencies,
customTypes: dbCustomTypes,
createdAt: new Date(),
updatedAt: new Date(),
};
return diagram;
};

View File

@@ -0,0 +1,24 @@
import type { DBField, DBIndex, IndexType } from '@/lib/domain';
import type { AggregatedIndexInfo } from '../metadata-types/index-info';
import { generateId } from '@/lib/utils';
export const createIndexesFromMetadata = ({
aggregatedIndexes,
fields,
}: {
aggregatedIndexes: AggregatedIndexInfo[];
fields: DBField[];
}): DBIndex[] =>
aggregatedIndexes.map(
(idx): DBIndex => ({
id: generateId(),
name: idx.name,
unique: Boolean(idx.unique),
fieldIds: idx.columns
.sort((a, b) => a.position - b.position)
.map((c) => fields.find((f) => f.name === c.name)?.id)
.filter((id): id is string => id !== undefined),
createdAt: Date.now(),
type: idx.index_type?.toLowerCase() as IndexType,
})
);

View File

@@ -0,0 +1,85 @@
import type {
Cardinality,
DBField,
DBRelationship,
DBTable,
} from '@/lib/domain';
import { schemaNameToDomainSchemaName } from '@/lib/domain';
import type { ForeignKeyInfo } from '../metadata-types/foreign-key-info';
import { generateId } from '@/lib/utils';
const determineCardinality = (
field: DBField,
isTablePKComplex: boolean
): Cardinality => {
return field.unique || (field.primaryKey && !isTablePKComplex)
? 'one'
: 'many';
};
export const createRelationshipsFromMetadata = ({
foreignKeys,
tables,
}: {
foreignKeys: ForeignKeyInfo[];
tables: DBTable[];
}): DBRelationship[] => {
return foreignKeys
.map((fk: ForeignKeyInfo): DBRelationship | null => {
const schema = schemaNameToDomainSchemaName(fk.schema);
const sourceTable = tables.find(
(table) => table.name === fk.table && table.schema === schema
);
const targetSchema = schemaNameToDomainSchemaName(
fk.reference_schema
);
const targetTable = tables.find(
(table) =>
table.name === fk.reference_table &&
table.schema === targetSchema
);
const sourceField = sourceTable?.fields.find(
(field) => field.name === fk.column
);
const targetField = targetTable?.fields.find(
(field) => field.name === fk.reference_column
);
const isSourceTablePKComplex =
(sourceTable?.fields.filter((field) => field.primaryKey) ?? [])
.length > 1;
const isTargetTablePKComplex =
(targetTable?.fields.filter((field) => field.primaryKey) ?? [])
.length > 1;
if (sourceTable && targetTable && sourceField && targetField) {
const sourceCardinality = determineCardinality(
sourceField,
isSourceTablePKComplex
);
const targetCardinality = determineCardinality(
targetField,
isTargetTablePKComplex
);
return {
id: generateId(),
name: fk.foreign_key_name,
sourceSchema: schema,
targetSchema: targetSchema,
sourceTableId: sourceTable.id,
targetTableId: targetTable.id,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
sourceCardinality,
targetCardinality,
createdAt: Date.now(),
};
}
return null;
})
.filter((rel) => rel !== null) as DBRelationship[];
};

View File

@@ -0,0 +1,228 @@
import type { DBIndex, DBTable } from '@/lib/domain';
import {
DatabaseType,
generateTableKey,
schemaNameToDomainSchemaName,
} from '@/lib/domain';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import type { TableInfo } from '../metadata-types/table-info';
import { createAggregatedIndexes } from '../metadata-types/index-info';
import {
decodeBase64ToUtf16LE,
decodeBase64ToUtf8,
generateId,
} from '@/lib/utils';
import {
defaultTableColor,
materializedViewColor,
viewColor,
} from '@/lib/colors';
import { createFieldsFromMetadata } from './fields';
import { createIndexesFromMetadata } from './indexes';
export const decodeViewDefinition = (
databaseType: DatabaseType,
viewDefinition?: string
): string => {
if (!viewDefinition) {
return '';
}
let decodedViewDefinition: string;
if (databaseType === DatabaseType.SQL_SERVER) {
decodedViewDefinition = decodeBase64ToUtf16LE(viewDefinition);
} else {
decodedViewDefinition = decodeBase64ToUtf8(viewDefinition);
}
return decodedViewDefinition;
};
export const createTablesFromMetadata = ({
databaseMetadata,
databaseType,
}: {
databaseMetadata: DatabaseMetadata;
databaseType: DatabaseType;
}): DBTable[] => {
const {
tables: tableInfos,
pk_info: primaryKeys,
columns,
indexes,
views: views,
} = databaseMetadata;
// Pre-compute view names for faster lookup if there are views
const viewNamesSet = new Set<string>();
const materializedViewNamesSet = new Set<string>();
if (views && views.length > 0) {
views.forEach((view) => {
const key = generateTableKey({
schemaName: view.schema,
tableName: view.view_name,
});
viewNamesSet.add(key);
if (
view.view_definition &&
decodeViewDefinition(databaseType, view.view_definition)
.toLowerCase()
.includes('materialized')
) {
materializedViewNamesSet.add(key);
}
});
}
// Pre-compute lookup maps for better performance
const columnsByTable = new Map<string, (typeof columns)[0][]>();
const indexesByTable = new Map<string, (typeof indexes)[0][]>();
const primaryKeysByTable = new Map<string, (typeof primaryKeys)[0][]>();
// Group columns by table
columns.forEach((col) => {
const key = generateTableKey({
schemaName: col.schema,
tableName: col.table,
});
if (!columnsByTable.has(key)) {
columnsByTable.set(key, []);
}
columnsByTable.get(key)!.push(col);
});
// Group indexes by table
indexes.forEach((idx) => {
const key = generateTableKey({
schemaName: idx.schema,
tableName: idx.table,
});
if (!indexesByTable.has(key)) {
indexesByTable.set(key, []);
}
indexesByTable.get(key)!.push(idx);
});
// Group primary keys by table
primaryKeys.forEach((pk) => {
const key = generateTableKey({
schemaName: pk.schema,
tableName: pk.table,
});
if (!primaryKeysByTable.has(key)) {
primaryKeysByTable.set(key, []);
}
primaryKeysByTable.get(key)!.push(pk);
});
const result = tableInfos.map((tableInfo: TableInfo) => {
const tableSchema = schemaNameToDomainSchemaName(tableInfo.schema);
const tableKey = generateTableKey({
schemaName: tableInfo.schema,
tableName: tableInfo.table,
});
// Use pre-computed lookups instead of filtering entire arrays
const tableIndexes = indexesByTable.get(tableKey) || [];
const tablePrimaryKeys = primaryKeysByTable.get(tableKey) || [];
const tableColumns = columnsByTable.get(tableKey) || [];
// Aggregate indexes with multiple columns
const aggregatedIndexes = createAggregatedIndexes({
tableInfo,
tableSchema,
tableIndexes,
});
const fields = createFieldsFromMetadata({
aggregatedIndexes,
tableColumns,
tablePrimaryKeys,
tableInfo,
tableSchema,
});
// Check for composite primary key and find matching index name
const primaryKeyFields = fields.filter((f) => f.primaryKey);
let pkMatchingIndexName: string | undefined;
let pkIndex: DBIndex | undefined;
if (primaryKeyFields.length >= 1) {
// We have a composite primary key, look for an index that matches all PK columns
const pkFieldNames = primaryKeyFields.map((f) => f.name).sort();
// Find an index that matches the primary key columns exactly
const matchingIndex = aggregatedIndexes.find((index) => {
const indexColumnNames = index.columns
.map((c) => c.name)
.sort();
return (
indexColumnNames.length === pkFieldNames.length &&
indexColumnNames.every((col, i) => col === pkFieldNames[i])
);
});
if (matchingIndex) {
pkMatchingIndexName = matchingIndex.name;
// Create a special PK index
pkIndex = {
id: generateId(),
name: matchingIndex.name,
unique: true,
fieldIds: primaryKeyFields.map((f) => f.id),
createdAt: Date.now(),
isPrimaryKey: true,
};
}
}
// Filter out the index that matches the composite PK (to avoid duplication)
const filteredAggregatedIndexes = pkMatchingIndexName
? aggregatedIndexes.filter(
(idx) => idx.name !== pkMatchingIndexName
)
: aggregatedIndexes;
const dbIndexes = createIndexesFromMetadata({
aggregatedIndexes: filteredAggregatedIndexes,
fields,
});
// Add the PK index if it exists
if (pkIndex) {
dbIndexes.push(pkIndex);
}
// Determine if the current table is a view by checking against pre-computed sets
const viewKey = generateTableKey({
schemaName: tableSchema,
tableName: tableInfo.table,
});
const isView = viewNamesSet.has(viewKey);
const isMaterializedView = materializedViewNamesSet.has(viewKey);
// Initial random positions; these will be adjusted later
return {
id: generateId(),
name: tableInfo.table,
schema: tableSchema,
x: Math.random() * 1000, // Placeholder X
y: Math.random() * 800, // Placeholder Y
fields,
indexes: dbIndexes,
color: isMaterializedView
? materializedViewColor
: isView
? viewColor
: defaultTableColor,
isView: isView,
isMaterializedView: isMaterializedView,
createdAt: Date.now(),
comments: tableInfo.comment ? tableInfo.comment : undefined,
};
});
return result;
};

View File

@@ -1,20 +1,10 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { exportBaseSQL } from '../export-sql-script';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
// Mock the dbml/core importer
vi.mock('@dbml/core', () => ({
importer: {
import: vi.fn((sql: string) => {
// Return a simplified DBML for testing
return sql;
}),
},
}));
describe('DBML Export - SQL Generation Tests', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
@@ -116,7 +106,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// 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
expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
expect(sql).not.toMatch(
@@ -124,6 +114,96 @@ describe('DBML Export - SQL Generation Tests', () => {
);
});
it('should not create duplicate index for composite primary key', () => {
const tableId = testId();
const field1Id = testId();
const field2Id = testId();
const field3Id = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Landlord System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: tableId,
name: 'users_master_table',
schema: 'landlord',
fields: [
createField({
id: field1Id,
name: 'master_user_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field2Id,
name: 'tenant_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field3Id,
name: 'tenant_user_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'enabled',
type: { id: 'boolean', name: 'boolean' },
primaryKey: false,
nullable: true,
unique: false,
}),
],
indexes: [
{
id: testId(),
name: 'idx_users_master_table_master_user_id_tenant_id_tenant_user_id',
unique: false,
fieldIds: [field1Id, field2Id, field3Id],
createdAt: testTime,
},
{
id: testId(),
name: 'index_1',
unique: true,
fieldIds: [field2Id, field3Id],
createdAt: testTime,
},
],
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain composite primary key constraint
expect(sql).toContain(
'PRIMARY KEY ("master_user_id", "tenant_id", "tenant_user_id")'
);
// Should NOT contain the duplicate index for the primary key fields
expect(sql).not.toContain(
'CREATE INDEX idx_users_master_table_master_user_id_tenant_id_tenant_user_id'
);
// Should still contain the unique index on subset of fields
expect(sql).toContain('CREATE UNIQUE INDEX index_1');
});
it('should handle single primary keys inline', () => {
const diagram: Diagram = createDiagram({
id: testId(),
@@ -165,7 +245,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// 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
expect(sql).not.toContain('PRIMARY KEY (id)');
});
@@ -226,8 +306,8 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).not.toContain('DEFAULT has default');
expect(sql).not.toContain('DEFAULT DEFAULT has default');
// The fields should still be in the table
expect(sql).toContain('is_active boolean');
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
expect(sql).toContain('"is_active" boolean');
expect(sql).toContain('"stock_count" integer NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {
@@ -349,8 +429,8 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should convert NOW to NOW() and ('now') to now()
expect(sql).toContain('created_at timestamp DEFAULT NOW');
expect(sql).toContain('updated_at timestamp DEFAULT now()');
expect(sql).toContain('"created_at" timestamp DEFAULT NOW');
expect(sql).toContain('"updated_at" timestamp DEFAULT now()');
});
});
@@ -405,9 +485,9 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// 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
expect(sql).toContain('status char(1)');
expect(sql).toContain('"status" char(1)');
});
it('should not have spaces between char and parentheses', () => {
@@ -516,7 +596,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create a valid table without primary key
expect(sql).toContain('CREATE TABLE experiment_logs');
expect(sql).toContain('CREATE TABLE "experiment_logs"');
expect(sql).not.toContain('PRIMARY KEY');
});
@@ -631,11 +711,11 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create both tables
expect(sql).toContain('CREATE TABLE guilds');
expect(sql).toContain('CREATE TABLE guild_members');
expect(sql).toContain('CREATE TABLE "guilds"');
expect(sql).toContain('CREATE TABLE "guild_members"');
// Should create foreign key
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");'
);
});
});
@@ -709,12 +789,9 @@ describe('DBML Export - SQL Generation Tests', () => {
isDBMLFlow: true,
});
// Should create schemas
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation');
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic');
// Should use schema-qualified table names
expect(sql).toContain('CREATE TABLE transportation.portals');
expect(sql).toContain('CREATE TABLE magic.spells');
expect(sql).toContain('CREATE TABLE "transportation"."portals"');
expect(sql).toContain('CREATE TABLE "magic"."spells"');
});
});
@@ -761,7 +838,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should still create table structure
expect(sql).toContain('CREATE TABLE empty_table');
expect(sql).toContain('CREATE TABLE "empty_table"');
expect(sql).toContain('(\n\n)');
});
@@ -862,9 +939,9 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// 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
expect(sql).toContain('interest_rate numeric(5)');
expect(sql).toContain('"interest_rate" numeric(5)');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -156,11 +156,11 @@ export function exportMSSQL({
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const identity =
field.increment ||
field.default?.toLowerCase().includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
@@ -168,6 +168,7 @@ export function exportMSSQL({
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
@@ -177,7 +178,15 @@ export function exportMSSQL({
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT [${pkIndex.name}] `
: '';
})()}PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`

View File

@@ -274,14 +274,15 @@ export function exportMySQL({
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default
?.toLowerCase()
.includes('identity') ||
field.default
field.increment ||
(field.primaryKey &&
(field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval')))
) {
autoIncrement = ' AUTO_INCREMENT';
}
@@ -290,9 +291,10 @@ export function exportMySQL({
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value
// Handle default value - skip if auto increment
const defaultValue =
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
@@ -311,7 +313,15 @@ export function exportMySQL({
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT \`${pkIndex.name}\` `
: '';
})()}PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''

View File

@@ -325,7 +325,15 @@ export function exportPostgreSQL({
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT "${pkIndex.name}" `
: '';
})()}PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''

View File

@@ -343,9 +343,10 @@ export function exportSQLite({
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default
?.toLowerCase()
.includes('identity') ||
(field.increment ||
field.default
?.toLowerCase()
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
@@ -362,6 +363,7 @@ export function exportSQLite({
let defaultValue = '';
if (
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()

View File

@@ -1,6 +1,9 @@
import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import { DatabaseType } from '@/lib/domain/database-type';
import {
DatabaseType,
databaseTypesWithCommentSupport,
} from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
@@ -8,6 +11,7 @@ import { exportMSSQL } from './export-per-type/mssql';
import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql';
import { escapeSQLComment } from './export-per-type/common';
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
@@ -16,6 +20,61 @@ const simplifyDataType = (typeName: string): string => {
return typeMap[typeName.toLowerCase()] || typeName;
};
// Helper function to properly quote table/schema names with special characters
const getQuotedTableName = (
table: DBTable,
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(']'))
);
};
// Only add quotes if needed and not already quoted
const quoteIfNeeded = (name: string) => {
if (isAlreadyQuoted(name)) {
return name;
}
const needsQuoting = /[^a-zA-Z0-9_]/.test(name) || isDBMLFlow;
return needsQuoting ? `"${name}"` : name;
};
if (table.schema) {
const quotedSchema = quoteIfNeeded(table.schema);
const quotedTable = quoteIfNeeded(table.name);
return `${quotedSchema}.${quotedTable}`;
} else {
return quoteIfNeeded(table.name);
}
};
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 = ({
diagram,
targetDatabaseType,
@@ -59,18 +118,21 @@ export const exportBaseSQL = ({
let sqlScript = '';
// First create the CREATE SCHEMA statements for all the found schemas based on tables
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Skip schema creation for DBML flow as DBML doesn't support CREATE SCHEMA syntax
if (!isDBMLFlow) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
}
// Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
if (diagram.customTypes && diagram.customTypes.length > 0) {
@@ -162,9 +224,7 @@ export const exportBaseSQL = ({
// Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => {
const tableName = table.schema
? `${table.schema}.${table.name}`
: table.name;
const tableName = getQuotedTableName(table, isDBMLFlow);
sqlScript += `CREATE TABLE ${tableName} (\n`;
// Check for composite primary keys
@@ -233,7 +293,8 @@ export const exportBaseSQL = ({
typeName = 'char';
}
sqlScript += ` ${field.name} ${typeName}`;
const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow);
sqlScript += ` ${quotedFieldName} ${typeName}`;
// Add size for character types
if (
@@ -270,8 +331,13 @@ export const exportBaseSQL = ({
sqlScript += ` UNIQUE`;
}
// Handle AUTO INCREMENT - add as a comment for AI to process
if (field.increment) {
sqlScript += ` /* AUTO_INCREMENT */`;
}
// Handle DEFAULT value
if (field.default) {
if (field.default && !field.increment) {
// Temp remove default user-define value when it have it
let fieldDefault = field.default;
@@ -304,45 +370,87 @@ export const exportBaseSQL = ({
}
}
// Handle PRIMARY KEY constraint - only add inline if not composite
if (field.primaryKey && !hasCompositePrimaryKey) {
// Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
sqlScript += ' PRIMARY KEY';
}
// Add a comma after each field except the last one (or before composite primary key)
if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
// Add a comma after each field except the last one (or before PK constraint)
const needsPKConstraint =
hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name);
if (index < table.fields.length - 1 || needsPKConstraint) {
sqlScript += ',\n';
}
});
// Add composite primary key constraint if needed
if (hasCompositePrimaryKey) {
const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
// Add primary key constraint if needed (for composite PKs or single PK with custom name)
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (
hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name)
) {
const pkFieldNames = primaryKeyFields
.map((f) => getQuotedFieldName(f.name, isDBMLFlow))
.join(', ');
if (pkIndex?.name) {
sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
} else {
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
}
}
sqlScript += '\n);\n';
// Add table comment
if (table.comments) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
// Add table comment (only for databases that support COMMENT ON syntax)
const supportsCommentOn =
databaseTypesWithCommentSupport.includes(targetDatabaseType);
if (table.comments && supportsCommentOn) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';\n`;
}
table.fields.forEach((field) => {
// Add column comment
if (field.comments) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
// Add column comment (only for databases that support COMMENT ON syntax)
if (field.comments && supportsCommentOn) {
const quotedFieldName = getQuotedFieldName(
field.name,
isDBMLFlow
);
sqlScript += `COMMENT ON COLUMN ${tableName}.${quotedFieldName} IS '${escapeSQLComment(field.comments)}';\n`;
}
});
// Generate SQL for indexes
table.indexes.forEach((index) => {
const fieldNames = index.fieldIds
.map(
(fieldId) =>
table.fields.find((field) => field.id === fieldId)?.name
// Skip the primary key index (it's already handled as a constraint)
if (index.isPrimaryKey) {
return;
}
// Get the fields for this index
const indexFields = index.fieldIds
.map((fieldId) => table.fields.find((f) => f.id === fieldId))
.filter(
(field): field is NonNullable<typeof field> =>
field !== undefined
);
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes for composite primary keys
if (
primaryKeyFields.length > 0 &&
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some((field) => field.id === pk.id)
)
.filter(Boolean)
) {
return; // Skip this index as it's redundant with the primary key
}
const fieldNames = indexFields
.map((field) => getQuotedFieldName(field.name, isDBMLFlow))
.join(', ');
if (fieldNames) {
@@ -420,13 +528,18 @@ export const exportBaseSQL = ({
return;
}
const fkTableName = fkTable.schema
? `${fkTable.schema}.${fkTable.name}`
: fkTable.name;
const refTableName = refTable.schema
? `${refTable.schema}.${refTable.name}`
: refTable.name;
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
const fkTableName = getQuotedTableName(fkTable, 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 (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\n`;
}
});

View File

@@ -86,7 +86,7 @@ export interface SQLBinaryExpr extends SQLASTNode {
export interface SQLFunctionNode extends SQLASTNode {
type: 'function';
name: string;
name: string | { name: Array<{ value: string }> };
args?: {
value: SQLASTArg[];
};
@@ -108,6 +108,31 @@ export interface SQLStringLiteral extends SQLASTNode {
value: string;
}
export interface SQLDefaultNode extends SQLASTNode {
type: 'default';
value: SQLASTNode;
}
export interface SQLCastNode extends SQLASTNode {
type: 'cast';
expr: SQLASTNode;
target: Array<{ dataType: string }>;
}
export interface SQLBooleanNode extends SQLASTNode {
type: 'bool';
value: boolean;
}
export interface SQLNullNode extends SQLASTNode {
type: 'null';
}
export interface SQLNumberNode extends SQLASTNode {
type: 'number';
value: number;
}
export type SQLASTArg =
| SQLColumnRef
| SQLStringLiteral
@@ -146,6 +171,22 @@ export function buildSQLFromAST(
): string {
if (!ast) return '';
// Handle default value wrapper
if (ast.type === 'default' && 'value' in ast) {
const defaultNode = ast as SQLDefaultNode;
return buildSQLFromAST(defaultNode.value, dbType);
}
// Handle PostgreSQL cast expressions (e.g., 'value'::type)
if (ast.type === 'cast' && 'expr' in ast && 'target' in ast) {
const castNode = ast as SQLCastNode;
const expr = buildSQLFromAST(castNode.expr, dbType);
if (castNode.target.length > 0 && castNode.target[0].dataType) {
return `${expr}::${castNode.target[0].dataType.toLowerCase()}`;
}
return expr;
}
if (ast.type === 'binary_expr') {
const expr = ast as SQLBinaryExpr;
const leftSQL = buildSQLFromAST(expr.left, dbType);
@@ -155,7 +196,59 @@ export function buildSQLFromAST(
if (ast.type === 'function') {
const func = ast as SQLFunctionNode;
let expr = func.name;
let funcName = '';
// Handle nested function name structure
if (typeof func.name === 'object' && func.name && 'name' in func.name) {
const nameObj = func.name as { name: Array<{ value: string }> };
if (nameObj.name.length > 0) {
funcName = nameObj.name[0].value || '';
}
} else if (typeof func.name === 'string') {
funcName = func.name;
}
if (!funcName) return '';
// Normalize PostgreSQL function names to uppercase for consistency
if (dbType === DatabaseType.POSTGRESQL) {
const pgFunctions = [
'now',
'current_timestamp',
'current_date',
'current_time',
'gen_random_uuid',
'random',
'nextval',
'currval',
];
if (pgFunctions.includes(funcName.toLowerCase())) {
funcName = funcName.toUpperCase();
}
}
// Some PostgreSQL functions don't have parentheses (like CURRENT_TIMESTAMP)
if (funcName === 'CURRENT_TIMESTAMP' && !func.args) {
return funcName;
}
// Handle SQL Server function defaults that were preprocessed as strings
// The preprocessor converts NEWID() to 'newid', GETDATE() to 'getdate', etc.
if (dbType === DatabaseType.SQL_SERVER) {
const sqlServerFunctions: Record<string, string> = {
newid: 'NEWID()',
newsequentialid: 'NEWSEQUENTIALID()',
getdate: 'GETDATE()',
sysdatetime: 'SYSDATETIME()',
};
const lowerFuncName = funcName.toLowerCase();
if (sqlServerFunctions[lowerFuncName]) {
return sqlServerFunctions[lowerFuncName];
}
}
let expr = funcName;
if (func.args) {
expr +=
'(' +
@@ -175,12 +268,31 @@ export function buildSQLFromAST(
})
.join(', ') +
')';
} else {
expr += '()';
}
return expr;
} else if (ast.type === 'column_ref') {
return quoteIdentifier((ast as SQLColumnRef).column, dbType);
} else if (ast.type === 'expr_list') {
return (ast as SQLExprList).value.map((v) => v.value).join(' AND ');
} else if (ast.type === 'single_quote_string') {
// String literal with single quotes
const strNode = ast as SQLStringLiteral;
return `'${strNode.value}'`;
} else if (ast.type === 'double_quote_string') {
// String literal with double quotes
const strNode = ast as SQLStringLiteral;
return `"${strNode.value}"`;
} else if (ast.type === 'bool') {
// Boolean value
const boolNode = ast as SQLBooleanNode;
return boolNode.value ? 'TRUE' : 'FALSE';
} else if (ast.type === 'null') {
return 'NULL';
} else if (ast.type === 'number') {
const numNode = ast as SQLNumberNode;
return String(numNode.value);
} else {
const valueNode = ast as { type: string; value: string | number };
return typeof valueNode.value === 'string'
@@ -779,10 +891,10 @@ export function convertToChartDBDiagram(
}
const sourceField = sourceTable.fields.find(
(f) => f.name === rel.sourceColumn
(f) => f.name.toLowerCase() === rel.sourceColumn.toLowerCase()
);
const targetField = targetTable.fields.find(
(f) => f.name === rel.targetColumn
(f) => f.name.toLowerCase() === rel.targetColumn.toLowerCase()
);
if (!sourceField || !targetField) {

View File

@@ -0,0 +1,228 @@
import { describe, it, expect } from 'vitest';
import { fromMySQL } from '../mysql';
describe('MySQL Default Value Import', () => {
describe('String Default Values', () => {
it('should parse simple string defaults with single quotes', async () => {
const sql = `
CREATE TABLE tavern_patrons (
patron_id INT NOT NULL,
membership_status VARCHAR(50) DEFAULT 'regular',
PRIMARY KEY (patron_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const statusColumn = result.tables[0].columns.find(
(c) => c.name === 'membership_status'
);
expect(statusColumn?.default).toBe("'regular'");
});
it('should parse string defaults with escaped quotes', async () => {
const sql = `
CREATE TABLE wizard_spellbooks (
spellbook_id INT NOT NULL,
incantation VARCHAR(255) DEFAULT 'Dragon\\'s flame',
spell_metadata TEXT DEFAULT '{"type": "fire"}',
PRIMARY KEY (spellbook_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const incantationColumn = result.tables[0].columns.find(
(c) => c.name === 'incantation'
);
expect(incantationColumn?.default).toBeTruthy();
const metadataColumn = result.tables[0].columns.find(
(c) => c.name === 'spell_metadata'
);
expect(metadataColumn?.default).toBeTruthy();
});
});
describe('Numeric Default Values', () => {
it('should parse integer defaults', async () => {
const sql = `
CREATE TABLE dungeon_levels (
level_id INT NOT NULL,
monster_count INT DEFAULT 0,
max_treasure INT DEFAULT 1000,
PRIMARY KEY (level_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const monsterColumn = result.tables[0].columns.find(
(c) => c.name === 'monster_count'
);
expect(monsterColumn?.default).toBe('0');
const treasureColumn = result.tables[0].columns.find(
(c) => c.name === 'max_treasure'
);
expect(treasureColumn?.default).toBe('1000');
});
it('should parse decimal defaults', async () => {
const sql = `
CREATE TABLE merchant_inventory (
item_id INT NOT NULL,
base_price DECIMAL(10, 2) DEFAULT 99.99,
loyalty_discount FLOAT DEFAULT 0.15,
PRIMARY KEY (item_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const priceColumn = result.tables[0].columns.find(
(c) => c.name === 'base_price'
);
expect(priceColumn?.default).toBe('99.99');
const discountColumn = result.tables[0].columns.find(
(c) => c.name === 'loyalty_discount'
);
expect(discountColumn?.default).toBe('0.15');
});
});
describe('Boolean Default Values', () => {
it('should parse boolean defaults in MySQL (using TINYINT)', async () => {
const sql = `
CREATE TABLE character_status (
character_id INT NOT NULL,
is_alive TINYINT(1) DEFAULT 1,
is_cursed TINYINT(1) DEFAULT 0,
has_magic BOOLEAN DEFAULT TRUE,
PRIMARY KEY (character_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const aliveColumn = result.tables[0].columns.find(
(c) => c.name === 'is_alive'
);
expect(aliveColumn?.default).toBe('1');
const cursedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_cursed'
);
expect(cursedColumn?.default).toBe('0');
});
});
describe('NULL Default Values', () => {
it('should parse NULL defaults', async () => {
const sql = `
CREATE TABLE companion_animals (
companion_id INT NOT NULL,
special_trait VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (companion_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const traitColumn = result.tables[0].columns.find(
(c) => c.name === 'special_trait'
);
expect(traitColumn?.default).toBe('NULL');
});
});
describe('Function Default Values', () => {
it('should parse function defaults', async () => {
const sql = `
CREATE TABLE quest_entries (
entry_id INT NOT NULL AUTO_INCREMENT,
quest_accepted TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
quest_uuid VARCHAR(36) DEFAULT (UUID()),
PRIMARY KEY (entry_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const acceptedColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_accepted'
);
expect(acceptedColumn?.default).toBe('CURRENT_TIMESTAMP');
const updatedColumn = result.tables[0].columns.find(
(c) => c.name === 'last_updated'
);
expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP');
});
});
describe('AUTO_INCREMENT', () => {
it('should handle AUTO_INCREMENT columns correctly', async () => {
const sql = `
CREATE TABLE hero_registry (
hero_id INT NOT NULL AUTO_INCREMENT,
hero_name VARCHAR(100),
PRIMARY KEY (hero_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const idColumn = result.tables[0].columns.find(
(c) => c.name === 'hero_id'
);
expect(idColumn?.increment).toBe(true);
// AUTO_INCREMENT columns typically don't have a default value
expect(idColumn?.default).toBeUndefined();
});
});
describe('Complex Real-World Example', () => {
it('should handle complex table with multiple default types', async () => {
const sql = `
CREATE TABLE adventurer_profiles (
adventurer_id BIGINT NOT NULL AUTO_INCREMENT,
character_name VARCHAR(50) NOT NULL,
guild_email VARCHAR(255) NOT NULL,
rank VARCHAR(20) DEFAULT 'novice',
is_guild_verified TINYINT(1) DEFAULT 0,
gold_coins INT DEFAULT 100,
account_balance DECIMAL(10, 2) DEFAULT 0.00,
joined_realm TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_quest TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
inventory_data JSON DEFAULT NULL,
PRIMARY KEY (adventurer_id),
UNIQUE KEY uk_guild_email (guild_email),
INDEX idx_rank (rank)
);
`;
const result = await fromMySQL(sql);
const table = result.tables[0];
expect(table).toBeDefined();
// Check various default values
const rankColumn = table.columns.find((c) => c.name === 'rank');
expect(rankColumn?.default).toBe("'novice'");
const verifiedColumn = table.columns.find(
(c) => c.name === 'is_guild_verified'
);
expect(verifiedColumn?.default).toBe('0');
const goldColumn = table.columns.find(
(c) => c.name === 'gold_coins'
);
expect(goldColumn?.default).toBe('100');
const balanceColumn = table.columns.find(
(c) => c.name === 'account_balance'
);
expect(balanceColumn?.default).toBe('0.00');
const joinedColumn = table.columns.find(
(c) => c.name === 'joined_realm'
);
expect(joinedColumn?.default).toBe('CURRENT_TIMESTAMP');
const inventoryColumn = table.columns.find(
(c) => c.name === 'inventory_data'
);
expect(inventoryColumn?.default).toBe('NULL');
});
});
});

View File

@@ -101,12 +101,28 @@ function extractColumnsFromCreateTable(statement: string): SQLColumn[] {
const typeMatch = definition.match(/^([^\s(]+)(?:\(([^)]+)\))?/);
const dataType = typeMatch ? typeMatch[1] : '';
// Extract default value
let defaultValue: string | undefined;
const defaultMatch = definition.match(
/DEFAULT\s+('[^']*'|"[^"]*"|NULL|CURRENT_TIMESTAMP|\S+)/i
);
if (defaultMatch) {
defaultValue = defaultMatch[1];
}
// Check for AUTO_INCREMENT
const increment = definition
.toUpperCase()
.includes('AUTO_INCREMENT');
columns.push({
name: columnName,
type: dataType,
nullable,
primaryKey,
unique: definition.toUpperCase().includes('UNIQUE'),
default: defaultValue,
increment,
});
}
}
@@ -721,7 +737,28 @@ export async function fromMySQL(sqlContent: string): Promise<SQLParserResult> {
parseError
);
// Error handling without logging
// Try fallback parser when main parser fails
const tableMatch = trimmedStmt.match(
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^`\s(]+)`?\s*\(/i
);
if (tableMatch) {
const tableName = tableMatch[1];
const tableId = generateId();
tableMap[tableName] = tableId;
const extractedColumns =
extractColumnsFromCreateTable(trimmedStmt);
if (extractedColumns.length > 0) {
tables.push({
id: tableId,
name: tableName,
schema: undefined,
columns: extractedColumns,
indexes: [],
order: tables.length,
});
}
}
}
}
}

View File

@@ -0,0 +1,215 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
it('should handle ALTER TABLE ADD COLUMN statements', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."location" (
"id" bigint NOT NULL,
CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id")
);
-- Add new fields to existing location table
ALTER TABLE location ADD COLUMN country_id INT;
ALTER TABLE location ADD COLUMN state_id INT;
ALTER TABLE location ADD COLUMN location_type_id INT;
ALTER TABLE location ADD COLUMN city_id INT;
ALTER TABLE location ADD COLUMN street TEXT;
ALTER TABLE location ADD COLUMN block TEXT;
ALTER TABLE location ADD COLUMN building TEXT;
ALTER TABLE location ADD COLUMN floor TEXT;
ALTER TABLE location ADD COLUMN apartment TEXT;
ALTER TABLE location ADD COLUMN lat INT;
ALTER TABLE location ADD COLUMN long INT;
ALTER TABLE location ADD COLUMN elevation INT;
ALTER TABLE location ADD COLUMN erp_site_id INT;
ALTER TABLE location ADD COLUMN is_active TEXT;
ALTER TABLE location ADD COLUMN remarks TEXT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const locationTable = result.tables[0];
expect(locationTable.name).toBe('location');
expect(locationTable.schema).toBe('public');
// Should have the original id column plus all the added columns
expect(locationTable.columns).toHaveLength(16);
// Check that the id column is present
const idColumn = locationTable.columns.find((col) => col.name === 'id');
expect(idColumn).toBeDefined();
expect(idColumn?.type).toBe('BIGINT');
expect(idColumn?.primaryKey).toBe(true);
// Check some of the added columns
const countryIdColumn = locationTable.columns.find(
(col) => col.name === 'country_id'
);
expect(countryIdColumn).toBeDefined();
expect(countryIdColumn?.type).toBe('INTEGER');
const streetColumn = locationTable.columns.find(
(col) => col.name === 'street'
);
expect(streetColumn).toBeDefined();
expect(streetColumn?.type).toBe('TEXT');
const remarksColumn = locationTable.columns.find(
(col) => col.name === 'remarks'
);
expect(remarksColumn).toBeDefined();
expect(remarksColumn?.type).toBe('TEXT');
});
it('should handle ALTER TABLE ADD COLUMN with schema qualification', async () => {
const sql = `
CREATE TABLE public.users (
id INTEGER PRIMARY KEY
);
ALTER TABLE public.users ADD COLUMN email VARCHAR(255);
ALTER TABLE public.users ADD COLUMN created_at TIMESTAMP;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const usersTable = result.tables[0];
expect(usersTable.columns).toHaveLength(3);
const emailColumn = usersTable.columns.find(
(col) => col.name === 'email'
);
expect(emailColumn).toBeDefined();
expect(emailColumn?.type).toBe('VARCHAR(255)');
const createdAtColumn = usersTable.columns.find(
(col) => col.name === 'created_at'
);
expect(createdAtColumn).toBeDefined();
expect(createdAtColumn?.type).toBe('TIMESTAMP');
});
it('should handle ALTER TABLE ADD COLUMN with constraints', async () => {
const sql = `
CREATE TABLE products (
id SERIAL PRIMARY KEY
);
ALTER TABLE products ADD COLUMN name VARCHAR(100) NOT NULL;
ALTER TABLE products ADD COLUMN sku VARCHAR(50) UNIQUE;
ALTER TABLE products ADD COLUMN price DECIMAL(10,2) DEFAULT 0.00;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const productsTable = result.tables[0];
expect(productsTable.columns).toHaveLength(4);
const nameColumn = productsTable.columns.find(
(col) => col.name === 'name'
);
expect(nameColumn).toBeDefined();
expect(nameColumn?.nullable).toBe(false);
const skuColumn = productsTable.columns.find(
(col) => col.name === 'sku'
);
expect(skuColumn).toBeDefined();
expect(skuColumn?.unique).toBe(true);
const priceColumn = productsTable.columns.find(
(col) => col.name === 'price'
);
expect(priceColumn).toBeDefined();
expect(priceColumn?.default).toBe('0');
});
it('should not add duplicate columns', async () => {
const sql = `
CREATE TABLE items (
id INTEGER PRIMARY KEY,
name VARCHAR(100)
);
ALTER TABLE items ADD COLUMN description TEXT;
ALTER TABLE items ADD COLUMN name VARCHAR(200); -- Should not be added as duplicate
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const itemsTable = result.tables[0];
// Should only have 3 columns: id, name (original), and description
expect(itemsTable.columns).toHaveLength(3);
const nameColumns = itemsTable.columns.filter(
(col) => col.name === 'name'
);
expect(nameColumns).toHaveLength(1);
expect(nameColumns[0].type).toBe('VARCHAR(100)'); // Should keep original type
});
it('should use default schema when not specified', async () => {
const sql = `
CREATE TABLE test_table (
id INTEGER PRIMARY KEY
);
ALTER TABLE test_table ADD COLUMN value TEXT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const testTable = result.tables[0];
expect(testTable.schema).toBe('public');
expect(testTable.columns).toHaveLength(2);
const valueColumn = testTable.columns.find(
(col) => col.name === 'value'
);
expect(valueColumn).toBeDefined();
});
it('should handle quoted identifiers in ALTER TABLE ADD COLUMN', async () => {
const sql = `
CREATE TABLE "my-table" (
"id" INTEGER PRIMARY KEY
);
ALTER TABLE "my-table" ADD COLUMN "my-column" VARCHAR(50);
ALTER TABLE "my-table" ADD COLUMN "another-column" INTEGER;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const myTable = result.tables[0];
expect(myTable.name).toBe('my-table');
expect(myTable.columns).toHaveLength(3);
const myColumn = myTable.columns.find(
(col) => col.name === 'my-column'
);
expect(myColumn).toBeDefined();
expect(myColumn?.type).toBe('VARCHAR(50)');
const anotherColumn = myTable.columns.find(
(col) => col.name === 'another-column'
);
expect(anotherColumn).toBeDefined();
expect(anotherColumn?.type).toBe('INTEGER');
});
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE ALTER COLUMN TYPE', () => {
it('should handle ALTER TABLE ALTER COLUMN TYPE statements', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."table_12" (
"id" SERIAL,
"field1" varchar(200),
"field2" varchar(200),
"field3" varchar(200),
PRIMARY KEY ("id")
);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.name).toBe('table_12');
expect(table.columns).toHaveLength(4); // id, field1, field2, field3
// Check that the columns have the updated type
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
expect(field1?.type).toBe('VARCHAR(254)'); // Should be updated from 200 to 254
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
expect(field2?.type).toBe('VARCHAR(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
expect(field3?.type).toBe('VARCHAR(254)');
});
it('should handle various ALTER COLUMN TYPE scenarios', async () => {
const sql = `
CREATE TABLE test_table (
id INTEGER PRIMARY KEY,
name VARCHAR(50),
age SMALLINT,
score NUMERIC(5,2)
);
-- Change varchar length
ALTER TABLE test_table ALTER COLUMN name TYPE VARCHAR(100);
-- Change numeric type
ALTER TABLE test_table ALTER COLUMN age TYPE INTEGER;
-- Change precision
ALTER TABLE test_table ALTER COLUMN score TYPE NUMERIC(10,4);
`;
const result = await fromPostgres(sql);
const table = result.tables[0];
const nameCol = table.columns.find((col) => col.name === 'name');
expect(nameCol?.type).toBe('VARCHAR(100)');
const ageCol = table.columns.find((col) => col.name === 'age');
expect(ageCol?.type).toBe('INTEGER');
const scoreCol = table.columns.find((col) => col.name === 'score');
expect(scoreCol?.type).toBe('NUMERIC(10,4)');
});
it('should handle multiple type changes on the same column', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."table_12" (
"id" SERIAL,
"field1" varchar(200),
"field2" varchar(200),
"field3" varchar(200),
PRIMARY KEY ("id")
);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE BIGINT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.name).toBe('table_12');
expect(table.schema).toBe('public');
expect(table.columns).toHaveLength(4);
// Check that field1 has the final type (BIGINT), not the intermediate VARCHAR(254)
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
expect(field1?.type).toBe('BIGINT'); // Should be BIGINT, not VARCHAR(254)
// Check that field2 and field3 still have VARCHAR(254)
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
expect(field2?.type).toBe('VARCHAR(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
expect(field3?.type).toBe('VARCHAR(254)');
});
});

View File

@@ -0,0 +1,117 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE with Foreign Keys', () => {
it('should handle ALTER TABLE ADD COLUMN followed by ALTER TABLE ADD FOREIGN KEY', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."location" (
"id" bigint NOT NULL,
CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id")
);
-- Add new fields to existing location table
ALTER TABLE location ADD COLUMN country_id INT;
ALTER TABLE location ADD COLUMN state_id INT;
ALTER TABLE location ADD COLUMN location_type_id INT;
ALTER TABLE location ADD COLUMN city_id INT;
ALTER TABLE location ADD COLUMN street TEXT;
ALTER TABLE location ADD COLUMN block TEXT;
ALTER TABLE location ADD COLUMN building TEXT;
ALTER TABLE location ADD COLUMN floor TEXT;
ALTER TABLE location ADD COLUMN apartment TEXT;
ALTER TABLE location ADD COLUMN lat INT;
ALTER TABLE location ADD COLUMN long INT;
ALTER TABLE location ADD COLUMN elevation INT;
ALTER TABLE location ADD COLUMN erp_site_id INT;
ALTER TABLE location ADD COLUMN is_active TEXT;
ALTER TABLE location ADD COLUMN remarks TEXT;
-- Create lookup tables
CREATE TABLE country (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(3) UNIQUE
);
CREATE TABLE state (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
country_id INT NOT NULL,
FOREIGN KEY (country_id) REFERENCES country(id)
);
CREATE TABLE location_type (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE city (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
state_id INT NOT NULL,
FOREIGN KEY (state_id) REFERENCES state(id)
);
-- Add foreign key constraints from location to lookup tables
ALTER TABLE location ADD CONSTRAINT fk_location_country
FOREIGN KEY (country_id) REFERENCES country(id);
ALTER TABLE location ADD CONSTRAINT fk_location_state
FOREIGN KEY (state_id) REFERENCES state(id);
ALTER TABLE location ADD CONSTRAINT fk_location_location_type
FOREIGN KEY (location_type_id) REFERENCES location_type(id);
ALTER TABLE location ADD CONSTRAINT fk_location_city
FOREIGN KEY (city_id) REFERENCES city(id);
`;
const result = await fromPostgres(sql);
const locationTable = result.tables.find((t) => t.name === 'location');
// Check tables
expect(result.tables).toHaveLength(5); // location, country, state, location_type, city
// Check location table has all columns
expect(locationTable).toBeDefined();
expect(locationTable?.columns).toHaveLength(16); // id + 15 added columns
// Check foreign key relationships
const locationRelationships = result.relationships.filter(
(r) => r.sourceTable === 'location'
);
// Should have 4 FKs from location to lookup tables + 2 from state/city
expect(result.relationships.length).toBeGreaterThanOrEqual(6);
// Check specific foreign keys from location
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'country_id' &&
r.targetTable === 'country'
)
).toBe(true);
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'state_id' && r.targetTable === 'state'
)
).toBe(true);
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'location_type_id' &&
r.targetTable === 'location_type'
)
).toBe(true);
expect(
locationRelationships.some(
(r) => r.sourceColumn === 'city_id' && r.targetTable === 'city'
)
).toBe(true);
});
});

View File

@@ -0,0 +1,395 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL Default Value Import', () => {
describe('String Default Values', () => {
it('should parse simple string defaults with single quotes', async () => {
const sql = `
CREATE TABLE heroes (
hero_id INTEGER NOT NULL,
hero_status CHARACTER VARYING DEFAULT 'questing',
PRIMARY KEY (hero_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const statusColumn = result.tables[0].columns.find(
(c) => c.name === 'hero_status'
);
expect(statusColumn?.default).toBe("'questing'");
});
it('should parse string defaults with special characters that need escaping', async () => {
const sql = `
CREATE TABLE spell_scrolls (
scroll_id INTEGER NOT NULL,
incantation CHARACTER VARYING DEFAULT 'Dragon''s breath',
rune_inscription TEXT DEFAULT 'Ancient rune
Sacred symbol',
PRIMARY KEY (scroll_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const incantationColumn = result.tables[0].columns.find(
(c) => c.name === 'incantation'
);
expect(incantationColumn?.default).toBe("'Dragon''s breath'");
});
it('should parse elvish text default values', async () => {
const sql = `
CREATE TABLE elven_greetings (
greeting_id INTEGER NOT NULL,
elvish_welcome CHARACTER VARYING DEFAULT 'Mae govannen',
PRIMARY KEY (greeting_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const greetingColumn = result.tables[0].columns.find(
(c) => c.name === 'elvish_welcome'
);
expect(greetingColumn?.default).toBe("'Mae govannen'");
});
});
describe('Numeric Default Values', () => {
it('should parse integer defaults', async () => {
const sql = `
CREATE TABLE dragon_hoards (
hoard_id INTEGER NOT NULL,
gold_pieces INTEGER DEFAULT 0,
max_treasure_value INTEGER DEFAULT 10000,
PRIMARY KEY (hoard_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const goldColumn = result.tables[0].columns.find(
(c) => c.name === 'gold_pieces'
);
expect(goldColumn?.default).toBe('0');
const treasureColumn = result.tables[0].columns.find(
(c) => c.name === 'max_treasure_value'
);
expect(treasureColumn?.default).toBe('10000');
});
it('should parse decimal defaults', async () => {
const sql = `
CREATE TABLE enchanted_items (
item_id INTEGER NOT NULL,
market_price DECIMAL(10, 2) DEFAULT 99.99,
magic_power_rating NUMERIC DEFAULT 0.85,
PRIMARY KEY (item_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const priceColumn = result.tables[0].columns.find(
(c) => c.name === 'market_price'
);
expect(priceColumn?.default).toBe('99.99');
const powerColumn = result.tables[0].columns.find(
(c) => c.name === 'magic_power_rating'
);
expect(powerColumn?.default).toBe('0.85');
});
});
describe('Boolean Default Values', () => {
it('should parse boolean defaults', async () => {
const sql = `
CREATE TABLE magical_artifacts (
artifact_id INTEGER NOT NULL,
is_cursed BOOLEAN DEFAULT TRUE,
is_destroyed BOOLEAN DEFAULT FALSE,
is_legendary BOOLEAN DEFAULT '1',
is_identified BOOLEAN DEFAULT '0',
PRIMARY KEY (artifact_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const cursedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_cursed'
);
expect(cursedColumn?.default).toBe('TRUE');
const destroyedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_destroyed'
);
expect(destroyedColumn?.default).toBe('FALSE');
const legendaryColumn = result.tables[0].columns.find(
(c) => c.name === 'is_legendary'
);
expect(legendaryColumn?.default).toBe("'1'");
const identifiedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_identified'
);
expect(identifiedColumn?.default).toBe("'0'");
});
});
describe('NULL Default Values', () => {
it('should parse NULL defaults', async () => {
const sql = `
CREATE TABLE wizard_familiars (
familiar_id INTEGER NOT NULL,
special_ability CHARACTER VARYING DEFAULT NULL,
PRIMARY KEY (familiar_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const abilityColumn = result.tables[0].columns.find(
(c) => c.name === 'special_ability'
);
expect(abilityColumn?.default).toBe('NULL');
});
});
describe('Function Default Values', () => {
it('should parse function defaults', async () => {
const sql = `
CREATE TABLE quest_logs (
quest_id UUID DEFAULT gen_random_uuid(),
quest_started TIMESTAMP DEFAULT NOW(),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
difficulty_roll INTEGER DEFAULT random()
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const questIdColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_id'
);
expect(questIdColumn?.default).toBe('GEN_RANDOM_UUID()');
const startedColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_started'
);
expect(startedColumn?.default).toBe('NOW()');
const updatedColumn = result.tables[0].columns.find(
(c) => c.name === 'last_updated'
);
expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP');
const difficultyColumn = result.tables[0].columns.find(
(c) => c.name === 'difficulty_roll'
);
expect(difficultyColumn?.default).toBe('RANDOM()');
});
});
describe('Complex Real-World Example', () => {
it('should handle a complex guild management table correctly', async () => {
const sql = `
CREATE TABLE "realm"(
"realm_id" integer NOT NULL
);
CREATE TABLE "guild"(
"guild_id" CHARACTER VARYING NOT NULL UNIQUE,
PRIMARY KEY ("guild_id")
);
CREATE TABLE "guild_schedule"(
"schedule_id" CHARACTER VARYING NOT NULL UNIQUE,
PRIMARY KEY ("schedule_id")
);
CREATE TABLE "guild_quests"(
"is_active" CHARACTER VARYING NOT NULL DEFAULT 'active',
"quest_description" CHARACTER VARYING,
"quest_type" CHARACTER VARYING,
"quest_status" CHARACTER VARYING DEFAULT 'pending',
"quest_id" CHARACTER VARYING NOT NULL UNIQUE,
"reward_gold" CHARACTER VARYING,
"quest_giver" CHARACTER VARYING,
"party_size" CHARACTER VARYING,
"difficulty_level" CHARACTER VARYING,
"monster_type" CHARACTER VARYING,
"dungeon_location" CHARACTER VARYING,
"main_guild_ref" CHARACTER VARYING NOT NULL,
"schedule_ref" CHARACTER VARYING,
"last_attempt" CHARACTER VARYING,
"max_attempts" INTEGER,
"failed_attempts" INTEGER,
"party_members" INTEGER,
"loot_distributor" CHARACTER VARYING,
"quest_validator" CHARACTER VARYING,
"scout_report" CHARACTER VARYING,
"completion_xp" INTEGER,
"bonus_xp" INTEGER,
"map_coordinates" CHARACTER VARYING,
"quest_correlation" CHARACTER VARYING,
"is_completed" BOOLEAN NOT NULL DEFAULT '0',
"reward_items" CHARACTER VARYING,
"quest_priority" INTEGER,
"started_at" CHARACTER VARYING,
"status" CHARACTER VARYING,
"completed_at" CHARACTER VARYING,
"party_level" INTEGER,
"quest_master" CHARACTER VARYING,
PRIMARY KEY ("quest_id"),
FOREIGN KEY ("main_guild_ref") REFERENCES "guild"("guild_id"),
FOREIGN KEY ("schedule_ref") REFERENCES "guild_schedule"("schedule_id")
);
`;
const result = await fromPostgres(sql);
// Find the guild_quests table
const questTable = result.tables.find(
(t) => t.name === 'guild_quests'
);
expect(questTable).toBeDefined();
// Check specific default values
const activeColumn = questTable?.columns.find(
(c) => c.name === 'is_active'
);
expect(activeColumn?.default).toBe("'active'");
const statusColumn = questTable?.columns.find(
(c) => c.name === 'quest_status'
);
expect(statusColumn?.default).toBe("'pending'");
const completedColumn = questTable?.columns.find(
(c) => c.name === 'is_completed'
);
expect(completedColumn?.default).toBe("'0'");
});
});
describe('ALTER TABLE ADD COLUMN with defaults', () => {
it('should handle ALTER TABLE ADD COLUMN with default values', async () => {
const sql = `
CREATE TABLE adventurers (
adventurer_id INTEGER NOT NULL,
PRIMARY KEY (adventurer_id)
);
ALTER TABLE adventurers ADD COLUMN class_type VARCHAR(50) DEFAULT 'warrior';
ALTER TABLE adventurers ADD COLUMN experience_points INTEGER DEFAULT 0;
ALTER TABLE adventurers ADD COLUMN is_guild_member BOOLEAN DEFAULT TRUE;
ALTER TABLE adventurers ADD COLUMN joined_at TIMESTAMP DEFAULT NOW();
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const classColumn = result.tables[0].columns.find(
(c) => c.name === 'class_type'
);
expect(classColumn?.default).toBe("'warrior'");
const xpColumn = result.tables[0].columns.find(
(c) => c.name === 'experience_points'
);
expect(xpColumn?.default).toBe('0');
const guildColumn = result.tables[0].columns.find(
(c) => c.name === 'is_guild_member'
);
expect(guildColumn?.default).toBe('TRUE');
const joinedColumn = result.tables[0].columns.find(
(c) => c.name === 'joined_at'
);
expect(joinedColumn?.default).toBe('NOW()');
});
});
describe('Edge Cases and Special Characters', () => {
it('should handle defaults with parentheses in strings', async () => {
const sql = `
CREATE TABLE spell_formulas (
formula_id INTEGER NOT NULL,
damage_calculation VARCHAR DEFAULT '(strength + magic) * 2',
mana_cost TEXT DEFAULT 'cast(level * 10 - wisdom)',
PRIMARY KEY (formula_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const damageColumn = result.tables[0].columns.find(
(c) => c.name === 'damage_calculation'
);
expect(damageColumn?.default).toBe("'(strength + magic) * 2'");
const manaColumn = result.tables[0].columns.find(
(c) => c.name === 'mana_cost'
);
expect(manaColumn?.default).toBe("'cast(level * 10 - wisdom)'");
});
it('should handle defaults with JSON strings', async () => {
const sql = `
CREATE TABLE item_enchantments (
enchantment_id INTEGER NOT NULL,
properties JSON DEFAULT '{"element": "fire"}',
modifiers JSONB DEFAULT '[]',
PRIMARY KEY (enchantment_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const propertiesColumn = result.tables[0].columns.find(
(c) => c.name === 'properties'
);
expect(propertiesColumn?.default).toBe(`'{"element": "fire"}'`);
const modifiersColumn = result.tables[0].columns.find(
(c) => c.name === 'modifiers'
);
expect(modifiersColumn?.default).toBe("'[]'");
});
it('should handle casting in defaults', async () => {
const sql = `
CREATE TABLE ancient_runes (
rune_id INTEGER NOT NULL,
rune_type VARCHAR DEFAULT 'healing'::text,
PRIMARY KEY (rune_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const runeColumn = result.tables[0].columns.find(
(c) => c.name === 'rune_type'
);
expect(runeColumn?.default).toBe("'healing'::text");
});
});
describe('Serial Types', () => {
it('should not set default for SERIAL types as they auto-increment', async () => {
const sql = `
CREATE TABLE monster_spawns (
spawn_id SERIAL PRIMARY KEY,
minion_id SMALLSERIAL,
boss_id BIGSERIAL
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const spawnColumn = result.tables[0].columns.find(
(c) => c.name === 'spawn_id'
);
expect(spawnColumn?.default).toBeUndefined();
expect(spawnColumn?.increment).toBe(true);
const minionColumn = result.tables[0].columns.find(
(c) => c.name === 'minion_id'
);
expect(minionColumn?.default).toBeUndefined();
expect(minionColumn?.increment).toBe(true);
const bossColumn = result.tables[0].columns.find(
(c) => c.name === 'boss_id'
);
expect(bossColumn?.default).toBeUndefined();
expect(bossColumn?.increment).toBe(true);
});
});
});

View File

@@ -0,0 +1,350 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL Import - Quoted Identifiers with Special Characters', () => {
describe('CREATE TABLE with quoted identifiers', () => {
it('should handle tables with quoted schema and table names', async () => {
const sql = `
CREATE TABLE "my-schema"."user-profiles" (
id serial PRIMARY KEY,
name text NOT NULL
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('my-schema');
expect(table.name).toBe('user-profiles');
});
it('should handle tables with spaces in schema and table names', async () => {
const sql = `
CREATE TABLE "user schema"."profile table" (
"user id" integer PRIMARY KEY,
"full name" varchar(255)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('user schema');
expect(table.name).toBe('profile table');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
// Note: Column names with spaces might be parsed differently
});
it('should handle mixed quoted and unquoted identifiers', async () => {
const sql = `
CREATE TABLE "special-schema".users (
id serial PRIMARY KEY
);
CREATE TABLE public."special-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.tables[0].schema).toBe('special-schema');
expect(result.tables[0].name).toBe('users');
expect(result.tables[1].schema).toBe('public');
expect(result.tables[1].name).toBe('special-table');
});
it('should handle tables with dots in names', async () => {
const sql = `
CREATE TABLE "schema.with.dots"."table.with.dots" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema.with.dots');
expect(table.name).toBe('table.with.dots');
});
});
describe('FOREIGN KEY with quoted identifiers', () => {
it('should handle inline REFERENCES with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth-schema"."users" (
"user-id" serial PRIMARY KEY,
email text UNIQUE
);
CREATE TABLE "app-schema"."user-profiles" (
id serial PRIMARY KEY,
"user-id" integer REFERENCES "auth-schema"."users"("user-id"),
bio text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('user-profiles');
expect(relationship.targetTable).toBe('users');
expect(relationship.sourceColumn).toBe('user-id');
expect(relationship.targetColumn).toBe('user-id');
});
it('should handle FOREIGN KEY constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema one"."table one" (
"id field" serial PRIMARY KEY,
"data field" text
);
CREATE TABLE "schema two"."table two" (
id serial PRIMARY KEY,
"ref id" integer,
FOREIGN KEY ("ref id") REFERENCES "schema one"."table one"("id field")
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('table two');
expect(relationship.targetTable).toBe('table one');
expect(relationship.sourceColumn).toBe('ref id');
expect(relationship.targetColumn).toBe('id field');
});
it('should handle named constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth"."users" (
id serial PRIMARY KEY
);
CREATE TABLE "app"."profiles" (
id serial PRIMARY KEY,
user_id integer,
CONSTRAINT "fk-user-profile" FOREIGN KEY (user_id) REFERENCES "auth"."users"(id)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
// Note: Constraint names with special characters might be normalized
expect(relationship.name).toBeDefined();
});
it('should handle ALTER TABLE ADD CONSTRAINT with quoted identifiers', async () => {
const sql = `
CREATE TABLE "user-schema"."user-accounts" (
"account-id" serial PRIMARY KEY,
username text
);
CREATE TABLE "order-schema"."user-orders" (
"order-id" serial PRIMARY KEY,
"account-id" integer
);
ALTER TABLE "order-schema"."user-orders"
ADD CONSTRAINT "fk_orders_accounts"
FOREIGN KEY ("account-id")
REFERENCES "user-schema"."user-accounts"("account-id");
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.name).toBe('fk_orders_accounts');
expect(relationship.sourceTable).toBe('user-orders');
expect(relationship.targetTable).toBe('user-accounts');
expect(relationship.sourceColumn).toBe('account-id');
expect(relationship.targetColumn).toBe('account-id');
});
it('should handle complex mixed quoting scenarios', async () => {
const sql = `
CREATE TABLE auth.users (
id serial PRIMARY KEY
);
CREATE TABLE "app-data"."user_profiles" (
profile_id serial PRIMARY KEY,
"user-id" integer REFERENCES auth.users(id)
);
CREATE TABLE "app-data".posts (
id serial PRIMARY KEY,
profile_id integer
);
ALTER TABLE "app-data".posts
ADD CONSTRAINT fk_posts_profiles
FOREIGN KEY (profile_id)
REFERENCES "app-data"."user_profiles"(profile_id);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(3);
expect(result.relationships).toHaveLength(2);
// Verify the relationships were correctly identified
const profilesTable = result.tables.find(
(t) => t.name === 'user_profiles'
);
expect(profilesTable?.schema).toBe('app-data');
const postsTable = result.tables.find((t) => t.name === 'posts');
expect(postsTable?.schema).toBe('app-data');
});
});
describe('Edge cases and special scenarios', () => {
it('should handle Unicode characters in quoted identifiers', async () => {
const sql = `
CREATE TABLE "схема"."таблица" (
"идентификатор" serial PRIMARY KEY,
"данные" text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('схема');
expect(table.name).toBe('таблица');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
});
it('should handle parentheses in quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema(prod)"."users(archived)" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema(prod)');
expect(table.name).toBe('users(archived)');
});
it('should handle forward slashes in quoted identifiers', async () => {
const sql = `
CREATE TABLE "api/v1"."users/profiles" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('api/v1');
expect(table.name).toBe('users/profiles');
});
it('should handle IF NOT EXISTS with quoted identifiers', async () => {
const sql = `
CREATE TABLE IF NOT EXISTS "test-schema"."test-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('test-schema');
expect(table.name).toBe('test-table');
});
it('should handle ONLY keyword with quoted identifiers', async () => {
const sql = `
CREATE TABLE ONLY "parent-schema"."parent-table" (
id serial PRIMARY KEY
);
ALTER TABLE ONLY "parent-schema"."parent-table"
ADD CONSTRAINT "unique-constraint" UNIQUE (id);
`;
const result = await fromPostgres(sql);
// ONLY keyword might trigger warnings
expect(result.warnings).toBeDefined();
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('parent-schema');
expect(table.name).toBe('parent-table');
});
it('should handle self-referencing foreign keys with quoted identifiers', async () => {
const sql = `
CREATE TABLE "org-schema"."departments" (
"dept-id" serial PRIMARY KEY,
"parent-dept-id" integer REFERENCES "org-schema"."departments"("dept-id"),
name text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('departments');
expect(relationship.targetTable).toBe('departments'); // Self-reference
expect(relationship.sourceColumn).toBe('parent-dept-id');
expect(relationship.targetColumn).toBe('dept-id');
});
});
});

View File

@@ -91,7 +91,38 @@ export interface AlterTableExprItem {
action: string;
resource?: string;
type?: string;
keyword?: string;
constraint?: { constraint_type?: string };
// Properties for ADD COLUMN
column?:
| {
column?:
| {
expr?: {
value?: string;
};
}
| string;
}
| string
| ColumnReference;
definition?: {
dataType?: string;
length?: number;
precision?: number;
scale?: number;
suffix?: unknown[];
nullable?: { type: string };
unique?: string;
primary_key?: string;
constraint?: string;
default_val?: unknown;
auto_increment?: string;
};
nullable?: { type: string; value?: string };
unique?: string;
default_val?: unknown;
// Properties for constraints
create_definitions?:
| AlterTableConstraintDefinition
| {

View File

@@ -7,6 +7,8 @@ import type {
SQLForeignKey,
SQLEnumType,
} from '../../common';
import { buildSQLFromAST } from '../../common';
import { DatabaseType } from '@/lib/domain/database-type';
import type {
TableReference,
ColumnReference,
@@ -347,13 +349,20 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
// Try to extract column definition
// Match: column_name TYPE[(params)][array]
// Updated regex to handle complex types like GEOGRAPHY(POINT, 4326) and custom types like subscription_status
const columnMatch = trimmedLine.match(
/^\s*["']?(\w+)["']?\s+([\w_]+(?:\([^)]+\))?(?:\[\])?)/i
);
// First extract column name and everything after it
const columnMatch = trimmedLine.match(/^\s*["']?(\w+)["']?\s+(.+)/i);
if (columnMatch) {
const columnName = columnMatch[1];
let columnType = columnMatch[2];
const restOfLine = columnMatch[2];
// Now extract the type from the rest of the line
// Match type which could be multi-word (like CHARACTER VARYING) with optional params
const typeMatch = restOfLine.match(
/^((?:CHARACTER\s+VARYING|DOUBLE\s+PRECISION|[\w]+)(?:\([^)]+\))?(?:\[\])?)/i
);
if (!typeMatch) continue;
let columnType = typeMatch[1].trim();
// Normalize PostGIS types
if (columnType.toUpperCase().startsWith('GEOGRAPHY')) {
@@ -380,7 +389,65 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
const isPrimary = trimmedLine.match(/PRIMARY\s+KEY/i) !== null;
const isNotNull = trimmedLine.match(/NOT\s+NULL/i) !== null;
const isUnique = trimmedLine.match(/\bUNIQUE\b/i) !== null;
const hasDefault = trimmedLine.match(/DEFAULT\s+/i) !== null;
// Extract default value
let defaultValue: string | undefined;
// Updated regex to handle casting with :: operator
const defaultMatch = trimmedLine.match(
/DEFAULT\s+((?:'[^']*'|"[^"]*"|\S+)(?:::\w+)?)/i
);
if (defaultMatch) {
let defVal = defaultMatch[1].trim();
// Remove trailing comma if present
defVal = defVal.replace(/,$/, '').trim();
// Handle string literals
if (defVal.startsWith("'") && defVal.endsWith("'")) {
// Keep the quotes for string literals
defaultValue = defVal;
} else if (defVal.match(/^\d+(\.\d+)?$/)) {
// Numeric value
defaultValue = defVal;
} else if (
defVal.toUpperCase() === 'TRUE' ||
defVal.toUpperCase() === 'FALSE'
) {
// Boolean value
defaultValue = defVal.toUpperCase();
} else if (defVal.toUpperCase() === 'NULL') {
// NULL value
defaultValue = 'NULL';
} else if (defVal.includes('(') && defVal.includes(')')) {
// Function call (like gen_random_uuid())
// Normalize PostgreSQL function names to uppercase
const funcMatch = defVal.match(/^(\w+)\(/);
if (funcMatch) {
const funcName = funcMatch[1];
const pgFunctions = [
'now',
'current_timestamp',
'current_date',
'current_time',
'gen_random_uuid',
'random',
'nextval',
'currval',
];
if (pgFunctions.includes(funcName.toLowerCase())) {
defaultValue = defVal.replace(
funcName,
funcName.toUpperCase()
);
} else {
defaultValue = defVal;
}
} else {
defaultValue = defVal;
}
} else {
// Other expressions
defaultValue = defVal;
}
}
columns.push({
name: columnName,
@@ -388,7 +455,7 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
nullable: !isNotNull && !isPrimary,
primaryKey: isPrimary,
unique: isUnique || isPrimary,
default: hasDefault ? 'has default' : undefined,
default: defaultValue,
increment:
isSerialType ||
trimmedLine.includes('gen_random_uuid()') ||
@@ -490,16 +557,21 @@ function extractForeignKeysFromCreateTable(
const tableBody = tableBodyMatch[1];
// Pattern for inline REFERENCES - more flexible to handle various formats
// Pattern for inline REFERENCES - handles quoted and unquoted identifiers
const inlineRefPattern =
/["']?(\w+)["']?\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi;
/(?:"([^"]+)"|([^"\s,()]+))\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi;
let match;
while ((match = inlineRefPattern.exec(tableBody)) !== null) {
const sourceColumn = match[1];
const targetSchema = match[2] || 'public';
const targetTable = match[3];
const targetColumn = match[4];
// Extract values from appropriate match groups
// Groups: 1=quoted source col, 2=unquoted source col,
// 3=quoted schema, 4=unquoted schema,
// 5=quoted target table, 6=unquoted target table,
// 7=quoted target col, 8=unquoted target col
const sourceColumn = match[1] || match[2];
const targetSchema = match[3] || match[4] || 'public';
const targetTable = match[5] || match[6];
const targetColumn = match[7] || match[8];
const targetTableKey = `${targetSchema}.${targetTable}`;
const targetTableId = tableMap[targetTableKey];
@@ -521,15 +593,16 @@ function extractForeignKeysFromCreateTable(
}
}
// Pattern for FOREIGN KEY constraints
// Pattern for FOREIGN KEY constraints - handles quoted and unquoted identifiers
const fkConstraintPattern =
/FOREIGN\s+KEY\s*\(\s*["']?(\w+)["']?\s*\)\s*REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi;
/FOREIGN\s+KEY\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)\s*REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi;
while ((match = fkConstraintPattern.exec(tableBody)) !== null) {
const sourceColumn = match[1];
const targetSchema = match[2] || 'public';
const targetTable = match[3];
const targetColumn = match[4];
// Extract values from appropriate match groups
const sourceColumn = match[1] || match[2];
const targetSchema = match[3] || match[4] || 'public';
const targetTable = match[5] || match[6];
const targetColumn = match[7] || match[8];
const targetTableKey = `${targetSchema}.${targetTable}`;
const targetTableId = tableMap[targetTableKey];
@@ -585,12 +658,16 @@ export async function fromPostgres(
? stmt.sql.substring(createTableIndex)
: stmt.sql;
// Updated regex to properly handle quoted identifiers with special characters
// Matches: schema.table, "schema"."table", "schema".table, schema."table"
const tableMatch = sqlFromCreate.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
);
if (tableMatch) {
const schemaName = tableMatch[1] || 'public';
const tableName = tableMatch[2];
// Extract schema and table names from the appropriate match groups
// Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table
const schemaName = tableMatch[1] || tableMatch[2] || 'public';
const tableName = tableMatch[3] || tableMatch[4];
const tableKey = `${schemaName}.${tableName}`;
tableMap[tableKey] = generateId();
}
@@ -938,12 +1015,16 @@ export async function fromPostgres(
? stmt.sql.substring(createTableIndex)
: stmt.sql;
// Updated regex to properly handle quoted identifiers with special characters
// Matches: schema.table, "schema"."table", "schema".table, schema."table"
const tableMatch = sqlFromCreate.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
);
if (tableMatch) {
const schemaName = tableMatch[1] || 'public';
const tableName = tableMatch[2];
// Extract schema and table names from the appropriate match groups
// Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table
const schemaName = tableMatch[1] || tableMatch[2] || 'public';
const tableName = tableMatch[3] || tableMatch[4];
const tableKey = `${schemaName}.${tableName}`;
const tableId = tableMap[tableKey];
@@ -982,7 +1063,7 @@ export async function fromPostgres(
}
}
// Fourth pass: process ALTER TABLE statements for foreign keys
// Fourth pass: process ALTER TABLE statements for foreign keys and ADD COLUMN
for (const stmt of statements) {
if (stmt.type === 'alter' && stmt.parsed) {
const alterTableStmt = stmt.parsed as AlterTableStatement;
@@ -1012,13 +1093,440 @@ export async function fromPostgres(
);
if (!table) continue;
// Process foreign key constraints in ALTER TABLE
// Process ALTER TABLE expressions
if (alterTableStmt.expr && Array.isArray(alterTableStmt.expr)) {
alterTableStmt.expr.forEach((expr: AlterTableExprItem) => {
if (expr.action === 'add' && expr.create_definitions) {
// Handle ALTER COLUMN TYPE
if (expr.action === 'alter' && expr.resource === 'column') {
// Extract column name
let columnName: string | undefined;
if (
typeof expr.column === 'object' &&
'column' in expr.column
) {
const innerColumn = expr.column.column;
if (
typeof innerColumn === 'object' &&
'expr' in innerColumn &&
innerColumn.expr?.value
) {
columnName = innerColumn.expr.value;
} else if (typeof innerColumn === 'string') {
columnName = innerColumn;
}
} else if (typeof expr.column === 'string') {
columnName = expr.column;
}
// Check if it's a TYPE change
if (
columnName &&
expr.type === 'alter' &&
expr.definition?.dataType
) {
// Find the column in the table and update its type
const column = table.columns.find(
(col) => (col as SQLColumn).name === columnName
);
if (column) {
const definition = expr.definition;
const rawDataType = String(definition.dataType);
// console.log('ALTER TYPE expr:', JSON.stringify(expr, null, 2));
// Normalize the type
let normalizedType =
normalizePostgreSQLType(rawDataType);
// Handle type parameters
if (
definition.scale !== undefined &&
definition.scale !== null
) {
// For NUMERIC/DECIMAL with scale, length is actually precision
const precision =
definition.length ||
definition.precision;
normalizedType = `${normalizedType}(${precision},${definition.scale})`;
} else if (
definition.length !== undefined &&
definition.length !== null
) {
normalizedType = `${normalizedType}(${definition.length})`;
} else if (definition.precision !== undefined) {
normalizedType = `${normalizedType}(${definition.precision})`;
} else if (
definition.suffix &&
Array.isArray(definition.suffix) &&
definition.suffix.length > 0
) {
const params = definition.suffix
.map((s: unknown) => {
if (
typeof s === 'object' &&
s !== null &&
'value' in s
) {
return String(s.value);
}
return String(s);
})
.join(',');
normalizedType = `${normalizedType}(${params})`;
}
// Update the column type
(column as SQLColumn).type = normalizedType;
// Update typeArgs if applicable
if (
definition.scale !== undefined &&
definition.scale !== null
) {
// For NUMERIC/DECIMAL with scale
const precision =
definition.length ||
definition.precision;
(column as SQLColumn).typeArgs = {
precision: precision,
scale: definition.scale,
};
} else if (definition.length) {
(column as SQLColumn).typeArgs = {
length: definition.length,
};
} else if (definition.precision) {
(column as SQLColumn).typeArgs = {
precision: definition.precision,
};
}
}
}
// Handle ADD COLUMN
} else if (
expr.action === 'add' &&
expr.resource === 'column'
) {
// Handle ADD COLUMN directly from expr structure
// Extract column name from the nested structure
let columnName: string | undefined;
if (
typeof expr.column === 'object' &&
'column' in expr.column
) {
const innerColumn = expr.column.column;
if (
typeof innerColumn === 'object' &&
'expr' in innerColumn &&
innerColumn.expr?.value
) {
columnName = innerColumn.expr.value;
} else if (typeof innerColumn === 'string') {
columnName = innerColumn;
}
} else if (typeof expr.column === 'string') {
columnName = expr.column;
}
if (columnName && typeof columnName === 'string') {
const definition = expr.definition || {};
const rawDataType = String(
definition?.dataType || 'TEXT'
);
// console.log('expr:', JSON.stringify(expr, null, 2));
// Normalize the type
let normalizedBaseType =
normalizePostgreSQLType(rawDataType);
// Check if it's a serial type
const upperType = rawDataType.toUpperCase();
const isSerialType = [
'SERIAL',
'SERIAL2',
'SERIAL4',
'SERIAL8',
'BIGSERIAL',
'SMALLSERIAL',
].includes(upperType.split('(')[0]);
if (isSerialType) {
const typeLength = definition?.length as
| number
| undefined;
if (upperType === 'SERIAL') {
if (typeLength === 2) {
normalizedBaseType = 'SMALLINT';
} else if (typeLength === 8) {
normalizedBaseType = 'BIGINT';
} else {
normalizedBaseType = 'INTEGER';
}
}
}
// Handle type parameters
let finalDataType = normalizedBaseType;
const isNormalizedIntegerType =
['INTEGER', 'BIGINT', 'SMALLINT'].includes(
normalizedBaseType
) &&
(upperType === 'INT' || upperType === 'SERIAL');
if (!isSerialType && !isNormalizedIntegerType) {
const precision = definition?.precision;
const scale = definition?.scale;
const length = definition?.length;
const suffix =
(definition?.suffix as unknown[]) || [];
if (suffix.length > 0) {
const params = suffix
.map((s: unknown) => {
if (
typeof s === 'object' &&
s !== null &&
'value' in s
) {
return String(
(s as { value: unknown })
.value
);
}
return String(s);
})
.join(',');
finalDataType = `${normalizedBaseType}(${params})`;
} else if (precision !== undefined) {
if (scale !== undefined) {
finalDataType = `${normalizedBaseType}(${precision},${scale})`;
} else {
finalDataType = `${normalizedBaseType}(${precision})`;
}
} else if (
length !== undefined &&
length !== null
) {
finalDataType = `${normalizedBaseType}(${length})`;
}
}
// Check for nullable constraint
let nullable = true;
if (isSerialType) {
nullable = false;
} else if (
expr.nullable &&
expr.nullable.type === 'not null'
) {
nullable = false;
} else if (
definition?.nullable &&
definition.nullable.type === 'not null'
) {
nullable = false;
}
// Check for unique constraint
const isUnique =
expr.unique === 'unique' ||
definition?.unique === 'unique';
// Check for default value
let defaultValue: string | undefined;
const defaultVal =
expr.default_val || definition?.default_val;
if (defaultVal && !isSerialType) {
// Create a temporary columnDef to use the getDefaultValueString function
const tempColumnDef = {
default_val: defaultVal,
} as ColumnDefinition;
defaultValue =
getDefaultValueString(tempColumnDef);
}
// Create the new column object
const newColumn: SQLColumn = {
name: columnName,
type: finalDataType,
nullable: nullable,
primaryKey:
definition?.primary_key === 'primary key' ||
definition?.constraint === 'primary key' ||
isSerialType,
unique: isUnique,
default: defaultValue,
increment:
isSerialType ||
definition?.auto_increment ===
'auto_increment' ||
(stmt.sql
.toUpperCase()
.includes('GENERATED') &&
stmt.sql
.toUpperCase()
.includes('IDENTITY')),
};
// Add the column to the table if it doesn't already exist
const tableColumns = table.columns as SQLColumn[];
if (
!tableColumns.some(
(col) => col.name === columnName
)
) {
tableColumns.push(newColumn);
}
}
} else if (
expr.action === 'add' &&
expr.create_definitions
) {
const createDefs = expr.create_definitions;
if (
// Check if it's adding a column (legacy structure)
if (createDefs.resource === 'column') {
const columnDef =
createDefs as unknown as ColumnDefinition;
const columnName = extractColumnName(
columnDef.column
);
if (columnName) {
// Extract the column type and properties
const definition =
columnDef.definition as Record<
string,
unknown
>;
const rawDataType = String(
definition?.dataType || 'TEXT'
);
// Normalize the type
let normalizedBaseType =
normalizePostgreSQLType(rawDataType);
// Check if it's a serial type
const upperType = rawDataType.toUpperCase();
const isSerialType = [
'SERIAL',
'SERIAL2',
'SERIAL4',
'SERIAL8',
'BIGSERIAL',
'SMALLSERIAL',
].includes(upperType.split('(')[0]);
if (isSerialType) {
const typeLength = definition?.length as
| number
| undefined;
if (upperType === 'SERIAL') {
if (typeLength === 2) {
normalizedBaseType = 'SMALLINT';
} else if (typeLength === 8) {
normalizedBaseType = 'BIGINT';
} else {
normalizedBaseType = 'INTEGER';
}
}
}
// Handle type parameters
let finalDataType = normalizedBaseType;
const isNormalizedIntegerType =
['INTEGER', 'BIGINT', 'SMALLINT'].includes(
normalizedBaseType
) &&
(upperType === 'INT' ||
upperType === 'SERIAL');
if (!isSerialType && !isNormalizedIntegerType) {
const precision =
columnDef.definition?.precision;
const scale = columnDef.definition?.scale;
const length = columnDef.definition?.length;
const suffix =
(definition?.suffix as unknown[]) || [];
if (suffix.length > 0) {
const params = suffix
.map((s: unknown) => {
if (
typeof s === 'object' &&
s !== null &&
'value' in s
) {
return String(
(
s as {
value: unknown;
}
).value
);
}
return String(s);
})
.join(',');
finalDataType = `${normalizedBaseType}(${params})`;
} else if (precision !== undefined) {
if (scale !== undefined) {
finalDataType = `${normalizedBaseType}(${precision},${scale})`;
} else {
finalDataType = `${normalizedBaseType}(${precision})`;
}
} else if (
length !== undefined &&
length !== null
) {
finalDataType = `${normalizedBaseType}(${length})`;
}
}
// Create the new column object
const newColumn: SQLColumn = {
name: columnName,
type: finalDataType,
nullable: isSerialType
? false
: columnDef.nullable?.type !==
'not null',
primaryKey:
columnDef.primary_key ===
'primary key' ||
columnDef.definition?.constraint ===
'primary key' ||
isSerialType,
unique: columnDef.unique === 'unique',
typeArgs: getTypeArgs(columnDef.definition),
default: isSerialType
? undefined
: getDefaultValueString(columnDef),
increment:
isSerialType ||
columnDef.auto_increment ===
'auto_increment' ||
(stmt.sql
.toUpperCase()
.includes('GENERATED') &&
stmt.sql
.toUpperCase()
.includes('IDENTITY')),
};
// Add the column to the table if it doesn't already exist
const tableColumns2 =
table.columns as SQLColumn[];
if (
!tableColumns2.some(
(col) => col.name === columnName
)
) {
tableColumns2.push(newColumn);
}
}
} else if (
createDefs.constraint_type === 'FOREIGN KEY' ||
createDefs.constraint_type === 'foreign key'
) {
@@ -1129,19 +1637,188 @@ export async function fromPostgres(
}
} else if (stmt.type === 'alter' && !stmt.parsed) {
// Handle ALTER TABLE statements that failed to parse
// First try to extract ALTER COLUMN TYPE statements
const alterTypeMatch = stmt.sql.match(
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ALTER\s+COLUMN\s+(?:"([^"]+)"|([^"\s]+))\s+TYPE\s+([\w_]+(?:\([^)]*\))?(?:\[\])?)/i
);
if (alterTypeMatch) {
const schemaName =
alterTypeMatch[1] || alterTypeMatch[2] || 'public';
const tableName = alterTypeMatch[3] || alterTypeMatch[4];
const columnName = alterTypeMatch[5] || alterTypeMatch[6];
let columnType = alterTypeMatch[7];
const table = findTableWithSchemaSupport(
tables,
tableName,
schemaName
);
if (table && columnName) {
const column = (table.columns as SQLColumn[]).find(
(col) => col.name === columnName
);
if (column) {
// Normalize and update the type
columnType = normalizePostgreSQLType(columnType);
column.type = columnType;
// Extract and update typeArgs if present
const typeMatch = columnType.match(
/^(\w+)(?:\(([^)]+)\))?$/
);
if (typeMatch && typeMatch[2]) {
const params = typeMatch[2]
.split(',')
.map((p) => p.trim());
if (params.length === 1) {
column.typeArgs = {
length: parseInt(params[0]),
};
} else if (params.length === 2) {
column.typeArgs = {
precision: parseInt(params[0]),
scale: parseInt(params[1]),
};
}
}
}
}
}
// Then try to extract ADD COLUMN statements
const alterColumnMatch = stmt.sql.match(
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ADD\s+COLUMN\s+(?:"([^"]+)"|([^"\s]+))\s+([\w_]+(?:\([^)]*\))?(?:\[\])?)/i
);
if (alterColumnMatch) {
const schemaName =
alterColumnMatch[1] || alterColumnMatch[2] || 'public';
const tableName = alterColumnMatch[3] || alterColumnMatch[4];
const columnName = alterColumnMatch[5] || alterColumnMatch[6];
let columnType = alterColumnMatch[7];
const table = findTableWithSchemaSupport(
tables,
tableName,
schemaName
);
if (table && columnName) {
const tableColumns = table.columns as SQLColumn[];
if (!tableColumns.some((col) => col.name === columnName)) {
// Normalize the type
columnType = normalizePostgreSQLType(columnType);
// Check for constraints in the statement
const columnDefPart = stmt.sql.substring(
stmt.sql.indexOf(columnName)
);
const isPrimary =
columnDefPart.match(/PRIMARY\s+KEY/i) !== null;
const isNotNull =
columnDefPart.match(/NOT\s+NULL/i) !== null;
const isUnique =
columnDefPart.match(/\bUNIQUE\b/i) !== null;
// Extract default value
let defaultValue: string | undefined;
// Updated regex to handle casting with :: operator
const defaultMatch = columnDefPart.match(
/DEFAULT\s+((?:'[^']*'|"[^"]*"|\S+)(?:::\w+)?)/i
);
if (defaultMatch) {
let defVal = defaultMatch[1].trim();
// Remove trailing comma or semicolon if present
defVal = defVal.replace(/[,;]$/, '').trim();
// Handle string literals
if (
defVal.startsWith("'") &&
defVal.endsWith("'")
) {
// Keep the quotes for string literals
defaultValue = defVal;
} else if (defVal.match(/^\d+(\.\d+)?$/)) {
// Numeric value
defaultValue = defVal;
} else if (
defVal.toUpperCase() === 'TRUE' ||
defVal.toUpperCase() === 'FALSE'
) {
// Boolean value
defaultValue = defVal.toUpperCase();
} else if (defVal.toUpperCase() === 'NULL') {
// NULL value
defaultValue = 'NULL';
} else if (
defVal.includes('(') &&
defVal.includes(')')
) {
// Function call
// Normalize PostgreSQL function names to uppercase
const funcMatch = defVal.match(/^(\w+)\(/);
if (funcMatch) {
const funcName = funcMatch[1];
const pgFunctions = [
'now',
'current_timestamp',
'current_date',
'current_time',
'gen_random_uuid',
'random',
'nextval',
'currval',
];
if (
pgFunctions.includes(
funcName.toLowerCase()
)
) {
defaultValue = defVal.replace(
funcName,
funcName.toUpperCase()
);
} else {
defaultValue = defVal;
}
} else {
defaultValue = defVal;
}
} else {
// Other expressions
defaultValue = defVal;
}
}
tableColumns.push({
name: columnName,
type: columnType,
nullable: !isNotNull && !isPrimary,
primaryKey: isPrimary,
unique: isUnique || isPrimary,
default: defaultValue,
increment: false,
});
}
}
}
// Extract foreign keys using regex as fallback
// Updated regex to handle quoted identifiers properly
const alterFKMatch = stmt.sql.match(
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s+ADD\s+CONSTRAINT\s+["']?([^"'\s]+)["']?\s+FOREIGN\s+KEY\s*\(["']?([^"'\s)]+)["']?\)\s+REFERENCES\s+(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s*\(["']?([^"'\s)]+)["']?\)/i
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ADD\s+CONSTRAINT\s+(?:"([^"]+)"|([^"\s]+))\s+FOREIGN\s+KEY\s*\((?:"([^"]+)"|([^"\s)]+))\)\s+REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\((?:"([^"]+)"|([^"\s)]+))\)/i
);
if (alterFKMatch) {
const sourceSchema = alterFKMatch[1] || 'public';
const sourceTable = alterFKMatch[2];
const constraintName = alterFKMatch[3];
const sourceColumn = alterFKMatch[4];
const targetSchema = alterFKMatch[5] || 'public';
const targetTable = alterFKMatch[6];
const targetColumn = alterFKMatch[7];
// Extract values from appropriate match groups
const sourceSchema =
alterFKMatch[1] || alterFKMatch[2] || 'public';
const sourceTable = alterFKMatch[3] || alterFKMatch[4];
const constraintName = alterFKMatch[5] || alterFKMatch[6];
const sourceColumn = alterFKMatch[7] || alterFKMatch[8];
const targetSchema =
alterFKMatch[9] || alterFKMatch[10] || 'public';
const targetTable = alterFKMatch[11] || alterFKMatch[12];
const targetColumn = alterFKMatch[13] || alterFKMatch[14];
const sourceTableId = getTableIdWithSchemaSupport(
tableMap,
@@ -1275,58 +1952,10 @@ export async function fromPostgres(
function getDefaultValueString(
columnDef: ColumnDefinition
): string | undefined {
let defVal = columnDef.default_val;
if (
defVal &&
typeof defVal === 'object' &&
defVal.type === 'default' &&
'value' in defVal
) {
defVal = defVal.value;
}
const defVal = columnDef.default_val;
if (defVal === undefined || defVal === null) return undefined;
let value: string | undefined;
switch (typeof defVal) {
case 'string':
value = defVal;
break;
case 'number':
value = String(defVal);
break;
case 'boolean':
value = defVal ? 'TRUE' : 'FALSE';
break;
case 'object':
if ('value' in defVal && typeof defVal.value === 'string') {
value = defVal.value;
} else if ('raw' in defVal && typeof defVal.raw === 'string') {
value = defVal.raw;
} else if (defVal.type === 'bool') {
value = defVal.value ? 'TRUE' : 'FALSE';
} else if (defVal.type === 'function' && defVal.name) {
const fnName = defVal.name;
if (
fnName &&
typeof fnName === 'object' &&
Array.isArray(fnName.name) &&
fnName.name.length > 0 &&
fnName.name[0].value
) {
value = fnName.name[0].value.toUpperCase();
} else if (typeof fnName === 'string') {
value = fnName.toUpperCase();
} else {
value = 'UNKNOWN_FUNCTION';
}
}
break;
default:
value = undefined;
}
return value;
// Use buildSQLFromAST to reconstruct the default value
return buildSQLFromAST(defVal, DatabaseType.POSTGRESQL);
}

View File

@@ -0,0 +1,252 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Default Value Import', () => {
describe('String Default Values', () => {
it('should parse simple string defaults with single quotes', async () => {
const sql = `
CREATE TABLE kingdom_citizens (
citizen_id INT NOT NULL,
allegiance NVARCHAR(50) DEFAULT 'neutral',
PRIMARY KEY (citizen_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const allegianceColumn = result.tables[0].columns.find(
(c) => c.name === 'allegiance'
);
expect(allegianceColumn?.default).toBe("'neutral'");
});
it('should parse string defaults with Unicode prefix', async () => {
const sql = `
CREATE TABLE ancient_scrolls (
scroll_id INT NOT NULL,
runic_inscription NVARCHAR(255) DEFAULT N'Ancient wisdom',
prophecy NVARCHAR(MAX) DEFAULT N'The chosen one shall rise',
PRIMARY KEY (scroll_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const runicColumn = result.tables[0].columns.find(
(c) => c.name === 'runic_inscription'
);
expect(runicColumn?.default).toBe("N'Ancient wisdom'");
const prophecyColumn = result.tables[0].columns.find(
(c) => c.name === 'prophecy'
);
expect(prophecyColumn?.default).toBe(
"N'The chosen one shall rise'"
);
});
});
describe('Numeric Default Values', () => {
it('should parse integer defaults', async () => {
const sql = `
CREATE TABLE castle_treasury (
treasury_id INT NOT NULL,
gold_count INT DEFAULT 0,
max_capacity BIGINT DEFAULT 100000,
guard_posts SMALLINT DEFAULT 5,
PRIMARY KEY (treasury_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const goldColumn = result.tables[0].columns.find(
(c) => c.name === 'gold_count'
);
expect(goldColumn?.default).toBe('0');
const capacityColumn = result.tables[0].columns.find(
(c) => c.name === 'max_capacity'
);
expect(capacityColumn?.default).toBe('100000');
const guardColumn = result.tables[0].columns.find(
(c) => c.name === 'guard_posts'
);
expect(guardColumn?.default).toBe('5');
});
it('should parse decimal defaults', async () => {
const sql = `
CREATE TABLE blacksmith_shop (
item_id INT NOT NULL,
weapon_price DECIMAL(10, 2) DEFAULT 99.99,
guild_discount FLOAT DEFAULT 0.15,
enchantment_tax NUMERIC(5, 4) DEFAULT 0.0825,
PRIMARY KEY (item_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const priceColumn = result.tables[0].columns.find(
(c) => c.name === 'weapon_price'
);
expect(priceColumn?.default).toBe('99.99');
const discountColumn = result.tables[0].columns.find(
(c) => c.name === 'guild_discount'
);
expect(discountColumn?.default).toBe('0.15');
const taxColumn = result.tables[0].columns.find(
(c) => c.name === 'enchantment_tax'
);
expect(taxColumn?.default).toBe('0.0825');
});
});
describe('Boolean Default Values', () => {
it('should parse BIT defaults', async () => {
const sql = `
CREATE TABLE magic_barriers (
barrier_id INT NOT NULL,
is_active BIT DEFAULT 1,
is_breached BIT DEFAULT 0,
PRIMARY KEY (barrier_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const activeColumn = result.tables[0].columns.find(
(c) => c.name === 'is_active'
);
expect(activeColumn?.default).toBe('1');
const breachedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_breached'
);
expect(breachedColumn?.default).toBe('0');
});
});
describe('Date and Time Default Values', () => {
it('should parse date/time function defaults', async () => {
const sql = `
CREATE TABLE battle_logs (
battle_id INT NOT NULL,
battle_started DATETIME DEFAULT GETDATE(),
last_action DATETIME2 DEFAULT SYSDATETIME(),
battle_date DATE DEFAULT GETDATE(),
PRIMARY KEY (battle_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const startedColumn = result.tables[0].columns.find(
(c) => c.name === 'battle_started'
);
expect(startedColumn?.default).toBe('GETDATE()');
const actionColumn = result.tables[0].columns.find(
(c) => c.name === 'last_action'
);
expect(actionColumn?.default).toBe('SYSDATETIME()');
const dateColumn = result.tables[0].columns.find(
(c) => c.name === 'battle_date'
);
expect(dateColumn?.default).toBe('GETDATE()');
});
});
describe('IDENTITY columns', () => {
it('should handle IDENTITY columns correctly', async () => {
const sql = `
CREATE TABLE legendary_weapons (
weapon_id INT IDENTITY(1,1) NOT NULL,
legacy_id BIGINT IDENTITY(100,10) NOT NULL,
weapon_name NVARCHAR(100),
PRIMARY KEY (weapon_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const weaponColumn = result.tables[0].columns.find(
(c) => c.name === 'weapon_id'
);
expect(weaponColumn?.increment).toBe(true);
const legacyColumn = result.tables[0].columns.find(
(c) => c.name === 'legacy_id'
);
expect(legacyColumn?.increment).toBe(true);
});
});
describe('Complex Real-World Example with Schema', () => {
it('should handle complex table with schema and multiple default types', async () => {
const sql = `
CREATE TABLE [dbo].[QuestContracts] (
[ContractID] INT IDENTITY(1,1) NOT NULL,
[AdventurerID] INT NOT NULL,
[QuestDate] DATETIME DEFAULT GETDATE(),
[QuestStatus] NVARCHAR(20) DEFAULT N'Available',
[RewardAmount] DECIMAL(10, 2) DEFAULT 0.00,
[IsCompleted] BIT DEFAULT 0,
[CompletedDate] DATETIME NULL,
[QuestNotes] NVARCHAR(MAX) DEFAULT NULL,
[DifficultyLevel] INT DEFAULT 5,
[QuestGuid] UNIQUEIDENTIFIER DEFAULT NEWID(),
PRIMARY KEY ([ContractID])
);
`;
const result = await fromSQLServer(sql);
const table = result.tables[0];
expect(table).toBeDefined();
expect(table.schema).toBe('dbo');
// Check various default values
const questDateColumn = table.columns.find(
(c) => c.name === 'QuestDate'
);
expect(questDateColumn?.default).toBe('GETDATE()');
const statusColumn = table.columns.find(
(c) => c.name === 'QuestStatus'
);
expect(statusColumn?.default).toBe("N'Available'");
const rewardColumn = table.columns.find(
(c) => c.name === 'RewardAmount'
);
expect(rewardColumn?.default).toBe('0.00');
const completedColumn = table.columns.find(
(c) => c.name === 'IsCompleted'
);
expect(completedColumn?.default).toBe('0');
const difficultyColumn = table.columns.find(
(c) => c.name === 'DifficultyLevel'
);
expect(difficultyColumn?.default).toBe('5');
const guidColumn = table.columns.find(
(c) => c.name === 'QuestGuid'
);
expect(guidColumn?.default).toBe('NEWID()');
});
});
describe('Expressions in defaults', () => {
it('should handle parentheses in default expressions', async () => {
const sql = `
CREATE TABLE spell_calculations (
calculation_id INT NOT NULL,
base_damage INT DEFAULT (10 + 5),
total_power DECIMAL(10,2) DEFAULT ((100.0 * 0.15) + 10),
PRIMARY KEY (calculation_id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const damageColumn = result.tables[0].columns.find(
(c) => c.name === 'base_damage'
);
expect(damageColumn?.default).toBe('(10 + 5)');
const powerColumn = result.tables[0].columns.find(
(c) => c.name === 'total_power'
);
expect(powerColumn?.default).toBe('((100.0 * 0.15) + 10)');
});
});
});

View File

@@ -0,0 +1,91 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Complex Fantasy Case', () => {
it('should parse complex SQL with SpellDefinition and SpellComponent tables', async () => {
// Complex SQL with same structure as user's case but fantasy-themed
const sql = `CREATE TABLE [DBO].[SpellDefinition](
[SPELLID] (VARCHAR)(32),
[HASVERBALCOMP] BOOLEAN,
[INCANTATION] [VARCHAR](128),
[INCANTATIONFIX] BOOLEAN,
[ITSCOMPONENTREL] [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),
[SHOWVISUALS] BOOLEAN, ) ON [PRIMARY]
CREATE TABLE [DBO].[SpellComponent](
[ALIAS] CHAR (50),
[SPELLID] (VARCHAR)(32),
[ISOPTIONAL] BOOLEAN,
[ITSPARENTCOMP] [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),
[ITSSCHOOLMETA] [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
console.log('Testing complex fantasy SQL...');
console.log(
'Number of CREATE TABLE statements:',
(sql.match(/CREATE\s+TABLE/gi) || []).length
);
const result = await fromSQLServer(sql);
console.log(
'Result tables:',
result.tables.map((t) => t.name)
);
console.log('Result relationships:', result.relationships.length);
// Debug: Show actual relationships
if (result.relationships.length === 0) {
console.log('WARNING: No relationships found!');
} else {
console.log('Relationships found:');
result.relationships.forEach((r) => {
console.log(
` ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
);
});
}
// Should create TWO tables
expect(result.tables).toHaveLength(2);
// Check first table
const spellDef = result.tables.find(
(t) => t.name === 'SpellDefinition'
);
expect(spellDef).toBeDefined();
expect(spellDef?.schema).toBe('DBO');
expect(spellDef?.columns).toHaveLength(6);
// Check second table
const spellComp = result.tables.find(
(t) => t.name === 'SpellComponent'
);
expect(spellComp).toBeDefined();
expect(spellComp?.schema).toBe('DBO');
expect(spellComp?.columns).toHaveLength(6);
// Check foreign key relationships (should have at least 2)
expect(result.relationships.length).toBeGreaterThanOrEqual(2);
// Check FK from SpellDefinition to SpellComponent
const fkDefToComp = result.relationships.find(
(r) =>
r.sourceTable === 'SpellDefinition' &&
r.targetTable === 'SpellComponent' &&
r.sourceColumn === 'itscomponentrel'
);
expect(fkDefToComp).toBeDefined();
expect(fkDefToComp?.targetColumn).toBe('SPELLID');
// Check self-referential FK in SpellComponent
const selfRefFK = result.relationships.find(
(r) =>
r.sourceTable === 'SpellComponent' &&
r.targetTable === 'SpellComponent' &&
r.sourceColumn === 'itsparentcomp'
);
expect(selfRefFK).toBeDefined();
expect(selfRefFK?.targetColumn).toBe('SPELLID');
});
});

View File

@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest';
import { sqlImportToDiagram } from '../../../index';
import { DatabaseType } from '@/lib/domain/database-type';
describe('SQL Server Full Import Flow', () => {
it('should create relationships when importing through the full flow', async () => {
const sql = `CREATE TABLE [DBO].[SpellDefinition](
[SPELLID] (VARCHAR)(32),
[HASVERBALCOMP] BOOLEAN,
[INCANTATION] [VARCHAR](128),
[INCANTATIONFIX] BOOLEAN,
[ITSCOMPONENTREL] [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),
[SHOWVISUALS] BOOLEAN, ) ON [PRIMARY]
CREATE TABLE [DBO].[SpellComponent](
[ALIAS] CHAR (50),
[SPELLID] (VARCHAR)(32),
[ISOPTIONAL] BOOLEAN,
[ITSPARENTCOMP] [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),
[ITSSCHOOLMETA] [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
// Test the full import flow as the application uses it
const diagram = await sqlImportToDiagram({
sqlContent: sql,
sourceDatabaseType: DatabaseType.SQL_SERVER,
targetDatabaseType: DatabaseType.SQL_SERVER,
});
// Verify tables
expect(diagram.tables).toHaveLength(2);
const tableNames = diagram.tables?.map((t) => t.name).sort();
expect(tableNames).toEqual(['SpellComponent', 'SpellDefinition']);
// Verify relationships are created in the diagram
expect(diagram.relationships).toBeDefined();
expect(diagram.relationships?.length).toBeGreaterThanOrEqual(2);
// Check specific relationships
const fk1 = diagram.relationships?.find(
(r) =>
r.sourceFieldId &&
r.targetFieldId && // Must have field IDs
diagram.tables?.some(
(t) =>
t.id === r.sourceTableId && t.name === 'SpellDefinition'
)
);
expect(fk1).toBeDefined();
const fk2 = diagram.relationships?.find(
(r) =>
r.sourceFieldId &&
r.targetFieldId && // Must have field IDs
diagram.tables?.some(
(t) =>
t.id === r.sourceTableId &&
t.name === 'SpellComponent' &&
t.id === r.targetTableId // self-reference
)
);
expect(fk2).toBeDefined();
console.log(
'Full flow test - Relationships created:',
diagram.relationships?.length
);
diagram.relationships?.forEach((r) => {
const sourceTable = diagram.tables?.find(
(t) => t.id === r.sourceTableId
);
const targetTable = diagram.tables?.find(
(t) => t.id === r.targetTableId
);
const sourceField = sourceTable?.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(f) => f.id === r.targetFieldId
);
console.log(
` ${sourceTable?.name}.${sourceField?.name} -> ${targetTable?.name}.${targetField?.name}`
);
});
});
it('should handle case-insensitive field matching', async () => {
const sql = `CREATE TABLE DragonLair (
[LAIRID] INT PRIMARY KEY,
[parentLairId] INT, FOREIGN KEY (PARENTLAIRID) REFERENCES DragonLair(lairid)
)`;
const diagram = await sqlImportToDiagram({
sqlContent: sql,
sourceDatabaseType: DatabaseType.SQL_SERVER,
targetDatabaseType: DatabaseType.SQL_SERVER,
});
// Should create the self-referential relationship despite case differences
expect(diagram.relationships?.length).toBe(1);
});
});

View File

@@ -0,0 +1,573 @@
import { describe, expect, it } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Multi-Schema Database Tests', () => {
it('should parse a fantasy-themed multi-schema database with cross-schema relationships', async () => {
const sql = `
-- =============================================
-- Magical Realm Multi-Schema Database
-- A comprehensive fantasy database with multiple schemas
-- =============================================
-- Create schemas
CREATE SCHEMA [realm];
CREATE SCHEMA [academy];
CREATE SCHEMA [treasury];
CREATE SCHEMA [combat];
CREATE SCHEMA [marketplace];
-- =============================================
-- REALM Schema - Core realm entities
-- =============================================
CREATE TABLE [realm].[kingdoms] (
[kingdom_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[kingdom_name] NVARCHAR(100) NOT NULL UNIQUE,
[ruler_name] NVARCHAR(100) NOT NULL,
[founding_date] DATE NOT NULL,
[capital_city] NVARCHAR(100),
[population] BIGINT,
[treasury_gold] DECIMAL(18, 2) DEFAULT 10000.00
);
CREATE TABLE [realm].[cities] (
[city_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[city_name] NVARCHAR(100) NOT NULL,
[kingdom_id] BIGINT NOT NULL,
[population] INT,
[has_walls] BIT DEFAULT 0,
[has_academy] BIT DEFAULT 0,
[has_marketplace] BIT DEFAULT 0
);
CREATE TABLE [realm].[guilds] (
[guild_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[guild_name] NVARCHAR(100) NOT NULL,
[guild_type] NVARCHAR(50) NOT NULL, -- 'Mages', 'Warriors', 'Thieves', 'Merchants'
[headquarters_city_id] BIGINT NOT NULL,
[founding_year] INT,
[member_count] INT DEFAULT 0,
[guild_master] NVARCHAR(100)
);
-- =============================================
-- ACADEMY Schema - Educational institutions
-- =============================================
CREATE TABLE [academy].[schools] (
[school_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[school_name] NVARCHAR(150) NOT NULL,
[city_id] BIGINT NOT NULL,
[specialization] NVARCHAR(100), -- 'Elemental Magic', 'Necromancy', 'Healing', 'Alchemy'
[founded_year] INT,
[tuition_gold] DECIMAL(10, 2),
[headmaster] NVARCHAR(100)
);
CREATE TABLE [academy].[students] (
[student_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[first_name] NVARCHAR(50) NOT NULL,
[last_name] NVARCHAR(50) NOT NULL,
[school_id] BIGINT NOT NULL,
[enrollment_date] DATE NOT NULL,
[graduation_date] DATE NULL,
[major_discipline] NVARCHAR(100),
[home_kingdom_id] BIGINT NOT NULL,
[sponsor_guild_id] BIGINT NULL
);
CREATE TABLE [academy].[courses] (
[course_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[course_name] NVARCHAR(200) NOT NULL,
[school_id] BIGINT NOT NULL,
[credit_hours] INT,
[difficulty_level] INT CHECK (difficulty_level BETWEEN 1 AND 10),
[prerequisites] NVARCHAR(MAX),
[professor_name] NVARCHAR(100)
);
CREATE TABLE [academy].[enrollments] (
[enrollment_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[student_id] BIGINT NOT NULL,
[course_id] BIGINT NOT NULL,
[enrollment_date] DATE NOT NULL,
[grade] NVARCHAR(2),
[completed] BIT DEFAULT 0
);
-- =============================================
-- TREASURY Schema - Financial entities
-- =============================================
CREATE TABLE [treasury].[currencies] (
[currency_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[currency_name] NVARCHAR(50) NOT NULL UNIQUE,
[symbol] NVARCHAR(10),
[gold_exchange_rate] DECIMAL(10, 4) NOT NULL,
[issuing_kingdom_id] BIGINT NOT NULL
);
CREATE TABLE [treasury].[banks] (
[bank_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[bank_name] NVARCHAR(100) NOT NULL,
[headquarters_city_id] BIGINT NOT NULL,
[total_deposits] DECIMAL(18, 2) DEFAULT 0,
[vault_security_level] INT CHECK (vault_security_level BETWEEN 1 AND 10),
[founding_date] DATE
);
CREATE TABLE [treasury].[accounts] (
[account_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[account_number] NVARCHAR(20) NOT NULL UNIQUE,
[bank_id] BIGINT NOT NULL,
[owner_type] NVARCHAR(20) NOT NULL, -- 'Student', 'Guild', 'Kingdom', 'Merchant'
[owner_id] BIGINT NOT NULL,
[balance] DECIMAL(18, 2) DEFAULT 0,
[currency_id] BIGINT NOT NULL,
[opened_date] DATE NOT NULL
);
CREATE TABLE [treasury].[transactions] (
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[from_account_id] BIGINT NULL,
[to_account_id] BIGINT NULL,
[amount] DECIMAL(18, 2) NOT NULL,
[currency_id] BIGINT NOT NULL,
[transaction_date] DATETIME NOT NULL,
[description] NVARCHAR(500),
[transaction_type] NVARCHAR(50) -- 'Deposit', 'Withdrawal', 'Transfer', 'Payment'
);
-- =============================================
-- COMBAT Schema - Battle and warrior entities
-- =============================================
CREATE TABLE [combat].[warriors] (
[warrior_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[warrior_name] NVARCHAR(100) NOT NULL,
[class] NVARCHAR(50) NOT NULL, -- 'Knight', 'Archer', 'Mage', 'Barbarian'
[level] INT DEFAULT 1,
[experience_points] BIGINT DEFAULT 0,
[guild_id] BIGINT NULL,
[home_city_id] BIGINT NOT NULL,
[strength] INT,
[agility] INT,
[intelligence] INT,
[current_hp] INT,
[max_hp] INT
);
CREATE TABLE [combat].[weapons] (
[weapon_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[weapon_name] NVARCHAR(100) NOT NULL,
[weapon_type] NVARCHAR(50), -- 'Sword', 'Bow', 'Staff', 'Axe'
[damage] INT,
[durability] INT,
[enchantment_level] INT DEFAULT 0,
[market_value] DECIMAL(10, 2),
[owner_warrior_id] BIGINT NULL
);
CREATE TABLE [combat].[battles] (
[battle_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[battle_name] NVARCHAR(200),
[battle_date] DATETIME NOT NULL,
[location_city_id] BIGINT NOT NULL,
[victor_warrior_id] BIGINT NULL,
[total_participants] INT,
[battle_type] NVARCHAR(50) -- 'Duel', 'Tournament', 'War', 'Training'
);
CREATE TABLE [combat].[battle_participants] (
[participant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[battle_id] BIGINT NOT NULL,
[warrior_id] BIGINT NOT NULL,
[damage_dealt] INT DEFAULT 0,
[damage_received] INT DEFAULT 0,
[survived] BIT DEFAULT 1,
[rewards_earned] DECIMAL(10, 2) DEFAULT 0
);
-- =============================================
-- MARKETPLACE Schema - Commerce entities
-- =============================================
CREATE TABLE [marketplace].[merchants] (
[merchant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[merchant_name] NVARCHAR(100) NOT NULL,
[shop_name] NVARCHAR(150),
[city_id] BIGINT NOT NULL,
[specialization] NVARCHAR(100), -- 'Weapons', 'Potions', 'Scrolls', 'Artifacts'
[reputation_score] INT DEFAULT 50,
[bank_account_id] BIGINT NULL
);
CREATE TABLE [marketplace].[items] (
[item_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[item_name] NVARCHAR(150) NOT NULL,
[item_type] NVARCHAR(50),
[base_price] DECIMAL(10, 2),
[rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
[merchant_id] BIGINT NOT NULL,
[stock_quantity] INT DEFAULT 0,
[magical_properties] NVARCHAR(MAX)
);
CREATE TABLE [marketplace].[trade_routes] (
[route_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[from_city_id] BIGINT NOT NULL,
[to_city_id] BIGINT NOT NULL,
[distance_leagues] INT,
[travel_days] INT,
[danger_level] INT CHECK (danger_level BETWEEN 1 AND 10),
[toll_cost] DECIMAL(10, 2),
[controlled_by_guild_id] BIGINT NULL
);
CREATE TABLE [marketplace].[transactions] (
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[buyer_type] NVARCHAR(20), -- 'Warrior', 'Student', 'Merchant'
[buyer_id] BIGINT NOT NULL,
[merchant_id] BIGINT NOT NULL,
[item_id] BIGINT NOT NULL,
[quantity] INT NOT NULL,
[total_price] DECIMAL(10, 2) NOT NULL,
[transaction_date] DATETIME NOT NULL,
[payment_account_id] BIGINT NULL
);
-- =============================================
-- Foreign Key Constraints - Cross-Schema Relationships
-- =============================================
-- Realm schema relationships
ALTER TABLE [realm].[cities] ADD CONSTRAINT [FK_Cities_Kingdoms]
FOREIGN KEY ([kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [realm].[guilds] ADD CONSTRAINT [FK_Guilds_Cities]
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
-- Academy schema relationships (references realm schema)
ALTER TABLE [academy].[schools] ADD CONSTRAINT [FK_Schools_Cities]
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Schools]
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Kingdoms]
FOREIGN KEY ([home_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Guilds]
FOREIGN KEY ([sponsor_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [academy].[courses] ADD CONSTRAINT [FK_Courses_Schools]
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Students]
FOREIGN KEY ([student_id]) REFERENCES [academy].[students]([student_id]);
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Courses]
FOREIGN KEY ([course_id]) REFERENCES [academy].[courses]([course_id]);
-- Treasury schema relationships (references realm schema)
ALTER TABLE [treasury].[currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
FOREIGN KEY ([issuing_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [treasury].[banks] ADD CONSTRAINT [FK_Banks_Cities]
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Banks]
FOREIGN KEY ([bank_id]) REFERENCES [treasury].[banks]([bank_id]);
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Currencies]
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_FromAccount]
FOREIGN KEY ([from_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_ToAccount]
FOREIGN KEY ([to_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_Currency]
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
-- Combat schema relationships (references realm and combat schemas)
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Guilds]
FOREIGN KEY ([guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Cities]
FOREIGN KEY ([home_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [combat].[weapons] ADD CONSTRAINT [FK_Weapons_Warriors]
FOREIGN KEY ([owner_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_Cities]
FOREIGN KEY ([location_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_VictorWarrior]
FOREIGN KEY ([victor_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
FOREIGN KEY ([battle_id]) REFERENCES [combat].[battles]([battle_id]);
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Warriors]
FOREIGN KEY ([warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
-- Marketplace schema relationships (references multiple schemas)
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_Cities]
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_BankAccounts]
FOREIGN KEY ([bank_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [marketplace].[items] ADD CONSTRAINT [FK_Items_Merchants]
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_FromCity]
FOREIGN KEY ([from_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_ToCity]
FOREIGN KEY ([to_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_Guilds]
FOREIGN KEY ([controlled_by_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Merchants]
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Items]
FOREIGN KEY ([item_id]) REFERENCES [marketplace].[items]([item_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_PaymentAccount]
FOREIGN KEY ([payment_account_id]) REFERENCES [treasury].[accounts]([account_id]);
-- Note: Testing table reference without schema prefix defaults to dbo schema
`;
const result = await fromSQLServer(sql);
// Verify all schemas are recognized
const schemas = new Set(result.tables.map((t) => t.schema));
expect(schemas.has('realm')).toBe(true);
expect(schemas.has('academy')).toBe(true);
expect(schemas.has('treasury')).toBe(true);
expect(schemas.has('combat')).toBe(true);
expect(schemas.has('marketplace')).toBe(true);
// Verify table count per schema
const tablesBySchema = {
realm: result.tables.filter((t) => t.schema === 'realm').length,
academy: result.tables.filter((t) => t.schema === 'academy').length,
treasury: result.tables.filter((t) => t.schema === 'treasury')
.length,
combat: result.tables.filter((t) => t.schema === 'combat').length,
marketplace: result.tables.filter((t) => t.schema === 'marketplace')
.length,
};
expect(tablesBySchema.realm).toBe(3); // kingdoms, cities, guilds
expect(tablesBySchema.academy).toBe(4); // schools, students, courses, enrollments
expect(tablesBySchema.treasury).toBe(4); // currencies, banks, accounts, transactions
expect(tablesBySchema.combat).toBe(4); // warriors, weapons, battles, battle_participants
expect(tablesBySchema.marketplace).toBe(4); // merchants, items, trade_routes, transactions
// Total tables should be 19
expect(result.tables.length).toBe(19);
// Debug: log which relationships are missing
const expectedRelationshipNames = [
'FK_Cities_Kingdoms',
'FK_Guilds_Cities',
'FK_Schools_Cities',
'FK_Students_Schools',
'FK_Students_Kingdoms',
'FK_Students_Guilds',
'FK_Courses_Schools',
'FK_Enrollments_Students',
'FK_Enrollments_Courses',
'FK_Currencies_Kingdoms',
'FK_Banks_Cities',
'FK_Accounts_Banks',
'FK_Accounts_Currencies',
'FK_Transactions_FromAccount',
'FK_Transactions_ToAccount',
'FK_Transactions_Currency',
'FK_Warriors_Guilds',
'FK_Warriors_Cities',
'FK_Weapons_Warriors',
'FK_Battles_Cities',
'FK_Battles_VictorWarrior',
'FK_BattleParticipants_Battles',
'FK_BattleParticipants_Warriors',
'FK_Merchants_Cities',
'FK_Merchants_BankAccounts',
'FK_Items_Merchants',
'FK_TradeRoutes_FromCity',
'FK_TradeRoutes_ToCity',
'FK_TradeRoutes_Guilds',
'FK_MarketTransactions_Merchants',
'FK_MarketTransactions_Items',
'FK_MarketTransactions_PaymentAccount',
];
const foundRelationshipNames = result.relationships.map((r) => r.name);
const missingRelationships = expectedRelationshipNames.filter(
(name) => !foundRelationshipNames.includes(name)
);
if (missingRelationships.length > 0) {
console.log('Missing relationships:', missingRelationships);
console.log('Found relationships:', foundRelationshipNames);
}
// Verify relationships count - we have 32 working relationships
expect(result.relationships.length).toBe(32);
// Verify some specific cross-schema relationships
const crossSchemaRelationships = result.relationships.filter(
(r) => r.sourceSchema !== r.targetSchema
);
expect(crossSchemaRelationships.length).toBeGreaterThan(10); // Many cross-schema relationships
// Check specific cross-schema relationships exist
const schoolsToCities = result.relationships.find(
(r) =>
r.sourceTable === 'schools' &&
r.sourceSchema === 'academy' &&
r.targetTable === 'cities' &&
r.targetSchema === 'realm'
);
expect(schoolsToCities).toBeDefined();
expect(schoolsToCities?.name).toBe('FK_Schools_Cities');
const studentsToKingdoms = result.relationships.find(
(r) =>
r.sourceTable === 'students' &&
r.sourceSchema === 'academy' &&
r.targetTable === 'kingdoms' &&
r.targetSchema === 'realm'
);
expect(studentsToKingdoms).toBeDefined();
expect(studentsToKingdoms?.name).toBe('FK_Students_Kingdoms');
const warriorsToGuilds = result.relationships.find(
(r) =>
r.sourceTable === 'warriors' &&
r.sourceSchema === 'combat' &&
r.targetTable === 'guilds' &&
r.targetSchema === 'realm'
);
expect(warriorsToGuilds).toBeDefined();
expect(warriorsToGuilds?.name).toBe('FK_Warriors_Guilds');
const merchantsToAccounts = result.relationships.find(
(r) =>
r.sourceTable === 'merchants' &&
r.sourceSchema === 'marketplace' &&
r.targetTable === 'accounts' &&
r.targetSchema === 'treasury'
);
expect(merchantsToAccounts).toBeDefined();
expect(merchantsToAccounts?.name).toBe('FK_Merchants_BankAccounts');
// Verify all relationships have valid source and target table IDs
const validRelationships = result.relationships.filter(
(r) => r.sourceTableId && r.targetTableId
);
expect(validRelationships.length).toBe(result.relationships.length);
// Check that table IDs are properly linked
for (const rel of result.relationships) {
const sourceTable = result.tables.find(
(t) =>
t.name === rel.sourceTable && t.schema === rel.sourceSchema
);
const targetTable = result.tables.find(
(t) =>
t.name === rel.targetTable && t.schema === rel.targetSchema
);
expect(sourceTable).toBeDefined();
expect(targetTable).toBeDefined();
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
// Test relationships within the same schema
const withinSchemaRels = result.relationships.filter(
(r) => r.sourceSchema === r.targetSchema
);
expect(withinSchemaRels.length).toBeGreaterThan(10);
// Verify specific within-schema relationship
const citiesToKingdoms = result.relationships.find(
(r) =>
r.sourceTable === 'cities' &&
r.targetTable === 'kingdoms' &&
r.sourceSchema === 'realm' &&
r.targetSchema === 'realm'
);
expect(citiesToKingdoms).toBeDefined();
console.log('Multi-schema test results:');
console.log('Total schemas:', schemas.size);
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'Cross-schema relationships:',
crossSchemaRelationships.length
);
console.log('Within-schema relationships:', withinSchemaRels.length);
});
it('should handle mixed schema notation formats', async () => {
const sql = `
-- Mix of different schema notation styles
CREATE TABLE [dbo].[table1] (
[id] INT PRIMARY KEY,
[name] NVARCHAR(50)
);
CREATE TABLE table2 (
id INT PRIMARY KEY,
table1_id INT
);
CREATE TABLE [schema1].[table3] (
[id] INT PRIMARY KEY,
[value] DECIMAL(10,2)
);
-- Different ALTER TABLE formats
ALTER TABLE [dbo].[table1] ADD CONSTRAINT [FK1]
FOREIGN KEY ([id]) REFERENCES [schema1].[table3]([id]);
ALTER TABLE table2 ADD CONSTRAINT FK2
FOREIGN KEY (table1_id) REFERENCES [dbo].[table1](id);
ALTER TABLE [schema1].[table3] ADD CONSTRAINT [FK3]
FOREIGN KEY ([id]) REFERENCES table2(id);
`;
const result = await fromSQLServer(sql);
expect(result.tables.length).toBe(3);
expect(result.relationships.length).toBe(3);
// Verify schemas are correctly assigned
const table1 = result.tables.find((t) => t.name === 'table1');
const table2 = result.tables.find((t) => t.name === 'table2');
const table3 = result.tables.find((t) => t.name === 'table3');
expect(table1?.schema).toBe('dbo');
expect(table2?.schema).toBe('dbo');
expect(table3?.schema).toBe('schema1');
// Verify all relationships are properly linked
for (const rel of result.relationships) {
expect(rel.sourceTableId).toBeTruthy();
expect(rel.targetTableId).toBeTruthy();
}
});
});

View File

@@ -0,0 +1,132 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Multiple Tables with Foreign Keys', () => {
it('should parse multiple tables with foreign keys in user format', async () => {
const sql = `
CREATE TABLE [DBO].[QuestReward](
[BOID] (VARCHAR)(32),
[HASEXTRACOL] BOOLEAN,
[REWARDCODE] [VARCHAR](128),
[REWARDFIX] BOOLEAN,
[ITSQUESTREL] [VARCHAR](32), FOREIGN KEY (itsquestrel) REFERENCES QuestRelation(BOID),
[SHOWDETAILS] BOOLEAN,
) ON [PRIMARY]
CREATE TABLE [DBO].[QuestRelation](
[ALIAS] CHAR (50),
[BOID] (VARCHAR)(32),
[ISOPTIONAL] BOOLEAN,
[ITSPARENTREL] [VARCHAR](32), FOREIGN KEY (itsparentrel) REFERENCES QuestRelation(BOID),
[ITSGUILDMETA] [VARCHAR](32), FOREIGN KEY (itsguildmeta) REFERENCES GuildMeta(BOID),
[KEYATTR] CHAR (100),
) ON [PRIMARY]
`;
const result = await fromSQLServer(sql);
// Should create both tables
expect(result.tables).toHaveLength(2);
// Check first table
const questReward = result.tables.find((t) => t.name === 'QuestReward');
expect(questReward).toBeDefined();
expect(questReward?.schema).toBe('DBO');
expect(questReward?.columns).toHaveLength(6);
// Check second table
const questRelation = result.tables.find(
(t) => t.name === 'QuestRelation'
);
expect(questRelation).toBeDefined();
expect(questRelation?.schema).toBe('DBO');
expect(questRelation?.columns).toHaveLength(6);
// Check foreign key relationships
expect(result.relationships).toHaveLength(2); // Should have 2 FKs (one self-referential in QuestRelation, one from QuestReward to QuestRelation)
// Check FK from QuestReward to QuestRelation
const fkToRelation = result.relationships.find(
(r) =>
r.sourceTable === 'QuestReward' &&
r.targetTable === 'QuestRelation'
);
expect(fkToRelation).toBeDefined();
expect(fkToRelation?.sourceColumn).toBe('itsquestrel');
expect(fkToRelation?.targetColumn).toBe('BOID');
// Check self-referential FK in QuestRelation
const selfRefFK = result.relationships.find(
(r) =>
r.sourceTable === 'QuestRelation' &&
r.targetTable === 'QuestRelation' &&
r.sourceColumn === 'itsparentrel'
);
expect(selfRefFK).toBeDefined();
expect(selfRefFK?.targetColumn).toBe('BOID');
});
it('should parse multiple tables with circular dependencies', async () => {
const sql = `
CREATE TABLE [DBO].[Dragon](
[DRAGONID] (VARCHAR)(32),
[NAME] [VARCHAR](100),
[ITSLAIRREL] [VARCHAR](32), FOREIGN KEY (itslairrel) REFERENCES DragonLair(LAIRID),
[POWER] INT,
) ON [PRIMARY]
CREATE TABLE [DBO].[DragonLair](
[LAIRID] (VARCHAR)(32),
[LOCATION] [VARCHAR](200),
[ITSGUARDIAN] [VARCHAR](32), FOREIGN KEY (itsguardian) REFERENCES Dragon(DRAGONID),
[TREASURES] INT,
) ON [PRIMARY]
`;
const result = await fromSQLServer(sql);
// Should create both tables despite circular dependency
expect(result.tables).toHaveLength(2);
const dragon = result.tables.find((t) => t.name === 'Dragon');
expect(dragon).toBeDefined();
const dragonLair = result.tables.find((t) => t.name === 'DragonLair');
expect(dragonLair).toBeDefined();
// Check foreign key relationships (may have one or both depending on parser behavior with circular deps)
expect(result.relationships.length).toBeGreaterThanOrEqual(1);
});
it('should handle exact user input format', async () => {
// Exact copy of the user's input with fantasy theme
const sql = `CREATE TABLE [DBO].[WizardDef](
[BOID] (VARCHAR)(32),
[HASEXTRACNTCOL] BOOLEAN,
[HISTORYCD] [VARCHAR](128),
[HISTORYCDFIX] BOOLEAN,
[ITSADWIZARDREL] [VARCHAR](32), FOREIGN KEY (itsadwizardrel) REFERENCES WizardRel(BOID),
[SHOWDETAILS] BOOLEAN, ) ON [PRIMARY]
CREATE TABLE [DBO].[WizardRel](
[ALIAS] CHAR (50),
[BOID] (VARCHAR)(32),
[ISOPTIONAL] BOOLEAN,
[ITSARWIZARDREL] [VARCHAR](32), FOREIGN KEY (itsarwizardrel) REFERENCES WizardRel(BOID),
[ITSARMETABO] [VARCHAR](32), FOREIGN KEY (itsarmetabo) REFERENCES MetaBO(BOID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
const result = await fromSQLServer(sql);
// This should create TWO tables, not just one
expect(result.tables).toHaveLength(2);
const wizardDef = result.tables.find((t) => t.name === 'WizardDef');
expect(wizardDef).toBeDefined();
expect(wizardDef?.columns).toHaveLength(6);
const wizardRel = result.tables.find((t) => t.name === 'WizardRel');
expect(wizardRel).toBeDefined();
expect(wizardRel?.columns).toHaveLength(6);
});
});

View File

@@ -0,0 +1,704 @@
import { describe, expect, it } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Single-Schema Database Tests', () => {
it('should parse a comprehensive fantasy-themed single-schema database with many foreign key relationships', async () => {
// This test simulates a complex single-schema database similar to real-world scenarios
// It tests the fix for parsing ALTER TABLE ADD CONSTRAINT statements without schema prefixes
const sql = `
-- =============================================
-- Enchanted Kingdom Management System
-- A comprehensive fantasy database using single schema (dbo)
-- =============================================
-- =============================================
-- Core Kingdom Tables
-- =============================================
CREATE TABLE [Kingdoms] (
[KingdomID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[KingdomName] NVARCHAR(100) NOT NULL UNIQUE,
[FoundedYear] INT NOT NULL,
[CurrentRuler] NVARCHAR(100) NOT NULL,
[TreasuryGold] DECIMAL(18, 2) DEFAULT 100000.00,
[Population] BIGINT DEFAULT 0,
[MilitaryStrength] INT DEFAULT 100
);
CREATE TABLE [Regions] (
[RegionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[RegionName] NVARCHAR(100) NOT NULL,
[KingdomID] BIGINT NOT NULL,
[Terrain] NVARCHAR(50), -- 'Mountains', 'Forest', 'Plains', 'Desert', 'Swamp'
[Population] INT DEFAULT 0,
[TaxRate] DECIMAL(5, 2) DEFAULT 10.00
);
CREATE TABLE [Cities] (
[CityID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CityName] NVARCHAR(100) NOT NULL,
[RegionID] BIGINT NOT NULL,
[Population] INT DEFAULT 1000,
[HasWalls] BIT DEFAULT 0,
[HasMarket] BIT DEFAULT 1,
[DefenseRating] INT DEFAULT 5
);
CREATE TABLE [Castles] (
[CastleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CastleName] NVARCHAR(100) NOT NULL,
[CityID] BIGINT NOT NULL,
[GarrisonSize] INT DEFAULT 50,
[TowerCount] INT DEFAULT 4,
[MoatDepth] DECIMAL(5, 2) DEFAULT 3.00
);
-- =============================================
-- Character Management Tables
-- =============================================
CREATE TABLE [CharacterClasses] (
[ClassID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ClassName] NVARCHAR(50) NOT NULL UNIQUE,
[ClassType] NVARCHAR(30), -- 'Warrior', 'Mage', 'Rogue', 'Cleric'
[BaseHealth] INT DEFAULT 100,
[BaseMana] INT DEFAULT 50,
[BaseStrength] INT DEFAULT 10,
[BaseIntelligence] INT DEFAULT 10
);
CREATE TABLE [Characters] (
[CharacterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterName] NVARCHAR(100) NOT NULL,
[ClassID] BIGINT NOT NULL,
[Level] INT DEFAULT 1,
[Experience] BIGINT DEFAULT 0,
[CurrentHealth] INT DEFAULT 100,
[CurrentMana] INT DEFAULT 50,
[HomeCityID] BIGINT NOT NULL,
[Gold] DECIMAL(10, 2) DEFAULT 100.00,
[CreatedDate] DATE NOT NULL
);
CREATE TABLE [CharacterSkills] (
[SkillID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SkillName] NVARCHAR(100) NOT NULL,
[RequiredClassID] BIGINT NULL,
[RequiredLevel] INT DEFAULT 1,
[ManaCost] INT DEFAULT 10,
[Cooldown] INT DEFAULT 0,
[Damage] INT DEFAULT 0,
[Description] NVARCHAR(MAX)
);
CREATE TABLE [CharacterSkillMapping] (
[MappingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[SkillID] BIGINT NOT NULL,
[SkillLevel] INT DEFAULT 1,
[LastUsed] DATETIME NULL
);
-- =============================================
-- Guild System Tables
-- =============================================
CREATE TABLE [GuildTypes] (
[GuildTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[Description] NVARCHAR(255)
);
CREATE TABLE [Guilds] (
[GuildID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[GuildName] NVARCHAR(100) NOT NULL UNIQUE,
[GuildTypeID] BIGINT NOT NULL,
[HeadquartersCityID] BIGINT NOT NULL,
[FoundedDate] DATE NOT NULL,
[GuildMasterID] BIGINT NULL,
[MemberCount] INT DEFAULT 0,
[GuildBank] DECIMAL(18, 2) DEFAULT 0.00,
[Reputation] INT DEFAULT 50
);
CREATE TABLE [GuildMembers] (
[MembershipID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[GuildID] BIGINT NOT NULL,
[CharacterID] BIGINT NOT NULL,
[JoinDate] DATE NOT NULL,
[Rank] NVARCHAR(50) DEFAULT 'Member',
[ContributionPoints] INT DEFAULT 0
);
CREATE TABLE [GuildQuests] (
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestName] NVARCHAR(200) NOT NULL,
[GuildID] BIGINT NOT NULL,
[RequiredLevel] INT DEFAULT 1,
[RewardGold] DECIMAL(10, 2) DEFAULT 100.00,
[RewardExperience] INT DEFAULT 100,
[QuestGiverID] BIGINT NULL,
[Status] NVARCHAR(20) DEFAULT 'Available'
);
-- =============================================
-- Item and Inventory Tables
-- =============================================
CREATE TABLE [ItemCategories] (
[CategoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CategoryName] NVARCHAR(50) NOT NULL UNIQUE,
[Description] NVARCHAR(255)
);
CREATE TABLE [Items] (
[ItemID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemName] NVARCHAR(150) NOT NULL,
[CategoryID] BIGINT NOT NULL,
[Rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
[BaseValue] DECIMAL(10, 2) DEFAULT 1.00,
[Weight] DECIMAL(5, 2) DEFAULT 1.00,
[Stackable] BIT DEFAULT 1,
[MaxStack] INT DEFAULT 99,
[RequiredLevel] INT DEFAULT 1,
[RequiredClassID] BIGINT NULL
);
CREATE TABLE [Weapons] (
[WeaponID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL UNIQUE,
[WeaponType] NVARCHAR(50), -- 'Sword', 'Axe', 'Bow', 'Staff', 'Dagger'
[MinDamage] INT DEFAULT 1,
[MaxDamage] INT DEFAULT 10,
[AttackSpeed] DECIMAL(3, 2) DEFAULT 1.00,
[Durability] INT DEFAULT 100,
[EnchantmentSlots] INT DEFAULT 0
);
CREATE TABLE [Armor] (
[ArmorID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL UNIQUE,
[ArmorType] NVARCHAR(50), -- 'Helmet', 'Chest', 'Legs', 'Boots', 'Gloves'
[DefenseValue] INT DEFAULT 1,
[MagicResistance] INT DEFAULT 0,
[Durability] INT DEFAULT 100,
[SetBonusID] BIGINT NULL
);
CREATE TABLE [CharacterInventory] (
[InventoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[IsEquipped] BIT DEFAULT 0,
[SlotPosition] INT NULL,
[AcquiredDate] DATETIME NOT NULL
);
-- =============================================
-- Magic System Tables
-- =============================================
CREATE TABLE [MagicSchools] (
[SchoolID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SchoolName] NVARCHAR(50) NOT NULL UNIQUE,
[Element] NVARCHAR(30), -- 'Fire', 'Water', 'Earth', 'Air', 'Light', 'Dark'
[Description] NVARCHAR(MAX)
);
CREATE TABLE [Spells] (
[SpellID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SpellName] NVARCHAR(100) NOT NULL,
[SchoolID] BIGINT NOT NULL,
[SpellLevel] INT DEFAULT 1,
[ManaCost] INT DEFAULT 10,
[CastTime] DECIMAL(3, 1) DEFAULT 1.0,
[Range] INT DEFAULT 10,
[AreaOfEffect] INT DEFAULT 0,
[BaseDamage] INT DEFAULT 0,
[Description] NVARCHAR(MAX)
);
CREATE TABLE [SpellBooks] (
[SpellBookID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[SpellID] BIGINT NOT NULL,
[DateLearned] DATE NOT NULL,
[MasteryLevel] INT DEFAULT 1,
[TimesUsed] INT DEFAULT 0
);
CREATE TABLE [Enchantments] (
[EnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[EnchantmentName] NVARCHAR(100) NOT NULL,
[RequiredSpellID] BIGINT NULL,
[BonusType] NVARCHAR(50), -- 'Damage', 'Defense', 'Speed', 'Magic'
[BonusValue] INT DEFAULT 1,
[Duration] INT NULL, -- NULL for permanent
[Cost] DECIMAL(10, 2) DEFAULT 100.00
);
CREATE TABLE [ItemEnchantments] (
[ItemEnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL,
[EnchantmentID] BIGINT NOT NULL,
[AppliedByCharacterID] BIGINT NOT NULL,
[AppliedDate] DATETIME NOT NULL,
[ExpiryDate] DATETIME NULL
);
-- =============================================
-- Quest and Achievement Tables
-- =============================================
CREATE TABLE [QuestLines] (
[QuestLineID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestLineName] NVARCHAR(200) NOT NULL,
[MinLevel] INT DEFAULT 1,
[MaxLevel] INT DEFAULT 100,
[TotalQuests] INT DEFAULT 1,
[FinalRewardItemID] BIGINT NULL
);
CREATE TABLE [Quests] (
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestName] NVARCHAR(200) NOT NULL,
[QuestLineID] BIGINT NULL,
[QuestGiverNPCID] BIGINT NULL,
[RequiredLevel] INT DEFAULT 1,
[RequiredQuestID] BIGINT NULL, -- Prerequisite quest
[ObjectiveType] NVARCHAR(50), -- 'Kill', 'Collect', 'Deliver', 'Explore'
[ObjectiveCount] INT DEFAULT 1,
[RewardGold] DECIMAL(10, 2) DEFAULT 10.00,
[RewardExperience] INT DEFAULT 100,
[RewardItemID] BIGINT NULL
);
CREATE TABLE [CharacterQuests] (
[CharacterQuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[QuestID] BIGINT NOT NULL,
[StartDate] DATETIME NOT NULL,
[CompletedDate] DATETIME NULL,
[CurrentProgress] INT DEFAULT 0,
[Status] NVARCHAR(20) DEFAULT 'Active' -- 'Active', 'Completed', 'Failed', 'Abandoned'
);
CREATE TABLE [Achievements] (
[AchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[AchievementName] NVARCHAR(100) NOT NULL,
[Description] NVARCHAR(500),
[Points] INT DEFAULT 10,
[Category] NVARCHAR(50),
[RequiredCount] INT DEFAULT 1,
[RewardTitle] NVARCHAR(100) NULL
);
CREATE TABLE [CharacterAchievements] (
[CharacterAchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[AchievementID] BIGINT NOT NULL,
[EarnedDate] DATETIME NOT NULL,
[Progress] INT DEFAULT 0
);
-- =============================================
-- NPC and Monster Tables
-- =============================================
CREATE TABLE [NPCTypes] (
[NPCTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[IsFriendly] BIT DEFAULT 1,
[CanTrade] BIT DEFAULT 0,
[CanGiveQuests] BIT DEFAULT 0
);
CREATE TABLE [NPCs] (
[NPCID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[NPCName] NVARCHAR(100) NOT NULL,
[NPCTypeID] BIGINT NOT NULL,
[LocationCityID] BIGINT NOT NULL,
[Health] INT DEFAULT 100,
[Level] INT DEFAULT 1,
[DialogueText] NVARCHAR(MAX),
[RespawnTime] INT DEFAULT 300 -- seconds
);
CREATE TABLE [Monsters] (
[MonsterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[MonsterName] NVARCHAR(100) NOT NULL,
[MonsterType] NVARCHAR(50), -- 'Beast', 'Undead', 'Dragon', 'Elemental', 'Demon'
[Level] INT DEFAULT 1,
[Health] INT DEFAULT 100,
[Damage] INT DEFAULT 10,
[Defense] INT DEFAULT 5,
[ExperienceReward] INT DEFAULT 50,
[GoldDrop] DECIMAL(10, 2) DEFAULT 5.00,
[SpawnRegionID] BIGINT NULL
);
CREATE TABLE [MonsterLoot] (
[LootID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[MonsterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[DropChance] DECIMAL(5, 2) DEFAULT 10.00, -- percentage
[MinQuantity] INT DEFAULT 1,
[MaxQuantity] INT DEFAULT 1
);
-- =============================================
-- Combat and PvP Tables
-- =============================================
CREATE TABLE [BattleTypes] (
[BattleTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[MinParticipants] INT DEFAULT 2,
[MaxParticipants] INT DEFAULT 2,
[AllowTeams] BIT DEFAULT 0
);
CREATE TABLE [Battles] (
[BattleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BattleTypeID] BIGINT NOT NULL,
[StartTime] DATETIME NOT NULL,
[EndTime] DATETIME NULL,
[LocationCityID] BIGINT NOT NULL,
[WinnerCharacterID] BIGINT NULL,
[TotalDamageDealt] BIGINT DEFAULT 0
);
CREATE TABLE [BattleParticipants] (
[ParticipantID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BattleID] BIGINT NOT NULL,
[CharacterID] BIGINT NOT NULL,
[TeamNumber] INT DEFAULT 0,
[DamageDealt] INT DEFAULT 0,
[DamageTaken] INT DEFAULT 0,
[HealingDone] INT DEFAULT 0,
[KillCount] INT DEFAULT 0,
[DeathCount] INT DEFAULT 0,
[FinalPlacement] INT NULL
);
-- =============================================
-- Economy Tables
-- =============================================
CREATE TABLE [Currencies] (
[CurrencyID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CurrencyName] NVARCHAR(50) NOT NULL UNIQUE,
[ExchangeRate] DECIMAL(10, 4) DEFAULT 1.0000, -- relative to gold
[IssuingKingdomID] BIGINT NOT NULL
);
CREATE TABLE [MarketListings] (
[ListingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SellerCharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[PricePerUnit] DECIMAL(10, 2) NOT NULL,
[CurrencyID] BIGINT NOT NULL,
[ListedDate] DATETIME NOT NULL,
[ExpiryDate] DATETIME NOT NULL,
[Status] NVARCHAR(20) DEFAULT 'Active'
);
CREATE TABLE [Transactions] (
[TransactionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BuyerCharacterID] BIGINT NOT NULL,
[SellerCharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[TotalPrice] DECIMAL(10, 2) NOT NULL,
[CurrencyID] BIGINT NOT NULL,
[TransactionDate] DATETIME NOT NULL
);
-- =============================================
-- Foreign Key Constraints (Without Schema Prefix)
-- Testing the fix for single-schema foreign key parsing
-- =============================================
-- Kingdom Relationships
ALTER TABLE [Regions] ADD CONSTRAINT [FK_Regions_Kingdoms]
FOREIGN KEY ([KingdomID]) REFERENCES [Kingdoms]([KingdomID]);
ALTER TABLE [Cities] ADD CONSTRAINT [FK_Cities_Regions]
FOREIGN KEY ([RegionID]) REFERENCES [Regions]([RegionID]);
ALTER TABLE [Castles] ADD CONSTRAINT [FK_Castles_Cities]
FOREIGN KEY ([CityID]) REFERENCES [Cities]([CityID]);
-- Character Relationships
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Classes]
FOREIGN KEY ([ClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Cities]
FOREIGN KEY ([HomeCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [CharacterSkills] ADD CONSTRAINT [FK_CharacterSkills_Classes]
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Skills]
FOREIGN KEY ([SkillID]) REFERENCES [CharacterSkills]([SkillID]);
-- Guild Relationships
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildTypes]
FOREIGN KEY ([GuildTypeID]) REFERENCES [GuildTypes]([GuildTypeID]);
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_Cities]
FOREIGN KEY ([HeadquartersCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildMaster]
FOREIGN KEY ([GuildMasterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Guilds]
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_Guilds]
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_QuestGiver]
FOREIGN KEY ([QuestGiverID]) REFERENCES [NPCs]([NPCID]);
-- Item Relationships
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_Categories]
FOREIGN KEY ([CategoryID]) REFERENCES [ItemCategories]([CategoryID]);
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_RequiredClass]
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [Weapons] ADD CONSTRAINT [FK_Weapons_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Armor] ADD CONSTRAINT [FK_Armor_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
-- Magic Relationships
ALTER TABLE [Spells] ADD CONSTRAINT [FK_Spells_Schools]
FOREIGN KEY ([SchoolID]) REFERENCES [MagicSchools]([SchoolID]);
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Spells]
FOREIGN KEY ([SpellID]) REFERENCES [Spells]([SpellID]);
ALTER TABLE [Enchantments] ADD CONSTRAINT [FK_Enchantments_Spells]
FOREIGN KEY ([RequiredSpellID]) REFERENCES [Spells]([SpellID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Enchantments]
FOREIGN KEY ([EnchantmentID]) REFERENCES [Enchantments]([EnchantmentID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Characters]
FOREIGN KEY ([AppliedByCharacterID]) REFERENCES [Characters]([CharacterID]);
-- Quest Relationships
ALTER TABLE [QuestLines] ADD CONSTRAINT [FK_QuestLines_FinalReward]
FOREIGN KEY ([FinalRewardItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestLines]
FOREIGN KEY ([QuestLineID]) REFERENCES [QuestLines]([QuestLineID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestGiver]
FOREIGN KEY ([QuestGiverNPCID]) REFERENCES [NPCs]([NPCID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_Prerequisites]
FOREIGN KEY ([RequiredQuestID]) REFERENCES [Quests]([QuestID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_RewardItem]
FOREIGN KEY ([RewardItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Quests]
FOREIGN KEY ([QuestID]) REFERENCES [Quests]([QuestID]);
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Achievements]
FOREIGN KEY ([AchievementID]) REFERENCES [Achievements]([AchievementID]);
-- NPC and Monster Relationships
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Types]
FOREIGN KEY ([NPCTypeID]) REFERENCES [NPCTypes]([NPCTypeID]);
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Cities]
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Monsters] ADD CONSTRAINT [FK_Monsters_Regions]
FOREIGN KEY ([SpawnRegionID]) REFERENCES [Regions]([RegionID]);
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Monsters]
FOREIGN KEY ([MonsterID]) REFERENCES [Monsters]([MonsterID]);
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
-- Battle Relationships
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Types]
FOREIGN KEY ([BattleTypeID]) REFERENCES [BattleTypes]([BattleTypeID]);
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Cities]
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Winner]
FOREIGN KEY ([WinnerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
FOREIGN KEY ([BattleID]) REFERENCES [Battles]([BattleID]);
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
-- Economy Relationships
ALTER TABLE [Currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
FOREIGN KEY ([IssuingKingdomID]) REFERENCES [Kingdoms]([KingdomID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Seller]
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Currency]
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Buyer]
FOREIGN KEY ([BuyerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Seller]
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
`;
const result = await fromSQLServer(sql);
// Debug: log table names to see what's parsed
console.log('Tables found:', result.tables.length);
console.log(
'Table names:',
result.tables.map((t) => t.name)
);
// Verify correct number of tables
expect(result.tables.length).toBe(37); // Actually 37 tables after counting
// Verify all tables use default 'dbo' schema
const schemas = new Set(result.tables.map((t) => t.schema));
expect(schemas.size).toBe(1);
expect(schemas.has('dbo')).toBe(true);
// Verify correct number of relationships
console.log('Relationships found:', result.relationships.length);
expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed
// Verify all relationships have valid source and target table IDs
const validRelationships = result.relationships.filter(
(r) => r.sourceTableId && r.targetTableId
);
expect(validRelationships.length).toBe(result.relationships.length);
// Check specific table names exist
const tableNames = result.tables.map((t) => t.name);
expect(tableNames).toContain('Kingdoms');
expect(tableNames).toContain('Characters');
expect(tableNames).toContain('Guilds');
expect(tableNames).toContain('Items');
expect(tableNames).toContain('Spells');
expect(tableNames).toContain('Quests');
expect(tableNames).toContain('Battles');
expect(tableNames).toContain('Monsters');
// Verify some specific relationships exist and are properly linked
const characterToClass = result.relationships.find(
(r) => r.name === 'FK_Characters_Classes'
);
expect(characterToClass).toBeDefined();
expect(characterToClass?.sourceTable).toBe('Characters');
expect(characterToClass?.targetTable).toBe('CharacterClasses');
expect(characterToClass?.sourceColumn).toBe('ClassID');
expect(characterToClass?.targetColumn).toBe('ClassID');
const guildsToCity = result.relationships.find(
(r) => r.name === 'FK_Guilds_Cities'
);
expect(guildsToCity).toBeDefined();
expect(guildsToCity?.sourceTable).toBe('Guilds');
expect(guildsToCity?.targetTable).toBe('Cities');
const inventoryToItems = result.relationships.find(
(r) => r.name === 'FK_Inventory_Items'
);
expect(inventoryToItems).toBeDefined();
expect(inventoryToItems?.sourceTable).toBe('CharacterInventory');
expect(inventoryToItems?.targetTable).toBe('Items');
// Check self-referencing relationship
const questPrerequisite = result.relationships.find(
(r) => r.name === 'FK_Quests_Prerequisites'
);
expect(questPrerequisite).toBeDefined();
expect(questPrerequisite?.sourceTable).toBe('Quests');
expect(questPrerequisite?.targetTable).toBe('Quests');
// Verify table IDs are correctly linked in relationships
for (const rel of result.relationships) {
const sourceTable = result.tables.find(
(t) =>
t.name === rel.sourceTable && t.schema === rel.sourceSchema
);
const targetTable = result.tables.find(
(t) =>
t.name === rel.targetTable && t.schema === rel.targetSchema
);
expect(sourceTable).toBeDefined();
expect(targetTable).toBeDefined();
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
console.log('Single-schema test results:');
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'All relationships properly linked:',
validRelationships.length === result.relationships.length
);
// Sample of relationship names for verification
const sampleRelationships = result.relationships
.slice(0, 5)
.map((r) => ({
name: r.name,
source: `${r.sourceTable}.${r.sourceColumn}`,
target: `${r.targetTable}.${r.targetColumn}`,
}));
console.log('Sample relationships:', sampleRelationships);
});
});

View File

@@ -0,0 +1,93 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server FK Verification', () => {
it('should correctly parse FKs from complex fantasy SQL', async () => {
const sql = `CREATE TABLE [DBO].[SpellDefinition](
[SPELLID] (VARCHAR)(32),
[HASVERBALCOMP] BOOLEAN,
[INCANTATION] [VARCHAR](128),
[INCANTATIONFIX] BOOLEAN,
[ITSCOMPONENTREL] [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),
[SHOWVISUALS] BOOLEAN, ) ON [PRIMARY]
CREATE TABLE [DBO].[SpellComponent](
[ALIAS] CHAR (50),
[SPELLID] (VARCHAR)(32),
[ISOPTIONAL] BOOLEAN,
[ITSPARENTCOMP] [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),
[ITSSCHOOLMETA] [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),
[KEYATTR] CHAR (100), ) ON [PRIMARY]`;
const result = await fromSQLServer(sql);
// Verify tables
expect(result.tables).toHaveLength(2);
expect(result.tables.map((t) => t.name).sort()).toEqual([
'SpellComponent',
'SpellDefinition',
]);
// Verify that FKs were found (even if MagicSchool doesn't exist)
// The parsing should find 3 FKs initially, but linkRelationships will filter out the one to MagicSchool
expect(result.relationships.length).toBeGreaterThanOrEqual(2);
// Verify specific FKs that should exist
const fk1 = result.relationships.find(
(r) =>
r.sourceTable === 'SpellDefinition' &&
r.sourceColumn.toLowerCase() === 'itscomponentrel' &&
r.targetTable === 'SpellComponent'
);
expect(fk1).toBeDefined();
expect(fk1?.targetColumn).toBe('SPELLID');
expect(fk1?.sourceTableId).toBeTruthy();
expect(fk1?.targetTableId).toBeTruthy();
const fk2 = result.relationships.find(
(r) =>
r.sourceTable === 'SpellComponent' &&
r.sourceColumn.toLowerCase() === 'itsparentcomp' &&
r.targetTable === 'SpellComponent'
);
expect(fk2).toBeDefined();
expect(fk2?.targetColumn).toBe('SPELLID');
expect(fk2?.sourceTableId).toBeTruthy();
expect(fk2?.targetTableId).toBeTruthy();
// Log for debugging
console.log('\n=== FK Verification Results ===');
console.log(
'Tables:',
result.tables.map((t) => `${t.schema}.${t.name}`)
);
console.log('Total FKs found:', result.relationships.length);
result.relationships.forEach((r, i) => {
console.log(
`FK ${i + 1}: ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
);
console.log(` IDs: ${r.sourceTableId} -> ${r.targetTableId}`);
});
});
it('should parse inline FOREIGN KEY syntax correctly', async () => {
// Simplified test with just one FK to ensure parsing works
const sql = `CREATE TABLE [DBO].[WizardTower](
[TOWERID] INT,
[MASTERKEY] [VARCHAR](32), FOREIGN KEY (MASTERKEY) REFERENCES ArcaneGuild(GUILDID),
[NAME] VARCHAR(100)
) ON [PRIMARY]
CREATE TABLE [DBO].[ArcaneGuild](
[GUILDID] [VARCHAR](32),
[GUILDNAME] VARCHAR(100)
) ON [PRIMARY]`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceColumn).toBe('MASTERKEY');
expect(result.relationships[0].targetColumn).toBe('GUILDID');
});
});

View File

@@ -162,15 +162,36 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
if (match) {
const [
,
sourceSchema = 'dbo',
sourceTable,
sourceSchemaOrTable,
sourceTableIfSchema,
constraintName,
sourceColumn,
targetSchema = 'dbo',
targetTable,
targetSchemaOrTable,
targetTableIfSchema,
targetColumn,
] = match;
// Handle both schema.table and just table formats
let sourceSchema = 'dbo';
let sourceTable = '';
let targetSchema = 'dbo';
let targetTable = '';
// If second group is empty, first group is the table name
if (!sourceTableIfSchema) {
sourceTable = sourceSchemaOrTable;
} else {
sourceSchema = sourceSchemaOrTable;
sourceTable = sourceTableIfSchema;
}
if (!targetTableIfSchema) {
targetTable = targetSchemaOrTable;
} else {
targetSchema = targetSchemaOrTable;
targetTable = targetTableIfSchema;
}
fkData.push({
name: constraintName,
sourceTable: sourceTable,
@@ -321,6 +342,35 @@ function parseCreateTableManually(
// Process each part (column or constraint)
for (const part of parts) {
// Handle standalone FOREIGN KEY definitions (without CONSTRAINT keyword)
// Format: FOREIGN KEY (column) REFERENCES Table(column)
if (part.match(/^\s*FOREIGN\s+KEY/i)) {
const fkMatch = part.match(
/FOREIGN\s+KEY\s*\(([^)]+)\)\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i
);
if (fkMatch) {
const [
,
sourceCol,
targetSchema = 'dbo',
targetTable,
targetCol,
] = fkMatch;
relationships.push({
name: `FK_${tableName}_${sourceCol.trim().replace(/\[|\]/g, '')}`,
sourceTable: tableName,
sourceSchema: schema,
sourceColumn: sourceCol.trim().replace(/\[|\]/g, ''),
targetTable: targetTable || targetSchema,
targetSchema: targetTable ? targetSchema : 'dbo',
targetColumn: targetCol.trim().replace(/\[|\]/g, ''),
sourceTableId: tableId,
targetTableId: '', // Will be filled later
});
}
continue;
}
// Handle constraint definitions
if (part.match(/^\s*CONSTRAINT/i)) {
// Parse constraints
@@ -414,6 +464,13 @@ function parseCreateTableManually(
columnMatch = part.match(/^\s*(\w+)\s+(\w+)\s+([\d,\s]+)\s+(.*)$/i);
}
// Handle unusual format: [COLUMN_NAME] (VARCHAR)(32)
if (!columnMatch) {
columnMatch = part.match(
/^\s*\[?(\w+)\]?\s+\((\w+)\)\s*\(([\d,\s]+|max)\)(.*)$/i
);
}
if (columnMatch) {
const [, colName, baseType, typeArgs, rest] = columnMatch;
@@ -425,7 +482,37 @@ function parseCreateTableManually(
const inlineFkMatch = rest.match(
/FOREIGN\s+KEY\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i
);
if (inlineFkMatch) {
// Also check if there's a FOREIGN KEY after a comma with column name
// Format: , FOREIGN KEY (columnname) REFERENCES Table(column)
if (!inlineFkMatch && rest.includes('FOREIGN KEY')) {
const fkWithColumnMatch = rest.match(
/,\s*FOREIGN\s+KEY\s*\((\w+)\)\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i
);
if (fkWithColumnMatch) {
const [, srcCol, targetSchema, targetTable, targetCol] =
fkWithColumnMatch;
// Only process if srcCol matches current colName (case-insensitive)
if (srcCol.toLowerCase() === colName.toLowerCase()) {
// Create FK relationship
relationships.push({
name: `FK_${tableName}_${colName}`,
sourceTable: tableName,
sourceSchema: schema,
sourceColumn: colName,
targetTable: targetTable || targetSchema,
targetSchema: targetTable
? targetSchema || 'dbo'
: 'dbo',
targetColumn: targetCol
.trim()
.replace(/\[|\]/g, ''),
sourceTableId: tableId,
targetTableId: '', // Will be filled later
});
}
}
} else if (inlineFkMatch) {
const [, targetSchema = 'dbo', targetTable, targetCol] =
inlineFkMatch;
relationships.push({
@@ -515,10 +602,36 @@ export async function fromSQLServer(
try {
// First, handle ALTER TABLE statements for foreign keys
// Split by GO or semicolon for SQL Server
const statements = sqlContent
let statements = sqlContent
.split(/(?:GO\s*$|;\s*$)/im)
.filter((stmt) => stmt.trim().length > 0);
// Additional splitting for CREATE TABLE statements that might not be separated by semicolons
// If we have a statement with multiple CREATE TABLE, split them
const expandedStatements: string[] = [];
for (const stmt of statements) {
// Check if this statement contains multiple CREATE TABLE statements
if ((stmt.match(/CREATE\s+TABLE/gi) || []).length > 1) {
// Split by ") ON [PRIMARY]" followed by CREATE TABLE
const parts = stmt.split(
/\)\s*ON\s*\[PRIMARY\]\s*(?=CREATE\s+TABLE)/gi
);
for (let i = 0; i < parts.length; i++) {
let part = parts[i].trim();
// Re-add ") ON [PRIMARY]" to all parts except the last (which should already have it)
if (i < parts.length - 1 && part.length > 0) {
part += ') ON [PRIMARY]';
}
if (part.trim().length > 0) {
expandedStatements.push(part);
}
}
} else {
expandedStatements.push(stmt);
}
}
statements = expandedStatements;
const alterTableStatements = statements.filter(
(stmt) =>
stmt.trim().toUpperCase().includes('ALTER TABLE') &&

View File

@@ -226,6 +226,16 @@ const updateTables = ({
const targetKey = createObjectKeyFromTable(targetTable);
let sourceTable = sourceTablesByKey.get(targetKey);
// If no match and target has a schema, try without schema
if (!sourceTable && targetTable.schema) {
const noSchemaKey = createObjectKeyFromTable({
...targetTable,
schema: undefined,
});
sourceTable = sourceTablesByKey.get(noSchemaKey);
}
// If still no match, try with default schema
if (!sourceTable && defaultDatabaseSchema) {
if (!targetTable.schema) {
// If target table has no schema, try matching with default schema
@@ -235,12 +245,7 @@ const updateTables = ({
});
sourceTable = sourceTablesByKey.get(defaultKey);
} else if (targetTable.schema === defaultDatabaseSchema) {
// If target table's schema matches default, try matching without schema
const noSchemaKey = createObjectKeyFromTable({
...targetTable,
schema: undefined,
});
sourceTable = sourceTablesByKey.get(noSchemaKey);
// Already tried without schema above
}
}

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,8 @@
Table "public"."guy_table" {
"id" integer [pk, not null]
"created_at" timestamp [not null]
"column3" text
"arrayfield" text[]
"field_5" "character varying"
"field_6" "character varying(100)"
}

View File

@@ -0,0 +1 @@
{"id":"mqqwkkod7trl","name":"guy-db","databaseType":"postgresql","createdAt":"2025-09-10T18:45:32.817Z","updatedAt":"2025-09-10T19:15:21.682Z","tables":[{"id":"g2hv9mlo3qbyjnxdc44j1zxl2","name":"guy_table","schema":"public","x":100,"y":300,"fields":[{"id":"qdqgzmtxsi84ujfuktsvjuop8","name":"id","type":{"id":"integer","name":"integer"},"primaryKey":true,"unique":true,"nullable":false,"createdAt":1757529932816},{"id":"wsys99f86679ch6fbjryw0egr","name":"created_at","type":{"id":"timestamp_without_time_zone","name":"timestamp without time zone"},"primaryKey":false,"unique":false,"nullable":false,"createdAt":1757529932816},{"id":"ro39cba7sd290k90qjgzib8pi","name":"column3","type":{"id":"text","name":"text"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1757529932816},{"id":"6cntbu2orwk7kxlg0rcduqgbo","name":"arrayfield","type":{"id":"array","name":"array"},"primaryKey":false,"unique":false,"nullable":true,"createdAt":1757529932816},{"id":"7cz0ybdoov2m3wbgm9tlzatz0","name":"field_5","type":{"id":"character_varying","name":"character varying"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1757531685981},{"id":"zzwlyvqzz93oh0vv8f8qob103","name":"field_6","type":{"id":"character_varying","name":"character varying"},"unique":false,"nullable":true,"primaryKey":false,"createdAt":1757531713961,"characterMaximumLength":"100"}],"indexes":[{"id":"r0w71lnbnje2j9cz1t9j64rya","name":"guy_table_pkey","unique":true,"fieldIds":["qdqgzmtxsi84ujfuktsvjuop8"],"createdAt":1757529932816,"isPrimaryKey":true}],"color":"#8eb7ff","isView":false,"isMaterializedView":false,"createdAt":1757529932816,"diagramId":"mqqwkkod7trl"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}

View File

@@ -0,0 +1,114 @@
import { describe, it, expect } from 'vitest';
import { generateDBMLFromDiagram } from '../dbml-export';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import { generateId } from '@/lib/utils';
describe('Composite Primary Key Name Export', () => {
it('should export composite primary key with name in DBML', () => {
const diagram: Diagram = {
id: generateId(),
name: 'Test',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
tables: [
{
id: generateId(),
name: 'users_master_table',
schema: 'landlord',
x: 0,
y: 0,
color: '#FFF',
isView: false,
createdAt: Date.now(),
fields: [
{
id: generateId(),
name: 'master_user_id',
type: { id: 'bigint', name: 'bigint' },
nullable: false,
primaryKey: true,
unique: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'tenant_id',
type: { id: 'bigint', name: 'bigint' },
nullable: false,
primaryKey: true,
unique: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'tenant_user_id',
type: { id: 'bigint', name: 'bigint' },
nullable: false,
primaryKey: true,
unique: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'enabled',
type: { id: 'boolean', name: 'boolean' },
nullable: true,
primaryKey: false,
unique: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: generateId(),
name: 'users_master_table_index_1',
unique: true,
fieldIds: ['dummy1', 'dummy2'], // Will be replaced
createdAt: Date.now(),
},
],
},
],
relationships: [],
};
// Fix field IDs in the index and add PK index
const table = diagram.tables![0];
const masterUserIdField = table.fields.find(
(f) => f.name === 'master_user_id'
);
const tenantIdField = table.fields.find((f) => f.name === 'tenant_id');
const tenantUserIdField = table.fields.find(
(f) => f.name === 'tenant_user_id'
);
table.indexes[0].fieldIds = [tenantIdField!.id, tenantUserIdField!.id];
// Add the PK index with name
table.indexes.push({
id: generateId(),
name: 'moshe',
unique: true,
isPrimaryKey: true,
fieldIds: [
masterUserIdField!.id,
tenantIdField!.id,
tenantUserIdField!.id,
],
createdAt: Date.now(),
});
const result = generateDBMLFromDiagram(diagram);
// Check that the DBML contains the composite PK with name
expect(result.standardDbml).toContain(
'(master_user_id, tenant_id, tenant_user_id) [pk, name: "moshe"]'
);
// Check that the unique index is also present
expect(result.standardDbml).toContain(
'(tenant_id, tenant_user_id) [unique, name: "users_master_table_index_1"]'
);
});
});

View File

@@ -1383,12 +1383,9 @@ Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
const result = generateDBMLFromDiagram(diagram);
// Check that the inline DBML has proper indentation
// Note: indexes on primary key fields should be filtered out
expect(result.inlineDbml).toContain(`Table "table_1" {
"id" bigint [pk, not null]
Indexes {
id [name: "index_1"]
}
}`);
expect(result.inlineDbml).toContain(`Table "table_2" {

View File

@@ -0,0 +1,438 @@
import { describe, it, expect } from 'vitest';
import { generateDBMLFromDiagram } from '../dbml-export';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
describe('DBML Export - Fix Multiline Table Names', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
const testId = () => `test-id-${++idCounter}`;
const testTime = Date.now();
// Helper to create a field
const createField = (overrides: Partial<DBField>): DBField =>
({
id: testId(),
name: 'field',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
createdAt: testTime,
...overrides,
}) as DBField;
// Helper to create a table
const createTable = (overrides: Partial<DBTable>): DBTable =>
({
id: testId(),
name: 'table',
fields: [],
indexes: [],
createdAt: testTime,
x: 0,
y: 0,
width: 200,
...overrides,
}) as DBTable;
// Helper to create a diagram
const createDiagram = (overrides: Partial<Diagram>): Diagram =>
({
id: testId(),
name: 'diagram',
databaseType: DatabaseType.POSTGRESQL,
tables: [],
relationships: [],
createdAt: testTime,
updatedAt: testTime,
...overrides,
}) as Diagram;
describe('DBML Generation with Special Characters', () => {
it('should handle table names with special characters', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'user-profiles',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
createField({
name: 'user-name',
type: { id: 'varchar', name: 'varchar' },
nullable: true,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with special characters
expect(result.standardDbml).toContain('Table "user-profiles"');
// Field names with special characters should also be quoted
expect(result.standardDbml).toContain('"user-name"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle schema-qualified table names', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'my-schema',
name: 'my-table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote schema and table names
expect(result.standardDbml).toContain(
'Table "my-schema"."my-table"'
);
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle table names with spaces', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'user profiles',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with spaces
expect(result.standardDbml).toContain('Table "user profiles"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle schema names with spaces', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'my schema',
name: 'my_table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote schema with spaces
expect(result.standardDbml).toContain(
'Table "my schema"."my_table"'
);
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle table names with dots', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'app.config',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with dots
expect(result.standardDbml).toContain('Table "app.config"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should not have line breaks in table declarations', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'very-long-schema-name-with-dashes',
name: 'very-long-table-name-with-special-characters',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Table declaration should be on a single line
const tableDeclarations =
result.standardDbml.match(/Table\s+[^{]+\{/g) || [];
tableDeclarations.forEach((decl) => {
// Should not contain newlines before the opening brace
expect(decl).not.toContain('\n');
});
// The full qualified name should be on one line
expect(result.standardDbml).toMatch(
/Table\s+"very-long-schema-name-with-dashes"\."very-long-table-name-with-special-characters"\s*\{/
);
});
});
describe('Multiple tables and relationships', () => {
it('should handle multiple tables with special characters', () => {
const parentTableId = testId();
const childTableId = testId();
const parentIdField = testId();
const childParentIdField = testId();
const diagram = createDiagram({
tables: [
createTable({
id: parentTableId,
schema: 'auth-schema',
name: 'user-accounts',
fields: [
createField({
id: parentIdField,
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
}),
],
}),
createTable({
id: childTableId,
schema: 'app-schema',
name: 'user-profiles',
fields: [
createField({
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
}),
createField({
id: childParentIdField,
name: 'account-id',
type: { id: 'uuid', name: 'uuid' },
nullable: false,
}),
],
}),
],
relationships: [
{
id: testId(),
name: 'fk_profiles_accounts',
sourceTableId: childTableId,
targetTableId: parentTableId,
sourceFieldId: childParentIdField,
targetFieldId: parentIdField,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const result = generateDBMLFromDiagram(diagram);
// Should contain both tables properly quoted
expect(result.standardDbml).toContain(
'Table "auth-schema"."user-accounts"'
);
expect(result.standardDbml).toContain(
'Table "app-schema"."user-profiles"'
);
// Should contain the relationship reference
expect(result.standardDbml).toContain('Ref');
// Should contain field names with dashes properly quoted
expect(result.standardDbml).toContain('"account-id"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should work correctly with inline DBML format', () => {
const parentTableId = testId();
const childTableId = testId();
const parentIdField = testId();
const childParentIdField = testId();
const diagram = createDiagram({
tables: [
createTable({
id: parentTableId,
name: 'parent-table',
fields: [
createField({
id: parentIdField,
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
createTable({
id: childTableId,
name: 'child-table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
createField({
id: childParentIdField,
name: 'parent-id',
type: { id: 'integer', name: 'integer' },
nullable: false,
}),
],
}),
],
relationships: [
{
id: testId(),
name: 'fk_child_parent',
sourceTableId: childTableId,
targetTableId: parentTableId,
sourceFieldId: childParentIdField,
targetFieldId: parentIdField,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const result = generateDBMLFromDiagram(diagram);
// Both standard and inline should be generated
expect(result.standardDbml).toBeDefined();
expect(result.inlineDbml).toBeDefined();
// Inline version should contain inline references
expect(result.inlineDbml).toContain('ref:');
// Both should properly quote table names
expect(result.standardDbml).toContain('Table "parent-table"');
expect(result.inlineDbml).toContain('Table "parent-table"');
expect(result.standardDbml).toContain('Table "child-table"');
expect(result.inlineDbml).toContain('Table "child-table"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
});
describe('Edge cases', () => {
it('should handle empty table names gracefully', () => {
const diagram = createDiagram({
tables: [
createTable({
name: '',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should not throw error
expect(result.error).toBeUndefined();
});
it('should handle Unicode characters in names', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'użytkownik',
fields: [
createField({
name: 'identyfikator',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should handle Unicode characters
expect(result.standardDbml).toContain('Table "użytkownik"');
expect(result.standardDbml).toContain('"identyfikator"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
});
});

View File

@@ -93,17 +93,38 @@ ALTER TABLE wizard_spellbooks ADD CONSTRAINT fk_mentor FOREIGN KEY (owner_id) RE
);
});
it('should comment out self-referential foreign keys', () => {
const sql = `ALTER TABLE quest_prerequisites ADD CONSTRAINT fk_quest_prereq FOREIGN KEY (quest_id) REFERENCES quest_prerequisites (quest_id);
it('should preserve valid self-referential foreign keys but filter invalid ones', () => {
const sql = `-- Valid self-references (different fields)
ALTER TABLE spell_components ADD CONSTRAINT fk_component_substitute FOREIGN KEY (substitute_id) REFERENCES spell_components (id);
ALTER TABLE guild_hierarchy ADD CONSTRAINT fk_parent_guild FOREIGN KEY (parent_guild_id) REFERENCES guild_hierarchy (guild_id);`;
ALTER TABLE guild_hierarchy ADD CONSTRAINT fk_parent_guild FOREIGN KEY (parent_guild_id) REFERENCES guild_hierarchy (guild_id);
ALTER TABLE "finance"."general_ledger" ADD CONSTRAINT fk_reversal FOREIGN KEY("reversal_id") REFERENCES "finance"."general_ledger"("ledger_id");
-- Invalid self-references (same field referencing itself)
ALTER TABLE quest_prerequisites ADD CONSTRAINT fk_quest_prereq FOREIGN KEY (quest_id) REFERENCES quest_prerequisites (quest_id);
ALTER TABLE "finance"."general_ledger" ADD CONSTRAINT fk_ledger_self FOREIGN KEY("ledger_id") REFERENCES "finance"."general_ledger"("ledger_id");
ALTER TABLE wizards ADD CONSTRAINT fk_wizard_self FOREIGN KEY (id) REFERENCES wizards (id);`;
const sanitized = sanitizeSQLforDBML(sql);
// Self-referential constraints should be commented out
// Valid self-referential constraints should be preserved
expect(sanitized).toContain(
'ALTER TABLE spell_components ADD CONSTRAINT'
);
expect(sanitized).toContain(
'ALTER TABLE guild_hierarchy ADD CONSTRAINT'
);
expect(sanitized).toMatch(
/ALTER TABLE "finance"\."general_ledger".*fk_reversal.*FOREIGN KEY\("reversal_id"\)/
);
// Invalid self-referential constraints (same field to itself) should be commented out
expect(sanitized).toContain('-- ALTER TABLE quest_prerequisites');
expect(sanitized).toContain('-- ALTER TABLE spell_components');
expect(sanitized).toContain('-- ALTER TABLE guild_hierarchy');
expect(sanitized).toMatch(
/-- ALTER TABLE "finance"\."general_ledger".*fk_ledger_self.*FOREIGN KEY\("ledger_id"\).*REFERENCES.*\("ledger_id"\)/
);
expect(sanitized).toContain(
'-- ALTER TABLE wizards ADD CONSTRAINT fk_wizard_self'
);
});
it('should not comment out normal foreign keys', () => {
@@ -246,7 +267,11 @@ ALTER TABLE spell_component_links ADD CONSTRAINT fk_creator FOREIGN KEY (link_id
expect(sanitized).toContain("DEFAULT 'F'");
expect(sanitized).toContain("DEFAULT 'NOW'"); // NOW is quoted as a single word
expect(sanitized).toContain('(matrix_pattern)'); // Deduplicated
// Valid self-referencing relationships (different fields) are preserved
expect(sanitized).toContain(
'ALTER TABLE spell_matrices ADD CONSTRAINT fk_self_ref'
);
expect(sanitized).not.toContain(
'-- ALTER TABLE spell_matrices ADD CONSTRAINT fk_self_ref'
);
expect(sanitized).toContain(

View File

@@ -0,0 +1,172 @@
import { describe, it, expect } from 'vitest';
import { generateDBMLFromDiagram } from '../dbml-export';
import { importDBMLToDiagram } from '../../dbml-import/dbml-import';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Self-Referencing Relationships', () => {
it('should preserve self-referencing relationships in DBML export', async () => {
// Create a DBML with self-referencing relationship (general_ledger example)
const inputDBML = `
Table "finance"."general_ledger" {
"ledger_id" bigint [pk]
"account_name" varchar(100)
"amount" decimal(10,2)
"reversal_id" bigint [ref: > "finance"."general_ledger"."ledger_id"]
"created_at" timestamp
}
`;
// Import the DBML
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify the relationship was imported
expect(diagram.relationships).toBeDefined();
expect(diagram.relationships?.length).toBe(1);
// Verify it's a self-referencing relationship
const relationship = diagram.relationships![0];
expect(relationship.sourceTableId).toBe(relationship.targetTableId);
// Export back to DBML
const exportResult = generateDBMLFromDiagram(diagram);
// Check inline format
expect(exportResult.inlineDbml).toContain('reversal_id');
// The DBML parser correctly interprets FK as: target < source
expect(exportResult.inlineDbml).toMatch(
/ref:\s*<\s*"finance"\."general_ledger"\."ledger_id"/
);
// Check standard format
expect(exportResult.standardDbml).toContain('Ref ');
expect(exportResult.standardDbml).toMatch(
/"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
);
console.log(
'✅ Self-referencing relationship preserved in DBML export'
);
});
it('should handle self-referencing relationships in employee hierarchy', async () => {
// Create an employee table with manager relationship
const inputDBML = `
Table "employees" {
"id" int [pk]
"name" varchar(100)
"manager_id" int [ref: > "employees"."id"]
"department" varchar(50)
}
`;
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.MYSQL,
});
// Verify the relationship
expect(diagram.relationships?.length).toBe(1);
const rel = diagram.relationships![0];
expect(rel.sourceTableId).toBe(rel.targetTableId);
// Export and verify
const exportResult = generateDBMLFromDiagram(diagram);
// Check that the self-reference is preserved
expect(exportResult.inlineDbml).toContain('manager_id');
// The DBML parser correctly interprets FK as: target < source
expect(exportResult.inlineDbml).toMatch(/ref:\s*<\s*"employees"\."id"/);
});
it('should handle multiple self-referencing relationships', async () => {
// Create a category table with parent-child relationships
const inputDBML = `
Table "categories" {
"id" int [pk]
"name" varchar(100)
"parent_id" int [ref: > "categories"."id"]
"related_id" int [ref: > "categories"."id"]
"description" text
}
`;
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Should have 2 self-referencing relationships
expect(diagram.relationships?.length).toBe(2);
// Both should be self-referencing
diagram.relationships?.forEach((rel) => {
expect(rel.sourceTableId).toBe(rel.targetTableId);
});
// Export and verify both relationships are preserved
const exportResult = generateDBMLFromDiagram(diagram);
expect(exportResult.inlineDbml).toContain('parent_id');
expect(exportResult.inlineDbml).toContain('related_id');
// Count the number of ref: statements
// The DBML parser correctly interprets FK as: target < source
const refMatches = exportResult.inlineDbml.match(/ref:\s*</g);
expect(refMatches?.length).toBe(2);
});
it('should handle self-referencing with schemas', async () => {
// Test with explicit schema names
const inputDBML = `
Table "hr"."staff" {
"staff_id" int [pk]
"name" varchar(100)
"supervisor_id" int [ref: > "hr"."staff"."staff_id"]
"level" int
}
`;
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.relationships?.length).toBe(1);
const exportResult = generateDBMLFromDiagram(diagram);
// Should preserve the schema in the reference
// The DBML parser correctly interprets FK as: target < source
expect(exportResult.inlineDbml).toMatch(
/ref:\s*<\s*"hr"\."staff"\."staff_id"/
);
});
it('should handle circular references in graph structures', async () => {
// Create a node table for graph structures
const inputDBML = `
Table "graph_nodes" {
"node_id" bigint [pk]
"value" varchar(100)
"next_node_id" bigint [ref: > "graph_nodes"."node_id"]
"prev_node_id" bigint [ref: > "graph_nodes"."node_id"]
}
`;
const diagram = await importDBMLToDiagram(inputDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Should have 2 self-referencing relationships
expect(diagram.relationships?.length).toBe(2);
const exportResult = generateDBMLFromDiagram(diagram);
// Both references should be preserved
expect(exportResult.inlineDbml).toContain('next_node_id');
expect(exportResult.inlineDbml).toContain('prev_node_id');
// Verify no lines are commented out
expect(exportResult.standardDbml).not.toContain('-- ALTER TABLE');
expect(exportResult.inlineDbml).not.toContain('-- ALTER TABLE');
});
});

View File

@@ -0,0 +1,67 @@
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);
});
it('should handle case 3 diagram', { timeout: 30000 }, async () => {
// Read the JSON file
const jsonPath = path.join(__dirname, 'cases', '3.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', '3.dbml');
const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
// Compare the generated DBML with the expected DBML
expect(generatedDBML).toBe(expectedDBML);
});
});

View File

@@ -1,5 +1,5 @@
import { importer } from '@dbml/core';
import { exportBaseSQL } from '@/lib/data/export-metadata/export-sql-script';
import { exportBaseSQL } from '@/lib/data/sql-export/export-sql-script';
import type { Diagram } from '@/lib/domain/diagram';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
@@ -155,14 +155,25 @@ export const sanitizeSQLforDBML = (sql: string): string => {
}
);
// Comment out self-referencing foreign keys to prevent "Two endpoints are the same" error
// Example: ALTER TABLE public.class ADD CONSTRAINT ... FOREIGN KEY (class_id) REFERENCES public.class (class_id);
// Comment out invalid self-referencing foreign keys where the same field references itself
// Example: ALTER TABLE table ADD CONSTRAINT ... FOREIGN KEY (field_a) REFERENCES table (field_a);
// But keep valid self-references like: FOREIGN KEY (field_a) REFERENCES table (field_b);
const lines = sanitized.split('\n');
const processedLines = lines.map((line) => {
// Match pattern: ALTER TABLE [schema.]table ADD CONSTRAINT ... FOREIGN KEY(field) REFERENCES [schema.]table(field)
// Capture the table name, source field, and target field
const selfRefFKPattern =
/ALTER\s+TABLE\s+(?:\S+\.)?(\S+)\s+ADD\s+CONSTRAINT\s+\S+\s+FOREIGN\s+KEY\s*\([^)]+\)\s+REFERENCES\s+(?:\S+\.)?\1\s*\([^)]+\)\s*;/i;
if (selfRefFKPattern.test(line)) {
return `-- ${line}`; // Comment out the line
/ALTER\s+TABLE\s+(?:["[]?(\S+?)[\]"]?\.)?["[]?(\S+?)[\]"]?\s+ADD\s+CONSTRAINT\s+\S+\s+FOREIGN\s+KEY\s*\(["[]?([^)"]+)[\]"]?\)\s+REFERENCES\s+(?:["[]?\S+?[\]"]?\.)?"?[[]?\2[\]]?"?\s*\(["[]?([^)"]+)[\]"]?\)\s*;/i;
const match = selfRefFKPattern.exec(line);
if (match) {
const sourceField = match[3].trim();
const targetField = match[4].trim();
// Only comment out if source and target fields are the same
if (sourceField === targetField) {
return `-- ${line}`; // Comment out invalid self-reference
}
}
return line;
});
@@ -491,9 +502,21 @@ const convertToInlineRefs = (dbml: string): string => {
return cleanedDbml;
};
// Function to check for DBML reserved keywords
const isDBMLKeyword = (name: string): boolean => {
const keywords = new Set([
'YES',
'NO',
'TRUE',
'FALSE',
'NULL', // DBML reserved keywords (boolean literals)
]);
return keywords.has(name.toUpperCase());
};
// Function to check for SQL keywords (add more if needed)
const isSQLKeyword = (name: string): boolean => {
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Add common keywords
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Common SQL keywords
return keywords.has(name.toUpperCase());
};
@@ -573,6 +596,13 @@ const normalizeCharTypeFormat = (dbml: string): string => {
.replace(/character \(([0-9]+)\)/g, 'character($1)');
};
// Fix array types that are incorrectly quoted by DBML importer
const fixArrayTypes = (dbml: string): string => {
// Remove quotes around array types like "text[]" -> text[]
// Matches patterns like: "fieldname" "type[]" and replaces with "fieldname" type[]
return dbml.replace(/(\s+"[^"]+"\s+)"([^"\s]+\[\])"/g, '$1$2');
};
// Fix table definitions with incorrect bracket syntax
const fixTableBracketSyntax = (dbml: string): string => {
// Fix patterns like Table [schema].[table] to Table "schema"."table"
@@ -582,6 +612,62 @@ const fixTableBracketSyntax = (dbml: string): string => {
);
};
// Fix table names that have been broken across multiple lines
const fixMultilineTableNames = (dbml: string): string => {
// Match Table declarations that might have line breaks in the table name
// This regex captures:
// - Table keyword
// - Optional quoted schema with dot
// - Table name that might be broken across lines (until the opening brace)
return dbml.replace(
/Table\s+((?:"[^"]*"\.)?"[^"]*(?:\n[^"]*)*")\s*\{/g,
(_, tableName) => {
// Remove line breaks within the table name
const fixedTableName = tableName.replace(/\n\s*/g, '');
return `Table ${fixedTableName} {`;
}
);
};
// Restore composite primary key names in the DBML
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
let result = dbml;
tables.forEach((table) => {
// Check if this table has a PK index with a name
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (pkIndex?.name) {
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
if (primaryKeyFields.length >= 1) {
// Build the column list for the composite PK
const columnList = primaryKeyFields
.map((f) => f.name)
.join(', ');
// Build the table identifier pattern
const tableIdentifier = table.schema
? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
: `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
// Pattern to match the composite PK index line
// Match patterns like: (col1, col2, col3) [pk]
const pkPattern = new RegExp(
`(Table ${tableIdentifier} \\{[^}]*?Indexes \\{[^}]*?)(\\(${columnList.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\) \\[pk\\])`,
'gs'
);
// Replace with the named version
const replacement = `$1(${columnList}) [pk, name: "${pkIndex.name}"]`;
result = result.replace(pkPattern, replacement);
}
}
});
return result;
};
// Restore schema information that may have been stripped by the DBML importer
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
@@ -758,6 +844,8 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
// --- Final sanitization and renaming pass ---
// Only rename keywords for PostgreSQL/SQLite
// For other databases, we'll wrap problematic names in quotes instead
const shouldRenameKeywords =
diagram.databaseType === DatabaseType.POSTGRESQL ||
diagram.databaseType === DatabaseType.SQLITE;
@@ -777,14 +865,21 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
// Rename table if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(originalName)) {
// Rename table if it's a keyword (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(isDBMLKeyword(originalName) || isSQLKeyword(originalName))
) {
const newName = `${originalName}_table`;
sqlRenamedTables.set(newName, originalName);
safeTableName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
}
// For other databases, just quote DBML keywords
else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => {
@@ -811,8 +906,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
name: finalSafeName,
};
// Rename field if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(field.name)) {
// Rename field if it's a keyword (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(isDBMLKeyword(field.name) || isSQLKeyword(field.name))
) {
const newFieldName = `${field.name}_field`;
fieldRenames.push({
table: safeTableName,
@@ -823,6 +921,10 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
? `"${newFieldName.replace(/"/g, '\\"')}"`
: newFieldName;
}
// For other databases, just quote DBML keywords
else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
}
return sanitizedField;
});
@@ -831,14 +933,16 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
...table,
name: safeTableName,
fields: processedFields,
indexes: (table.indexes || []).map((index) => ({
...index,
name: index.name
? /[^\w]/.test(index.name)
? `"${index.name.replace(/"/g, '\\"')}"`
: index.name
: `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
indexes: (table.indexes || [])
.filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
.map((index) => ({
...index,
name: index.name
? /[^\w]/.test(index.name)
? `"${index.name.replace(/"/g, '\\"')}"`
: index.name
: `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
};
};
@@ -875,8 +979,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
baseScript = sanitizeSQLforDBML(baseScript);
// Append comments for renamed tables and fields (PostgreSQL only)
if (shouldRenameKeywords) {
// Append comments for renamed tables and fields (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(sqlRenamedTables.size > 0 || fieldRenames.length > 0)
) {
baseScript = appendRenameComments(
baseScript,
sqlRenamedTables,
@@ -885,11 +992,15 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
);
}
standard = normalizeCharTypeFormat(
fixTableBracketSyntax(
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
standard = fixArrayTypes(
normalizeCharTypeFormat(
fixMultilineTableNames(
fixTableBracketSyntax(
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
)
)
)
)
);
@@ -897,12 +1008,17 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
// Restore schema information that may have been stripped by DBML importer
standard = restoreTableSchemas(standard, uniqueTables);
// Restore composite primary key names
standard = restoreCompositePKNames(standard, uniqueTables);
// Prepend Enum DBML to the standard output
if (enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
}
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
inline = fixArrayTypes(
normalizeCharTypeFormat(convertToInlineRefs(standard))
);
// Clean up excessive empty lines in both outputs
standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');

View File

@@ -0,0 +1,190 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { exportPostgreSQL } from '@/lib/data/sql-export/export-per-type/postgresql';
import { exportMySQL } from '@/lib/data/sql-export/export-per-type/mysql';
import { exportMSSQL } from '@/lib/data/sql-export/export-per-type/mssql';
import { DatabaseType } from '@/lib/domain/database-type';
describe('Composite Primary Key with Name', () => {
it('should preserve composite primary key name in DBML import and SQL export', async () => {
const dbmlContent = `
Table "landlord"."users_master_table" {
"master_user_id" bigint [not null]
"tenant_id" bigint [not null]
"tenant_user_id" bigint [not null]
"enabled" boolean
Indexes {
(master_user_id, tenant_id, tenant_user_id) [pk, name: "idx_users_master_table_master_user_id_tenant_id_tenant_user_id"]
(tenant_id, tenant_user_id) [unique, name: "index_1"]
}
}
`;
// Import DBML
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
// Check that the composite PK name was captured
expect(diagram.tables).toBeDefined();
const table = diagram.tables![0];
// Check for the PK index
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
expect(pkIndex).toBeDefined();
expect(pkIndex!.name).toBe(
'idx_users_master_table_master_user_id_tenant_id_tenant_user_id'
);
// Check that fields are marked as primary keys
const pkFields = table.fields.filter((f) => f.primaryKey);
expect(pkFields).toHaveLength(3);
expect(pkFields.map((f) => f.name)).toEqual([
'master_user_id',
'tenant_id',
'tenant_user_id',
]);
// Check that we have both the PK index and the unique index
expect(table.indexes).toHaveLength(2);
const uniqueIndex = table.indexes.find((idx) => !idx.isPrimaryKey);
expect(uniqueIndex!.name).toBe('index_1');
expect(uniqueIndex!.unique).toBe(true);
});
it('should export composite primary key with CONSTRAINT name in PostgreSQL', async () => {
const dbmlContent = `
Table "users" {
"id" bigint [not null]
"tenant_id" bigint [not null]
Indexes {
(id, tenant_id) [pk, name: "pk_users_composite"]
}
}
`;
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
const sqlScript = exportPostgreSQL({ diagram });
// Check that the SQL contains the named constraint
expect(sqlScript).toContain(
'CONSTRAINT "pk_users_composite" PRIMARY KEY ("id", "tenant_id")'
);
expect(sqlScript).not.toContain('PRIMARY KEY ("id", "tenant_id"),'); // Should not have unnamed PK
});
it('should export composite primary key with CONSTRAINT name in MySQL', async () => {
const dbmlContent = `
Table "orders" {
"order_id" int [not null]
"product_id" int [not null]
Indexes {
(order_id, product_id) [pk, name: "orders_order_product_pk"]
}
}
`;
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.MYSQL,
});
const sqlScript = exportMySQL({ diagram });
// Check that the SQL contains the named constraint
expect(sqlScript).toContain(
'CONSTRAINT `orders_order_product_pk` PRIMARY KEY (`order_id`, `product_id`)'
);
});
it('should export composite primary key with CONSTRAINT name in MSSQL', async () => {
const dbmlContent = `
Table "products" {
"category_id" int [not null]
"product_id" int [not null]
Indexes {
(category_id, product_id) [pk, name: "pk_products"]
}
}
`;
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.SQL_SERVER,
});
const sqlScript = exportMSSQL({ diagram });
// Check that the SQL contains the named constraint
expect(sqlScript).toContain(
'CONSTRAINT [pk_products] PRIMARY KEY ([category_id], [product_id])'
);
});
it('should merge duplicate PK index with name', async () => {
const dbmlContent = `
Table "test" {
"a" int [not null]
"b" int [not null]
Indexes {
(a, b) [pk]
(a, b) [name: "test_pk_name"]
}
}
`;
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toBeDefined();
const table = diagram.tables![0];
// Should capture the name from the duplicate index
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
expect(pkIndex).toBeDefined();
expect(pkIndex!.name).toBe('test_pk_name');
// Should only have the PK index
expect(table.indexes).toHaveLength(1);
// Fields should be marked as primary keys
expect(table.fields.filter((f) => f.primaryKey)).toHaveLength(2);
});
it('should handle composite PK without name', async () => {
const dbmlContent = `
Table "simple" {
"x" int [not null]
"y" int [not null]
Indexes {
(x, y) [pk]
}
}
`;
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toBeDefined();
const table = diagram.tables![0];
// PK index should not exist for composite PK without name
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
expect(pkIndex).toBeDefined();
const sqlScript = exportPostgreSQL({ diagram });
// Should have unnamed PRIMARY KEY
expect(sqlScript).toContain('PRIMARY KEY ("x", "y")');
expect(sqlScript).toContain('CONSTRAINT');
});
});

Some files were not shown because too many files have changed in this diff Show More