mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 13:03:17 +00:00
Compare commits
40 Commits
jf/fix_lar
...
jf/right_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b23c3f4b | ||
|
|
8954d893bb | ||
|
|
1a6688e85e | ||
|
|
5e81c1848a | ||
|
|
2bd9ca25b2 | ||
|
|
b016a70691 | ||
|
|
a0fb1ed08b | ||
|
|
ffddcdcc98 | ||
|
|
fe9ef275b8 | ||
|
|
df89f0b6b9 | ||
|
|
534d2858af | ||
|
|
2a64deebb8 | ||
|
|
e5e1d59327 | ||
|
|
aa290615ca | ||
|
|
ec6e46fe81 | ||
|
|
ac128d67de | ||
|
|
07937a2f51 | ||
|
|
d8e0bc7db8 | ||
|
|
1ce265781b | ||
|
|
60c5675cbf | ||
|
|
66b086378c | ||
|
|
abd2a6ccbe | ||
|
|
459c5f1ce3 | ||
|
|
44be48ff3a | ||
|
|
ad8e34483f | ||
|
|
215d57979d | ||
|
|
ec3719ebce | ||
|
|
0a5874a69b | ||
|
|
7e0fdd1595 | ||
|
|
2531a7023f | ||
|
|
73daf0df21 | ||
|
|
c77c983989 | ||
|
|
0aaa451479 | ||
|
|
b697e26170 | ||
|
|
04d91c67b1 | ||
|
|
d0dee84970 | ||
|
|
b4ccfcdcde | ||
|
|
1759b0b9f2 | ||
|
|
ab4845c772 | ||
|
|
0545b41140 |
2
.github/workflows/cla.yaml
vendored
2
.github/workflows/cla.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -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
281
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
137
src/components/button/button-with-alternatives.tsx
Normal file
137
src/components/button/button-with-alternatives.tsx
Normal 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 };
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
94
src/hooks/use-click-outside.ts
Normal file
94
src/hooks/use-click-outside.ts
Normal 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
142
src/hooks/use-focus-on.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
320
src/hooks/use-update-table-field.ts
Normal file
320
src/hooks/use-update-table-field.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
42
src/hooks/use-update-table.ts
Normal file
42
src/hooks/use-update-table.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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: 'إيقاف',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'বন্ধ',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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é',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'બંધ',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'बंद',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'オフ',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: '끄기',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'बंद',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'निष्क्रिय',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'Выкл',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'ఆఫ్',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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ı',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: 'Вимк',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: '关闭',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: '關閉',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,4 +18,7 @@
|
||||
|
||||
.marker-definitions {
|
||||
}
|
||||
|
||||
.nodrag {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
21
src/lib/data/import-metadata/import/custom-types.ts
Normal file
21
src/lib/data/import-metadata/import/custom-types.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
};
|
||||
351
src/lib/data/import-metadata/import/dependencies.ts
Normal file
351
src/lib/data/import-metadata/import/dependencies.ts
Normal 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());
|
||||
}
|
||||
64
src/lib/data/import-metadata/import/fields.ts
Normal file
64
src/lib/data/import-metadata/import/fields.ts
Normal 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,
|
||||
})
|
||||
);
|
||||
};
|
||||
82
src/lib/data/import-metadata/import/index.ts
Normal file
82
src/lib/data/import-metadata/import/index.ts
Normal 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;
|
||||
};
|
||||
24
src/lib/data/import-metadata/import/indexes.ts
Normal file
24
src/lib/data/import-metadata/import/indexes.ts
Normal 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,
|
||||
})
|
||||
);
|
||||
85
src/lib/data/import-metadata/import/relationships.ts
Normal file
85
src/lib/data/import-metadata/import/relationships.ts
Normal 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[];
|
||||
};
|
||||
228
src/lib/data/import-metadata/import/tables.ts
Normal file
228
src/lib/data/import-metadata/import/tables.ts
Normal 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;
|
||||
};
|
||||
@@ -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
@@ -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(', ')})`
|
||||
@@ -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(', ')})`
|
||||
: ''
|
||||
@@ -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(', ')})`
|
||||
: ''
|
||||
@@ -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()
|
||||
@@ -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`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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') &&
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7273
src/lib/dbml/dbml-export/__tests__/cases/1.dbml
Normal file
7273
src/lib/dbml/dbml-export/__tests__/cases/1.dbml
Normal file
File diff suppressed because it is too large
Load Diff
73546
src/lib/dbml/dbml-export/__tests__/cases/1.json
Normal file
73546
src/lib/dbml/dbml-export/__tests__/cases/1.json
Normal file
File diff suppressed because it is too large
Load Diff
17
src/lib/dbml/dbml-export/__tests__/cases/2.dbml
Normal file
17
src/lib/dbml/dbml-export/__tests__/cases/2.dbml
Normal 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)
|
||||
}
|
||||
1
src/lib/dbml/dbml-export/__tests__/cases/2.json
Normal file
1
src/lib/dbml/dbml-export/__tests__/cases/2.json
Normal 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":[]}
|
||||
8
src/lib/dbml/dbml-export/__tests__/cases/3.dbml
Normal file
8
src/lib/dbml/dbml-export/__tests__/cases/3.dbml
Normal 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)"
|
||||
}
|
||||
1
src/lib/dbml/dbml-export/__tests__/cases/3.json
Normal file
1
src/lib/dbml/dbml-export/__tests__/cases/3.json
Normal 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":[]}
|
||||
114
src/lib/dbml/dbml-export/__tests__/composite-pk-export.test.ts
Normal file
114
src/lib/dbml/dbml-export/__tests__/composite-pk-export.test.ts
Normal 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"]'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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" {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
172
src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
Normal file
172
src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
190
src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
Normal file
190
src/lib/dbml/dbml-import/__tests__/composite-pk-name.test.ts
Normal 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
Reference in New Issue
Block a user