Compare commits

...

8 Commits

Author SHA1 Message Date
Guy Ben-Aharon
60fe0843ac chore(main): release 1.10.0 (#619) 2025-03-25 11:18:58 +02:00
Jonathan Fishner
794f226209 feat(cloudflare-d1): add support to cloudflare-d1 + wrangler cli (#632)
* feat(cloudflare-d1): add support to Sqlite cloudflare-d1 database + wrangler cli

* revert export-per-type :: sqlite, no need special export for now

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-25 11:15:23 +02:00
Jonathan Fishner
2fbf3476b8 fix(export-sql): move from AI sql-export for MySQL&MariaDB to deterministic script (#628) 2025-03-20 11:26:45 +02:00
Jonathan Fishner
897ac60a82 fix(export-sql): move from AI sql-export for sqlite to deterministic script (#627) 2025-03-20 11:17:30 +02:00
Jonathan Fishner
18f228ca1d fix(export-sql): move from AI sql-export for postgres to deterministic script (#626)
* fix: move from AI export sql for postgres to export script

* update export for postgres to be without AI

* fix build

* make isDBMLFlow optional

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-19 11:02:01 +02:00
Jonathan Fishner
14de30b7aa fix(dbml-editor): dealing with dbml editor for non-generic db-type (#624)
* fix(dbml-editor): dealing with dbml editor for non-generic db-type

* small change

* Handle ENUM type when exporting as varchar

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-17 19:40:12 +02:00
Guy Ben-Aharon
3faa39e787 fix(sidebar): opens sidepanel in case its closed and click on sidebar (#620) 2025-03-13 15:41:39 +02:00
Guy Ben-Aharon
63b5ba0bb9 fix(sidebar): add sidebar for diagram objects (#618)
* fix(sidebar): add sidebar for diagram objects

* fix
2025-03-13 15:07:01 +02:00
29 changed files with 3315 additions and 220 deletions

View File

@@ -1,5 +1,22 @@
# Changelog
## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
### Features
* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
### Bug Fixes
* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)

View File

@@ -49,13 +49,13 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom
**What it does**:
- **Instant Schema Import**
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
- **Instant Schema Import**
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
- **AI-Powered Export for Easy Migration**
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether youre migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
- **Interactive Editing**
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
- **AI-Powered Export for Easy Migration**
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether you're migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
- **Interactive Editing**
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
### Status
@@ -63,13 +63,13 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
### Supported Databases
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
- ✅ MySQL
- ✅ SQL Server
- ✅ MariaDB
- ✅ SQLite
- ✅ CockroachDB
- ✅ ClickHouse
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
- ✅ MySQL
- ✅ SQL Server
- ✅ MariaDB
- ✅ SQLite (<img src="./src/assets/sqlite_logo_2.png" width="15"/> + <img src="./src/assets/cloudflare_d1.png" alt="Cloudflare D1" width="15"/> Cloudflare D1)
- ✅ CockroachDB
- ✅ ClickHouse
## Getting Started
@@ -91,17 +91,19 @@ npm run build
Or like this if you want to have AI capabilities:
```
```bash
npm install
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
```
### Run the Docker Container
```bash
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
```
#### Build and Run locally
```bash
docker build -t chartdb .
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
@@ -145,9 +147,9 @@ VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
## 💚 Community & Support
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
- [Twitter](https://x.com/chartdb_io) (Get news fast)
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
- [Twitter](https://x.com/chartdb_io) (Get news fast)
## Contributing

482
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.9.0",
"version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.9.0",
"version": "1.10.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
@@ -18,7 +18,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
@@ -27,18 +27,18 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
@@ -1834,6 +1834,60 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
@@ -1969,6 +2023,24 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@@ -2028,25 +2100,124 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.2"
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2295,6 +2466,24 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menubar": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.5.tgz",
@@ -2364,6 +2553,24 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
@@ -2467,6 +2674,24 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@@ -2658,13 +2883,54 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
"integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2682,9 +2948,9 @@
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
@@ -2818,23 +3084,175 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
"integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.1"
"@radix-ui/react-visually-hidden": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.9.0",
"version": "1.10.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,7 +22,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
@@ -31,18 +31,18 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

View File

@@ -0,0 +1,135 @@
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Cross2Icon } from '@radix-ui/react-icons';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,790 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from '@/lib/utils';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { Separator } from '@/components/separator/separator';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/sheet/sheet';
import { Skeleton } from '@/components/skeleton/skeleton';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { ViewVerticalIcon } from '@radix-ui/react-icons';
import { useSidebar } from './use-sidebar';
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
state: 'expanded' | 'collapsed';
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState =
typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
]
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
);
SidebarProvider.displayName = 'SidebarProvider';
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
}
>(
(
{
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === 'none') {
return (
<div
className={cn(
'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
className
)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet
open={openMobile}
onOpenChange={setOpenMobile}
{...props}
>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>
Displays the mobile sidebar.
</SheetDescription>
</SheetHeader>
<div className="flex size-full flex-col">
{children}
</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
)}
/>
<div
className={cn(
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
}
);
Sidebar.displayName = 'Sidebar';
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn('h-7 w-7', className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<ViewVerticalIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = 'SidebarTrigger';
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'>
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className
)}
{...props}
/>
);
});
SidebarRail.displayName = 'SidebarRail';
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'main'>
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
'relative flex w-full flex-1 flex-col bg-background',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className
)}
{...props}
/>
);
});
SidebarInset.displayName = 'SidebarInset';
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
className
)}
{...props}
/>
);
});
SidebarInput.displayName = 'SidebarInput';
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
});
SidebarHeader.displayName = 'SidebarHeader';
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
});
SidebarFooter.displayName = 'SidebarFooter';
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn('mx-2 w-auto bg-sidebar-border', className)}
{...props}
/>
);
});
SidebarSeparator.displayName = 'SidebarSeparator';
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className
)}
{...props}
/>
);
});
SidebarContent.displayName = 'SidebarContent';
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn(
'relative flex w-full min-w-0 flex-col p-2',
className
)}
{...props}
/>
);
});
SidebarGroup.displayName = 'SidebarGroup';
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className
)}
{...props}
/>
);
});
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
);
});
SidebarGroupAction.displayName = 'SidebarGroupAction';
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn('w-full text-sm', className)}
{...props}
/>
));
SidebarGroupContent.displayName = 'SidebarGroupContent';
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
));
SidebarMenu.displayName = 'SidebarMenu';
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn('group/menu-item relative', className)}
{...props}
/>
));
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default:
'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = 'default',
size = 'default',
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(
sidebarMenuButtonVariants({ variant, size }),
className
)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== 'collapsed' || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
);
SidebarMenuButton.displayName = 'SidebarMenuButton';
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
className
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = 'SidebarMenuAction';
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
));
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn(
'flex h-8 items-center gap-2 rounded-md px-2',
className
)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
));
SidebarMenuSub.displayName = 'SidebarMenuSub';
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<'a'> & {
asChild?: boolean;
size?: 'sm' | 'md';
isActive?: boolean;
}
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
SidebarContext,
};

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { SidebarContext } from './sidebar';
export const useSidebar = () => {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.');
}
return context;
};

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { cn } from '@/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-primary/10', className)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
@@ -31,6 +31,7 @@ import type { DatabaseClient } from '@/lib/domain/database-clients';
import {
databaseClientToLabelMap,
databaseTypeToClientsMap,
databaseEditionToClientsMap,
} from '@/lib/domain/database-clients';
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
@@ -71,7 +72,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
keepDialogAfterImport,
title,
}) => {
const databaseClients = databaseTypeToClientsMap[databaseType];
const databaseClients = useMemo(
() => [
...databaseTypeToClientsMap[databaseType],
...(databaseEdition
? databaseEditionToClientsMap[databaseEdition]
: []),
],
[databaseType, databaseEdition]
);
const [errorMessage, setErrorMessage] = useState('');
const [databaseClient, setDatabaseClient] = useState<
DatabaseClient | undefined
@@ -258,7 +267,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
/>
)}
</div>
{databaseTypeToClientsMap[databaseType].length > 0 ? (
{databaseClients.length > 0 ? (
<Tabs
value={
!databaseClient

View File

@@ -41,6 +41,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate();
useEffect(() => {
setDatabaseEdition(undefined);
}, [databaseType]);
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams();

View File

@@ -87,7 +87,12 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
};
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(filteredDiagram));
return Promise.resolve(
exportBaseSQL({
diagram: filteredDiagram,
targetDatabaseType,
})
);
} else {
return exportSQL(filteredDiagram, targetDatabaseType, {
stream: true,

View File

@@ -43,6 +43,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
DatabaseEdition | undefined
>();
useEffect(() => {
setDatabaseEdition(undefined);
}, [databaseType]);
useEffect(() => {
if (!dialog.open) return;
setDatabaseEdition(undefined);

View File

@@ -30,6 +30,14 @@
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -58,6 +66,14 @@
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

23
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,23 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,447 @@
import {
exportFieldComment,
isFunction,
isKeyword,
strHasQuotes,
} from './common';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parseMySQLDefault(field: DBField): string {
if (!field.default) {
return '';
}
const defaultValue = field.default.trim();
// Handle specific MySQL default values
if (
defaultValue.toLowerCase() === 'now()' ||
defaultValue.toLowerCase() === 'current_timestamp'
) {
return 'CURRENT_TIMESTAMP';
}
// Handle MySQL auto-increment, which is handled via AUTO_INCREMENT
if (
defaultValue.toLowerCase().includes('identity') ||
defaultValue.toLowerCase().includes('autoincrement') ||
defaultValue.includes('nextval')
) {
return ''; // MySQL handles this with AUTO_INCREMENT
}
// If it's a function call, convert to MySQL equivalents
if (isFunction(defaultValue)) {
// Map common PostgreSQL/MSSQL functions to MySQL equivalents
if (
defaultValue.toLowerCase().includes('newid()') ||
defaultValue.toLowerCase().includes('uuid()')
) {
return 'UUID()';
}
// For functions we can't translate, return as is (MySQL might not support them)
return defaultValue;
}
// If it's a keyword, keep it as is
if (isKeyword(defaultValue)) {
return defaultValue;
}
// If it already has quotes, keep it as is
if (strHasQuotes(defaultValue)) {
return defaultValue;
}
// If it's a number, keep it as is
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
return defaultValue;
}
// For other cases, add quotes
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Map problematic types to MySQL compatible types
function mapMySQLType(typeName: string): string {
typeName = typeName.toLowerCase();
// Map common types to MySQL type system
switch (typeName) {
case 'int':
case 'integer':
return 'INT';
case 'smallint':
return 'SMALLINT';
case 'bigint':
return 'BIGINT';
case 'decimal':
case 'numeric':
return 'DECIMAL';
case 'float':
return 'FLOAT';
case 'double':
case 'real':
return 'DOUBLE';
case 'char':
case 'character':
return 'CHAR';
case 'varchar':
case 'character varying':
case 'nvarchar':
return 'VARCHAR';
case 'text':
case 'ntext':
return 'TEXT';
case 'longtext':
return 'LONGTEXT';
case 'mediumtext':
return 'MEDIUMTEXT';
case 'tinytext':
return 'TINYTEXT';
case 'date':
return 'DATE';
case 'datetime':
case 'timestamp':
case 'datetime2':
return 'DATETIME';
case 'time':
return 'TIME';
case 'blob':
case 'binary':
return 'BLOB';
case 'varbinary':
return 'VARBINARY';
case 'bit':
return 'BIT';
case 'boolean':
case 'bool':
return 'TINYINT(1)'; // MySQL uses TINYINT(1) for boolean
case 'enum':
return 'VARCHAR(50)'; // Convert ENUM to VARCHAR instead of assuming values
case 'json':
case 'jsonb':
return 'JSON'; // MySQL has JSON type since 5.7.8
case 'uuid':
return 'CHAR(36)'; // MySQL doesn't have a UUID type, use CHAR(36)
case 'geometry':
case 'geography':
return 'GEOMETRY'; // If MySQL has spatial extensions
case 'array':
case 'user-defined':
return 'JSON'; // Use JSON for complex types like arrays or user-defined
}
// If type has array notation (ends with []), treat as JSON
if (typeName.endsWith('[]')) {
return 'JSON';
}
// For any other types, default to original type
return typeName;
}
export function exportMySQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
const tables = diagram.tables;
const relationships = diagram.relationships;
// Start SQL script
let sqlScript = '-- MySQL database export\n\n';
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n\n';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
}
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
return `${
table.comments ? `-- ${table.comments}\n` : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Handle MySQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'varbinary'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
// Set a default size for VARCHAR columns if not specified
if (
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
) {
typeWithSize = `${typeName}(255)`;
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
? ` DEFAULT ${parseMySQLDefault(field)}`
: '';
// MySQL supports inline comments
const comment = field.comments
? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
}\n)${
// MySQL supports table comments
table.comments
? ` COMMENT='${table.comments.replace(/'/g, "''")}'`
: ''
};\n\n${
// Add indexes - MySQL creates them separately from the table definition
table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
const fieldNamesForIndex = indexFields
.map((field) => field?.name || '')
.join('_');
const uniqueIndicator = index.unique ? '_unique' : '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `\`${field.name}\`` : ''))
.filter(Boolean);
// Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some((field) => {
const typeName =
field?.type.name.toLowerCase() || '';
return (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
);
});
// If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob
? indexFieldNames.map((name) => {
const field = indexFields.find(
(f) => `\`${f?.name}\`` === name
);
if (!field) return name;
const typeName =
field.type.name.toLowerCase();
if (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
) {
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
return `${name}(255)`;
}
return name;
})
: indexFieldNames;
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
: '';
})
.filter(Boolean)
.join('\n')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n\n';
sqlScript += relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
const sourceTableName = sourceTable.schema
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
: `\`${sourceTable.name}\``;
const targetTableName = targetTable.schema
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
: `\`${targetTable.name}\``;
// Create a descriptive constraint name
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
// MySQL supports ON DELETE and ON UPDATE actions
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n');
}
// Commit transaction
sqlScript += '\nCOMMIT;\n';
return sqlScript;
}

View File

@@ -0,0 +1,364 @@
import {
exportFieldComment,
isFunction,
isKeyword,
strHasQuotes,
} from './common';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parsePostgresDefault(field: DBField): string {
if (!field.default) {
return '';
}
const defaultValue = field.default.trim();
// Handle specific PostgreSQL default values
if (defaultValue.toLowerCase() === 'now()') {
return 'CURRENT_TIMESTAMP';
}
// Handle PostgreSQL functions for JSON/JSONB types
if (
(field.type.name.toLowerCase() === 'json' ||
field.type.name.toLowerCase() === 'jsonb') &&
(defaultValue.includes('json_build_object') ||
defaultValue.includes('jsonb_build_object') ||
defaultValue.includes('json_build_array') ||
defaultValue.includes('jsonb_build_array') ||
defaultValue.includes('to_json') ||
defaultValue.includes('to_jsonb'))
) {
// Remove any enclosing quotes and return the function call as is
return defaultValue.replace(/^'(.*)'$/, '$1').replace(/''/, "'");
}
// Handle nextval sequences for PostgreSQL
if (defaultValue.includes('nextval')) {
return defaultValue; // Keep it as is for PostgreSQL
}
// If it's a function call, keep it as is
if (isFunction(defaultValue)) {
return defaultValue;
}
// If it's a keyword, keep it as is
if (isKeyword(defaultValue)) {
return defaultValue;
}
// If it already has quotes, keep it as is
if (strHasQuotes(defaultValue)) {
return defaultValue;
}
// If it's a number, keep it as is
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
return defaultValue;
}
// For other cases, add quotes
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Map problematic types to PostgreSQL compatible types
function mapPostgresType(typeName: string, fieldName: string): string {
typeName = typeName.toLowerCase();
fieldName = fieldName.toLowerCase();
// Handle known problematic types
if (typeName === 'user-defined') {
return 'jsonb'; // Default fallback for user-defined types
}
// Handle generic "array" type (when not specified as array of what)
if (typeName === 'array') {
return 'text[]'; // Default to text array
}
// Handle array type notation
if (typeName.endsWith('[]')) {
const baseType = mapPostgresType(typeName.slice(0, -2), fieldName);
return `${baseType}[]`;
}
// Default case: return the type as is
return typeName;
}
export function exportPostgreSQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
const tables = diagram.tables;
const relationships = diagram.relationships;
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
sqlScript += '\n';
// Add sequence creation statements
const sequences = new Set<string>();
tables.forEach((table) => {
table.fields.forEach((field) => {
if (field.default) {
// Match nextval('schema.sequence_name') or nextval('sequence_name')
const match = field.default.match(
/nextval\('([^']+)'(?:::[^)]+)?\)/
);
if (match) {
sequences.add(match[1]);
}
}
});
});
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
sqlScript += '\n';
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
return `${
table.comments ? `-- ${table.comments}\n` : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
);
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize = typeWithSize.replace('[]', '') + '[]';
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle identity generation
let identity = '';
if (field.default && field.default.includes('nextval')) {
// PostgreSQL already handles this with DEFAULT nextval()
} else if (
field.default &&
field.default.toLowerCase().includes('identity')
) {
identity = ' GENERATED BY DEFAULT AS IDENTITY';
}
// Only add UNIQUE constraint if the field is not part of the primary key
// This avoids redundant uniqueness constraints
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using PostgreSQL specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parsePostgresDefault(field)}`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n\n${
// Add table comments
table.comments
? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n`
)
.join('')
}\n${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
const safeIndexName = index.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
let combinedName = `${safeTableName}_${safeIndexName}`;
if (combinedName.length > 60) {
// If too long, use just the index name or a truncated version
combinedName =
safeIndexName.length > 60
? safeIndexName.substring(0, 60)
: safeIndexName;
}
const indexName = `"${combinedName}"`;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : ''))
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
: '';
})
.filter(Boolean)
.join('')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
sqlScript += `\n${relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
const sourceTableName = sourceTable.schema
? `"${sourceTable.schema}"."${sourceTable.name}"`
: `"${sourceTable.name}"`;
const targetTableName = targetTable.schema
? `"${targetTable.schema}"."${targetTable.name}"`
: `"${targetTable.name}"`;
// Create a unique constraint name by combining table and field names
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
// and doesn't get truncated in a way that breaks SQL syntax
const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`;
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
const safeConstraintName =
baseName.length > 60
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
const constraintName = `"${safeConstraintName}"`;
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
return sqlScript;
}

View File

@@ -0,0 +1,358 @@
import {
exportFieldComment,
isFunction,
isKeyword,
strHasQuotes,
} from './common';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parseSQLiteDefault(field: DBField): string {
if (!field.default) {
return '';
}
const defaultValue = field.default.trim();
// Handle specific SQLite default values
if (
defaultValue.toLowerCase() === 'now()' ||
defaultValue.toLowerCase() === 'current_timestamp'
) {
return 'CURRENT_TIMESTAMP';
}
// Handle SQLite auto-increment
if (
defaultValue.toLowerCase().includes('identity') ||
defaultValue.toLowerCase().includes('autoincrement') ||
defaultValue.includes('nextval')
) {
return ''; // SQLite handles this differently with INTEGER PRIMARY KEY AUTOINCREMENT
}
// If it's a function call, convert to SQLite equivalents
if (isFunction(defaultValue)) {
// Map common PostgreSQL/MSSQL functions to SQLite equivalents
if (
defaultValue.toLowerCase().includes('newid()') ||
defaultValue.toLowerCase().includes('uuid()')
) {
return 'lower(hex(randomblob(16)))';
}
// For functions we can't translate, return as is (SQLite might not support them)
return defaultValue;
}
// If it's a keyword, keep it as is
if (isKeyword(defaultValue)) {
return defaultValue;
}
// If it already has quotes, keep it as is
if (strHasQuotes(defaultValue)) {
return defaultValue;
}
// If it's a number, keep it as is
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
return defaultValue;
}
// For other cases, add quotes
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Map problematic types to SQLite compatible types
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
typeName = typeName.toLowerCase();
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
if (isPrimaryKey && (typeName === 'integer' || typeName === 'int')) {
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
}
// Map common types to SQLite's simplified type system
switch (typeName) {
case 'int':
case 'smallint':
case 'tinyint':
case 'mediumint':
case 'bigint':
return 'INTEGER';
case 'decimal':
case 'numeric':
case 'float':
case 'double':
case 'real':
return 'REAL';
case 'char':
case 'nchar':
case 'varchar':
case 'nvarchar':
case 'text':
case 'ntext':
case 'character varying':
case 'character':
return 'TEXT';
case 'date':
case 'datetime':
case 'timestamp':
case 'datetime2':
return 'TEXT'; // SQLite doesn't have dedicated date types
case 'blob':
case 'binary':
case 'varbinary':
case 'image':
return 'BLOB';
case 'bit':
case 'boolean':
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
case 'user-defined':
case 'json':
case 'jsonb':
return 'TEXT'; // Store as JSON text
case 'array':
return 'TEXT'; // Store as serialized array text
case 'geometry':
case 'geography':
return 'BLOB'; // Store spatial data as BLOB in SQLite
}
// If type has array notation (ends with []), treat as TEXT
if (typeName.endsWith('[]')) {
return 'TEXT';
}
// For any other types, default to TEXT
return typeName;
}
export function exportSQLite(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
const tables = diagram.tables;
const relationships = diagram.relationships;
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
let sqlScript = '-- SQLite database export\n\n';
// Begin transaction for faster import
sqlScript += 'BEGIN TRANSACTION;\n\n';
// SQLite doesn't have sequences, so we skip sequence creation
// SQLite system tables that should be skipped
const sqliteSystemTables = [
'sqlite_sequence',
'sqlite_stat1',
'sqlite_stat2',
'sqlite_stat3',
'sqlite_stat4',
'sqlite_master',
];
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Skip SQLite system tables
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
return `-- Skipping SQLite system table: "${table.name}"\n`;
}
// SQLite doesn't use schema prefixes, so we use just the table name
// Include the schema in a comment if it exists
const schemaComment = table.schema
? `-- Original schema: ${table.schema}\n`
: '';
const tableName = `"${table.name}"`;
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
return `${schemaComment}${
table.comments ? `-- ${table.comments}\n` : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// SQLite ignores length specifiers, so we don't add them
// We'll keep this simple without size info
const typeWithoutSize = typeName;
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle autoincrement - only works with INTEGER PRIMARY KEY
let autoIncrement = '';
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTOINCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value - Special handling for datetime() function
let defaultValue = '';
if (
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
) {
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
if (field.default.includes("datetime(''now'')")) {
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
} else {
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
}
}
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
table.indexes
.map((index) => {
// Skip indexes that exactly match the primary key
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : ''))
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create safe index name
const safeIndexName = `${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
: '';
})
.filter(Boolean)
.join('\n')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate table constraints and triggers for foreign keys
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
// But we'll also provide individual ALTER TABLE statements as comments for reference
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
sqlScript +=
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
relationships.forEach((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView ||
sqliteSystemTables.includes(sourceTable.name.toLowerCase()) ||
sqliteSystemTables.includes(targetTable.name.toLowerCase())
) {
return;
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return;
}
// Create commented out version of what would be ALTER TABLE statement
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
});
}
// Commit transaction
sqlScript += '\nCOMMIT;\n';
return sqlScript;
}

View File

@@ -5,16 +5,39 @@ import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
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';
export const exportBaseSQL = (diagram: Diagram): string => {
export const exportBaseSQL = ({
diagram,
targetDatabaseType,
isDBMLFlow = false,
}: {
diagram: Diagram;
targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean;
}): string => {
const { tables, relationships } = diagram;
if (!tables || tables.length === 0) {
return '';
}
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
return exportMSSQL(diagram);
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
switch (diagram.databaseType) {
case DatabaseType.SQL_SERVER:
return exportMSSQL(diagram);
case DatabaseType.POSTGRESQL:
return exportPostgreSQL(diagram);
case DatabaseType.SQLITE:
return exportSQLite(diagram);
case DatabaseType.MYSQL:
case DatabaseType.MARIADB:
return exportMySQL(diagram);
default:
return exportPostgreSQL(diagram);
}
}
// Filter out the tables that are views
@@ -72,6 +95,11 @@ export const exportBaseSQL = (diagram: Diagram): string => {
table.fields.forEach((field, index) => {
let typeName = field.type.name;
// Handle ENUM type
if (typeName.toLowerCase() === 'enum') {
typeName = 'varchar';
}
// Temp fix for 'array' to be text[]
if (typeName.toLowerCase() === 'array') {
typeName = 'text[]';
@@ -244,8 +272,12 @@ export const exportSQL = async (
signal?: AbortSignal;
}
): Promise<string> => {
const sqlScript = exportBaseSQL(diagram);
if (databaseType === DatabaseType.SQL_SERVER) {
const sqlScript = exportBaseSQL({
diagram,
targetDatabaseType: databaseType,
});
if (databaseType === diagram.databaseType) {
return sqlScript;
}
@@ -409,7 +441,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
- **Conditional Statements**: Utilize PostgreSQLs support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
`,
mysql: `
- **Table Creation**: Use \`CREATE TABLE IF NOT EXISTS\` for creating tables. While creating the table structure, ensure that all foreign key columns use the correct data types as determined in the foreign key review.
@@ -429,7 +461,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
sql_server: `
- **Sequence Creation**: Use \`CREATE SEQUENCE\` without \`IF NOT EXISTS\`, and employ conditional logic (\`IF NOT EXISTS\`) to check for sequence existence before creation.
- **Identity Columns**: Always prefer using the \`IDENTITY\` keyword (e.g., \`INT IDENTITY(1,1)\`) for auto-incrementing primary key columns when possible.
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesnt support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesn't support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
- **Avoid Unsupported Syntax**: Ensure the script does not include unsupported statements like \`CREATE TABLE IF NOT EXISTS\`.
**Reminder**: Ensure all column names that conflict with reserved keywords or data types (e.g., key, primary, column, table), escape the column name by enclosing it.
@@ -463,7 +495,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
- **Conditional Statements**: Utilize PostgreSQLs support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
`,
};

View File

@@ -1,7 +1,7 @@
import { DatabaseType } from '@/lib/domain/database-type';
import { getPostgresQuery } from './postgres-script';
import { getMySQLQuery } from './mysql-script';
import { sqliteQuery } from './sqlite-script';
import { getSQLiteQuery } from './sqlite-script';
import { getSqlServerQuery } from './sqlserver-script';
import { mariaDBQuery } from './maria-script';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
@@ -21,7 +21,7 @@ export const importMetadataScripts: ImportMetadataScripts = {
[DatabaseType.GENERIC]: () => '',
[DatabaseType.POSTGRESQL]: getPostgresQuery,
[DatabaseType.MYSQL]: getMySQLQuery,
[DatabaseType.SQLITE]: () => sqliteQuery,
[DatabaseType.SQLITE]: getSQLiteQuery,
[DatabaseType.SQL_SERVER]: getSqlServerQuery,
[DatabaseType.MARIADB]: () => mariaDBQuery,
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,

View File

@@ -1,4 +1,8 @@
export const sqliteQuery = `WITH fk_info AS (
import { DatabaseEdition } from '@/lib/domain/database-edition';
import { DatabaseClient } from '@/lib/domain/database-clients';
const sqliteQuery = `${`/* Standard SQLite */`}
WITH fk_info AS (
SELECT
json_group_array(
json_object(
@@ -163,3 +167,225 @@ replace(replace(replace(
'\\"', '"'),'"[', '['), ']"', ']'
) AS metadata_json_to_import;
`;
const cloudflareD1Query = `${`/* Cloudflare D1 SQLite */`}
WITH fk_info AS (
SELECT
json_group_array(
json_object(
'schema', '',
'table', m.name,
'column', fk.[from],
'foreign_key_name',
'fk_' || m.name || '_' || fk.[from] || '_' || fk.[table] || '_' || fk.[to],
'reference_schema', '',
'reference_table', fk.[table],
'reference_column', fk.[to],
'fk_def',
'FOREIGN KEY (' || fk.[from] || ') REFERENCES ' || fk.[table] || '(' || fk.[to] || ')' ||
' ON UPDATE ' || fk.on_update || ' ON DELETE ' || fk.on_delete
)
) AS fk_metadata
FROM
sqlite_master m
JOIN
pragma_foreign_key_list(m.name) fk
ON
m.type = 'table'
WHERE
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
), pk_info AS (
SELECT
json_group_array(
json_object(
'schema', '',
'table', pk.table_name,
'field_count', pk.field_count,
'column', pk.pk_column,
'pk_def', 'PRIMARY KEY (' || pk.pk_column || ')'
)
) AS pk_metadata
FROM
(
SELECT
m.name AS table_name,
COUNT(p.name) AS field_count,
GROUP_CONCAT(p.name) AS pk_column
FROM
sqlite_master m
JOIN
pragma_table_info(m.name) p
ON
m.type = 'table' AND p.pk > 0
WHERE
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
GROUP BY
m.name
) pk
), indexes_metadata AS (
SELECT
json_group_array(
json_object(
'schema', '',
'table', m.name,
'name', idx.name,
'column', ic.name,
'index_type', 'B-TREE',
'cardinality', '',
'size', '',
'unique', (CASE WHEN idx.[unique] = 1 THEN 'true' ELSE 'false' END),
'direction', '',
'column_position', ic.seqno + 1
)
) AS indexes_metadata
FROM
sqlite_master m
JOIN
pragma_index_list(m.name) idx
ON
m.type = 'table'
JOIN
pragma_index_info(idx.name) ic
WHERE
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
), cols AS (
SELECT
json_group_array(
json_object(
'schema', '',
'table', m.name,
'name', p.name,
'type',
CASE
WHEN INSTR(LOWER(p.type), '(') > 0 THEN
SUBSTR(LOWER(p.type), 1, INSTR(LOWER(p.type), '(') - 1)
ELSE LOWER(p.type)
END,
'ordinal_position', p.cid,
'nullable', (CASE WHEN p.[notnull] = 0 THEN true ELSE false END),
'collation', '',
'character_maximum_length',
CASE
WHEN LOWER(p.type) LIKE 'char%' OR LOWER(p.type) LIKE 'varchar%' THEN
CASE
WHEN INSTR(p.type, '(') > 0 THEN
REPLACE(SUBSTR(p.type, INSTR(p.type, '(') + 1, LENGTH(p.type) - INSTR(p.type, '(') - 1), ')', '')
ELSE 'null'
END
ELSE 'null'
END,
'precision',
CASE
WHEN LOWER(p.type) LIKE 'decimal%' OR LOWER(p.type) LIKE 'numeric%' THEN
CASE
WHEN instr(p.type, '(') > 0 THEN
json_object(
'precision', substr(p.type, instr(p.type, '(') + 1, instr(p.type, ',') - instr(p.type, '(') - 1),
'scale', substr(p.type, instr(p.type, ',') + 1, instr(p.type, ')') - instr(p.type, ',') - 1)
)
ELSE 'null'
END
ELSE 'null'
END,
'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
)
) AS cols_metadata
FROM
sqlite_master m
JOIN
pragma_table_info(m.name) p
ON
m.type in ('table', 'view')
WHERE
m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
), tbls AS (
SELECT
json_group_array(
json_object(
'schema', '',
'table', m.name,
'rows', -1,
'type', 'table',
'engine', '',
'collation', ''
)
) AS tbls_metadata
FROM
sqlite_master m
WHERE
m.type in ('table', 'view') AND m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
), views AS (
SELECT
json_group_array(
json_object(
'schema', '',
'view_name', m.name
)
) AS views_metadata
FROM
sqlite_master m
WHERE
m.type = 'view' AND m.name NOT LIKE '\\_cf\\_%' ESCAPE '\\'
)
SELECT
replace(replace(replace(
json_object(
'fk_info', (SELECT fk_metadata FROM fk_info),
'pk_info', (SELECT pk_metadata FROM pk_info),
'columns', (SELECT cols_metadata FROM cols),
'indexes', (SELECT indexes_metadata FROM indexes_metadata),
'tables', (SELECT tbls_metadata FROM tbls),
'views', (SELECT views_metadata FROM views),
'database_name', 'sqlite',
'version', ''
),
'\\"', '"'),'"[', '['), ']"', ']'
) AS metadata_json_to_import;
`;
// Generate Wrangler CLI command wrapper around the D1 query
const generateWranglerCommand = (): string => {
return `# Cloudflare D1 (via Wrangler CLI) Import Script
# ------------------------------------------------------
# This query will extract your D1 database schema using Cloudflare's Wrangler CLI
#
# Prerequisites:
# 1. Install Wrangler CLI if you haven't already: npm install -g wrangler
# 2. Login to your Cloudflare account: wrangler login
# 3. Make sure that your wrangler.jsonc or wrangler.toml file has the following:
# [d1_databases]
# [d1_databases.DB]
# database_name = "YOUR_DB_NAME"
# database_id = "YOUR_DB_ID"
# 4. Replace YOUR_DB_NAME with your actual D1 database name
# 5. Replace YOUR_DB_ID with your actual D1 database ID
# Step 1: Write the query to a file
wrangler d1 execute YOUR_DB_NAME --command $'WITH fk_info AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'column\\', fk.[from], \\'foreign_key_name\\', \\'fk_\\' || m.name || \\'_\\' || fk.[from] || \\'_\\' || fk.[table] || \\'_\\' || fk.[to], \\'reference_schema\\', \\'\\', \\'reference_table\\', fk.[table], \\'reference_column\\', fk.[to], \\'fk_def\\', \\'FOREIGN KEY (\\' || fk.[from] || \\') REFERENCES \\' || fk.[table] || \\'(\\' || fk.[to] || \\')\\' || \\' ON UPDATE \\' || fk.on_update || \\' ON DELETE \\' || fk.on_delete ) ) AS fk_metadata FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) fk ON m.type = \\'table\\' WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), pk_info AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', pk.table_name, \\'field_count\\', pk.field_count, \\'column\\', pk.pk_column, \\'pk_def\\', \\'PRIMARY KEY (\\' || pk.pk_column || \\')\\' ) ) AS pk_metadata FROM ( SELECT m.name AS table_name, COUNT(p.name) AS field_count, GROUP_CONCAT(p.name) AS pk_column FROM sqlite_master m JOIN pragma_table_info(m.name) p ON m.type = \\'table\\' AND p.pk > 0 WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' GROUP BY m.name ) pk ), indexes_metadata AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'name\\', idx.name, \\'column\\', ic.name, \\'index_type\\', \\'B-TREE\\', \\'cardinality\\', \\'\\', \\'size\\', \\'\\', \\'unique\\', CASE WHEN idx.[unique] = 1 THEN \\'true\\' ELSE \\'false\\' END, \\'direction\\', \\'\\', \\'column_position\\', ic.seqno + 1 ) ) AS indexes_metadata FROM sqlite_master m JOIN pragma_index_list(m.name) idx ON m.type = \\'table\\' JOIN pragma_index_info(idx.name) ic WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), cols AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'name\\', p.name, \\'type\\', CASE WHEN INSTR(LOWER(p.type), \\'(\\') > 0 THEN SUBSTR(LOWER(p.type), 1, INSTR(LOWER(p.type), \\'(\\') - 1) ELSE LOWER(p.type) END, \\'ordinal_position\\', p.cid, \\'nullable\\', CASE WHEN p.[notnull] = 0 THEN true ELSE false END, \\'collation\\', \\'\\', \\'character_maximum_length\\', CASE WHEN LOWER(p.type) LIKE \\'char%\\' OR LOWER(p.type) LIKE \\'varchar%\\' THEN CASE WHEN INSTR(p.type, \\'(\\') > 0 THEN REPLACE( SUBSTR(p.type, INSTR(p.type, \\'(\\') + 1, LENGTH(p.type) - INSTR(p.type, \\'(\\') - 1), \\')\\', \\'\\' ) ELSE \\'null\\' END ELSE \\'null\\' END, \\'precision\\', CASE WHEN LOWER(p.type) LIKE \\'decimal%\\' OR LOWER(p.type) LIKE \\'numeric%\\' THEN CASE WHEN instr(p.type, \\'(\\') > 0 THEN json_object( \\'precision\\', substr(p.type, instr(p.type, \\'(\\') + 1, instr(p.type, \\',\\') - instr(p.type, \\'(\\') - 1), \\'scale\\', substr(p.type, instr(p.type, \\',\\') + 1, instr(p.type, \\')\\') - instr(p.type, \\',\\') - 1) ) ELSE \\'null\\' END ELSE \\'null\\' END, \\'default\\', COALESCE(REPLACE(p.dflt_value, \\'"\\', \\'\\\\\\"\\'), \\'\\') ) ) AS cols_metadata FROM sqlite_master m JOIN pragma_table_info(m.name) p ON m.type in (\\'table\\', \\'view\\') WHERE m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), tbls AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'table\\', m.name, \\'rows\\', -1, \\'type\\', \\'table\\', \\'engine\\', \\'\\', \\'collation\\', \\'\\' ) ) AS tbls_metadata FROM sqlite_master m WHERE m.type in (\\'table\\', \\'view\\') AND m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ), views AS ( SELECT json_group_array( json_object( \\'schema\\', \\'\\', \\'view_name\\', m.name ) ) AS views_metadata FROM sqlite_master m WHERE m.type = \\'view\\' AND m.name NOT LIKE \\'\\\\_cf\\\\_%\\' ESCAPE \\'\\\\\\' ) SELECT json_object( \\'fk_info\\', json((SELECT fk_metadata FROM fk_info)), \\'pk_info\\', json((SELECT pk_metadata FROM pk_info)), \\'columns\\', json((SELECT cols_metadata FROM cols)), \\'indexes\\', json((SELECT indexes_metadata FROM indexes_metadata)), \\'tables\\', json((SELECT tbls_metadata FROM tbls)), \\'views\\', json((SELECT views_metadata FROM views)), \\'database_name\\', \\'sqlite\\', \\'version\\', \\'\\' ) AS metadata_json_to_import;' --remote
# Step 2: Copy the output of the command above and paste it into app.chartdb.io
`;
};
export const getSQLiteQuery = (
options: {
databaseEdition?: DatabaseEdition;
databaseClient?: DatabaseClient;
} = {}
): string => {
// For Cloudflare D1 edition, return the D1 script
if (options.databaseEdition === DatabaseEdition.SQLITE_CLOUDFLARE_D1) {
// Generate the Wrangler CLI command based on client
const isWranglerClient =
options?.databaseClient === DatabaseClient.SQLITE_WRANGLER;
if (isWranglerClient) {
return generateWranglerCommand();
}
return cloudflareD1Query;
}
// Default SQLite script
return sqliteQuery;
};

View File

@@ -1,13 +1,20 @@
import { DatabaseType } from './database-type';
import { DatabaseEdition } from './database-edition';
export enum DatabaseClient {
// PostgreSQL
POSTGRESQL_PSQL = 'psql',
// SQLite
SQLITE_WRANGLER = 'wrangler',
}
export const databaseClientToLabelMap: Record<DatabaseClient, string> = {
// PostgreSQL
[DatabaseClient.POSTGRESQL_PSQL]: 'PSQL',
// SQLite
[DatabaseClient.SQLITE_WRANGLER]: 'Wrangler',
};
export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
@@ -21,3 +28,21 @@ export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
[DatabaseType.CLICKHOUSE]: [],
[DatabaseType.COCKROACHDB]: [],
};
export const databaseEditionToClientsMap: Record<
DatabaseEdition,
DatabaseClient[]
> = {
// PostgreSQL
[DatabaseEdition.POSTGRESQL_SUPABASE]: [],
[DatabaseEdition.POSTGRESQL_TIMESCALE]: [],
// MySQL
[DatabaseEdition.MYSQL_5_7]: [],
// SQL Server
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: [],
// SQLite
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: [DatabaseClient.SQLITE_WRANGLER],
};

View File

@@ -3,6 +3,7 @@ import SupabaseImage from '@/assets/supabase.png';
import TimescaleImage from '@/assets/timescale.png';
import MySql5_7Image from '@/assets/mysql_5_7.png';
import SqlServerImage from '@/assets/sql_server_logo_2.png';
import CloudflareD1Image from '@/assets/cloudflare_d1.png';
export enum DatabaseEdition {
// PostgreSQL
@@ -14,6 +15,9 @@ export enum DatabaseEdition {
// SQL Server
SQL_SERVER_2016_AND_BELOW = 'sql_server_2016_and_below',
// SQLite
SQLITE_CLOUDFLARE_D1 = 'cloudflare_d1',
}
export const databaseEditionToLabelMap: Record<DatabaseEdition, string> = {
@@ -26,6 +30,9 @@ export const databaseEditionToLabelMap: Record<DatabaseEdition, string> = {
// SQL Server
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: '2016 and below',
// SQLite
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: 'Cloudflare D1',
};
export const databaseEditionToImageMap: Record<DatabaseEdition, string> = {
@@ -38,6 +45,9 @@ export const databaseEditionToImageMap: Record<DatabaseEdition, string> = {
// SQL Server
[DatabaseEdition.SQL_SERVER_2016_AND_BELOW]: SqlServerImage,
// SQLite
[DatabaseEdition.SQLITE_CLOUDFLARE_D1]: CloudflareD1Image,
};
export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
@@ -48,7 +58,7 @@ export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
],
[DatabaseType.MYSQL]: [DatabaseEdition.MYSQL_5_7],
[DatabaseType.SQL_SERVER]: [DatabaseEdition.SQL_SERVER_2016_AND_BELOW],
[DatabaseType.SQLITE]: [],
[DatabaseType.SQLITE]: [DatabaseEdition.SQLITE_CLOUDFLARE_D1],
[DatabaseType.GENERIC]: [],
[DatabaseType.MARIADB]: [],
[DatabaseType.CLICKHOUSE]: [],

View File

@@ -9,6 +9,8 @@ import { Canvas } from './canvas/canvas';
import { useLayout } from '@/hooks/use-layout';
import type { Diagram } from '@/lib/domain/diagram';
import { cn } from '@/lib/utils';
import { SidebarProvider } from '@/components/sidebar/sidebar';
import { EditorSidebar } from './editor-sidebar/editor-sidebar';
export interface EditorDesktopLayoutProps {
initialDiagram?: Diagram;
@@ -19,22 +21,32 @@ export const EditorDesktopLayout: React.FC<EditorDesktopLayoutProps> = ({
const { isSidePanelShowed } = useLayout();
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
defaultSize={25}
minSize={25}
maxSize={isSidePanelShowed ? 99 : 0}
className={cn('transition-[flex-grow] duration-200', {
'min-w-[350px]': isSidePanelShowed,
})}
>
<SidePanel />
</ResizablePanel>
<ResizableHandle disabled={!isSidePanelShowed} />
<ResizablePanel defaultSize={75}>
<Canvas initialTables={initialDiagram?.tables ?? []} />
</ResizablePanel>
</ResizablePanelGroup>
<SidebarProvider
defaultOpen={false}
open={false}
className="h-full min-h-0"
>
<EditorSidebar />
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
defaultSize={25}
minSize={25}
maxSize={isSidePanelShowed ? 99 : 0}
className={cn('transition-[flex-grow] duration-200', {
'min-w-[350px]': isSidePanelShowed,
})}
>
<SidePanel />
</ResizablePanel>
<ResizableHandle
disabled={!isSidePanelShowed}
className={!isSidePanelShowed ? 'hidden' : ''}
/>
<ResizablePanel defaultSize={75}>
<Canvas initialTables={initialDiagram?.tables ?? []} />
</ResizablePanel>
</ResizablePanelGroup>
</SidebarProvider>
);
};

View File

@@ -0,0 +1,94 @@
import React, { useMemo } from 'react';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/sidebar/sidebar';
import { SquareStack, Table, Workflow } from 'lucide-react';
import { useLayout } from '@/hooks/use-layout';
import { useTranslation } from 'react-i18next';
export interface SidebarItem {
title: string;
icon: React.FC;
onClick: () => void;
active: boolean;
}
export interface EditorSidebarProps {}
export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
const { selectSidebarSection, selectedSidebarSection, showSidePanel } =
useLayout();
const { t } = useTranslation();
const items: SidebarItem[] = useMemo(
() => [
{
title: t('side_panel.tables_section.tables'),
icon: Table,
onClick: () => {
showSidePanel();
selectSidebarSection('tables');
},
active: selectedSidebarSection === 'tables',
},
{
title: t('side_panel.relationships_section.relationships'),
icon: Workflow,
onClick: () => {
showSidePanel();
selectSidebarSection('relationships');
},
active: selectedSidebarSection === 'relationships',
},
{
title: t('side_panel.dependencies_section.dependencies'),
icon: SquareStack,
onClick: () => {
showSidePanel();
selectSidebarSection('dependencies');
},
active: selectedSidebarSection === 'dependencies',
},
],
[selectSidebarSection, selectedSidebarSection, t, showSidePanel]
);
return (
<Sidebar
side="left"
collapsible="icon"
variant="sidebar"
className="relative h-full"
>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel />
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
isActive={item.active}
asChild
tooltip={item.title}
>
<button onClick={item.onClick}>
<item.icon />
<span>{item.title}</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
};

View File

@@ -16,6 +16,7 @@ import type { SelectBoxOption } from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import { useChartDB } from '@/hooks/use-chartdb';
import { DependenciesSection } from './dependencies-section/dependencies-section';
import { useBreakpoint } from '@/hooks/use-breakpoint';
export interface SidePanelProps {}
@@ -29,6 +30,7 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
openSelectSchema,
closeSelectSchema,
} = useLayout();
const { isMd: isDesktop } = useBreakpoint('md');
const schemasOptions: SelectBoxOption[] = useMemo(
() =>
@@ -82,38 +84,40 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
</div>
) : null}
<div className="flex justify-center border-b pt-0.5">
<Select
value={selectedSidebarSection}
onValueChange={(value) =>
selectSidebarSection(value as SidebarSection)
}
>
<SelectTrigger className="rounded-none border-none font-semibold shadow-none hover:bg-secondary hover:underline focus:border-transparent focus:ring-0">
<SelectValue />
<div className="flex flex-1 justify-end px-2 text-xs font-normal text-muted-foreground">
{t('side_panel.view_all_options')}
</div>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="tables">
{t('side_panel.tables_section.tables')}
</SelectItem>
<SelectItem value="relationships">
{t(
'side_panel.relationships_section.relationships'
)}
</SelectItem>
<SelectItem value="dependencies">
{t(
'side_panel.dependencies_section.dependencies'
)}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
{!isDesktop ? (
<div className="flex justify-center border-b pt-0.5">
<Select
value={selectedSidebarSection}
onValueChange={(value) =>
selectSidebarSection(value as SidebarSection)
}
>
<SelectTrigger className="rounded-none border-none font-semibold shadow-none hover:bg-secondary hover:underline focus:border-transparent focus:ring-0">
<SelectValue />
<div className="flex flex-1 justify-end px-2 text-xs font-normal text-muted-foreground">
{t('side_panel.view_all_options')}
</div>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="tables">
{t('side_panel.tables_section.tables')}
</SelectItem>
<SelectItem value="relationships">
{t(
'side_panel.relationships_section.relationships'
)}
</SelectItem>
<SelectItem value="dependencies">
{t(
'side_panel.dependencies_section.dependencies'
)}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
) : null}
{selectedSidebarSection === 'tables' ? (
<TablesSection />
) : selectedSidebarSection === 'relationships' ? (

View File

@@ -9,6 +9,7 @@ import { exportBaseSQL } from '@/lib/data/export-metadata/export-sql-script';
import type { Diagram } from '@/lib/domain/diagram';
import { useToast } from '@/components/toast/use-toast';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import { DatabaseType } from '@/lib/domain/database-type';
export interface TableDBMLProps {
filteredTables: DBTable[];
@@ -18,6 +19,24 @@ const getEditorTheme = (theme: EffectiveTheme) => {
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
};
const databaseTypeToImportFormat = (
type: DatabaseType
): 'mysql' | 'postgres' | 'mssql' => {
switch (type) {
case DatabaseType.SQL_SERVER:
return 'mssql';
case DatabaseType.MYSQL:
case DatabaseType.MARIADB:
return 'mysql';
case DatabaseType.POSTGRESQL:
case DatabaseType.COCKROACHDB:
case DatabaseType.SQLITE:
return 'postgres';
default:
return 'postgres';
}
};
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
const { currentDiagram } = useChartDB();
const { effectiveTheme } = useTheme();
@@ -57,10 +76,17 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
})) ?? [],
} satisfies Diagram;
const baseScript = exportBaseSQL(filteredDiagramWithoutSpaces);
const baseScript = exportBaseSQL({
diagram: filteredDiagramWithoutSpaces,
targetDatabaseType: currentDiagram.databaseType,
isDBMLFlow: true,
});
try {
return importer.import(baseScript, 'postgres');
const importFormat = databaseTypeToImportFormat(
currentDiagram.databaseType
);
return importer.import(baseScript, importFormat);
} catch (e) {
console.error(e);

View File

@@ -300,17 +300,21 @@ export const Menu: React.FC<MenuProps> = () => {
}
>
{databaseTypeToLabelMap['postgresql']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
{databaseType !== DatabaseType.POSTGRESQL && (
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
)}
</MenubarItem>
<MenubarItem
onClick={() => exportSQL(DatabaseType.MYSQL)}
>
{databaseTypeToLabelMap['mysql']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
{databaseType !== DatabaseType.MYSQL && (
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
)}
</MenubarItem>
<MenubarItem
onClick={() =>
@@ -318,25 +322,31 @@ export const Menu: React.FC<MenuProps> = () => {
}
>
{databaseTypeToLabelMap['sql_server']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
{databaseType !== DatabaseType.SQL_SERVER && (
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
)}
</MenubarItem>
<MenubarItem
onClick={() => exportSQL(DatabaseType.MARIADB)}
>
{databaseTypeToLabelMap['mariadb']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
{databaseType !== DatabaseType.MARIADB && (
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
)}
</MenubarItem>
<MenubarItem
onClick={() => exportSQL(DatabaseType.SQLITE)}
>
{databaseTypeToLabelMap['sqlite']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
{databaseType !== DatabaseType.SQLITE && (
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
)}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>

View File

@@ -6,90 +6,127 @@ module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
prefix: '',
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
fontFamily: {
primary: ['"Raleway"', ...defaultTheme.fontFamily.sans],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
subtitle: 'hsl(var(--subtitle))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
scale: {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.05)' },
'100%': { transform: 'scale(1)' },
},
'scale-2': {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.05)' },
'100%': { transform: 'scale(1)' },
},
blink: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
scale: 'scale 1s ease-in-out 1',
'scale-2': 'scale-2 1s ease-in-out 2',
blink: 'blink 1s infinite',
},
},
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
fontFamily: {
primary: [
'Raleway"',
...defaultTheme.fontFamily.sans
]
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
subtitle: 'hsl(var(--subtitle))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
},
scale: {
'0%': {
transform: 'scale(1)'
},
'50%': {
transform: 'scale(1.05)'
},
'100%': {
transform: 'scale(1)'
}
},
'scale-2': {
'0%': {
transform: 'scale(1)'
},
'50%': {
transform: 'scale(1.05)'
},
'100%': {
transform: 'scale(1)'
}
},
blink: {
'0%, 100%': {
opacity: '1'
},
'50%': {
opacity: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
scale: 'scale 1s ease-in-out 1',
'scale-2': 'scale-2 1s ease-in-out 2',
blink: 'blink 1s infinite'
}
}
},
plugins: [require('tailwindcss-animate')],
};