Compare commits

..

61 Commits

Author SHA1 Message Date
johnnyfish
62155a49f9 fix: prevent Portal rendering in SelectBox to fix search input focus in dialogs 2025-08-10 12:07:58 +03:00
Guy Ben-Aharon
41ba251377 fix: update filter on adding table (#838) 2025-08-10 11:04:52 +03:00
Jonathan Fishner
e9c5442d9d feat(filter): filter tables by areas (#836)
* feat: auto-hide/show areas based on table visibility in canvas filter

* fix build

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-10 10:13:28 +03:00
Guy Ben-Aharon
4f1d3295c0 fix(filters): refactor diagram filters - remove schema filter (#832)
* refactor(filters): refactor diagram filters

* replace old filters

* fix storage

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-08-07 14:55:35 +03:00
Jonathan Fishner
5936500ca0 fix: for sqlite import - add more types & include type parameters (#834)
* fix: for sqlite import - add more types

* fix: preserve original data types in SQLite export instead of converting to storage classes

* fix: include type parameters (length, precision, scale) in SQLite export
2025-08-07 14:50:55 +03:00
Jonathan Fishner
43fc1d7fc2 feat: include foreign keys inline in SQLite CREATE TABLE statements (#833) 2025-08-07 11:55:15 +03:00
Guy Ben-Aharon
8dfa7cc62e chore(main): release 1.14.0 (#758) 2025-08-05 12:24:38 +03:00
Guy Ben-Aharon
23e93bfd01 fix: area resizers size (#830) 2025-08-04 20:56:19 +03:00
Guy Ben-Aharon
16f9f4671e fix(dbml): dbml indentation (#829) 2025-08-04 20:32:48 +03:00
Guy Ben-Aharon
0c300e5e72 fix(dbml): fix schemas with same table names (#828) 2025-08-04 17:50:28 +03:00
Guy Ben-Aharon
b9a1e78b53 fix(dbml): import dbml notes (table + fields) (#827) 2025-08-04 12:43:02 +03:00
Guy Ben-Aharon
337f7cdab4 fix(dbml): dbml note syntax (#826) 2025-08-04 12:21:37 +03:00
Guy Ben-Aharon
1b0390f0b7 feat(dbml): Edit Diagram Directly from DBML (#819)
* initial dbml apply

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-08-04 11:35:34 +03:00
Guy Ben-Aharon
bc52933b58 fix: update relationship when table width changes via expand/shrink (#825) 2025-08-03 22:41:11 +03:00
Guy Ben-Aharon
2fdad2344c solve issue with multiple render of tables (#824) 2025-08-03 22:13:18 +03:00
Guy Ben-Aharon
0c7eaa2df2 fix: solve issue with multiple render of tables (#823) 2025-08-03 21:52:17 +03:00
Guy Ben-Aharon
a5f8e56b3c fix(dbml): support multiple relationships on same field in inline DBML (#822) 2025-08-03 12:04:05 +03:00
Guy Ben-Aharon
8ffde62c1a fix(readonly): fix zoom out on readonly (#818) 2025-07-31 15:27:38 +03:00
Guy Ben-Aharon
39247b77a2 feat: enhance primary key and unique field handling logic (#817) 2025-07-31 11:38:33 +03:00
Guy Ben-Aharon
984b2aeee2 fix(ui): reduce spacing between primary key icon and short field types (#816) 2025-07-31 11:04:48 +03:00
Guy Ben-Aharon
eed104be5b fix(dbml): fix dbml output format (#815) 2025-07-30 14:31:56 +03:00
Guy Ben-Aharon
00bd535b3c fix(dbml import): fix dbml import types + schemas (#808)
* fix(dbml import): fix dbml import types + schemas

* fix(dbml import): fix dbml import types + schemas

* fix(dbml import): fix dbml import types + schemas

* fix
2025-07-29 17:55:29 +03:00
Guy Ben-Aharon
18e914242f fix(dbml export): fix handle tables with same name under different schemas (#807) 2025-07-29 16:22:09 +03:00
Guy Ben-Aharon
e68837a34a fix(dbml export): handle tables with same name under different schemas (#806) 2025-07-29 14:59:08 +03:00
Guy Ben-Aharon
b30162d98b fix: clone of custom types (#804) 2025-07-29 12:49:28 +03:00
Guy Ben-Aharon
dba372d25a fix(cockroachdb): support schema creation for cockroachdb (#803) 2025-07-28 18:55:05 +03:00
Jonathan Fishner
2eb48e75d3 fix(i18n): add Croatian (hr) language support (#802)
* feat: add Croatian (hr) language support

* fix translation

* fix: change langs order

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-28 18:12:17 +03:00
Guy Ben-Aharon
867903cd5f feat(schema): support create new schema (#801)
* feat(schema): support create new schema

* fix

* fix
2025-07-28 17:32:28 +03:00
Guy Ben-Aharon
8aeb1df0ad fix: fix screen freeze after schema select (#800)
* fix: fix screen freeze after schema select

* fix: fix screen freeze after schema select
2025-07-28 12:00:34 +03:00
Guy Ben-Aharon
6bea827293 fix(canvas filter): improve scroller on canvas filter (#799) 2025-07-28 11:48:45 +03:00
Guy Ben-Aharon
a119854da7 fix(dbml actions): set dbml tooltips side (#798) 2025-07-28 10:22:35 +03:00
Jonathan Fishner
bfbfd7b843 fix(dbml editor): move tooltips button to be on the right (#797) 2025-07-27 23:11:34 +03:00
Guy Ben-Aharon
0ca7008735 fix(dbml field comments): support export field comments in dbml (#796)
* fix(dbml field comments): support export field comments in dbml

* add tests
2025-07-27 20:53:55 +03:00
Guy Ben-Aharon
4bc71c52ff fix(scroll): disable scroll x behavior (#795) 2025-07-27 20:15:04 +03:00
Guy Ben-Aharon
8f27f10dec fix(dbml): support spaces in names (#794) 2025-07-27 19:44:43 +03:00
Guy Ben-Aharon
a93ec2cab9 fix: lost in canvas button animation (#793) 2025-07-27 17:34:48 +03:00
Jonathan Fishner
386e40a0bf fix: update MariaDB database import smart query (#792) 2025-07-27 16:29:16 +03:00
Jonathan Fishner
bda150d4b6 feat: add floating "Show All" button when tables are out of view (#787)
* feat: add floating "Show All" button when tables are out of view

* fix view of show all for mobile

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-27 16:25:14 +03:00
Guy Ben-Aharon
87836e53d1 fix: remove unnecessary import (#791) 2025-07-27 12:29:19 +03:00
Jonathan Fishner
7e0483f1a5 feat(custom-types): add highlight fields option for custom types (#726)
* feat(custom-types): add highlight feilds option for custom types

* fix(custom-types): show indicator when custom type is in used

* feat(canvas): add enum highlight indicator with pulse animation and double-click to clear

* some fixes

* some fixes

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-27 12:17:57 +03:00
Jonathan Fishner
309ee9cb0f fix(dbml-export): merge field attributes into single brackets and fix schema syntax (#790)
* fix(dbml-export): merge field attributes into single brackets and fix schema syntax

* fix build
2025-07-26 22:03:02 +03:00
Jonathan Fishner
79b885502e fix(sql-server): improvment for sql-server import via sql script (#789)
* feat: improvment for sql-server import via sql script

* fix for test

* some fixes

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-25 19:16:35 +03:00
Guy Ben-Aharon
745bdee86d fix(table-node): fix comment icon on field (#786) 2025-07-24 17:27:23 +03:00
Guy Ben-Aharon
08eb9cc55f fix(table-node): improve field spacing (#785) 2025-07-24 16:41:45 +03:00
Jonathan Fishner
778f85d492 feat(datatypes): Add decimal / numeric attribute support + organize field row (#715)
* added decimal scale and precision support

* update i18n

* added button to reset - made values always enabled in pairs

* made button use ml

* added fix for when manually defined scales are set to 0

* fix

* some fixes

* some fixes

* some fixes

* some fixes

* some fixes

---------

Co-authored-by: Alexander Harris <mcalapurge@techie.com>
Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-24 15:18:33 +03:00
Guy Ben-Aharon
fb92be7d3e alignment sql export scripts (#784) 2025-07-23 21:00:52 +03:00
Jonathan Fishner
6df588f40e fix: improve SQL export formatting and add schema-aware FK grouping (#783)
* fix: correct foreign key direction based on relationship cardinality in SQL exports

* fix: improve SQL export formatting and add schema-aware FK grouping

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-23 18:05:59 +03:00
Guy Ben-Aharon
b46ed58dff fix(table-select): add loading indication for import (#782) 2025-07-23 15:53:26 +03:00
Jonathan Fishner
0d9f57a9c9 feat: add table selection for large database imports (#776)
* feat: add table selection UI for large database imports (>50 tables)

* some changes

* some changes

* some changes

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-23 11:35:27 +03:00
Guy Ben-Aharon
b7dbe54c83 fix(canvas): fix filter eye button (#780) 2025-07-21 19:01:52 +03:00
Guy Ben-Aharon
43d1dfff71 fix: fix hotkeys on form elements (#778) 2025-07-21 17:31:42 +03:00
Guy Ben-Aharon
9949a46ee3 fix: set focus on filter search (#775) 2025-07-21 16:18:18 +03:00
Guy Ben-Aharon
dfbcf05b2f feat(canvas): Add filter tables on canvas (#774)
* feat(canvas): filter tables on canvas

* fix build

* fix

* fix
2025-07-21 15:54:27 +03:00
Jonathan Fishner
f56fab9876 fix: update multiple schemas toast to require user action (#771)
* fix: update multiple schemas toast to require user action

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 12:43:57 +03:00
Jonathan Fishner
c9ea7da092 feat(default value): add default value option to table field settings (#770)
* feat: add default value option to table field settings

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 12:29:53 +03:00
Jonathan Fishner
22d46e1e90 fix(dbml-import): handle unsupported DBML features and add comprehensive tests (#766)
* fix(dbml-import): handle unsupported DBML features and add comprehensive tests

* fix build

* fix(dbml-export): handle composite primary keys, invalid defaults, and char type formatting

* fix build

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 11:33:34 +03:00
Guy Ben-Aharon
6af94afc56 fix(area): redo/undo after dragging an area with tables (#767) 2025-07-17 16:49:05 +03:00
Jonathan Fishner
f7f92903de fix(sql-export): escape newlines and quotes in multi-line comments (#765) 2025-07-16 15:18:52 +03:00
Jonathan Fishner
b35e17526b feat: implement area grouping with parent-child relationships (#762)
* feat: implement area grouping with parent-child relationships

* fix: improve area node visual appearance and text visibility

* update area header color

* fix build

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-16 15:11:16 +03:00
Guy Ben-Aharon
bf32c08d37 fix: remove error lag after autofix (#764) 2025-07-14 21:27:05 +03:00
Jonathan Fishner
5d337409d6 fix: add PostgreSQL tests and fix parsing SQL (#760)
* feat: add PostgreSQL tests and fix parsing SQL

* fix: disable format on paste for SQL DDL import

* some ui fixes + add tests to the ci

* fix

* fix validator and importer

* fix for maria-db

* fix

* remove improved

* fix

* fix

* fix

* fix for test

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-14 19:25:44 +03:00
221 changed files with 22490 additions and 12311 deletions

View File

@@ -24,4 +24,7 @@ jobs:
run: npm run lint
- name: Build
run: npm run build
run: npm run build
- name: Run tests
run: npm run test:ci

View File

@@ -1,97 +0,0 @@
# Smart Pre-commit Hooks
This directory contains intelligent pre-commit hooks that run relevant tests based on the files being committed.
## Features
- **Smart Test Detection**: Automatically detects which tests to run based on changed files
- **Configurable Mappings**: Easy to configure via `test-mapping.json` (optional)
- **Performance Optimized**: Only runs tests for affected code
- **Skip Option**: Temporarily skip tests when needed
- **Progressive Enhancement**: Works without dependencies, enhanced with `jq` if available
## How It Works
1. **Linting**: Always runs linting first
2. **File Analysis**: Examines staged files to determine which are SQL import related
3. **Test Selection**: Maps changed files to relevant test suites
4. **Test Execution**: Runs only the necessary tests
## Configuration
The test runner works in two modes:
### Basic Mode (No Dependencies)
- Uses built-in patterns for common SQL import files
- Works out of the box without any additional tools
### Enhanced Mode (With `jq`)
- Reads configuration from `test-mapping.json`
- Allows custom patterns and mappings
- More flexible and maintainable
### Automatic Behaviors
- **Documentation Changes**: Tests are automatically skipped for .md, .txt, and .rst files
- **Verbose Output**: Always shows matched files and test paths for better visibility
## File Mappings
Built-in mappings:
- PostgreSQL import files → PostgreSQL tests
- MySQL import files → MySQL tests
- SQLite import files → SQLite tests
- SQL Server import files → SQL Server tests
- Common SQL files → All dialect tests
- SQL validator → PostgreSQL tests
## Usage
### Normal Operation
Just commit as usual. The hooks will automatically run relevant tests.
### Skip Tests Temporarily
```bash
# Create skip file
touch .husky/.skip-tests
# Commit without tests
git commit -m "WIP: debugging"
# Remove skip file to re-enable
rm .husky/.skip-tests
```
### Customize Mappings
1. Install `jq`: `brew install jq` (macOS) or `apt-get install jq` (Linux)
2. Edit `test-mapping.json` to add new patterns or modify existing ones
## Requirements
- **Required**: None (works with bash only)
- **Optional**: `jq` for JSON configuration support
## Examples
### Example 1: PostgreSQL Parser Change
```bash
# Changed: src/lib/data/sql-import/dialect-importers/postgresql/postgresql-improved.ts
# Runs: src/lib/data/sql-import/dialect-importers/postgresql/__tests__
```
### Example 2: Common SQL Import Change
```bash
# Changed: src/lib/data/sql-import/common.ts
# Runs: All dialect tests (PostgreSQL, MySQL, SQLite, SQL Server)
```
### Example 3: Test File Change
```bash
# Changed: src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-types.test.ts
# Runs: That specific test file
```
## Troubleshooting
1. **Tests not running**: Check if `.husky/.skip-tests` exists
2. **Wrong tests running**: Check `test-mapping.json` patterns
3. **All tests running**: You may have exceeded the change threshold

View File

@@ -1,13 +1,2 @@
#!/bin/sh
# Run linting first
npm run lint || { echo "lint failed, please run \"npm run lint:fix\" to fix the errors." ; exit 1; }
# Check if tests should be skipped
if [ -f .husky/.skip-tests ]; then
echo "⚠️ Tests skipped (remove .husky/.skip-tests to enable)"
exit 0
fi
# Run smart test runner for SQL import related changes
.husky/smart-test-runner.sh || exit 1

View File

@@ -1,214 +0,0 @@
#!/usr/bin/env bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
CONFIG_FILE="$SCRIPT_DIR/test-mapping.json"
# Get list of staged files
STAGED_FILES=$(git diff --cached --name-only)
# Check if only documentation files are staged
DOC_ONLY=true
NON_DOC_COUNT=0
while IFS= read -r file; do
[ -z "$file" ] && continue
if [[ ! "$file" =~ \.(md|txt|rst)$ ]]; then
DOC_ONLY=false
((NON_DOC_COUNT++))
fi
done <<< "$STAGED_FILES"
# Skip tests if only docs are changed
if [ "$DOC_ONLY" = "true" ]; then
echo -e "${YELLOW} Only documentation files changed, skipping tests.${NC}"
exit 0
fi
# Initialize test tracking
TESTS_TO_RUN=""
MATCHED_FILES=()
# Function to add test path
add_test() {
local test_path=$1
if [ -d "$test_path" ] || [ -f "$test_path" ]; then
# Add to list if not already present
if [[ ! "$TESTS_TO_RUN" =~ "$test_path" ]]; then
if [ -z "$TESTS_TO_RUN" ]; then
TESTS_TO_RUN="$test_path"
else
TESTS_TO_RUN="$TESTS_TO_RUN $test_path"
fi
fi
fi
}
# Function to check if file matches pattern (simple glob matching)
matches_pattern() {
local file=$1
local pattern=$2
# Use bash pattern matching
case "$file" in
$pattern) return 0 ;;
*) return 1 ;;
esac
}
# Always verbose by default
VERBOSE=true
# Process files based on available tools
if command -v jq &> /dev/null && [ -f "$CONFIG_FILE" ]; then
echo -e "${YELLOW}Using configuration from test-mapping.json${NC}"
# Process each staged file
while IFS= read -r file; do
[ -z "$file" ] && continue
# Check against each mapping rule
jq -c '.mappings[]' "$CONFIG_FILE" 2>/dev/null | while read -r mapping; do
name=$(echo "$mapping" | jq -r '.name')
# Check patterns
echo "$mapping" | jq -r '.patterns[]' | while read -r pattern; do
if matches_pattern "$file" "$pattern"; then
# Check exclusions
excluded=false
echo "$mapping" | jq -r '.excludePatterns[]?' 2>/dev/null | while read -r exclude; do
if matches_pattern "$file" "$exclude"; then
excluded=true
break
fi
done
if [ "$excluded" = "false" ]; then
[ "$VERBOSE" = "true" ] && echo -e "${GREEN}✓ Matched rule '$name' for file: $file${NC}"
MATCHED_FILES+=("$file")
# Add tests for this mapping
echo "$mapping" | jq -r '.tests[]' | while read -r test_path; do
[ -n "$test_path" ] && echo "$test_path" >> /tmp/test_paths_$$
done
fi
break
fi
done
done
done <<< "$STAGED_FILES"
# Read test paths from temp file
if [ -f /tmp/test_paths_$$ ]; then
while read -r test_path; do
add_test "$test_path"
done < /tmp/test_paths_$$
rm -f /tmp/test_paths_$$
fi
else
echo -e "${YELLOW}Using built-in patterns (install jq for config file support)${NC}"
# Fallback to hardcoded patterns
while IFS= read -r file; do
[ -z "$file" ] && continue
case "$file" in
# PostgreSQL import files
src/lib/data/sql-import/dialect-importers/postgresql/*.ts)
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
[ "$VERBOSE" = "true" ] && echo "📝 Changed PostgreSQL import file: $file"
MATCHED_FILES+=("$file")
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
fi
;;
# MySQL import files
src/lib/data/sql-import/dialect-importers/mysql/*.ts)
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
[ "$VERBOSE" = "true" ] && echo "📝 Changed MySQL import file: $file"
MATCHED_FILES+=("$file")
add_test "src/lib/data/sql-import/dialect-importers/mysql/__tests__"
fi
;;
# SQLite import files
src/lib/data/sql-import/dialect-importers/sqlite/*.ts)
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQLite import file: $file"
MATCHED_FILES+=("$file")
add_test "src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
fi
;;
# SQL Server import files
src/lib/data/sql-import/dialect-importers/sql-server/*.ts)
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]]; then
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQL Server import file: $file"
MATCHED_FILES+=("$file")
add_test "src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
fi
;;
# Common SQL import files
src/lib/data/sql-import/*.ts)
if [[ ! "$file" =~ \.test\.ts$ ]] && [[ ! "$file" =~ \.spec\.ts$ ]] && [[ ! "$file" =~ /dialect-importers/ ]]; then
[ "$VERBOSE" = "true" ] && echo "📝 Changed common SQL import file: $file"
MATCHED_FILES+=("$file")
# Run all dialect tests if common files change
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
add_test "src/lib/data/sql-import/dialect-importers/mysql/__tests__"
add_test "src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
add_test "src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
fi
;;
# SQL validator
src/lib/data/sql-import/sql-validator.ts)
[ "$VERBOSE" = "true" ] && echo "📝 Changed SQL validator"
MATCHED_FILES+=("$file")
add_test "src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
;;
# Test files themselves
src/lib/data/sql-import/**/*.test.ts|src/lib/data/sql-import/**/*.spec.ts)
[ "$VERBOSE" = "true" ] && echo "📝 Changed test file: $file"
MATCHED_FILES+=("$file")
add_test "$file"
;;
esac
done <<< "$STAGED_FILES"
fi
# Run tests if any were found
if [ -n "$TESTS_TO_RUN" ]; then
echo ""
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${YELLOW}🧪 Running SQL import tests...${NC}"
[ "$VERBOSE" = "true" ] && echo -e "Matched files: ${#MATCHED_FILES[@]}"
[ "$VERBOSE" = "true" ] && echo -e "Test paths: $TESTS_TO_RUN"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Run the tests
npm test -- $TESTS_TO_RUN --run
TEST_RESULT=$?
if [ $TEST_RESULT -ne 0 ]; then
echo ""
echo -e "${RED}❌ SQL import tests failed! Please fix the tests before committing.${NC}"
exit 1
else
echo ""
echo -e "${GREEN}✅ SQL import tests passed!${NC}"
fi
else
echo -e "${YELLOW} No SQL import related changes detected, skipping SQL import tests.${NC}"
fi
exit 0

View File

@@ -1,95 +0,0 @@
{
"mappings": [
{
"name": "PostgreSQL Import",
"patterns": [
"src/lib/data/sql-import/dialect-importers/postgresql/*.ts"
],
"excludePatterns": [
"*.test.ts",
"*.spec.ts"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
]
},
{
"name": "MySQL Import",
"patterns": [
"src/lib/data/sql-import/dialect-importers/mysql/*.ts"
],
"excludePatterns": [
"*.test.ts",
"*.spec.ts"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/mysql/__tests__"
]
},
{
"name": "SQLite Import",
"patterns": [
"src/lib/data/sql-import/dialect-importers/sqlite/*.ts"
],
"excludePatterns": [
"*.test.ts",
"*.spec.ts"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/sqlite/__tests__"
]
},
{
"name": "SQL Server Import",
"patterns": [
"src/lib/data/sql-import/dialect-importers/sql-server/*.ts"
],
"excludePatterns": [
"*.test.ts",
"*.spec.ts"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
]
},
{
"name": "Common SQL Import",
"patterns": [
"src/lib/data/sql-import/*.ts",
"src/lib/data/sql-import/common/*.ts"
],
"excludePatterns": [
"*.test.ts",
"*.spec.ts",
"*/dialect-importers/*"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__",
"src/lib/data/sql-import/dialect-importers/mysql/__tests__",
"src/lib/data/sql-import/dialect-importers/sqlite/__tests__",
"src/lib/data/sql-import/dialect-importers/sql-server/__tests__"
]
},
{
"name": "SQL Validator",
"patterns": [
"src/lib/data/sql-import/sql-validator.ts"
],
"tests": [
"src/lib/data/sql-import/dialect-importers/postgresql/__tests__"
]
},
{
"name": "Import Dialog",
"patterns": [
"src/dialogs/common/import-database/*.tsx",
"src/dialogs/common/import-database/*.ts"
],
"excludePatterns": [
"*.test.tsx",
"*.spec.tsx"
],
"tests": []
}
]
}

View File

@@ -1,5 +1,68 @@
# Changelog
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
### Features
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
### Bug Fixes
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="max-image-preview:large" />
<title>ChartDB - Create & Visualize Database Schema Diagrams</title>
<link rel="canonical" href="https://chartdb.io" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link

174
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.13.2",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.13.2",
"version": "1.14.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
@@ -28,14 +28,14 @@
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-slot": "^1.2.3",
"@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.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",
@@ -46,8 +46,9 @@
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
"react": "^18.3.1",
@@ -2254,6 +2255,24 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"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"
},
"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-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@@ -2967,7 +2986,7 @@
}
}
},
"node_modules/@radix-ui/react-slot": {
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"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==",
@@ -2985,6 +3004,39 @@
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"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-slot/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"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-tabs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
@@ -3266,6 +3318,24 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"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"
},
"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-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",
@@ -4533,12 +4603,12 @@
"license": "MIT"
},
"node_modules/@xyflow/react": {
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
"integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.50",
"@xyflow/system": "0.0.66",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -4548,16 +4618,18 @@
}
},
"node_modules/@xyflow/system": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
"integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
"version": "0.0.66",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
@@ -6767,6 +6839,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.6.tgz",
"integrity": "sha512-dsJ389QImVE3lQvM8Mnk99/j8tiZDM/7706PCqvkQ8sSCnpmWxsgX+g0lj7r5OBVL0U36pIecCTBoIWcM2RuKw==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.6",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -8052,12 +8151,12 @@
}
},
"node_modules/lucide-react": {
"version": "0.441.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz",
"integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==",
"version": "0.525.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
"integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
@@ -8198,6 +8297,47 @@
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"license": "MIT"
},
"node_modules/motion": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.6.tgz",
"integrity": "sha512-6U55IW5i6Vut2ryKEhrZKg55490k9d6qdGXZoNSf98oQgDj5D7bqTnVJotQ6UW3AS6QfbW6KSLa7/e1gy+a07g==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.6.tgz",
"integrity": "sha512-G2w6Nw7ZOVSzcQmsdLc0doMe64O/Sbuc2bVAbgMz6oP/6/pRStKRiVRV4bQfHp5AHYAKEGhEdVHTM+R3FDgi5w==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.13.2",
"version": "1.14.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,6 +11,7 @@
"preview": "vite preview",
"prepare": "husky",
"test": "vitest",
"test:ci": "vitest run --reporter=verbose --bail=1",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
@@ -35,14 +36,14 @@
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-slot": "^1.2.3",
"@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.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",
@@ -53,8 +54,9 @@
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
"react": "^18.3.1",

View File

@@ -1,166 +0,0 @@
# PostgreSQL vs MySQL Parser Comparison Analysis
## Overview
This document compares how the PostgreSQL and MySQL parsers in ChartDB handle SQL parsing, focusing on the differences that could cause the same SQL file to produce different results.
## 1. SQL Sanitization and Comment Handling
### PostgreSQL Parser (`postgresql-improved.ts`)
#### Comment Removal Strategy:
1. **Order**: Comments are removed FIRST, before any other processing
2. **Multi-line comments**: Removed using regex: `/\/\*[\s\S]*?\*\//g`
3. **Single-line comments**: Removed line-by-line, checking for `--` while respecting string boundaries
4. **String-aware**: Preserves `--` inside quoted strings
```typescript
// PostgreSQL approach (lines 60-100)
// 1. First removes ALL multi-line comments
cleanedSQL = cleanedSQL.replace(/\/\*[\s\S]*?\*\//g, '');
// 2. Then processes single-line comments while respecting strings
for (let i = 0; i < line.length; i++) {
// Tracks if we're inside a string to avoid removing -- inside quotes
}
```
### MySQL Parser (`mysql-improved.ts`)
#### Comment Removal Strategy:
1. **Order**: Comments are sanitized but with special handling for problematic patterns
2. **Special handling**: Specifically fixes multi-line comments that contain quotes or JSON
3. **Line-by-line**: Processes comments line by line, removing lines that start with `--` or `#`
```typescript
// MySQL approach (lines 35-67)
// 1. First fixes specific problematic patterns
result = result.replace(/--\s*"[^"]*",?\s*\n\s*"[^"]*".*$/gm, function(match) {
return match.replace(/\n/g, ' ');
});
// 2. Then removes comment lines entirely
.map((line) => {
if (trimmed.startsWith('--') || trimmed.startsWith('#')) {
return '';
}
return line;
})
```
**Key Difference**: PostgreSQL removes ALL comments upfront, while MySQL tries to fix problematic comment patterns first, then removes comment lines.
## 2. Order of Operations
### PostgreSQL Parser
1. **Preprocess SQL** (removes all comments first)
2. **Split statements** by semicolons (handles dollar quotes)
3. **Categorize statements** (table, index, alter, etc.)
4. **Parse with node-sql-parser**
5. **Fallback to regex** if parser fails
6. **Extract relationships**
### MySQL Parser
1. **Validate syntax** (checks for known issues)
2. **Sanitize SQL** (fixes problematic patterns)
3. **Extract statements** by semicolons
4. **Parse with node-sql-parser**
5. **Fallback to regex** if parser fails
6. **Process relationships**
**Key Difference**: MySQL validates BEFORE sanitizing, while PostgreSQL sanitizes first. This means MySQL can detect and report issues that PostgreSQL might silently fix.
## 3. Multi-line Comment Handling
### PostgreSQL
- Removes ALL multi-line comments using `[\s\S]*?` pattern
- No special handling for comments containing quotes or JSON
- Clean removal before any parsing
### MySQL
- Specifically detects and fixes multi-line comments with quotes:
```sql
-- "Beliebt",
"Empfohlen" -- This breaks MySQL parser
```
- Detects JSON arrays in comments spanning lines:
```sql
-- [
"Ubuntu 22.04",
"CentOS 8"
] -- This also breaks MySQL parser
```
- Converts these to single-line comments before parsing
**Key Difference**: MySQL has specific handling for problematic comment patterns that PostgreSQL simply removes entirely.
## 4. Statement Splitting
### PostgreSQL
- Handles PostgreSQL-specific dollar quotes (`$$ ... $$`)
- Tracks quote depth for proper splitting
- Supports function bodies with dollar quotes
### MySQL
- Simple quote tracking (single, double, backtick)
- Handles escape sequences (`\`)
- No special quote constructs
## 5. Validation Approach
### PostgreSQL
- No pre-validation
- Relies on parser and fallback regex
- Reports warnings for unsupported features
### MySQL
- Pre-validates SQL before parsing
- Detects known problematic patterns:
- Multi-line comments with quotes
- JSON arrays in comments
- Inline REFERENCES (PostgreSQL syntax)
- Missing semicolons
- Can reject SQL before attempting to parse
## 6. Why Same SQL Gives Different Results
### Example Problematic SQL:
```sql
CREATE TABLE products (
id INT PRIMARY KEY,
status VARCHAR(50), -- "active",
"inactive", "pending"
data JSON -- [
{"key": "value"},
{"key": "value2"}
]
);
```
### PostgreSQL Result:
- Successfully parses (comments are removed entirely)
- Table created with proper columns
### MySQL Result:
- Validation fails with errors:
- MULTILINE_COMMENT_QUOTE at line 3
- MULTILINE_JSON_COMMENT at line 5
- Import blocked unless validation is skipped
## 7. Recommendations
1. **For Cross-Database Compatibility**:
- Avoid multi-line comments with quotes or JSON
- Keep comments on single lines
- Use proper FOREIGN KEY syntax instead of inline REFERENCES
2. **For MySQL Import**:
- Fix validation errors before import
- Or use `skipValidation: true` option if SQL is known to work
3. **For PostgreSQL Import**:
- Be aware that comments are stripped entirely
- Complex comments might hide syntax issues
## Conclusion
The main difference is that PostgreSQL takes a "remove all comments first" approach, while MySQL tries to detect and handle problematic comment patterns. This makes PostgreSQL more forgiving but MySQL more explicit about potential issues. The same SQL file can succeed in PostgreSQL but fail in MySQL if it contains multi-line comments with special characters.

View File

@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
className?: string;
}
export interface CodeSnippetProps {
@@ -43,6 +44,8 @@ export interface CodeSnippetProps {
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
allowCopy?: boolean;
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -56,6 +59,8 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
isComplete = true,
editorProps,
actions,
actionsTooltipSide,
allowCopy = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -129,33 +134,37 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Suspense fallback={<Spinner />}>
{isComplete ? (
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
{allowCopy ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
) : null}
{actions &&
actions.length > 0 &&
@@ -164,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
className={cn(
'h-fit p-1.5',
action.className
)}
variant="outline"
onClick={action.onClick}
>
@@ -174,7 +186,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<TooltipContent
side={actionsTooltipSide}
>
{action.label}
</TooltipContent>
</Tooltip>

View File

@@ -0,0 +1,51 @@
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import * as monaco from 'monaco-editor';
export const highlightErrorLine = ({
error,
model,
editorDecorationsCollection,
}: {
error: DBMLError;
model?: monaco.editor.ITextModel | null;
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined;
}) => {
if (!model) return;
if (!editorDecorationsCollection) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
editorDecorationsCollection?.set(decorations);
};
export const clearErrorHighlight = (
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined
) => {
if (editorDecorationsCollection) {
editorDecorationsCollection.clear();
}
};

View File

@@ -37,18 +37,28 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
const datatypePattern = dataTypesNames.join('|');
monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes'],
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
datatypes: dataTypesNames,
tokenizer: {
root: [
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
[
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
'keyword',
],
[/\[.*?\]/, 'annotation'],
[/'''/, 'string', '@tripleQuoteString'],
[/".*?"/, 'string'],
[/'.*?'/, 'string'],
[/`.*?`/, 'string'],
[/[{}]/, 'delimiter'],
[/[<>]/, 'operator'],
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
],
tripleQuoteString: [
[/[^']+/, 'string'],
[/'''/, 'string', '@pop'],
[/'/, 'string'],
],
},
});
};

View File

@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
</Label>
<Label
className={cn(
'text-sm font-normal text-muted-foreground',
'text-sm text-center font-normal text-muted-foreground',
descriptionClassName
)}
>

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { cn } from '@/lib/utils';
import type { ButtonProps } from '../button/button';
import { buttonVariants } from '../button/button-variants';
import {
ChevronLeftIcon,
ChevronRightIcon,
DotsHorizontalIcon,
} from '@radix-ui/react-icons';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeftIcon className="size-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="size-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@@ -11,21 +11,40 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
container?: HTMLElement | null;
}
>(
(
{ className, align = 'center', sideOffset = 4, container, ...props },
ref
) => {
const Content = (
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
);
// If container is explicitly null, don't use Portal
if (container === null) {
return Content;
}
// Otherwise, use Portal (default behavior)
return (
<PopoverPrimitive.Portal container={container}>
{Content}
</PopoverPrimitive.Portal>
);
}
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -93,6 +93,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(isOpen: boolean) => {
setOpen?.(isOpen);
setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]
);
@@ -223,11 +229,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<CommandItem
className="flex items-center"
key={option.value}
value={option.label}
keywords={option.regex ? [option.regex] : undefined}
onSelect={() =>
handleSelect(
option.value,
matches?.map((match) => match.toString())
matches?.map((match) => match?.toString())
)
}
>
@@ -274,7 +281,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<Popover open={isOpen} onOpenChange={onOpenChange}>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<div
className={cn(
@@ -348,6 +355,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
popoverClassName
)}
align="center"
container={null}
>
<Command
filter={(value, search, keywords) => {
@@ -415,25 +423,30 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<CommandEmpty>
{emptyPlaceholder ?? 'No results found.'}
</CommandEmpty>
<ScrollArea>
<div className="max-h-64 w-full">
<CommandList className="max-h-fit w-full">
{hasGroups
? Object.entries(groups).map(
([groupName, groupOptions]) => (
<CommandGroup
key={groupName}
heading={groupName}
>
{groupOptions.map(
renderOption
)}
</CommandGroup>
)
)
: options.map(renderOption)}
</CommandList>
<div className="max-h-64">
{hasGroups ? (
Object.entries(groups).map(
([groupName, groupOptions]) => (
<CommandGroup
key={groupName}
heading={groupName}
>
<CommandList>
{groupOptions.map(
renderOption
)}
</CommandList>
</CommandGroup>
)
)
) : (
<CommandGroup>
<CommandList>
{options.map(renderOption)}
</CommandList>
</CommandGroup>
)}
</div>
</ScrollArea>
</Command>

View File

@@ -20,6 +20,7 @@ export function Toaster() {
description,
action,
layout = 'row',
hideCloseButton = false,
...props
}) {
return (
@@ -38,7 +39,7 @@ export function Toaster() {
) : null}
</div>
{layout === 'row' ? action : null}
<ToastClose />
{!hideCloseButton ? <ToastClose /> : null}
</Toast>
);
})}

View File

@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
description?: React.ReactNode;
action?: ToastActionElement;
layout?: 'row' | 'column';
hideCloseButton?: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Skeleton } from '../skeleton/skeleton';
import { cn } from '@/lib/utils';
export interface TreeItemSkeletonProps
extends React.HTMLAttributes<HTMLDivElement> {}
export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
className,
style,
}) => {
return (
<div className={cn('px-2 py-1', className)} style={style}>
<Skeleton className="h-3.5 w-full rounded-sm" />
</div>
);
};

View File

@@ -0,0 +1,461 @@
import {
ChevronRight,
File,
Folder,
Loader2,
type LucideIcon,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
import { Button } from '@/components/button/button';
import type {
TreeNode,
FetchChildrenFunction,
SelectableTreeProps,
} from './tree';
import type { ExpandedState } from './use-tree';
import { useTree } from './use-tree';
import type { Dispatch, ReactNode, SetStateAction } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { TreeItemSkeleton } from './tree-item-skeleton';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
interface TreeViewProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
data: TreeNode<Type, Context>[];
fetchChildren?: FetchChildrenFunction<Type, Context>;
onNodeClick?: (node: TreeNode<Type, Context>) => void;
className?: string;
defaultIcon?: LucideIcon;
defaultFolderIcon?: LucideIcon;
defaultIconProps?: React.ComponentProps<LucideIcon>;
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
selectable?: SelectableTreeProps<Type, Context>;
expanded?: ExpandedState;
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[];
}
export function TreeView<
Type extends string,
Context extends Record<Type, unknown>,
>({
data,
fetchChildren,
onNodeClick,
className,
defaultIcon = File,
defaultFolderIcon = Folder,
defaultIconProps,
defaultFolderIconProps,
selectable,
expanded: expandedProp,
setExpanded: setExpandedProp,
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
}: TreeViewProps<Type, Context>) {
const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
useTree({
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
});
const [selectedIdInternal, setSelectedIdInternal] = React.useState<
string | undefined
>(selectable?.defaultSelectedId);
const selectedId = useMemo(() => {
return selectable?.selectedId ?? selectedIdInternal;
}, [selectable?.selectedId, selectedIdInternal]);
const setSelectedId = useCallback(
(value: SetStateAction<string | undefined>) => {
if (selectable?.setSelectedId) {
selectable.setSelectedId(value);
} else {
setSelectedIdInternal(value);
}
},
[selectable, setSelectedIdInternal]
);
useEffect(() => {
if (selectable?.enabled && selectable.defaultSelectedId) {
if (selectable.defaultSelectedId === selectedId) return;
setSelectedId(selectable.defaultSelectedId);
const { node, path } = findNodeById(
data,
selectable.defaultSelectedId
);
if (node) {
selectable.onSelectedChange?.(node);
// Expand all parent nodes
for (const parent of path) {
if (expanded[parent.id]) continue;
toggleNode(
parent.id,
parent.type,
parent.context,
parent.children
);
}
}
}
}, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
const handleNodeSelect = (node: TreeNode<Type, Context>) => {
if (selectable?.enabled) {
setSelectedId(node.id);
selectable.onSelectedChange?.(node);
}
};
return (
<div className={cn('w-full', className)}>
{data.map((node, index) => (
<TreeNode
key={node.id}
node={node}
level={0}
expanded={expanded}
loading={loading}
loadedChildren={loadedChildren}
hasMoreChildren={hasMoreChildren}
onToggle={toggleNode}
onNodeClick={onNodeClick}
defaultIcon={defaultIcon}
defaultFolderIcon={defaultFolderIcon}
defaultIconProps={defaultIconProps}
defaultFolderIconProps={defaultFolderIconProps}
selectable={selectable?.enabled}
selectedId={selectedId}
onSelect={handleNodeSelect}
className={index > 0 ? 'mt-0.5' : ''}
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
/>
))}
</div>
);
}
interface TreeNodeProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
node: TreeNode<Type, Context>;
level: number;
expanded: Record<string, boolean>;
loading: Record<string, boolean>;
loadedChildren: Record<string, TreeNode<Type, Context>[]>;
hasMoreChildren: Record<string, boolean>;
onToggle: (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type],
staticChildren?: TreeNode<Type, Context>[]
) => void;
onNodeClick?: (node: TreeNode<Type, Context>) => void;
defaultIcon: LucideIcon;
defaultFolderIcon: LucideIcon;
defaultIconProps?: React.ComponentProps<LucideIcon>;
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
selectable?: boolean;
selectedId?: string;
onSelect: (node: TreeNode<Type, Context>) => void;
className?: string;
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[];
}
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
node,
level,
expanded,
loading,
loadedChildren,
hasMoreChildren,
onToggle,
onNodeClick,
defaultIcon: DefaultIcon,
defaultFolderIcon: DefaultFolderIcon,
defaultIconProps,
defaultFolderIconProps,
selectable,
selectedId,
onSelect,
className,
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
}: TreeNodeProps<Type, Context>) {
const [isHovered, setIsHovered] = useState(false);
const isExpanded = expanded[node.id];
const isLoading = loading[node.id];
const children = loadedChildren[node.id] || node.children;
const isSelected = selectedId === node.id;
const IconComponent =
node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
const iconProps: React.ComponentProps<LucideIcon> = {
strokeWidth: isSelected ? 2.5 : 2,
...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
...node.iconProps,
className: cn(
'h-3.5 w-3.5 text-muted-foreground flex-none',
isSelected && 'text-primary text-white',
node.iconProps?.className
),
};
return (
<div className={cn(className)}>
<div
className={cn(
'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
'transition-colors duration-200',
isSelected
? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
: 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
node.className
)}
{...(isSelected ? { 'data-selected': true } : {})}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={(e) => {
e.stopPropagation();
if (selectable && !node.unselectable) {
onSelect(node);
}
// if (node.isFolder) {
// onToggle(node.id, node.children);
// }
// called only once in case of double click
if (e.detail !== 2) {
onNodeClick?.(node);
}
}}
onDoubleClick={(e) => {
e.stopPropagation();
if (node.isFolder) {
onToggle(
node.id,
node.type,
node.context,
node.children
);
}
}}
>
<div className="flex flex-none items-center gap-1.5">
<Button
variant="ghost"
size="icon"
className={cn(
'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
isExpanded && 'rotate-90',
'transition-transform duration-200'
)}
onClick={(e) => {
e.stopPropagation();
if (node.isFolder) {
onToggle(
node.id,
node.type,
node.context,
node.children
);
}
}}
>
{node.isFolder &&
(isLoading ? (
<Loader2
className={cn('size-3.5 animate-spin', {
'text-white': isSelected,
})}
/>
) : (
<ChevronRight
className={cn('size-3.5', {
'text-white': isSelected,
})}
strokeWidth={2}
/>
))}
</Button>
{node.tooltip ? (
<Tooltip>
<TooltipTrigger asChild>
{loadingNodeIds?.includes(node.id) ? (
<Loader2
className={cn('size-3.5 animate-spin', {
'text-white': isSelected,
})}
/>
) : (
<IconComponent
{...(isSelected
? { 'data-selected': true }
: {})}
{...iconProps}
/>
)}
</TooltipTrigger>
<TooltipContent
align="center"
className="max-w-[400px]"
>
{node.tooltip}
</TooltipContent>
</Tooltip>
) : node.empty ? null : loadingNodeIds?.includes(
node.id
) ? (
<Loader2
className={cn('size-3.5 animate-spin', {
// 'text-white': isSelected,
})}
/>
) : (
<IconComponent
{...(isSelected ? { 'data-selected': true } : {})}
{...iconProps}
/>
)}
</div>
<span
{...node.labelProps}
className={cn(
'text-xs truncate min-w-0 flex-1 w-0',
isSelected && 'font-medium text-primary text-white',
node.labelProps?.className
)}
{...(isSelected ? { 'data-selected': true } : {})}
>
{node.empty ? '' : node.name}
</span>
{renderActionsComponent && renderActionsComponent(node)}
{isHovered && renderHoverComponent
? renderHoverComponent(node)
: null}
</div>
<AnimatePresence initial={false}>
{isExpanded && children && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: 'auto',
opacity: 1,
transition: {
height: {
duration: Math.min(
0.3 + children.length * 0.018,
0.7
),
ease: 'easeInOut',
},
opacity: {
duration: Math.min(
0.2 + children.length * 0.012,
0.4
),
ease: 'easeInOut',
},
},
}}
exit={{
height: 0,
opacity: 0,
transition: {
height: {
duration: Math.min(
0.2 + children.length * 0.01,
0.45
),
ease: 'easeInOut',
},
opacity: {
duration: 0.1,
ease: 'easeOut',
},
},
}}
style={{ overflow: 'hidden' }}
>
{children.map((child) => (
<TreeNode
key={child.id}
node={child}
level={level + 1}
expanded={expanded}
loading={loading}
loadedChildren={loadedChildren}
hasMoreChildren={hasMoreChildren}
onToggle={onToggle}
onNodeClick={onNodeClick}
defaultIcon={DefaultIcon}
defaultFolderIcon={DefaultFolderIcon}
defaultIconProps={defaultIconProps}
defaultFolderIconProps={defaultFolderIconProps}
selectable={selectable}
selectedId={selectedId}
onSelect={onSelect}
className="mt-0.5"
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
/>
))}
{isLoading ? (
<TreeItemSkeleton
style={{
paddingLeft: `${level + 2 * 16 + 8}px`,
}}
/>
) : null}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function findNodeById<
Type extends string,
Context extends Record<Type, unknown>,
>(
nodes: TreeNode<Type, Context>[],
id: string,
initialPath: TreeNode<Type, Context>[] = []
): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
const path: TreeNode<Type, Context>[] = [...initialPath];
for (const node of nodes) {
if (node.id === id) return { node, path };
if (node.children) {
const found = findNodeById(node.children, id, [...path, node]);
if (found.node) {
return found;
}
}
}
return { node: null, path };
}

View File

@@ -0,0 +1,41 @@
import type { LucideIcon } from 'lucide-react';
import type React from 'react';
export interface TreeNode<
Type extends string,
Context extends Record<Type, unknown>,
> {
id: string;
name: string;
isFolder?: boolean;
children?: TreeNode<Type, Context>[];
icon?: LucideIcon;
iconProps?: React.ComponentProps<LucideIcon>;
labelProps?: React.ComponentProps<'span'>;
type: Type;
unselectable?: boolean;
tooltip?: string;
context: Context[Type];
empty?: boolean;
className?: string;
}
export type FetchChildrenFunction<
Type extends string,
Context extends Record<Type, unknown>,
> = (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type]
) => Promise<TreeNode<Type, Context>[]>;
export interface SelectableTreeProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
enabled: boolean;
defaultSelectedId?: string;
onSelectedChange?: (node: TreeNode<Type, Context>) => void;
selectedId?: string;
setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
}

View File

@@ -0,0 +1,153 @@
import type { Dispatch, SetStateAction } from 'react';
import { useState, useCallback, useMemo } from 'react';
import type { TreeNode, FetchChildrenFunction } from './tree';
export interface ExpandedState {
[key: string]: boolean;
}
interface LoadingState {
[key: string]: boolean;
}
interface LoadedChildren<
Type extends string,
Context extends Record<Type, unknown>,
> {
[key: string]: TreeNode<Type, Context>[];
}
interface HasMoreChildrenState {
[key: string]: boolean;
}
export function useTree<
Type extends string,
Context extends Record<Type, unknown>,
>({
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
}: {
fetchChildren?: FetchChildrenFunction<Type, Context>;
expanded?: ExpandedState;
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
}) {
const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
const expanded = useMemo(
() => expandedProp ?? expandedInternal,
[expandedProp, expandedInternal]
);
const setExpanded = useCallback(
(value: SetStateAction<ExpandedState>) => {
if (setExpandedProp) {
setExpandedProp(value);
} else {
setExpandedInternal(value);
}
},
[setExpandedProp, setExpandedInternal]
);
const [loading, setLoading] = useState<LoadingState>({});
const [loadedChildren, setLoadedChildren] = useState<
LoadedChildren<Type, Context>
>({});
const [hasMoreChildren, setHasMoreChildren] =
useState<HasMoreChildrenState>({});
const mergeChildren = useCallback(
(
staticChildren: TreeNode<Type, Context>[] = [],
fetchedChildren: TreeNode<Type, Context>[] = []
) => {
const fetchedChildrenIds = new Set(
fetchedChildren.map((child) => child.id)
);
const uniqueStaticChildren = staticChildren.filter(
(child) => !fetchedChildrenIds.has(child.id)
);
return [...uniqueStaticChildren, ...fetchedChildren];
},
[]
);
const toggleNode = useCallback(
async (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type],
staticChildren?: TreeNode<Type, Context>[]
) => {
if (expanded[nodeId]) {
// If we're collapsing, just update expanded state
setExpanded((prev) => ({ ...prev, [nodeId]: false }));
return;
}
// Get any previously fetched children
const previouslyFetchedChildren = loadedChildren[nodeId] || [];
// If we have static children, merge them with any previously fetched children
if (staticChildren?.length) {
const mergedChildren = mergeChildren(
staticChildren,
previouslyFetchedChildren
);
setLoadedChildren((prev) => ({
...prev,
[nodeId]: mergedChildren,
}));
// Only show "more loading" if we haven't fetched children before
setHasMoreChildren((prev) => ({
...prev,
[nodeId]: !previouslyFetchedChildren.length,
}));
}
// Set expanded state immediately to show static/previously fetched children
setExpanded((prev) => ({ ...prev, [nodeId]: true }));
// If we haven't loaded dynamic children yet
if (!previouslyFetchedChildren.length) {
setLoading((prev) => ({ ...prev, [nodeId]: true }));
try {
const fetchedChildren = await fetchChildren?.(
nodeId,
nodeType,
nodeContext
);
// Merge static and newly fetched children
const allChildren = mergeChildren(
staticChildren || [],
fetchedChildren
);
setLoadedChildren((prev) => ({
...prev,
[nodeId]: allChildren,
}));
setHasMoreChildren((prev) => ({
...prev,
[nodeId]: false,
}));
} catch (error) {
console.error('Error loading children:', error);
} finally {
setLoading((prev) => ({ ...prev, [nodeId]: false }));
}
}
},
[expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
);
return {
expanded,
loading,
loadedChildren,
hasMoreChildren,
toggleNode,
};
}

View File

@@ -12,6 +12,8 @@ export interface CanvasContext {
}) => void;
setOverlapGraph: (graph: Graph<string>) => void;
overlapGraph: Graph<string>;
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
showFilter: boolean;
}
export const canvasContext = createContext<CanvasContext>({
@@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({
fitView: emptyFn,
setOverlapGraph: emptyFn,
overlapGraph: createGraph(),
setShowFilter: emptyFn,
showFilter: false,
});

View File

@@ -1,26 +1,29 @@
import React, { type ReactNode, useCallback, useState } from 'react';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import {
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { adjustTablePositions } from '@/lib/domain/db-table';
import { useReactFlow } from '@xyflow/react';
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
import type { Graph } from '@/lib/graph';
import { createGraph } from '@/lib/graph';
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
interface CanvasProviderProps {
children: ReactNode;
}
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, filteredSchemas } =
const { tables, relationships, updateTablesState, databaseType } =
useChartDB();
const { filter } = useDiagramFilter();
const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
const [showFilter, setShowFilter] = useState(false);
const reorderTables = useCallback(
(
options: { updateHistory?: boolean } = {
@@ -30,9 +33,18 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
),
mode: 'all', // Use 'all' mode for manual reordering
mode: 'all',
});
const updatedOverlapGraph = findOverlappingTables({
@@ -67,7 +79,14 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
});
}, 500);
},
[filteredSchemas, relationships, tables, updateTablesState, fitView]
[
filter,
relationships,
tables,
updateTablesState,
fitView,
databaseType,
]
);
return (
@@ -77,6 +96,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
fitView,
setOverlapGraph,
overlapGraph,
setShowFilter,
showFilter,
}}
>
{children}

View File

@@ -78,8 +78,8 @@ export interface ChartDBContext {
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
filteredSchemas?: string[];
filterSchemas: (schemaIds: string[]) => void;
highlightedCustomType?: DBCustomType;
highlightCustomTypeId: (id?: string) => void;
// General operations
updateDiagramId: (id: string) => Promise<void>;
@@ -92,6 +92,10 @@ export interface ChartDBContext {
updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>;
updateDiagramData: (
diagram: Diagram,
options?: { forceUpdateStorage?: boolean }
) => Promise<void>;
// Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -289,8 +293,7 @@ export const chartDBContext = createContext<ChartDBContext>({
areas: [],
customTypes: [],
schemas: [],
filteredSchemas: [],
filterSchemas: emptyFn,
highlightCustomTypeId: emptyFn,
currentDiagram: {
id: '',
name: '',
@@ -308,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
loadDiagramFromData: emptyFn,
clearDiagramData: emptyFn,
deleteDiagram: emptyFn,
updateDiagramData: emptyFn,
// Database type operations
updateDatabaseType: emptyFn,

View File

@@ -17,7 +17,6 @@ import {
databasesWithSchemas,
schemaNameToSchemaId,
} from '@/lib/domain/db-schema';
import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
@@ -39,11 +38,12 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const dbStorage = useStorage();
let db = dbStorage;
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
@@ -65,8 +65,12 @@ export const ChartDBProvider: React.FC<
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? []
);
const { events: diffEvents } = useDiff();
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
useState<string>();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
setTables((tables) =>
@@ -122,34 +126,6 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
);
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
if (schemas.length === 0) {
return undefined;
}
const schemasFilterFromCache =
(schemasFilter[diagramId] ?? []).length === 0
? undefined // in case of empty filter, skip cache
: schemasFilter[diagramId];
return (
schemasFilterFromCache ?? [
schemas.find((s) => s.name === defaultSchemaName)?.id ??
schemas[0]?.id,
]
);
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
const currentDiagram: Diagram = useMemo(
() => ({
id: diagramId,
@@ -1106,12 +1082,15 @@ export const ChartDBProvider: React.FC<
const sourceFieldName = sourceField?.name ?? '';
const targetTable = getTable(targetTableId);
const targetTableSchema = targetTable?.schema;
const relationship: DBRelationship = {
id: generateId(),
name: `${sourceTableName}_${sourceFieldName}_fk`,
sourceSchema: sourceTable?.schema,
sourceTableId,
targetSchema: sourceTable?.schema,
targetSchema: targetTableSchema,
targetTableId,
sourceFieldId,
targetFieldId,
@@ -1516,22 +1495,37 @@ export const ChartDBProvider: React.FC<
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
);
const highlightCustomTypeId = useCallback(
(id?: string) => setHighlightedCustomTypeId(id),
[setHighlightedCustomTypeId]
);
const highlightedCustomType = useMemo(() => {
return highlightedCustomTypeId
? customTypes.find((type) => type.id === highlightedCustomTypeId)
: undefined;
}, [highlightedCustomTypeId, customTypes]);
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
useCallback(
async (diagram) => {
(diagram) => {
setDiagramId(diagram.id);
setDiagramName(diagram.name);
setDatabaseType(diagram.databaseType);
setDatabaseEdition(diagram.databaseEdition);
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setAreas(diagram?.areas ?? []);
setCustomTypes(diagram?.customTypes ?? []);
setTables(diagram.tables ?? []);
setRelationships(diagram.relationships ?? []);
setDependencies(diagram.dependencies ?? []);
setAreas(diagram.areas ?? []);
setCustomTypes(diagram.customTypes ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
setHighlightedCustomTypeId(undefined);
events.emit({ action: 'load_diagram', data: { diagram } });
resetRedoStack();
resetUndoStack();
},
[
setDiagramId,
@@ -1545,10 +1539,23 @@ export const ChartDBProvider: React.FC<
setCustomTypes,
setDiagramCreatedAt,
setDiagramUpdatedAt,
setHighlightedCustomTypeId,
events,
resetRedoStack,
resetUndoStack,
]
);
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? dbStorage : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, dbStorage, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
@@ -1724,10 +1731,9 @@ export const ChartDBProvider: React.FC<
areas,
currentDiagram,
schemas,
filteredSchemas,
events,
readonly,
filterSchemas,
updateDiagramData,
updateDiagramId,
updateDiagramName,
loadDiagram,
@@ -1784,6 +1790,8 @@ export const ChartDBProvider: React.FC<
removeCustomType,
removeCustomTypes,
updateCustomType,
highlightCustomTypeId,
highlightedCustomType,
}}
>
{children}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { ConfigContext } from './config-context';
import { useStorage } from '@/hooks/use-storage';
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { getConfig, updateConfig: updateDataConfig } = useStorage();
const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
const [config, setConfig] = useState<ChartDBConfig | undefined>();
useEffect(() => {
const loadConfig = async () => {
@@ -45,7 +45,12 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
};
return (
<ConfigContext.Provider value={{ config, updateConfig }}>
<ConfigContext.Provider
value={{
config,
updateConfig,
}}
>
{children}
</ConfigContext.Provider>
);

View File

@@ -0,0 +1,48 @@
import type { DBSchema } from '@/lib/domain';
import type {
DiagramFilter,
FilterTableInfo,
} from '@/lib/domain/diagram-filter/diagram-filter';
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export interface DiagramFilterContext {
filter?: DiagramFilter;
hasActiveFilter: boolean;
schemasDisplayed: DBSchema[];
clearSchemaIdsFilter: () => void;
clearTableIdsFilter: () => void;
setTableIdsFilterEmpty: () => void;
// reset
resetFilter: () => void;
toggleSchemaFilter: (schemaId: string) => void;
toggleTableFilter: (tableId: string) => void;
addSchemaToFilter: (schemaId: string) => void;
addTablesToFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
removeTablesFromFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
}
export const diagramFilterContext = createContext<DiagramFilterContext>({
hasActiveFilter: false,
clearSchemaIdsFilter: emptyFn,
clearTableIdsFilter: emptyFn,
setTableIdsFilterEmpty: emptyFn,
resetFilter: emptyFn,
toggleSchemaFilter: emptyFn,
toggleTableFilter: emptyFn,
addSchemaToFilter: emptyFn,
schemasDisplayed: [],
addTablesToFilter: emptyFn,
removeTablesFromFilter: emptyFn,
});

View File

@@ -0,0 +1,522 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { DiagramFilterContext } from './diagram-filter-context';
import { diagramFilterContext } from './diagram-filter-context';
import type {
DiagramFilter,
FilterTableInfo,
} from '@/lib/domain/diagram-filter/diagram-filter';
import {
reduceFilter,
spreadFilterTables,
} from '@/lib/domain/diagram-filter/diagram-filter';
import { useStorage } from '@/hooks/use-storage';
import { useChartDB } from '@/hooks/use-chartdb';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { schemaNameToSchemaId } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { diagramId, tables, schemas, databaseType, events } = useChartDB();
const { getDiagramFilter, updateDiagramFilter } = useStorage();
const [filter, setFilter] = useState<DiagramFilter>({});
const allSchemasIds = useMemo(() => {
return schemas.map((schema) => schema.id);
}, [schemas]);
const allTables: FilterTableInfo[] = useMemo(() => {
return tables.map(
(table) =>
({
id: table.id,
schemaId: table.schema
? schemaNameToSchemaId(table.schema)
: defaultSchemas[databaseType],
schema: table.schema ?? defaultSchemas[databaseType],
areaId: table.parentAreaId ?? undefined,
}) satisfies FilterTableInfo
);
}, [tables, databaseType]);
const diagramIdOfLoadedFilter = useRef<string | null>(null);
useEffect(() => {
if (diagramId && diagramId === diagramIdOfLoadedFilter.current) {
updateDiagramFilter(diagramId, filter);
}
}, [diagramId, filter, updateDiagramFilter]);
// Reset filter when diagram changes
useEffect(() => {
if (diagramIdOfLoadedFilter.current === diagramId) {
// If the diagramId hasn't changed, do not reset the filter
return;
}
const loadFilterFromStorage = async (diagramId: string) => {
if (diagramId) {
const storedFilter = await getDiagramFilter(diagramId);
setFilter(storedFilter ?? {});
}
};
setFilter({});
if (diagramId) {
loadFilterFromStorage(diagramId);
diagramIdOfLoadedFilter.current = diagramId;
}
}, [diagramId, getDiagramFilter]);
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
schemaIds: undefined,
}) satisfies DiagramFilter
);
}, []);
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
tableIds: undefined,
}) satisfies DiagramFilter
);
}, []);
const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
tableIds: [],
}) satisfies DiagramFilter
);
}, []);
// Reset filter
const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => {
setFilter({});
}, []);
const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] =
useCallback(
(schemaId: string) => {
setFilter((prev) => {
const currentSchemaIds = prev.schemaIds;
// Check if schema is currently visible
const isSchemaVisible = !allTables.some(
(table) =>
table.schemaId === schemaId &&
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter: prev,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
let newSchemaIds: string[] | undefined;
let newTableIds: string[] | undefined = prev.tableIds;
if (isSchemaVisible) {
// Schema is visible, make it not visible
if (!currentSchemaIds) {
// All schemas are visible, create filter with all except this one
newSchemaIds = allSchemasIds.filter(
(id) => id !== schemaId
);
} else {
// Remove this schema from the filter
newSchemaIds = currentSchemaIds.filter(
(id) => id !== schemaId
);
}
// Remove tables from this schema from tableIds if present
if (prev.tableIds) {
const schemaTableIds = allTables
.filter((table) => table.schemaId === schemaId)
.map((table) => table.id);
newTableIds = prev.tableIds.filter(
(id) => !schemaTableIds.includes(id)
);
}
} else {
// Schema is not visible, make it visible
newSchemaIds = [
...new Set([...(currentSchemaIds || []), schemaId]),
];
// Add tables from this schema to tableIds if tableIds is defined
if (prev.tableIds) {
const schemaTableIds = allTables
.filter((table) => table.schemaId === schemaId)
.map((table) => table.id);
newTableIds = [
...new Set([
...prev.tableIds,
...schemaTableIds,
]),
];
}
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allSchemasIds, allTables, databaseType]
);
const toggleTableFilterForNoSchema = useCallback(
(tableId: string) => {
setFilter((prev) => {
const currentTableIds = prev.tableIds;
// Check if table is currently visible
const isTableVisible = filterTable({
table: { id: tableId, schema: undefined },
filter: prev,
options: { defaultSchema: undefined },
});
let newTableIds: string[] | undefined;
if (isTableVisible) {
// Table is visible, make it not visible
if (!currentTableIds) {
// All tables are visible, create filter with all except this one
newTableIds = allTables
.filter((t) => t.id !== tableId)
.map((t) => t.id);
} else {
// Remove this table from the filter
newTableIds = currentTableIds.filter(
(id) => id !== tableId
);
}
} else {
// Table is not visible, make it visible
newTableIds = [
...new Set([...(currentTableIds || []), tableId]),
];
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: undefined,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables]
);
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
useCallback(
(tableId: string) => {
if (!defaultSchemas[databaseType]) {
// No schemas, toggle table filter without schema context
toggleTableFilterForNoSchema(tableId);
return;
}
setFilter((prev) => {
// Find the table in the tables list
const tableInfo = allTables.find((t) => t.id === tableId);
if (!tableInfo) {
return prev;
}
// Check if table is currently visible using filterTable
const isTableVisible = filterTable({
table: {
id: tableInfo.id,
schema: tableInfo.schema,
},
filter: prev,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
let newSchemaIds = prev.schemaIds;
let newTableIds = prev.tableIds;
if (isTableVisible) {
// Table is visible, make it not visible
// If the table is visible due to its schema being in schemaIds
if (
tableInfo?.schemaId &&
prev.schemaIds?.includes(tableInfo.schemaId)
) {
// Remove the schema from schemaIds and add all other tables from that schema to tableIds
newSchemaIds = prev.schemaIds.filter(
(id) => id !== tableInfo.schemaId
);
// Get all other tables from this schema (except the one being toggled)
const otherTablesFromSchema = allTables
.filter(
(t) =>
t.schemaId === tableInfo.schemaId &&
t.id !== tableId
)
.map((t) => t.id);
// Add these tables to tableIds
newTableIds = [
...(prev.tableIds || []),
...otherTablesFromSchema,
];
} else if (prev.tableIds?.includes(tableId)) {
// Table is visible because it's in tableIds, remove it
newTableIds = prev.tableIds.filter(
(id) => id !== tableId
);
} else if (!prev.tableIds && !prev.schemaIds) {
// No filters = all visible, create filter with all tables except this one
newTableIds = allTables
.filter((t) => t.id !== tableId)
.map((t) => t.id);
}
} else {
// Table is not visible, make it visible by adding to tableIds
newTableIds = [...(prev.tableIds || []), tableId];
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables, databaseType, toggleTableFilterForNoSchema]
);
const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
useCallback(
(schemaId: string) => {
setFilter((prev) => {
const currentSchemaIds = prev.schemaIds;
if (!currentSchemaIds) {
// No schemas are filtered
return prev;
}
// If schema is already filtered, do nothing
if (currentSchemaIds.includes(schemaId)) {
return prev;
}
// Add schema to the filter
const newSchemaIds = [...currentSchemaIds, schemaId];
if (newSchemaIds.length === allSchemasIds.length) {
// All schemas are now filtered, set to undefined
return {
...prev,
schemaIds: undefined,
} satisfies DiagramFilter;
}
return {
...prev,
schemaIds: newSchemaIds,
} satisfies DiagramFilter;
});
},
[allSchemasIds.length]
);
const hasActiveFilter: boolean = useMemo(() => {
return !!filter.schemaIds || !!filter.tableIds;
}, [filter]);
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
useMemo(() => {
if (!hasActiveFilter) {
return schemas;
}
const displayedSchemaIds = new Set<string>();
for (const table of allTables) {
if (
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
) {
if (table.schemaId) {
displayedSchemaIds.add(table.schemaId);
}
}
}
return schemas.filter((schema) =>
displayedSchemaIds.has(schema.id)
);
}, [hasActiveFilter, schemas, allTables, filter, databaseType]);
const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] =
useCallback(
({ tableIds, filterCallback }) => {
setFilter((prev) => {
let tableIdsToAdd: string[];
if (tableIds) {
// If tableIds are provided, use them directly
tableIdsToAdd = tableIds;
} else if (filterCallback) {
// If filterCallback is provided, filter tables based on it
tableIdsToAdd = allTables
.filter(filterCallback)
.map((table) => table.id);
} else {
// If neither is provided, do nothing
return prev;
}
const filterByTableIds = spreadFilterTables(
prev,
allTables satisfies FilterTableInfo[]
);
const currentTableIds = filterByTableIds.tableIds || [];
const newTableIds = [
...new Set([...currentTableIds, ...tableIdsToAdd]),
];
return reduceFilter(
{
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables]
);
const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
useCallback(
({ tableIds, filterCallback }) => {
setFilter((prev) => {
let tableIdsToRemovoe: string[];
if (tableIds) {
// If tableIds are provided, use them directly
tableIdsToRemovoe = tableIds;
} else if (filterCallback) {
// If filterCallback is provided, filter tables based on it
tableIdsToRemovoe = allTables
.filter(filterCallback)
.map((table) => table.id);
} else {
// If neither is provided, do nothing
return prev;
}
const filterByTableIds = spreadFilterTables(
prev,
allTables satisfies FilterTableInfo[]
);
const currentTableIds = filterByTableIds.tableIds || [];
const newTableIds = currentTableIds.filter(
(id) => !tableIdsToRemovoe.includes(id)
);
return reduceFilter(
{
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables]
);
const eventConsumer = useCallback(
(event: ChartDBEvent) => {
if (!hasActiveFilter) {
return;
}
if (event.action === 'add_tables') {
addTablesToFilter({
tableIds: event.data.tables.map((table) => table.id),
});
}
},
[hasActiveFilter, addTablesToFilter]
);
events.useSubscription(eventConsumer);
const value: DiagramFilterContext = {
filter,
clearSchemaIdsFilter: clearSchemaIds,
setTableIdsFilterEmpty: setTableIdsEmpty,
clearTableIdsFilter: clearTableIds,
resetFilter,
toggleSchemaFilter,
toggleTableFilter,
addSchemaToFilter,
hasActiveFilter,
schemasDisplayed,
addTablesToFilter,
removeTablesFromFilter,
};
return (
<diagramFilterContext.Provider value={value}>
{children}
</diagramFilterContext.Provider>
);
};

View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { diagramFilterContext } from './diagram-filter-context';
export const useDiagramFilter = () => useContext(diagramFilterContext);

View File

@@ -32,14 +32,20 @@ export interface DiffContext {
originalDiagram: Diagram | null;
diffMap: DiffMap;
hasDiff: boolean;
isSummaryOnly: boolean;
calculateDiff: ({
diagram,
newDiagram,
options,
}: {
diagram: Diagram;
newDiagram: Diagram;
options?: {
summaryOnly?: boolean;
};
}) => void;
resetDiff: () => void;
// table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
@@ -60,6 +66,15 @@ export interface DiffContext {
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewCharacterMaximumLength: ({
fieldId,
}: {
fieldId: string;
}) => string | null;
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
// relationship diff
checkIfNewRelationship: ({

View File

@@ -32,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
const [fieldsChanged, setFieldsChanged] = React.useState<
Map<string, boolean>
>(new Map<string, boolean>());
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
const events = useEventEmitter<DiffEvent>();
@@ -127,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
);
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
({ diagram, newDiagram: newDiagramArg }) => {
({ diagram, newDiagram: newDiagramArg, options }) => {
const {
diffMap: newDiffs,
changedTables: newChangedTables,
@@ -139,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
setFieldsChanged(newChangedFields);
setNewDiagram(newDiagramArg);
setOriginalDiagram(diagram);
setIsSummaryOnly(options?.summaryOnly ?? false);
events.emit({
action: 'diff_calculated',
@@ -305,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const getFieldNewPrimaryKey = useCallback<
DiffContext['getFieldNewPrimaryKey']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'primaryKey',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'nullable',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewCharacterMaximumLength = useCallback<
DiffContext['getFieldNewCharacterMaximumLength']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'characterMaximumLength',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as string;
}
}
return null;
},
[diffMap]
);
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'scale',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const getFieldNewPrecision = useCallback<
DiffContext['getFieldNewPrecision']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'precision',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
@@ -339,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
setDiffMap(new Map<string, ChartDBDiff>());
setTablesChanged(new Map<string, boolean>());
setFieldsChanged(new Map<string, boolean>());
setNewDiagram(null);
setOriginalDiagram(null);
setIsSummaryOnly(false);
}, []);
return (
<diffContext.Provider
value={{
@@ -346,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
originalDiagram,
diffMap,
hasDiff: diffMap.size > 0,
isSummaryOnly,
calculateDiff,
resetDiff,
// table diff
getTableNewName,
@@ -362,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
checkIfNewField,
getFieldNewName,
getFieldNewType,
getFieldNewPrimaryKey,
getFieldNewNullable,
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
// relationship diff
checkIfNewRelationship,

View File

@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
SHOW_ALL = 'show_all',
TOGGLE_THEME = 'toggle_theme',
TOGGLE_FILTER = 'toggle_filter',
}
export interface KeyboardShortcut {
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
keyCombinationMac: 'meta+m',
keyCombinationWin: 'ctrl+m',
},
[KeyboardShortcutAction.TOGGLE_FILTER]: {
action: KeyboardShortcutAction.TOGGLE_FILTER,
keyCombinationLabelMac: '⌘F',
keyCombinationLabelWin: 'Ctrl+F',
keyCombinationMac: 'meta+f',
keyCombinationWin: 'ctrl+f',
},
};
export interface KeyboardShortcutForOS {

View File

@@ -36,10 +36,6 @@ export interface LayoutContext {
hideSidePanel: () => void;
showSidePanel: () => void;
toggleSidePanel: () => void;
isSelectSchemaOpen: boolean;
openSelectSchema: () => void;
closeSelectSchema: () => void;
}
export const layoutContext = createContext<LayoutContext>({
@@ -70,8 +66,4 @@ export const layoutContext = createContext<LayoutContext>({
hideSidePanel: emptyFn,
showSidePanel: emptyFn,
toggleSidePanel: emptyFn,
isSelectSchemaOpen: false,
openSelectSchema: emptyFn,
closeSelectSchema: emptyFn,
});

View File

@@ -23,8 +23,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] =
React.useState<boolean>(isDesktop);
const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
React.useState<boolean>(false);
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
() => setOpenedTableInSidebar('');
@@ -88,11 +86,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedTableInSidebar(customTypeId);
};
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
setIsSelectSchemaOpen(false);
return (
<layoutContext.Provider
value={{
@@ -108,9 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
hideSidePanel,
showSidePanel,
toggleSidePanel,
isSelectSchemaOpen,
openSelectSchema,
closeSelectSchema,
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,

View File

@@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context';
export type ScrollAction = 'pan' | 'zoom';
export type SchemasFilter = Record<string, string[]>;
export interface LocalConfigContext {
theme: Theme;
setTheme: (theme: Theme) => void;
@@ -13,16 +11,11 @@ export interface LocalConfigContext {
scrollAction: ScrollAction;
setScrollAction: (action: ScrollAction) => void;
schemasFilter: SchemasFilter;
setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
showCardinality: boolean;
setShowCardinality: (showCardinality: boolean) => void;
hideMultiSchemaNotification: boolean;
setHideMultiSchemaNotification: (
hideMultiSchemaNotification: boolean
) => void;
showFieldAttributes: boolean;
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
githubRepoOpened: boolean;
setGithubRepoOpened: (githubRepoOpened: boolean) => void;
@@ -44,14 +37,11 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
scrollAction: 'pan',
setScrollAction: emptyFn,
schemasFilter: {},
setSchemasFilter: emptyFn,
showCardinality: true,
setShowCardinality: emptyFn,
hideMultiSchemaNotification: false,
setHideMultiSchemaNotification: emptyFn,
showFieldAttributes: true,
setShowFieldAttributes: emptyFn,
githubRepoOpened: false,
setGithubRepoOpened: emptyFn,

View File

@@ -1,13 +1,12 @@
import React, { useEffect } from 'react';
import type { SchemasFilter, ScrollAction } from './local-config-context';
import type { ScrollAction } from './local-config-context';
import { LocalConfigContext } from './local-config-context';
import type { Theme } from '../theme-context/theme-context';
const themeKey = 'theme';
const scrollActionKey = 'scroll_action';
const schemasFilterKey = 'schemas_filter';
const showCardinalityKey = 'show_cardinality';
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
const showFieldAttributesKey = 'show_field_attributes';
const githubRepoOpenedKey = 'github_repo_opened';
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
@@ -24,20 +23,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
);
const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>(
JSON.parse(
localStorage.getItem(schemasFilterKey) || '{}'
) as SchemasFilter
);
const [showCardinality, setShowCardinality] = React.useState<boolean>(
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
);
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
const [showFieldAttributes, setShowFieldAttributes] =
React.useState<boolean>(
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
'false') === 'true'
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
);
const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>(
@@ -71,13 +63,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
}, [githubRepoOpened]);
useEffect(() => {
localStorage.setItem(
hideMultiSchemaNotificationKey,
hideMultiSchemaNotification.toString()
);
}, [hideMultiSchemaNotification]);
useEffect(() => {
localStorage.setItem(themeKey, theme);
}, [theme]);
@@ -86,10 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(scrollActionKey, scrollAction);
}, [scrollAction]);
useEffect(() => {
localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter));
}, [schemasFilter]);
useEffect(() => {
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
@@ -115,12 +96,10 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setTheme,
scrollAction,
setScrollAction,
schemasFilter,
setSchemasFilter,
showCardinality,
setShowCardinality,
hideMultiSchemaNotification,
setHideMultiSchemaNotification,
showFieldAttributes,
setShowFieldAttributes,
setGithubRepoOpened,
githubRepoOpened,
starUsDialogLastOpen,

View File

@@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export interface StorageContext {
// Config operations
getConfig: () => Promise<ChartDBConfig | undefined>;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
// Diagram filter operations
getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>;
updateDiagramFilter: (
diagramId: string,
filter: DiagramFilter
) => Promise<void>;
deleteDiagramFilter: (diagramId: string) => Promise<void>;
// Diagram operations
addDiagram: (params: { diagram: Diagram }) => Promise<void>;
listDiagrams: (options?: {
@@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = {
getConfig: emptyFn,
updateConfig: emptyFn,
getDiagramFilter: emptyFn,
updateDiagramFilter: emptyFn,
deleteDiagramFilter: emptyFn,
addDiagram: emptyFn,
listDiagrams: emptyFn,
getDiagram: emptyFn,

View File

@@ -10,6 +10,7 @@ import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -44,6 +45,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
ChartDBConfig & { id: number },
'id' // primary key "id" (for the typings only)
>;
diagram_filters: EntityTable<
DiagramFilter & { diagramId: string },
'diagramId' // primary key "id" (for the typings only)
>;
};
// Schema declaration:
@@ -190,6 +195,27 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
config: '++id, defaultDiagramId',
});
dexieDB
.version(12)
.stores({
diagrams:
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
db_tables:
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
db_relationships:
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
db_dependencies:
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
areas: '++id, diagramId, name, x, y, width, height, color',
db_custom_types:
'++id, diagramId, schema, type, kind, values, fields',
config: '++id, defaultDiagramId',
diagram_filters: 'diagramId, tableIds, schemasIds',
})
.upgrade((tx) => {
tx.table('config').clear();
});
dexieDB.on('ready', async () => {
const config = await dexieDB.config.get(1);
@@ -217,6 +243,32 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
[db]
);
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
async (diagramId: string): Promise<DiagramFilter | undefined> => {
return await db.diagram_filters.get({ diagramId });
},
[db]
);
const updateDiagramFilter: StorageContext['updateDiagramFilter'] =
useCallback(
async (diagramId, filter): Promise<void> => {
await db.diagram_filters.put({
diagramId,
...filter,
});
},
[db]
);
const deleteDiagramFilter: StorageContext['deleteDiagramFilter'] =
useCallback(
async (diagramId: string): Promise<void> => {
await db.diagram_filters.where({ diagramId }).delete();
},
[db]
);
const addTable: StorageContext['addTable'] = useCallback(
async ({ diagramId, table }) => {
await db.db_tables.add({
@@ -756,6 +808,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteCustomType,
listCustomTypes,
deleteDiagramCustomTypes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
}}
>
{children}

View File

@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
handleThemeToggle,
{
preventDefault: true,
enableOnFormTags: true,
},
[handleThemeToggle]
);

View File

@@ -35,12 +35,23 @@ import type { OnChange } from '@monaco-editor/react';
import { useDebounce } from '@/hooks/use-debounce-v2';
import { InstructionsSection } from './instructions-section/instructions-section';
import { parseSQLError } from '@/lib/data/sql-import';
import type * as monaco from 'monaco-editor';
import type { editor, IDisposable } from 'monaco-editor';
import { waitFor } from '@/lib/utils';
import { type ValidationResult } from '@/lib/data/sql-import/sql-validator';
import { validateSQL } from '@/lib/data/sql-import/unified-sql-validator';
import {
validateSQL,
type ValidationResult,
} from '@/lib/data/sql-import/sql-validator';
import { SQLValidationStatus } from './sql-validation-status';
const calculateContentSizeMB = (content: string): number => {
return content.length / (1024 * 1024); // Convert to MB
};
const calculateIsLargeFile = (content: string): boolean => {
const contentSizeMB = calculateContentSizeMB(content);
return contentSizeMB > 2; // Consider large if over 2MB
};
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
@@ -120,8 +131,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}) => {
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const pasteDisposableRef = useRef<monaco.IDisposable | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const pasteDisposableRef = useRef<IDisposable | null>(null);
const { t } = useTranslation();
const { isSm: isDesktop } = useBreakpoint('sm');
@@ -220,11 +231,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
if (sqlValidation?.fixedSQL) {
setIsAutoFixing(true);
setShowAutoFixButton(false);
setErrorMessage('');
// Apply the fix with a delay so user sees the fixing message
setTimeout(() => {
setScriptResult(sqlValidation.fixedSQL!);
setIsAutoFixing(false);
setTimeout(() => {
setIsAutoFixing(false);
}, 100);
}, 1000);
}
}, [sqlValidation, setScriptResult]);
@@ -240,6 +255,16 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const formatEditor = useCallback(() => {
if (editorRef.current) {
const model = editorRef.current.getModel();
if (model) {
const content = model.getValue();
// Skip formatting for large files (> 2MB)
if (calculateIsLargeFile(content)) {
return;
}
}
setTimeout(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
@@ -293,7 +318,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}, []);
const handleEditorDidMount = useCallback(
(editor: monaco.editor.IStandaloneCodeEditor) => {
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
// Cleanup previous disposable if it exists
@@ -309,14 +334,17 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const content = model.getValue();
// Skip formatting for large files (> 2MB) to prevent browser freezing
const isLargeFile = calculateIsLargeFile(content);
// First, detect content type to determine if we should switch modes
const detectedType = detectContentType(content);
if (detectedType && detectedType !== importMethod) {
// Switch to the detected mode immediately
setImportMethod(detectedType);
// Only format if it's JSON (query mode)
if (detectedType === 'query') {
// Only format if it's JSON (query mode) AND file is not too large
if (detectedType === 'query' && !isLargeFile) {
// For JSON mode, format after a short delay
setTimeout(() => {
editor
@@ -327,15 +355,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// For DDL mode, do NOT format as it can break the SQL
} else {
// Content type didn't change, apply formatting based on current mode
if (importMethod === 'query') {
// Only format JSON content
if (importMethod === 'query' && !isLargeFile) {
// Only format JSON content if not too large
setTimeout(() => {
editor
.getAction('editor.action.formatDocument')
?.run();
}, 100);
}
// For DDL mode, do NOT format
// For DDL mode or large files, do NOT format
}
});
@@ -428,20 +456,12 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</div>
{errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
importMethod === 'ddl' ? (
<SQLValidationStatus
validation={sqlValidation}
errorMessage={errorMessage}
isAutoFixing={isAutoFixing}
onErrorClick={handleErrorClick}
/>
) : (
<div className="mt-2 flex shrink-0 items-center gap-2">
<p className="text-xs text-red-700">
{errorMessage}
</p>
</div>
)
<SQLValidationStatus
validation={sqlValidation}
errorMessage={errorMessage}
isAutoFixing={isAutoFixing}
onErrorClick={handleErrorClick}
/>
) : null}
</div>
),
@@ -546,7 +566,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
variant="secondary"
onClick={handleAutoFix}
disabled={isAutoFixing}
className="bg-blue-600 text-white hover:bg-blue-700"
className="bg-sky-600 text-white hover:bg-sky-700"
>
{isAutoFixing ? (
<Spinner size="small" />

View File

@@ -1,15 +1,13 @@
import React from 'react';
import {
AlertCircle,
CheckCircle,
AlertTriangle,
Lightbulb,
} from 'lucide-react';
import React, { useMemo } from 'react';
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/alert/alert';
import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
import { Separator } from '@/components/separator/separator';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { Spinner } from '@/components/spinner/spinner';
interface SQLValidationStatusProps {
validation: ValidationResult | null;
validation?: ValidationResult | null;
errorMessage: string;
isAutoFixing?: boolean;
onErrorClick?: (line: number) => void;
@@ -21,71 +19,113 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
isAutoFixing = false,
onErrorClick,
}) => {
const hasErrors = useMemo(
() => validation?.errors.length && validation.errors.length > 0,
[validation?.errors]
);
const hasWarnings = useMemo(
() => validation?.warnings && validation.warnings.length > 0,
[validation?.warnings]
);
const wasAutoFixed = useMemo(
() =>
validation?.warnings?.some((w) =>
w.message.includes('Auto-fixed')
) || false,
[validation?.warnings]
);
if (!validation && !errorMessage && !isAutoFixing) return null;
const hasErrors = validation?.errors && validation.errors.length > 0;
const hasWarnings = validation?.warnings && validation.warnings.length > 0;
const wasAutoFixed =
validation?.warnings?.some((w) => w.message.includes('Auto-fixed')) ||
false;
if (isAutoFixing) {
return (
<>
<Separator className="mb-1 mt-2" />
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
<div className="flex items-start gap-2">
<Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
Auto-fixing SQL syntax errors...
</div>
</div>
</div>
</div>
</>
);
}
// If we have parser errors (errorMessage) after validation
if (errorMessage && !hasErrors) {
return (
<Alert variant="destructive" className="mt-2">
<AlertCircle className="size-4" />
<AlertDescription className="text-sm">
{errorMessage}
</AlertDescription>
</Alert>
<>
<Separator className="mb-1 mt-2" />
<div className="mb-1 flex shrink-0 items-center gap-2">
<p className="text-xs text-red-700">{errorMessage}</p>
</div>
</>
);
}
return (
<div className="mt-2 space-y-2">
{isAutoFixing && (
<Alert className="border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950">
<Lightbulb className="size-4 animate-pulse text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-sm text-blue-700 dark:text-blue-300">
Auto-fixing SQL syntax errors...
</AlertDescription>
</Alert>
)}
<>
<Separator className="mb-1 mt-2" />
{hasErrors && !isAutoFixing && (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertDescription className="space-y-1 text-sm">
<div className="font-medium">SQL Syntax Errors:</div>
{validation.errors.slice(0, 3).map((error, idx) => (
<div key={idx} className="ml-2">
{' '}
<button
onClick={() => onErrorClick?.(error.line)}
className="rounded underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500"
type="button"
>
Line {error.line}
</button>
: {error.message}
{error.suggestion && (
<div className="ml-4 text-xs opacity-80">
{error.suggestion}
{hasErrors ? (
<div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
<ScrollArea className="h-24">
<div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
{validation?.errors
.slice(0, 3)
.map((error, idx) => (
<div
key={idx}
className="flex items-start gap-2"
>
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
<div className="flex-1 text-sm text-red-700 dark:text-red-300">
<button
onClick={() =>
onErrorClick?.(error.line)
}
className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200"
type="button"
>
Line {error.line}
</button>
<span className="mx-1">:</span>
<span className="text-xs">
{error.message}
</span>
{error.suggestion && (
<div className="mt-1 flex items-start gap-2">
<span className="text-xs font-medium ">
{error.suggestion}
</span>
</div>
)}
</div>
</div>
)}
</div>
))}
{validation.errors.length > 3 && (
<div className="ml-2 text-xs opacity-70">
... and {validation.errors.length - 3} more
errors
</div>
)}
</AlertDescription>
</Alert>
)}
))}
{validation?.errors &&
validation?.errors.length > 3 ? (
<div className="flex items-center gap-2">
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
<span className="text-xs font-medium">
{validation.errors.length - 3} more
error
{validation.errors.length - 3 > 1
? 's'
: ''}
</span>
</div>
) : null}
</div>
</ScrollArea>
</div>
) : null}
{wasAutoFixed && !hasErrors && (
{wasAutoFixed && !hasErrors ? (
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
<CheckCircle className="size-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-sm text-green-700 dark:text-green-300">
@@ -93,30 +133,47 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
now ready to import.
</AlertDescription>
</Alert>
)}
) : null}
{hasWarnings && !hasErrors && (
<Alert>
<AlertTriangle className="size-4" />
<AlertDescription className="space-y-1 text-sm">
<div className="font-medium">Import Info:</div>
{validation.warnings.map((warning, idx) => (
<div key={idx} className="ml-2">
{warning.message}
{hasWarnings && !hasErrors ? (
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
<ScrollArea className="h-24">
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
<div className="mb-1 font-medium">
Import Info:
</div>
{validation?.warnings.map(
(warning, idx) => (
<div
key={idx}
className="ml-2 text-xs"
>
{warning.message}
</div>
)
)}
</div>
</div>
))}
</AlertDescription>
</Alert>
)}
</div>
</ScrollArea>
</div>
) : null}
{!hasErrors && !hasWarnings && !errorMessage && validation && (
<Alert className="flex border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950 [&>svg]:static [&>svg]:mr-2 [&>svg~*]:pl-0">
<CheckCircle className="size-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-sm text-green-700 dark:text-green-300">
SQL syntax validated successfully
</AlertDescription>
</Alert>
)}
</div>
{!hasErrors && !hasWarnings && !errorMessage && validation ? (
<div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
<div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300">
<div className="flex items-start gap-2">
<CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
<div className="flex-1 text-sm text-green-700 dark:text-green-300">
SQL syntax validated successfully
</div>
</div>
</div>
</div>
) : null}
</>
);
};

View File

@@ -0,0 +1,2 @@
export const MAX_TABLES_IN_DIAGRAM = 500;
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;

View File

@@ -0,0 +1,683 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
import { Checkbox } from '@/components/checkbox/checkbox';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
import { cn } from '@/lib/utils';
import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogInternalContent,
DialogTitle,
} from '@/components/dialog/dialog';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { generateTableKey } from '@/lib/domain';
import { Spinner } from '@/components/spinner/spinner';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationPrevious,
PaginationNext,
} from '@/components/pagination/pagination';
import { MAX_TABLES_IN_DIAGRAM } from './constants';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
export interface SelectTablesProps {
databaseMetadata?: DatabaseMetadata;
onImport: ({
selectedTables,
databaseMetadata,
}: {
selectedTables?: SelectedTable[];
databaseMetadata?: DatabaseMetadata;
}) => Promise<void>;
onBack: () => void;
isLoading?: boolean;
}
const TABLES_PER_PAGE = 10;
interface TableInfo {
key: string;
schema?: string;
tableName: string;
fullName: string;
type: 'table' | 'view';
}
export const SelectTables: React.FC<SelectTablesProps> = ({
databaseMetadata,
onImport,
onBack,
isLoading = false,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [showTables, setShowTables] = useState(true);
const [showViews, setShowViews] = useState(false);
const { t } = useTranslation();
const [isImporting, setIsImporting] = useState(false);
// Prepare all tables and views with their metadata
const allTables = useMemo(() => {
const tables: TableInfo[] = [];
// Add regular tables
databaseMetadata?.tables.forEach((table) => {
const schema = schemaNameToDomainSchemaName(table.schema);
const tableName = table.table;
const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
tables.push({
key,
schema,
tableName,
fullName: schema ? `${schema}.${tableName}` : tableName,
type: 'table',
});
});
// Add views
databaseMetadata?.views?.forEach((view) => {
const schema = schemaNameToDomainSchemaName(view.schema);
const viewName = view.view_name;
if (!viewName) {
return;
}
const key = `view:${generateTableKey({
tableName: viewName,
schemaName: schema,
})}`;
tables.push({
key,
schema,
tableName: viewName,
fullName:
schema === 'default' ? viewName : `${schema}.${viewName}`,
type: 'view',
});
});
return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
}, [databaseMetadata?.tables, databaseMetadata?.views]);
// Count tables and views separately
const tableCount = useMemo(
() => allTables.filter((t) => t.type === 'table').length,
[allTables]
);
const viewCount = useMemo(
() => allTables.filter((t) => t.type === 'view').length,
[allTables]
);
// Initialize selectedTables with all tables (not views) if less than 100 tables
const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
const tables = allTables.filter((t) => t.type === 'table');
if (tables.length < MAX_TABLES_IN_DIAGRAM) {
return new Set(tables.map((t) => t.key));
}
return new Set();
});
// Filter tables based on search term and type filters
const filteredTables = useMemo(() => {
let filtered = allTables;
// Filter by type
filtered = filtered.filter((table) => {
if (table.type === 'table' && !showTables) return false;
if (table.type === 'view' && !showViews) return false;
return true;
});
// Filter by search term
if (searchTerm.trim()) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) ||
table.schema?.toLowerCase().includes(searchLower) ||
table.fullName.toLowerCase().includes(searchLower)
);
}
return filtered;
}, [allTables, searchTerm, showTables, showViews]);
// Calculate pagination
const totalPages = useMemo(
() => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
[filteredTables.length]
);
const paginatedTables = useMemo(() => {
const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
const endIndex = startIndex + TABLES_PER_PAGE;
return filteredTables.slice(startIndex, endIndex);
}, [filteredTables, currentPage]);
// Get currently visible selected tables
const visibleSelectedTables = useMemo(() => {
return paginatedTables.filter((table) => selectedTables.has(table.key));
}, [paginatedTables, selectedTables]);
const canAddMore = useMemo(
() => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
[selectedTables.size]
);
const hasSearchResults = useMemo(
() => filteredTables.length > 0,
[filteredTables.length]
);
const allVisibleSelected = useMemo(
() =>
visibleSelectedTables.length === paginatedTables.length &&
paginatedTables.length > 0,
[visibleSelectedTables.length, paginatedTables.length]
);
const canSelectAllFiltered = useMemo(
() =>
filteredTables.length > 0 &&
filteredTables.some((table) => !selectedTables.has(table.key)) &&
canAddMore,
[filteredTables, selectedTables, canAddMore]
);
// Reset to first page when search changes
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
const handleTableToggle = useCallback(
(tableKey: string) => {
const newSelected = new Set(selectedTables);
if (newSelected.has(tableKey)) {
newSelected.delete(tableKey);
} else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
newSelected.add(tableKey);
}
setSelectedTables(newSelected);
},
[selectedTables]
);
const handleTogglePageSelection = useCallback(() => {
const newSelected = new Set(selectedTables);
if (allVisibleSelected) {
// Deselect all on current page
for (const table of paginatedTables) {
newSelected.delete(table.key);
}
} else {
// Select all on current page
for (const table of paginatedTables) {
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
newSelected.add(table.key);
}
}
setSelectedTables(newSelected);
}, [allVisibleSelected, paginatedTables, selectedTables]);
const handleSelectAllFiltered = useCallback(() => {
const newSelected = new Set(selectedTables);
for (const table of filteredTables) {
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
newSelected.add(table.key);
}
setSelectedTables(newSelected);
}, [filteredTables, selectedTables]);
const handleNextPage = useCallback(() => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
}, [currentPage, totalPages]);
const handlePrevPage = useCallback(() => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
}, [currentPage]);
const handleClearSelection = useCallback(() => {
setSelectedTables(new Set());
}, []);
const handleConfirm = useCallback(async () => {
if (isImporting) {
return;
}
setIsImporting(true);
try {
const selectedTableObjects: SelectedTable[] = Array.from(
selectedTables
)
.map((key): SelectedTable | null => {
const table = allTables.find((t) => t.key === key);
if (!table) return null;
return {
schema: table.schema,
table: table.tableName,
type: table.type,
} satisfies SelectedTable;
})
.filter((t): t is SelectedTable => t !== null);
await onImport({
selectedTables: selectedTableObjects,
databaseMetadata,
});
} finally {
setIsImporting(false);
}
}, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
const { isMd: isDesktop } = useBreakpoint('md');
const renderPagination = useCallback(
() => (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={handlePrevPage}
className={cn(
'cursor-pointer',
currentPage === 1 &&
'pointer-events-none opacity-50'
)}
/>
</PaginationItem>
<PaginationItem>
<span className="px-3 text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</span>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={handleNextPage}
className={cn(
'cursor-pointer',
(currentPage >= totalPages ||
filteredTables.length === 0) &&
'pointer-events-none opacity-50'
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
),
[
currentPage,
totalPages,
handlePrevPage,
handleNextPage,
filteredTables.length,
]
);
if (isLoading) {
return (
<div className="flex h-[400px] items-center justify-center">
<div className="text-center">
<Spinner className="mb-4" />
<p className="text-sm text-muted-foreground">
Parsing database metadata...
</p>
</div>
</div>
);
}
return (
<>
<DialogHeader>
<DialogTitle>Select Tables to Import</DialogTitle>
<DialogDescription>
{tableCount} {tableCount === 1 ? 'table' : 'tables'}
{viewCount > 0 && (
<>
{' and '}
{viewCount} {viewCount === 1 ? 'view' : 'views'}
</>
)}
{' found. '}
{allTables.length > MAX_TABLES_IN_DIAGRAM
? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
: 'Choose which ones to import.'}
</DialogDescription>
</DialogHeader>
<DialogInternalContent>
<div className="flex h-full flex-col space-y-4">
{/* Warning/Info Banner */}
{allTables.length > MAX_TABLES_IN_DIAGRAM ? (
<div
className={cn(
'flex items-center gap-2 rounded-lg p-3 text-sm',
'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
)}
>
<AlertCircle className="size-4 shrink-0" />
<span>
Due to performance limitations, you can import a
maximum of {MAX_TABLES_IN_DIAGRAM} tables.
</span>
</div>
) : null}
{/* Search Input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search tables..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="px-9"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
)}
</div>
{/* Selection Status and Actions - Responsive layout */}
<div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
{/* Left side: selection count -> checkboxes -> results found */}
<div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
<div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
<span className="text-center font-medium">
{selectedTables.size} /{' '}
{Math.min(
MAX_TABLES_IN_DIAGRAM,
allTables.length
)}{' '}
items selected
</span>
</div>
<div className="flex items-center gap-3 sm:border-x sm:px-4">
<div className="flex items-center gap-2">
<Checkbox
checked={showTables}
onCheckedChange={(checked) => {
// Prevent unchecking if it's the only one checked
if (!checked && !showViews) return;
setShowTables(!!checked);
}}
/>
<Table
className="size-4"
strokeWidth={1.5}
/>
<span>tables</span>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={showViews}
onCheckedChange={(checked) => {
// Prevent unchecking if it's the only one checked
if (!checked && !showTables) return;
setShowViews(!!checked);
}}
/>
<View
className="size-4"
strokeWidth={1.5}
/>
<span>views</span>
</div>
</div>
<span className="hidden text-muted-foreground sm:inline">
{filteredTables.length}{' '}
{filteredTables.length === 1
? 'result'
: 'results'}{' '}
found
</span>
</div>
{/* Right side: action buttons */}
<div className="flex flex-wrap items-center justify-center gap-2">
{hasSearchResults && (
<>
{/* Show page selection button when not searching and no selection */}
{!searchTerm &&
selectedTables.size === 0 && (
<Button
variant="outline"
size="sm"
onClick={
handleTogglePageSelection
}
disabled={
paginatedTables.length === 0
}
>
{allVisibleSelected
? 'Deselect'
: 'Select'}{' '}
page
</Button>
)}
{/* Show Select all button when there are unselected tables */}
{canSelectAllFiltered &&
selectedTables.size === 0 && (
<Button
variant="outline"
size="sm"
onClick={
handleSelectAllFiltered
}
disabled={!canSelectAllFiltered}
title={(() => {
const unselectedCount =
filteredTables.filter(
(table) =>
!selectedTables.has(
table.key
)
).length;
const remainingCapacity =
MAX_TABLES_IN_DIAGRAM -
selectedTables.size;
if (
unselectedCount >
remainingCapacity
) {
return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
}
return undefined;
})()}
>
{(() => {
const unselectedCount =
filteredTables.filter(
(table) =>
!selectedTables.has(
table.key
)
).length;
const remainingCapacity =
MAX_TABLES_IN_DIAGRAM -
selectedTables.size;
if (
unselectedCount >
remainingCapacity
) {
return `Select ${remainingCapacity} of ${unselectedCount}`;
}
return `Select all ${unselectedCount}`;
})()}
</Button>
)}
</>
)}
{selectedTables.size > 0 && (
<>
{/* Show page selection/deselection button when user has selections */}
{paginatedTables.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleTogglePageSelection}
>
{allVisibleSelected
? 'Deselect'
: 'Select'}{' '}
page
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={handleClearSelection}
>
Clear selection
</Button>
</>
)}
</div>
</div>
</div>
{/* Table List */}
<div className="flex min-h-[428px] flex-1 flex-col">
{hasSearchResults ? (
<>
<div className="flex-1 py-4">
<div className="space-y-1">
{paginatedTables.map((table) => {
const isSelected = selectedTables.has(
table.key
);
const isDisabled =
!isSelected &&
selectedTables.size >=
MAX_TABLES_IN_DIAGRAM;
return (
<div
key={table.key}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
{
'cursor-not-allowed':
isDisabled,
'bg-muted hover:bg-muted/80':
isSelected,
'hover:bg-accent':
!isSelected &&
!isDisabled,
}
)}
>
<Checkbox
checked={isSelected}
disabled={isDisabled}
onCheckedChange={() =>
handleTableToggle(
table.key
)
}
/>
{table.type === 'view' ? (
<View
className="size-4"
strokeWidth={1.5}
/>
) : (
<Table
className="size-4"
strokeWidth={1.5}
/>
)}
<span className="flex-1">
{table.schema ? (
<span className="text-muted-foreground">
{table.schema}.
</span>
) : null}
<span className="font-medium">
{table.tableName}
</span>
{table.type === 'view' && (
<span className="ml-2 text-xs text-muted-foreground">
(view)
</span>
)}
</span>
{isSelected && (
<Check className="size-4 text-pink-600" />
)}
</div>
);
})}
</div>
</div>
</>
) : (
<div className="flex h-full items-center justify-center py-4">
<p className="text-sm text-muted-foreground">
{searchTerm
? 'No tables found matching your search.'
: 'Start typing to search for tables...'}
</p>
</div>
)}
</div>
{isDesktop ? renderPagination() : null}
</DialogInternalContent>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
<Button
type="button"
variant="secondary"
onClick={onBack}
disabled={isImporting}
>
{t('new_diagram_dialog.back')}
</Button>
<Button
onClick={handleConfirm}
disabled={selectedTables.size === 0 || isImporting}
className="bg-pink-500 text-white hover:bg-pink-600"
>
{isImporting ? (
<>
<Spinner className="mr-2 size-4 text-white" />
Importing...
</>
) : (
`Import ${selectedTables.size} Tables`
)}
</Button>
{!isDesktop ? renderPagination() : null}
</DialogFooter>
</>
);
};

View File

@@ -1,4 +1,5 @@
export enum CreateDiagramDialogStep {
SELECT_DATABASE = 'SELECT_DATABASE',
IMPORT_DATABASE = 'IMPORT_DATABASE',
SELECT_TABLES = 'SELECT_TABLES',
}

View File

@@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import { SelectDatabase } from './select-database/select-database';
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
import { ImportDatabase } from '../common/import-database/import-database';
import { SelectTables } from '../common/select-tables/select-tables';
import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
export interface CreateDiagramDialogProps extends BaseDialogProps {}
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const { listDiagrams, addDiagram } = useStorage();
const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate();
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
useEffect(() => {
setDatabaseEdition(undefined);
@@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setDatabaseEdition(undefined);
setScriptResult('');
setImportMethod('query');
setParsedMetadata(undefined);
}, [dialog.open]);
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
const importNewDiagram = useCallback(async () => {
let diagram: Diagram | undefined;
const importNewDiagram = useCallback(
async ({
selectedTables,
databaseMetadata,
}: {
selectedTables?: SelectedTable[];
databaseMetadata?: DatabaseMetadata;
} = {}) => {
let diagram: Diagram | undefined;
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
} else {
let metadata: DatabaseMetadata | undefined = databaseMetadata;
if (!metadata) {
metadata = loadDatabaseMetadata(scriptResult);
}
if (selectedTables && selectedTables.length > 0) {
metadata = filterMetadataByTables({
metadata,
selectedTables,
});
}
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata: metadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
await addDiagram({ diagram });
await updateConfig({
config: { defaultDiagramId: diagram.id },
});
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
await addDiagram({ diagram });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
importMethod,
databaseType,
addDiagram,
databaseEdition,
closeCreateDiagramDialog,
navigate,
updateConfig,
scriptResult,
diagramNumber,
]);
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
},
[
importMethod,
databaseType,
addDiagram,
databaseEdition,
closeCreateDiagramDialog,
navigate,
updateConfig,
scriptResult,
diagramNumber,
]
);
const createEmptyDiagram = useCallback(async () => {
const diagram: Diagram = {
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
openImportDBMLDialog,
]);
const importNewDiagramOrFilterTables = useCallback(async () => {
try {
setIsParsingMetadata(true);
if (importMethod === 'ddl') {
await importNewDiagram();
} else {
// Parse metadata asynchronously to avoid blocking the UI
const metadata = await new Promise<DatabaseMetadata>(
(resolve, reject) => {
setTimeout(() => {
try {
const result =
loadDatabaseMetadata(scriptResult);
resolve(result);
} catch (err) {
reject(err);
}
}, 0);
}
);
const totalTablesAndViews =
metadata.tables.length + (metadata.views?.length || 0);
setParsedMetadata(metadata);
// Check if it's a large database that needs table selection
if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
setStep(CreateDiagramDialogStep.SELECT_TABLES);
} else {
await importNewDiagram({
databaseMetadata: metadata,
});
}
}
} finally {
setIsParsingMetadata(false);
}
}, [importMethod, scriptResult, importNewDiagram]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
// Don't allow closing while parsing metadata
if (isParsingMetadata) {
return;
}
if (!hasExistingDiagram) {
return;
}
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
<DialogContent
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
showClose={hasExistingDiagram}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
<SelectDatabase
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : (
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
<ImportDatabase
onImport={importNewDiagram}
onImport={importNewDiagramOrFilterTables}
onCreateEmptyDiagram={createEmptyDiagram}
databaseEdition={databaseEdition}
databaseType={databaseType}
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
title={t('new_diagram_dialog.import_database.title')}
importMethod={importMethod}
setImportMethod={setImportMethod}
keepDialogAfterImport={true}
/>
)}
) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
<SelectTables
isLoading={isParsingMetadata || !parsedMetadata}
databaseMetadata={parsedMetadata}
onImport={importNewDiagram}
onBack={() =>
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : null}
</DialogContent>
</Dialog>
);

View File

@@ -20,12 +20,18 @@ import {
} from '@/lib/data/export-metadata/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import {
filterDependency,
filterRelationship,
filterTable,
} from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
export interface ExportSQLDialogProps extends BaseDialogProps {
targetDatabaseType: DatabaseType;
@@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
targetDatabaseType,
}) => {
const { closeExportSQLDialog } = useDialog();
const { currentDiagram, filteredSchemas } = useChartDB();
const { currentDiagram } = useChartDB();
const { filter } = useDiagramFilter();
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
@@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
@@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
sourceTable &&
targetTable &&
shouldShowTablesBySchemaFilter(
sourceTable,
filteredSchemas
) &&
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
filterRelationship({
tableA: {
id: sourceTable.id,
schema: sourceTable.schema,
},
tableB: {
id: targetTable.id,
schema: targetTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
@@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
table &&
dependentTable &&
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
shouldShowTablesBySchemaFilter(
dependentTable,
filteredSchemas
)
filterDependency({
tableA: {
id: table.id,
schema: table.schema,
},
tableB: {
id: dependentTable.id,
schema: dependentTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
};
@@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
}, [targetDatabaseType, currentDiagram, filter]);
useEffect(() => {
if (!dialog.open) {

View File

@@ -58,16 +58,13 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
const importDatabase = useCallback(async () => {
let diagram: Diagram | undefined;
let warnings: string[] | undefined;
if (importMethod === 'ddl') {
const result = await sqlImportToDiagram({
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
diagram = result;
warnings = result.warnings;
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
@@ -322,38 +319,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
resetRedoStack();
resetUndoStack();
// Show warnings if any
if (warnings && warnings.length > 0) {
const warningContent = (
<div className="space-y-2">
<div className="font-semibold">
The following SQL statements were skipped:
</div>
<ul className="list-inside list-disc space-y-1">
{warnings.map((warning, index) => (
<li key={index} className="text-sm">
{warning}
</li>
))}
</ul>
<div className="mt-3 text-sm text-muted-foreground">
Only table definitions, indexes, and foreign key
constraints are currently supported.
</div>
</div>
);
showAlert({
title: 'Import completed with warnings',
content: warningContent,
actionLabel: 'OK',
onAction: () => {
closeImportDatabaseDialog();
},
});
} else {
closeImportDatabaseDialog();
}
closeImportDatabaseDialog();
}, [
importMethod,
databaseEdition,

View File

@@ -5,7 +5,7 @@ import React, {
Suspense,
useRef,
} from 'react';
import * as monaco from 'monaco-editor';
import type * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -23,53 +23,24 @@ import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
import {
importDBMLToDiagram,
sanitizeDBML,
preprocessDBML,
} from '@/lib/dbml/dbml-import/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import type { DBTable } from '@/lib/domain/db-table';
import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import {
clearErrorHighlight,
highlightErrorLine,
} from '@/components/code-snippet/dbml/utils';
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
@@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}, [reorder, reorderTables]);
const highlightErrorLine = useCallback((error: DBMLError) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear();
clearErrorHighlight(decorationsCollection.current);
}, []);
const validateDBML = useCallback(
@@ -189,7 +129,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!content.trim()) return;
try {
const sanitizedContent = sanitizeDBML(content);
const preprocessedContent = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser();
parser.parse(sanitizedContent, 'dbml');
} catch (e) {
@@ -199,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})`
);
highlightErrorLine(parsedError);
highlightErrorLine({
error: parsedError,
model: editorRef.current?.getModel(),
editorDecorationsCollection:
decorationsCollection.current,
});
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
@@ -207,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}
},
[clearDecorations, highlightErrorLine, t]
[clearDecorations, t]
);
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
@@ -242,13 +188,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!dbmlContent.trim() || errorMessage) return;
try {
// Sanitize DBML content before importing
const sanitizedContent = sanitizeDBML(dbmlContent);
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
const importedDiagram = await importDBMLToDiagram(dbmlContent);
const tableIdsToRemove = tables
.filter((table) =>
importedDiagram.tables?.some(
(t) =>
(t: DBTable) =>
t.name === table.name && t.schema === table.schema
)
)
@@ -257,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const relationshipIdsToRemove = relationships
.filter((relationship) => {
const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId
(table: DBTable) =>
table.id === relationship.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === relationship.targetTableId
(table: DBTable) =>
table.id === relationship.targetTableId
);
if (!sourceTable || !targetTable) return true;
const replacementSourceTable = importedDiagram.tables?.find(
(table) =>
(table: DBTable) =>
table.name === sourceTable.name &&
table.schema === sourceTable.schema
);
const replacementTargetTable = importedDiagram.tables?.find(
(table) =>
(table: DBTable) =>
table.name === targetTable.name &&
table.schema === targetTable.schema
);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/input/input';
import { Separator } from '@/components/separator/separator';
import { Group, SquarePlus } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { Label } from '@/components/label/label';
export interface TableSchemaDialogProps extends BaseDialogProps {
table?: DBTable;
schemas: DBSchema[];
onConfirm: (schema: string) => void;
onConfirm: ({ schema }: { schema: DBSchema }) => void;
allowSchemaCreation?: boolean;
}
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
@@ -29,27 +41,73 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
table,
schemas,
onConfirm,
allowSchemaCreation = false,
}) => {
const { t } = useTranslation();
const [selectedSchema, setSelectedSchema] = React.useState<string>(
const { databaseType } = useChartDB();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
table?.schema
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
);
const allowSchemaSelection = useMemo(
() => schemas && schemas.length > 0,
[schemas]
);
const defaultSchemaName = useMemo(
() => defaultSchemas?.[databaseType],
[databaseType]
);
const [isCreatingNew, setIsCreatingNew] =
useState<boolean>(!allowSchemaSelection);
const [newSchemaName, setNewSchemaName] = useState<string>(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
useEffect(() => {
if (!dialog.open) return;
setSelectedSchema(
setSelectedSchemaId(
table?.schema
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
);
}, [dialog.open, schemas, table?.schema]);
setIsCreatingNew(!allowSchemaSelection);
setNewSchemaName(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
}, [
defaultSchemaName,
dialog.open,
schemas,
table?.schema,
allowSchemaSelection,
allowSchemaCreation,
]);
const { closeTableSchemaDialog } = useDialog();
const handleConfirm = useCallback(() => {
onConfirm(selectedSchema);
}, [onConfirm, selectedSchema]);
if (isCreatingNew && newSchemaName.trim()) {
const newSchema: DBSchema = {
id: schemaNameToSchemaId(newSchemaName.trim()),
name: newSchemaName.trim(),
tableCount: 0,
};
onConfirm({ schema: newSchema });
} else {
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
onConfirm({ schema });
}
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
const schemaOptions: SelectBoxOption[] = useMemo(
() =>
@@ -60,6 +118,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
[schemas]
);
const renderSwitchCreateOrSelectButton = useCallback(
() => (
<Button
variant="outline"
className="w-full justify-start"
onClick={() => setIsCreatingNew(!isCreatingNew)}
disabled={!allowSchemaSelection || !allowSchemaCreation}
>
{!isCreatingNew ? (
<SquarePlus className="mr-2 size-4 " />
) : (
<Group className="mr-2 size-4 " />
)}
{isCreatingNew ? 'Select existing schema' : 'Create new schema'}
</Button>
),
[isCreatingNew, allowSchemaSelection, allowSchemaCreation]
);
return (
<Dialog
{...dialog}
@@ -67,48 +144,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
if (!open) {
closeTableSchemaDialog();
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.title')
: table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
</DialogTitle>
<DialogDescription>
{table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.description')
: table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchema}
onChange={(value) =>
setSelectedSchema(value as string)
}
/>
{!isCreatingNew ? (
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchemaId}
onChange={(value) =>
setSelectedSchemaId(value as string)
}
/>
) : (
<div className="flex flex-col gap-2">
{allowSchemaCreation &&
!allowSchemaSelection ? (
<Label htmlFor="new-schema-name">
Schema Name
</Label>
) : null}
<Input
id="new-schema-name"
value={newSchemaName}
onChange={(e) =>
setNewSchemaName(e.target.value)
}
placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
autoFocus
/>
</div>
)}
{allowSchemaCreation && allowSchemaSelection ? (
<>
<div className="relative">
<Separator className="my-2" />
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
or
</span>
</div>
{allowSchemaSelection ? (
renderSwitchCreateOrSelectButton()
) : (
<Tooltip>
<TooltipTrigger asChild>
<span>
{renderSwitchCreateOrSelectButton()}
</span>
</TooltipTrigger>
<TooltipContent>
<p>No existing schemas available</p>
</TooltipContent>
</Tooltip>
)}
</>
) : null}
</div>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
{isCreatingNew
? t('create_table_schema_dialog.cancel')
: table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={handleConfirm}>
{table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
<Button
onClick={handleConfirm}
disabled={isCreatingNew && !newSchemaName.trim()}
>
{isCreatingNew
? t('create_table_schema_dialog.create')
: table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
</Button>
</DialogClose>
</DialogFooter>

View File

@@ -83,6 +83,7 @@
}
body {
@apply bg-background text-foreground;
overscroll-behavior-x: none;
}
.text-editable {
@@ -154,3 +155,29 @@
background-size: 650%;
}
}
/* Edit button emphasis animation */
@keyframes dbml_edit-button-emphasis {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
background-color: rgba(59, 130, 246, 0);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0.1);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0);
}
}
.dbml-edit-button-emphasis {
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
animation-iteration-count: 1;
position: relative;
z-index: 10;
}

View File

@@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn';
import { gu, guMetadata } from './locales/gu';
import { vi, viMetadata } from './locales/vi';
import { ar, arMetadata } from './locales/ar';
import { hr, hrMetadata } from './locales/hr';
export const languages: LanguageMetadata[] = [
enMetadata,
esMetadata,
frMetadata,
deMetadata,
esMetadata,
ukMetadata,
ruMetadata,
trMetadata,
hrMetadata,
pt_BRMetadata,
hiMetadata,
jaMetadata,
ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata,
zh_TWMetadata,
neMetadata,
mrMetadata,
trMetadata,
id_IDMetadata,
teMetadata,
bnMetadata,
@@ -70,6 +72,7 @@ const resources = {
gu,
vi,
ar,
hr,
};
i18n.use(LanguageDetector)

View File

@@ -26,6 +26,8 @@ export const ar: LanguageTranslation = {
hide_sidebar: 'إخفاء الشريط الجانبي',
hide_cardinality: 'إخفاء الكاردينالية',
show_cardinality: 'إظهار الكاردينالية',
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
@@ -70,15 +72,6 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
},
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
dont_show_again: 'لا تظهره مجدداً',
change_schema: 'تغيير',
none: 'لا شيء',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'فشل النسخ',
@@ -113,10 +106,6 @@ export const ar: LanguageTranslation = {
copied: '!تم النسخ',
side_panel: {
schema: ':المخطط',
filter_by_schema: 'تصفية حسب المخطط',
search_schema: '...بحث في المخطط',
no_schemas_found: '.لم يتم العثور على مخططات',
view_all_options: '...عرض جميع الخيارات',
tables_section: {
tables: 'الجداول',
@@ -151,6 +140,10 @@ export const ar: LanguageTranslation = {
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
precision: 'الدقة',
scale: 'النطاق',
default_value: 'Default Value',
no_default: 'No default',
},
index_actions: {
title: 'خصائص الفهرس',
@@ -251,9 +244,12 @@ export const ar: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,6 +265,11 @@ export const ar: LanguageTranslation = {
redo: 'إعادة',
reorder_diagram: 'إعادة ترتيب الرسم البياني',
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
// TODO: Translate
filter: 'Filter Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
},
new_diagram_dialog: {
@@ -400,6 +401,13 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
confirm: 'تغيير',
},
create_table_schema_dialog: {
title: 'إنشاء مخطط جديد',
description:
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
create: 'إنشاء',
cancel: 'إلغاء',
},
star_us_dialog: {
title: '!ساعدنا على التحسن',

View File

@@ -26,6 +26,8 @@ export const bn: LanguageTranslation = {
hide_sidebar: 'সাইডবার লুকান',
hide_cardinality: 'কার্ডিনালিটি লুকান',
show_cardinality: 'কার্ডিনালিটি দেখান',
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
@@ -71,15 +73,6 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
},
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
dont_show_again: 'পুনরায় দেখাবেন না',
change_schema: 'পরিবর্তন করুন',
none: 'কিছুই না',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'কপি ব্যর্থ হয়েছে',
@@ -114,10 +107,6 @@ export const bn: LanguageTranslation = {
copied: 'অনুলিপি সম্পন্ন!',
side_panel: {
schema: 'স্কিমা:',
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
search_schema: 'স্কিমা খুঁজুন...',
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
view_all_options: 'সমস্ত বিকল্প দেখুন...',
tables_section: {
tables: 'টেবিল',
@@ -151,7 +140,12 @@ export const bn: LanguageTranslation = {
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'নির্ভুলতা',
scale: 'স্কেল',
},
index_actions: {
title: 'ইনডেক্স কর্ম',
@@ -251,9 +245,12 @@ export const bn: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,6 +266,12 @@ export const bn: LanguageTranslation = {
redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -400,6 +403,13 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
confirm: 'পরিবর্তন করুন',
},
create_table_schema_dialog: {
title: 'নতুন স্কিমা তৈরি করুন',
description:
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
create: 'তৈরি করুন',
cancel: 'বাতিল করুন',
},
star_us_dialog: {
title: 'আমাদের উন্নত করতে সাহায্য করুন!',

View File

@@ -26,6 +26,8 @@ export const de: LanguageTranslation = {
hide_sidebar: 'Seitenleiste ausblenden',
hide_cardinality: 'Kardinalität ausblenden',
show_cardinality: 'Kardinalität anzeigen',
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen',
@@ -71,15 +73,6 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
},
multiple_schemas_alert: {
title: 'Mehrere Schemas',
description:
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
dont_show_again: 'Nicht erneut anzeigen',
change_schema: 'Schema ändern',
none: 'Keine',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopieren fehlgeschlagen',
@@ -115,10 +108,6 @@ export const de: LanguageTranslation = {
copied: 'Kopiert!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Nach Schema filtern',
search_schema: 'Schema suchen...',
no_schemas_found: 'Keine Schemas gefunden.',
view_all_options: 'Alle Optionen anzeigen...',
tables_section: {
tables: 'Tabellen',
@@ -152,7 +141,12 @@ export const de: LanguageTranslation = {
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Präzision',
scale: 'Skalierung',
},
index_actions: {
title: 'Indexattribute',
@@ -253,9 +247,12 @@ export const de: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -270,7 +267,14 @@ export const de: LanguageTranslation = {
undo: 'Rückgängig',
redo: 'Wiederholen',
reorder_diagram: 'Diagramm neu anordnen',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -403,6 +407,13 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
confirm: 'Ändern',
},
create_table_schema_dialog: {
title: 'Neues Schema erstellen',
description:
'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
create: 'Erstellen',
cancel: 'Abbrechen',
},
star_us_dialog: {
title: 'Hilf uns, uns zu verbessern!',

View File

@@ -26,6 +26,8 @@ export const en = {
hide_sidebar: 'Hide Sidebar',
hide_cardinality: 'Hide Cardinality',
show_cardinality: 'Show Cardinality',
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll',
theme: 'Theme',
show_dependencies: 'Show Dependencies',
@@ -69,15 +71,6 @@ export const en = {
cancel: 'Cancel',
},
multiple_schemas_alert: {
title: 'Multiple Schemas',
description:
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
dont_show_again: "Don't show again",
change_schema: 'Change',
none: 'none',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Copy failed',
@@ -112,10 +105,6 @@ export const en = {
copied: 'Copied!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Filter by schema',
search_schema: 'Search schema...',
no_schemas_found: 'No schemas found.',
view_all_options: 'View all Options...',
tables_section: {
tables: 'Tables',
@@ -144,8 +133,12 @@ export const en = {
title: 'Field Attributes',
unique: 'Unique',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
comments: 'Comments',
no_comments: 'No comments',
default_value: 'Default Value',
no_default: 'No default',
delete_field: 'Delete Field',
},
index_actions: {
@@ -245,8 +238,11 @@ export const en = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
clear_field_highlight: 'Clear Highlight',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
@@ -263,6 +259,10 @@ export const en = {
redo: 'Redo',
reorder_diagram: 'Reorder Diagram',
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -394,6 +394,14 @@ export const en = {
confirm: 'Change',
},
create_table_schema_dialog: {
title: 'Create New Schema',
description:
'No schemas exist yet. Create your first schema to organize your tables.',
create: 'Create',
cancel: 'Cancel',
},
star_us_dialog: {
title: 'Help us improve!',
description:

View File

@@ -24,6 +24,8 @@ export const es: LanguageTranslation = {
view: 'Ver',
hide_cardinality: 'Ocultar Cardinalidad',
show_cardinality: 'Mostrar Cardinalidad',
show_field_attributes: 'Mostrar Atributos de Campo',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
@@ -104,10 +106,6 @@ export const es: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'No se encontraron esquemas.',
view_all_options: 'Ver todas las opciones...',
tables_section: {
tables: 'Tablas',
@@ -141,7 +139,12 @@ export const es: LanguageTranslation = {
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precisión',
scale: 'Escala',
},
index_actions: {
title: 'Atributos del Índice',
@@ -241,9 +244,12 @@ export const es: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -258,7 +264,13 @@ export const es: LanguageTranslation = {
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -392,6 +404,13 @@ export const es: LanguageTranslation = {
cancel: 'Cancelar',
confirm: 'Cambiar',
},
create_table_schema_dialog: {
title: 'Crear Nuevo Esquema',
description:
'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
create: 'Crear',
cancel: 'Cancelar',
},
star_us_dialog: {
title: '¡Ayúdanos a mejorar!',
@@ -401,14 +420,6 @@ export const es: LanguageTranslation = {
confirm: '¡Claro!',
},
multiple_schemas_alert: {
title: 'Múltiples Esquemas',
description:
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
dont_show_again: 'No mostrar de nuevo',
change_schema: 'Cambiar',
none: 'nada',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',

View File

@@ -26,6 +26,8 @@ export const fr: LanguageTranslation = {
hide_sidebar: 'Cacher la Barre Latérale',
hide_cardinality: 'Cacher la Cardinalité',
show_cardinality: 'Afficher la Cardinalité',
hide_field_attributes: 'Masquer les Attributs de Champ',
show_field_attributes: 'Afficher les Attributs de Champ',
zoom_on_scroll: 'Zoom sur le Défilement',
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
@@ -103,10 +105,6 @@ export const fr: LanguageTranslation = {
copied: 'Copié !',
side_panel: {
schema: 'Schéma:',
filter_by_schema: 'Filtrer par schéma',
search_schema: 'Rechercher un schéma...',
no_schemas_found: 'Aucun schéma trouvé.',
view_all_options: 'Voir toutes les Options...',
tables_section: {
tables: 'Tables',
@@ -139,7 +137,12 @@ export const fr: LanguageTranslation = {
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Précision',
scale: 'Échelle',
},
index_actions: {
title: "Attributs de l'Index",
@@ -239,9 +242,12 @@ export const fr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -256,7 +262,13 @@ export const fr: LanguageTranslation = {
undo: 'Annuler',
redo: 'Rétablir',
reorder_diagram: 'Réorganiser le Diagramme',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Surligner les tables chevauchées',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -341,15 +353,6 @@ export const fr: LanguageTranslation = {
transparent_description: 'Remove background color from image.',
},
multiple_schemas_alert: {
title: 'Schémas Multiples',
description:
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
dont_show_again: 'Ne plus afficher',
change_schema: 'Changer',
none: 'Aucun',
},
new_table_schema_dialog: {
title: 'Sélectionner un Schéma',
description:
@@ -372,6 +375,13 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
confirm: 'Modifier',
},
create_table_schema_dialog: {
title: 'Créer un Nouveau Schéma',
description:
"Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
create: 'Créer',
cancel: 'Annuler',
},
create_relationship_dialog: {
title: 'Créer une Relation',

View File

@@ -26,6 +26,8 @@ export const gu: LanguageTranslation = {
hide_sidebar: 'સાઇડબાર છુપાવો',
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
@@ -71,15 +73,6 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો',
},
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
dont_show_again: 'ફરીથી ન બતાવો',
change_schema: 'બદલો',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'નકલ નિષ્ફળ',
@@ -114,10 +107,6 @@ export const gu: LanguageTranslation = {
copied: 'નકલ થયું!',
side_panel: {
schema: 'સ્કીમા:',
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
search_schema: 'સ્કીમા શોધો...',
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
view_all_options: 'બધા વિકલ્પો જુઓ...',
tables_section: {
tables: 'ટેબલ્સ',
@@ -152,7 +141,12 @@ export const gu: LanguageTranslation = {
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'ચોકસાઈ',
scale: 'માપ',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',
@@ -252,9 +246,12 @@ export const gu: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,7 +266,13 @@ export const gu: LanguageTranslation = {
undo: 'અનડુ',
redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -401,6 +404,14 @@ export const gu: LanguageTranslation = {
confirm: 'બદલો',
},
create_table_schema_dialog: {
title: 'નવું સ્કીમા બનાવો',
description:
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
create: 'બનાવો',
cancel: 'રદ કરો',
},
star_us_dialog: {
title: 'અમને સુધારવામાં મદદ કરો!',
description:

View File

@@ -26,6 +26,8 @@ export const hi: LanguageTranslation = {
hide_sidebar: 'साइडबार छिपाएँ',
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
@@ -70,15 +72,6 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
dont_show_again: 'फिर से न दिखाएँ',
change_schema: 'बदलें',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी असफल',
@@ -114,10 +107,6 @@ export const hi: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
search_schema: 'स्कीमा खोजें...',
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
view_all_options: 'सभी विकल्प देखें...',
tables_section: {
tables: 'तालिकाएँ',
@@ -151,7 +140,12 @@ export const hi: LanguageTranslation = {
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',
@@ -252,9 +246,12 @@ export const hi: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,7 +266,13 @@ export const hi: LanguageTranslation = {
undo: 'पूर्ववत करें',
redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -404,6 +407,14 @@ export const hi: LanguageTranslation = {
confirm: 'बदलें',
},
create_table_schema_dialog: {
title: 'नया स्कीमा बनाएं',
description:
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
create: 'बनाएं',
cancel: 'रद्द करें',
},
star_us_dialog: {
title: 'हमें सुधारने में मदद करें!',
description:

491
src/i18n/locales/hr.ts Normal file
View File

@@ -0,0 +1,491 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hr: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'Datoteka',
new: 'Nova',
open: 'Otvori',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
exit: 'Izađi',
},
edit: {
edit: 'Uredi',
undo: 'Poništi',
redo: 'Ponovi',
clear: 'Očisti',
},
view: {
view: 'Prikaz',
show_sidebar: 'Prikaži bočnu traku',
hide_sidebar: 'Sakrij bočnu traku',
hide_cardinality: 'Sakrij kardinalnost',
show_cardinality: 'Prikaži kardinalnost',
hide_field_attributes: 'Sakrij atribute polja',
show_field_attributes: 'Prikaži atribute polja',
zoom_on_scroll: 'Zumiranje pri skrolanju',
theme: 'Tema',
show_dependencies: 'Prikaži ovisnosti',
hide_dependencies: 'Sakrij ovisnosti',
show_minimap: 'Prikaži mini kartu',
hide_minimap: 'Sakrij mini kartu',
},
backup: {
backup: 'Sigurnosna kopija',
export_diagram: 'Izvezi dijagram',
restore_diagram: 'Vrati dijagram',
},
help: {
help: 'Pomoć',
docs_website: 'Dokumentacija',
join_discord: 'Pridružite nam se na Discordu',
},
},
delete_diagram_alert: {
title: 'Izbriši dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati dijagram.',
cancel: 'Odustani',
delete: 'Izbriši',
},
clear_diagram_alert: {
title: 'Očisti dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve podatke u dijagramu.',
cancel: 'Odustani',
clear: 'Očisti',
},
reorder_diagram_alert: {
title: 'Preuredi dijagram',
description:
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
reorder: 'Preuredi',
cancel: 'Odustani',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopiranje neuspješno',
description: 'Međuspremnik nije podržan.',
},
failed: {
title: 'Kopiranje neuspješno',
description: 'Nešto je pošlo po zlu. Molimo pokušajte ponovno.',
},
},
theme: {
system: 'Sustav',
light: 'Svijetla',
dark: 'Tamna',
},
zoom: {
on: 'Uključeno',
off: 'Isključeno',
},
last_saved: 'Zadnje spremljeno',
saved: 'Spremljeno',
loading_diagram: 'Učitavanje dijagrama...',
deselect_all: 'Odznači sve',
select_all: 'Označi sve',
clear: 'Očisti',
show_more: 'Prikaži više',
show_less: 'Prikaži manje',
copy_to_clipboard: 'Kopiraj u međuspremnik',
copied: 'Kopirano!',
side_panel: {
view_all_options: 'Prikaži sve opcije...',
tables_section: {
tables: 'Tablice',
add_table: 'Dodaj tablicu',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
clear: 'Očisti filter',
no_results:
'Nema pronađenih tablica koje odgovaraju vašem filteru.',
show_list: 'Prikaži popis tablica',
show_dbml: 'Prikaži DBML uređivač',
table: {
fields: 'Polja',
nullable: 'Može biti null?',
primary_key: 'Primarni ključ',
indexes: 'Indeksi',
comments: 'Komentari',
no_comments: 'Nema komentara',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
index_select_fields: 'Odaberi polja',
no_types_found: 'Nema pronađenih tipova',
field_name: 'Naziv',
field_type: 'Tip',
field_actions: {
title: 'Atributi polja',
unique: 'Jedinstven',
character_length: 'Maksimalna dužina',
precision: 'Preciznost',
scale: 'Skala',
comments: 'Komentari',
no_comments: 'Nema komentara',
default_value: 'Zadana vrijednost',
no_default: 'Nema zadane vrijednosti',
delete_field: 'Izbriši polje',
},
index_actions: {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
delete_index: 'Izbriši indeks',
},
table_actions: {
title: 'Radnje nad tablicom',
change_schema: 'Promijeni shemu',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
},
},
empty_state: {
title: 'Nema tablica',
description: 'Stvorite tablicu za početak',
},
},
relationships_section: {
relationships: 'Veze',
filter: 'Filtriraj',
add_relationship: 'Dodaj vezu',
collapse: 'Sažmi sve',
relationship: {
primary: 'Primarna tablica',
foreign: 'Referentna tablica',
cardinality: 'Kardinalnost',
delete_relationship: 'Izbriši',
relationship_actions: {
title: 'Radnje',
delete_relationship: 'Izbriši',
},
},
empty_state: {
title: 'Nema veza',
description: 'Stvorite vezu za povezivanje tablica',
},
},
dependencies_section: {
dependencies: 'Ovisnosti',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
dependency: {
table: 'Tablica',
dependent_table: 'Ovisni pogled',
delete_dependency: 'Izbriši',
dependency_actions: {
title: 'Radnje',
delete_dependency: 'Izbriši',
},
},
empty_state: {
title: 'Nema ovisnosti',
description: 'Stvorite pogled za početak',
},
},
areas_section: {
areas: 'Područja',
add_area: 'Dodaj područje',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih područja koja odgovaraju vašem filteru.',
area: {
area_actions: {
title: 'Radnje nad područjem',
edit_name: 'Uredi naziv',
delete_area: 'Izbriši područje',
},
},
empty_state: {
title: 'Nema područja',
description: 'Stvorite područje za početak',
},
},
custom_types_section: {
custom_types: 'Prilagođeni tipovi',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.',
empty_state: {
title: 'Nema prilagođenih tipova',
description:
'Prilagođeni tipovi će se pojaviti ovdje kada budu dostupni u vašoj bazi podataka',
},
custom_type: {
kind: 'Vrsta',
enum_values: 'Enum vrijednosti',
composite_fields: 'Polja',
no_fields: 'Nema definiranih polja',
field_name_placeholder: 'Naziv polja',
field_type_placeholder: 'Odaberi tip',
add_field: 'Dodaj polje',
no_fields_tooltip:
'Nema definiranih polja za ovaj prilagođeni tip',
custom_type_actions: {
title: 'Radnje',
highlight_fields: 'Istakni polja',
clear_field_highlight: 'Ukloni isticanje',
delete_custom_type: 'Izbriši',
},
delete_custom_type: 'Izbriši tip',
},
},
},
toolbar: {
zoom_in: 'Uvećaj',
zoom_out: 'Smanji',
save: 'Spremi',
show_all: 'Prikaži sve',
undo: 'Poništi',
redo: 'Ponovi',
reorder_diagram: 'Preuredi dijagram',
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
custom_type_highlight_tooltip:
'Isticanje "{{typeName}}" - Kliknite za uklanjanje',
filter: 'Filtriraj tablice',
},
new_diagram_dialog: {
database_selection: {
title: 'Koja je vaša baza podataka?',
description:
'Svaka baza podataka ima svoje jedinstvene značajke i mogućnosti.',
check_examples_long: 'Pogledaj primjere',
check_examples_short: 'Primjeri',
},
import_database: {
title: 'Uvezite svoju bazu podataka',
database_edition: 'Verzija baze podataka:',
step_1: 'Pokrenite ovu skriptu u svojoj bazi podataka:',
step_2: 'Zalijepite rezultat skripte u ovaj dio →',
script_results_placeholder: 'Rezultati skripte ovdje...',
ssms_instructions: {
button_text: 'SSMS upute',
title: 'Upute',
step_1: 'Idite na Tools > Options > Query Results > SQL Server.',
step_2: 'Ako koristite "Results to Grid," promijenite Maximum Characters Retrieved za Non-XML podatke (postavite na 9999999).',
},
instructions_link: 'Trebate pomoć? Pogledajte kako',
check_script_result: 'Provjeri rezultat skripte',
},
cancel: 'Odustani',
import_from_file: 'Uvezi iz datoteke',
back: 'Natrag',
empty_diagram: 'Prazan dijagram',
continue: 'Nastavi',
import: 'Uvezi',
},
open_diagram_dialog: {
title: 'Otvori dijagram',
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
table_columns: {
name: 'Naziv',
created_at: 'Stvoreno',
last_modified: 'Zadnje izmijenjeno',
tables_count: 'Tablice',
},
cancel: 'Odustani',
open: 'Otvori',
},
export_sql_dialog: {
title: 'Izvezi SQL',
description:
'Izvezite shemu vašeg dijagrama u {{databaseType}} skriptu',
close: 'Zatvori',
loading: {
text: 'AI generira SQL za {{databaseType}}...',
description: 'Ovo bi trebalo potrajati do 30 sekundi.',
},
error: {
message:
'Greška pri generiranju SQL skripte. Molimo pokušajte ponovno kasnije ili <0>kontaktirajte nas</0>.',
description:
'Slobodno koristite svoj OPENAI_TOKEN, pogledajte priručnik <0>ovdje</0>.',
},
},
create_relationship_dialog: {
title: 'Kreiraj vezu',
primary_table: 'Primarna tablica',
primary_field: 'Primarno polje',
referenced_table: 'Referentna tablica',
referenced_field: 'Referentno polje',
primary_table_placeholder: 'Odaberi tablicu',
primary_field_placeholder: 'Odaberi polje',
referenced_table_placeholder: 'Odaberi tablicu',
referenced_field_placeholder: 'Odaberi polje',
no_tables_found: 'Nema pronađenih tablica',
no_fields_found: 'Nema pronađenih polja',
create: 'Kreiraj',
cancel: 'Odustani',
},
import_database_dialog: {
title: 'Uvezi u trenutni dijagram',
override_alert: {
title: 'Uvezi bazu podataka',
content: {
alert: 'Uvoz ovog dijagrama će utjecati na postojeće tablice i veze.',
new_tables:
'<bold>{{newTablesNumber}}</bold> novih tablica će biti dodano.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> novih veza će biti stvoreno.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> tablica će biti prepisano.',
proceed: 'Želite li nastaviti?',
},
import: 'Uvezi',
cancel: 'Odustani',
},
},
export_image_dialog: {
title: 'Izvezi sliku',
description: 'Odaberite faktor veličine za izvoz:',
scale_1x: '1x Obično',
scale_2x: '2x (Preporučeno)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Odustani',
export: 'Izvezi',
advanced_options: 'Napredne opcije',
pattern: 'Uključi pozadinski uzorak',
pattern_description: 'Dodaj suptilni mrežni uzorak u pozadinu.',
transparent: 'Prozirna pozadina',
transparent_description: 'Ukloni boju pozadine iz slike.',
},
new_table_schema_dialog: {
title: 'Odaberi shemu',
description:
'Trenutno je prikazano više shema. Odaberite jednu za novu tablicu.',
cancel: 'Odustani',
confirm: 'Potvrdi',
},
update_table_schema_dialog: {
title: 'Promijeni shemu',
description: 'Ažuriraj shemu tablice "{{tableName}}"',
cancel: 'Odustani',
confirm: 'Promijeni',
},
create_table_schema_dialog: {
title: 'Stvori novu shemu',
description:
'Još ne postoje sheme. Stvorite svoju prvu shemu za organiziranje tablica.',
create: 'Stvori',
cancel: 'Odustani',
},
star_us_dialog: {
title: 'Pomozite nam da se poboljšamo!',
description:
'Želite li nam dati zvjezdicu na GitHubu? Samo je jedan klik!',
close: 'Ne sada',
confirm: 'Naravno!',
},
export_diagram_dialog: {
title: 'Izvezi dijagram',
description: 'Odaberite format za izvoz:',
format_json: 'JSON',
cancel: 'Odustani',
export: 'Izvezi',
error: {
title: 'Greška pri izvozu dijagrama',
description:
'Nešto je pošlo po zlu. Trebate pomoć? support@chartdb.io',
},
},
import_diagram_dialog: {
title: 'Uvezi dijagram',
description: 'Uvezite dijagram iz JSON datoteke.',
cancel: 'Odustani',
import: 'Uvezi',
error: {
title: 'Greška pri uvozu dijagrama',
description:
'JSON dijagrama je nevažeći. Molimo provjerite JSON i pokušajte ponovno. Trebate pomoć? support@chartdb.io',
},
},
import_dbml_dialog: {
example_title: 'Uvezi primjer DBML-a',
title: 'Uvezi DBML',
description: 'Uvezite shemu baze podataka iz DBML formata.',
import: 'Uvezi',
cancel: 'Odustani',
skip_and_empty: 'Preskoči i isprazni',
show_example: 'Prikaži primjer',
error: {
title: 'Greška pri uvozu DBML-a',
description:
'Neuspješno parsiranje DBML-a. Molimo provjerite sintaksu.',
},
},
relationship_type: {
one_to_one: 'Jedan na jedan',
one_to_many: 'Jedan na više',
many_to_one: 'Više na jedan',
many_to_many: 'Više na više',
},
canvas_context_menu: {
new_table: 'Nova tablica',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
},
table_node_context_menu: {
edit_table: 'Uredi tablicu',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
add_relationship: 'Dodaj vezu',
},
snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})',
tool_tips: {
double_click_to_edit: 'Dvostruki klik za uređivanje',
},
language_select: {
change_language: 'Jezik',
},
},
};
export const hrMetadata: LanguageMetadata = {
name: 'Croatian',
nativeName: 'Hrvatski',
code: 'hr',
};

View File

@@ -26,6 +26,8 @@ export const id_ID: LanguageTranslation = {
hide_sidebar: 'Sembunyikan Sidebar',
hide_cardinality: 'Sembunyikan Kardinalitas',
show_cardinality: 'Tampilkan Kardinalitas',
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll',
theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi',
@@ -70,15 +72,6 @@ export const id_ID: LanguageTranslation = {
cancel: 'Batal',
},
multiple_schemas_alert: {
title: 'Schema Lebih dari satu',
description:
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
dont_show_again: 'Jangan tampilkan lagi',
change_schema: 'Ubah',
none: 'Tidak ada',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Gagal menyalin',
@@ -113,10 +106,6 @@ export const id_ID: LanguageTranslation = {
copied: 'Tersalin!',
side_panel: {
schema: 'Skema:',
filter_by_schema: 'Saring berdasarkan skema',
search_schema: 'Cari skema...',
no_schemas_found: 'Tidak ada skema yang ditemukan.',
view_all_options: 'Tampilkan Semua Pilihan...',
tables_section: {
tables: 'Tabel',
@@ -150,7 +139,12 @@ export const id_ID: LanguageTranslation = {
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Presisi',
scale: 'Skala',
},
index_actions: {
title: 'Atribut Indeks',
@@ -250,9 +244,12 @@ export const id_ID: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,7 +264,13 @@ export const id_ID: LanguageTranslation = {
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Atur Ulang Diagram',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -399,6 +402,14 @@ export const id_ID: LanguageTranslation = {
confirm: 'Ubah',
},
create_table_schema_dialog: {
title: 'Buat Skema Baru',
description:
'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
create: 'Buat',
cancel: 'Batal',
},
star_us_dialog: {
title: 'Bantu kami meningkatkan!',
description:

View File

@@ -26,6 +26,8 @@ export const ja: LanguageTranslation = {
hide_sidebar: 'サイドバーを非表示',
hide_cardinality: 'カーディナリティを非表示',
show_cardinality: 'カーディナリティを表示',
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
theme: 'テーマ',
// TODO: Translate
@@ -72,15 +74,6 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
},
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
dont_show_again: '再表示しない',
change_schema: '変更',
none: 'なし',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'コピー失敗',
@@ -117,10 +110,6 @@ export const ja: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'スキーマ:',
filter_by_schema: 'スキーマでフィルタ',
search_schema: 'スキーマを検索...',
no_schemas_found: 'スキーマが見つかりません。',
view_all_options: 'すべてのオプションを表示...',
tables_section: {
tables: 'テーブル',
@@ -154,7 +143,12 @@ export const ja: LanguageTranslation = {
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小数点以下桁数',
},
index_actions: {
title: 'インデックス属性',
@@ -256,9 +250,12 @@ export const ja: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -275,6 +272,10 @@ export const ja: LanguageTranslation = {
reorder_diagram: 'ダイアグラムを並べ替え',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -408,6 +409,14 @@ export const ja: LanguageTranslation = {
confirm: '変更',
},
create_table_schema_dialog: {
title: '新しいスキーマを作成',
description:
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
create: '作成',
cancel: 'キャンセル',
},
star_us_dialog: {
title: '改善をサポートしてください!',
description:

View File

@@ -26,6 +26,8 @@ export const ko_KR: LanguageTranslation = {
hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기',
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
theme: '테마',
show_dependencies: '종속성 보이기',
@@ -70,15 +72,6 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
copy_to_clipboard_toast: {
unsupported: {
title: '복사 실패',
@@ -113,10 +106,6 @@ export const ko_KR: LanguageTranslation = {
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
@@ -150,7 +139,12 @@ export const ko_KR: LanguageTranslation = {
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '정밀도',
scale: '소수점 자릿수',
},
index_actions: {
title: '인덱스 속성',
@@ -250,9 +244,12 @@ export const ko_KR: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,7 +264,13 @@ export const ko_KR: LanguageTranslation = {
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -399,6 +402,14 @@ export const ko_KR: LanguageTranslation = {
confirm: '변경',
},
create_table_schema_dialog: {
title: '새 스키마 생성',
description:
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
create: '생성',
cancel: '취소',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:

View File

@@ -26,6 +26,8 @@ export const mr: LanguageTranslation = {
hide_sidebar: 'साइडबार लपवा',
hide_cardinality: 'कार्डिनॅलिटी लपवा',
show_cardinality: 'कार्डिनॅलिटी दाखवा',
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
@@ -71,15 +73,6 @@ export const mr: LanguageTranslation = {
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
dont_show_again: 'पुन्हा दाखवू नका',
change_schema: 'बदला',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी अयशस्वी',
@@ -116,10 +109,6 @@ export const mr: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...',
tables_section: {
tables: 'टेबल्स',
@@ -153,7 +142,12 @@ export const mr: LanguageTranslation = {
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'अचूकता',
scale: 'प्रमाण',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
@@ -255,9 +249,12 @@ export const mr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -272,7 +269,13 @@ export const mr: LanguageTranslation = {
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -407,6 +410,14 @@ export const mr: LanguageTranslation = {
confirm: 'बदला',
},
create_table_schema_dialog: {
title: 'नवीन स्कीमा तयार करा',
description:
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
create: 'तयार करा',
cancel: 'रद्द करा',
},
star_us_dialog: {
title: 'आम्हाला सुधारण्यास मदत करा!',
description:

View File

@@ -26,6 +26,8 @@ export const ne: LanguageTranslation = {
hide_sidebar: 'साइडबार लुकाउनुहोस्',
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
@@ -71,15 +73,6 @@ export const ne: LanguageTranslation = {
cancel: 'रद्द गर्नुहोस्',
},
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
dont_show_again: 'फेरि देखाउन नदिनुहोस्',
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
none: 'कुनै पनि छैन',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'प्रतिलिपि असफल',
@@ -114,10 +107,6 @@ export const ne: LanguageTranslation = {
copied: 'प्रतिलिपि गरियो!',
side_panel: {
schema: 'स्कीम:',
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
search_schema: 'स्कीम खोज्नुहोस्...',
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
tables_section: {
tables: 'तालिकाहरू',
@@ -151,7 +140,12 @@ export const ne: LanguageTranslation = {
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'परिशुद्धता',
scale: 'स्केल',
},
index_actions: {
title: 'सूचक विशेषताहरू',
@@ -252,9 +246,12 @@ export const ne: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,8 +266,14 @@ export const ne: LanguageTranslation = {
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables:
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -404,6 +407,14 @@ export const ne: LanguageTranslation = {
confirm: 'परिवर्तन गर्नुहोस्',
},
create_table_schema_dialog: {
title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
description:
'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
create: 'सिर्जना गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
star_us_dialog: {
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
description:

View File

@@ -26,6 +26,8 @@ export const pt_BR: LanguageTranslation = {
hide_sidebar: 'Ocultar Barra Lateral',
hide_cardinality: 'Ocultar Cardinalidade',
show_cardinality: 'Mostrar Cardinalidade',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_field_attributes: 'Mostrar Atributos de Campo',
zoom_on_scroll: 'Zoom ao Rolar',
theme: 'Tema',
show_dependencies: 'Mostrar Dependências',
@@ -71,15 +73,6 @@ export const pt_BR: LanguageTranslation = {
cancel: 'Cancelar',
},
multiple_schemas_alert: {
title: 'Múltiplos Esquemas',
description:
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
dont_show_again: 'Não mostrar novamente',
change_schema: 'Alterar',
none: 'nenhum',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Falha na cópia',
@@ -114,10 +107,6 @@ export const pt_BR: LanguageTranslation = {
copied: 'Copiado!',
side_panel: {
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'Nenhum esquema encontrado.',
view_all_options: 'Ver todas as Opções...',
tables_section: {
tables: 'Tabelas',
@@ -151,7 +140,12 @@ export const pt_BR: LanguageTranslation = {
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precisão',
scale: 'Escala',
},
index_actions: {
title: 'Atributos do Índice',
@@ -251,9 +245,12 @@ export const pt_BR: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -268,7 +265,13 @@ export const pt_BR: LanguageTranslation = {
undo: 'Desfazer',
redo: 'Refazer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -402,6 +405,14 @@ export const pt_BR: LanguageTranslation = {
confirm: 'Alterar',
},
create_table_schema_dialog: {
title: 'Criar Novo Esquema',
description:
'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.',
create: 'Criar',
cancel: 'Cancelar',
},
star_us_dialog: {
title: 'Ajude-nos a melhorar!',
description:

View File

@@ -26,6 +26,8 @@ export const ru: LanguageTranslation = {
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть виды связи',
show_cardinality: 'Показать виды связи',
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
@@ -69,15 +71,6 @@ export const ru: LanguageTranslation = {
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
dont_show_again: 'Больше не показывать',
change_schema: 'Изменить',
none: 'никто',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Ошибка копирования',
@@ -111,10 +104,6 @@ export const ru: LanguageTranslation = {
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
@@ -147,7 +136,12 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
character_length: 'Макс. длина',
precision: 'Точность',
scale: 'Масштаб',
},
index_actions: {
title: 'Атрибуты индекса',
@@ -248,9 +242,12 @@ export const ru: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -265,7 +262,13 @@ export const ru: LanguageTranslation = {
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -399,6 +402,14 @@ export const ru: LanguageTranslation = {
confirm: 'Изменить',
},
create_table_schema_dialog: {
title: 'Создать новую схему',
description:
'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
create: 'Создать',
cancel: 'Отменить',
},
star_us_dialog: {
title: 'Помогите нам стать лучше!',
description:

View File

@@ -26,6 +26,8 @@ export const te: LanguageTranslation = {
hide_sidebar: 'సైడ్‌బార్ దాచండి',
hide_cardinality: 'కార్డినాలిటీని దాచండి',
show_cardinality: 'కార్డినాలిటీని చూపించండి',
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి',
@@ -71,15 +73,6 @@ export const te: LanguageTranslation = {
cancel: 'రద్దు',
},
multiple_schemas_alert: {
title: 'బహుళ స్కీమాలు',
description:
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
dont_show_again: 'మరలా చూపించవద్దు',
change_schema: 'మార్చు',
none: 'ఎదరికాదు',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'కాపీ విఫలమైంది',
@@ -114,10 +107,6 @@ export const te: LanguageTranslation = {
copied: 'కాపీ చేయబడింది!',
side_panel: {
schema: 'స్కీమా:',
filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి',
search_schema: 'స్కీమా కోసం శోధించండి...',
no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.',
view_all_options: 'అన్ని ఎంపికలను చూడండి...',
tables_section: {
tables: 'పట్టికలు',
@@ -151,7 +140,12 @@ export const te: LanguageTranslation = {
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'సూక్ష్మత',
scale: 'స్కేల్',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',
@@ -252,9 +246,12 @@ export const te: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,7 +266,13 @@ export const te: LanguageTranslation = {
undo: 'తిరిగి చేయు',
redo: 'మరలా చేయు',
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -403,6 +406,14 @@ export const te: LanguageTranslation = {
confirm: 'మార్చు',
},
create_table_schema_dialog: {
title: 'కొత్త స్కీమా సృష్టించండి',
description:
'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
create: 'సృష్టించు',
cancel: 'రద్దు',
},
star_us_dialog: {
title: 'మా సహాయంతో మెరుగుపరచండి!',
description:

View File

@@ -26,6 +26,8 @@ export const tr: LanguageTranslation = {
hide_sidebar: 'Kenar Çubuğunu Gizle',
hide_cardinality: 'Kardinaliteyi Gizle',
show_cardinality: 'Kardinaliteyi Göster',
show_field_attributes: 'Alan Özelliklerini Göster',
hide_field_attributes: 'Alan Özelliklerini Gizle',
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
theme: 'Tema',
show_dependencies: 'Bağımlılıkları Göster',
@@ -71,15 +73,6 @@ export const tr: LanguageTranslation = {
cancel: 'İptal',
},
multiple_schemas_alert: {
title: 'Birden Fazla Şema',
description:
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
dont_show_again: 'Tekrar gösterme',
change_schema: 'Değiştir',
none: 'yok',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopyalama başarısız',
@@ -113,10 +106,6 @@ export const tr: LanguageTranslation = {
copy_to_clipboard: 'Panoya Kopyala',
copied: 'Kopyalandı!',
side_panel: {
schema: 'Şema:',
filter_by_schema: 'Şemaya Göre Filtrele',
search_schema: 'Şema ara...',
no_schemas_found: 'Şema bulunamadı.',
view_all_options: 'Tüm Seçenekleri Gör...',
tables_section: {
tables: 'Tablolar',
@@ -150,7 +139,12 @@ export const tr: LanguageTranslation = {
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Hassasiyet',
scale: 'Ölçek',
},
index_actions: {
title: 'İndeks Özellikleri',
@@ -251,9 +245,12 @@ export const tr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,7 +264,13 @@ export const tr: LanguageTranslation = {
undo: 'Geri Al',
redo: 'Yinele',
reorder_diagram: 'Diyagramı Yeniden Sırala',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
database_selection: {
@@ -392,6 +395,14 @@ export const tr: LanguageTranslation = {
cancel: 'İptal',
confirm: 'Değiştir',
},
create_table_schema_dialog: {
title: 'Yeni Şema Oluştur',
description:
'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.',
create: 'Oluştur',
cancel: 'İptal',
},
star_us_dialog: {
title: 'Bize yardım et!',
description:

View File

@@ -26,6 +26,8 @@ export const uk: LanguageTranslation = {
hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність',
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням',
theme: 'Тема',
show_dependencies: 'Показати залежності',
@@ -69,15 +71,6 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
},
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
dont_show_again: 'Більше не показувати',
change_schema: 'Зміна',
none: 'немає',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Помилка копіювання',
@@ -112,10 +105,6 @@ export const uk: LanguageTranslation = {
copied: 'Скопійовано!',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Пошук схеми…',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри…',
tables_section: {
tables: 'Таблиці',
@@ -149,7 +138,12 @@ export const uk: LanguageTranslation = {
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Точність',
scale: 'Масштаб',
},
index_actions: {
title: 'Атрибути індексу',
@@ -249,9 +243,12 @@ export const uk: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,7 +263,13 @@ export const uk: LanguageTranslation = {
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Перевпорядкувати діаграму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -400,6 +403,14 @@ export const uk: LanguageTranslation = {
confirm: 'Змінити',
},
create_table_schema_dialog: {
title: 'Створити нову схему',
description:
'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
create: 'Створити',
cancel: 'Скасувати',
},
star_us_dialog: {
title: 'Допоможіть нам покращитися!',
description: 'Поставне на зірку на GitHub? Це лише один клік!',

View File

@@ -26,6 +26,8 @@ export const vi: LanguageTranslation = {
hide_sidebar: 'Ẩn thanh bên',
hide_cardinality: 'Ẩn số lượng',
show_cardinality: 'Hiển thị số lượng',
show_field_attributes: 'Hiển thị thuộc tính trường',
hide_field_attributes: 'Ẩn thuộc tính trường',
zoom_on_scroll: 'Thu phóng khi cuộn',
theme: 'Chủ đề',
show_dependencies: 'Hiển thị các phụ thuộc',
@@ -70,15 +72,6 @@ export const vi: LanguageTranslation = {
cancel: 'Hủy',
},
multiple_schemas_alert: {
title: 'Có nhiều lược đồ',
description:
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
dont_show_again: 'Không hiển thị lại',
change_schema: 'Thay đổi',
none: 'không có',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Sao chép thất bại',
@@ -113,10 +106,6 @@ export const vi: LanguageTranslation = {
copied: 'Đã sao chép!',
side_panel: {
schema: 'Lược đồ:',
filter_by_schema: 'Lọc bởi lược đồ',
search_schema: 'Tìm kiếm lược đồ...',
no_schemas_found: 'Không tìm thấy lược đồ.',
view_all_options: 'Xem tất cả tùy chọn...',
tables_section: {
tables: 'Bảng',
@@ -150,7 +139,12 @@ export const vi: LanguageTranslation = {
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Độ chính xác',
scale: 'Tỷ lệ',
},
index_actions: {
title: 'Thuộc tính chỉ mục',
@@ -250,9 +244,12 @@ export const vi: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,7 +264,13 @@ export const vi: LanguageTranslation = {
undo: 'Hoàn tác',
redo: 'Làm lại',
reorder_diagram: 'Sắp xếp lại sơ đồ',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -399,6 +402,14 @@ export const vi: LanguageTranslation = {
confirm: 'Xác nhận',
},
create_table_schema_dialog: {
title: 'Tạo lược đồ mới',
description:
'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.',
create: 'Tạo',
cancel: 'Hủy',
},
star_us_dialog: {
title: 'Hãy giúp chúng tôi cải thiện!',
description:

View File

@@ -26,6 +26,8 @@ export const zh_CN: LanguageTranslation = {
hide_sidebar: '隐藏侧边栏',
hide_cardinality: '隐藏基数',
show_cardinality: '展示基数',
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放',
theme: '主题',
show_dependencies: '展示依赖',
@@ -67,15 +69,6 @@ export const zh_CN: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
dont_show_again: '不再展示',
change_schema: '更改',
none: '无',
},
copy_to_clipboard_toast: {
unsupported: {
title: '复制失败',
@@ -110,10 +103,6 @@ export const zh_CN: LanguageTranslation = {
copied: '复制了!',
side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...',
tables_section: {
tables: '表',
@@ -147,7 +136,12 @@ export const zh_CN: LanguageTranslation = {
no_comments: '空',
delete_field: '删除字段',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小数位',
},
index_actions: {
title: '索引属性',
@@ -247,9 +241,12 @@ export const zh_CN: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,7 +261,13 @@ export const zh_CN: LanguageTranslation = {
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出显示重叠的表',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -395,6 +398,13 @@ export const zh_CN: LanguageTranslation = {
confirm: '更改',
},
create_table_schema_dialog: {
title: '创建新模式',
description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
create: '创建',
cancel: '取消',
},
star_us_dialog: {
title: '帮助我们改进!',
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',

View File

@@ -26,6 +26,8 @@ export const zh_TW: LanguageTranslation = {
hide_sidebar: '隱藏側邊欄',
hide_cardinality: '隱藏基數',
show_cardinality: '顯示基數',
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放',
theme: '主題',
show_dependencies: '顯示相依性',
@@ -67,15 +69,6 @@ export const zh_TW: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
dont_show_again: '不再顯示',
change_schema: '變更',
none: '無',
},
copy_to_clipboard_toast: {
unsupported: {
title: '複製失敗',
@@ -110,10 +103,6 @@ export const zh_TW: LanguageTranslation = {
copied: '已複製!',
side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...',
tables_section: {
tables: '表格',
@@ -147,7 +136,12 @@ export const zh_TW: LanguageTranslation = {
no_comments: '無註解',
delete_field: '刪除欄位',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小數位',
},
index_actions: {
title: '索引屬性',
@@ -247,9 +241,12 @@ export const zh_TW: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,7 +261,13 @@ export const zh_TW: LanguageTranslation = {
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出顯示重疊表格',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -394,6 +397,14 @@ export const zh_TW: LanguageTranslation = {
confirm: '變更',
},
create_table_schema_dialog: {
title: '建立新 Schema',
description:
'尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
create: '建立',
cancel: '取消',
},
star_us_dialog: {
title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',

View File

@@ -1,3 +1,4 @@
import type { DBCustomType } from './domain';
import type { Area } from './domain/area';
import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field';
@@ -48,6 +49,10 @@ const generateIdsMapFromDiagram = (
idsMap.set(area.id, generateId());
});
diagram.customTypes?.forEach((customType) => {
idsMap.set(customType.id, generateId());
});
return idsMap;
};
@@ -213,6 +218,22 @@ export const cloneDiagram = (
})
.filter((area): area is Area => area !== null) ?? [];
const customTypes: DBCustomType[] =
diagram.customTypes
?.map((customType) => {
const id = getNewId(customType.id);
if (!id) {
return null;
}
return {
...customType,
id,
} satisfies DBCustomType;
})
.filter(
(customType): customType is DBCustomType => customType !== null
) ?? [];
return {
diagram: {
...diagram,
@@ -221,6 +242,7 @@ export const cloneDiagram = (
relationships,
tables,
areas,
customTypes,
createdAt: diagram.createdAt
? new Date(diagram.createdAt)
: new Date(),

View File

@@ -48,18 +48,30 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char large object', id: 'char_large_object' },
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
{
name: 'char varying',
id: 'char_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'character large object', id: 'character_large_object' },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'nchar large object', id: 'nchar_large_object' },
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
{
name: 'nchar varying',
id: 'nchar_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national character large object',
id: 'national_character_large_object',
@@ -67,22 +79,34 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{
name: 'national character varying',
id: 'national_character_varying',
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national char varying',
id: 'national_char_varying',
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national character',
id: 'national_character',
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national char',
id: 'national_char',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
{ name: 'binary large object', id: 'binary_large_object' },
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
{
name: 'binary varying',
id: 'binary_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'fixedstring',
id: 'fixedstring',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'string', id: 'string' },
// Date Types

View File

@@ -14,9 +14,23 @@ export interface DataType {
name: string;
}
export interface DataTypeData extends DataType {
export interface FieldAttributeRange {
max: number;
min: number;
default: number;
}
interface FieldAttributes {
hasCharMaxLength?: boolean;
hasCharMaxLengthOption?: boolean;
precision?: FieldAttributeRange;
scale?: FieldAttributeRange;
maxLength?: number;
}
export interface DataTypeData extends DataType {
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
fieldAttributes?: FieldAttributes;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({

View File

@@ -2,7 +2,12 @@ import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
@@ -10,23 +15,62 @@ export const genericDataTypes: readonly DataTypeData[] = [
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ name: 'bigint', id: 'bigint' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'blob', id: 'blob' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'double', id: 'double' },
{ name: 'enum', id: 'enum' },
{ name: 'float', id: 'float' },
{ name: 'numeric', id: 'numeric' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'real', id: 'real' },
{ name: 'set', id: 'set' },
{ name: 'smallint', id: 'smallint' },
{ name: 'time', id: 'time' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
] as const;

View File

@@ -4,12 +4,32 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 1,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'text', id: 'text', usageLevel: 1 },
// Level 2 - Second most common types
@@ -20,16 +40,39 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{ name: 'numeric', id: 'numeric' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -11,7 +16,23 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
@@ -22,7 +43,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },

View File

@@ -2,15 +2,30 @@ import type { DataTypeData } from './data-types';
export const oracleDataTypes: readonly DataTypeData[] = [
// Character types
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
{
name: 'VARCHAR2',
id: 'varchar2',
usageLevel: 1,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'NVARCHAR2',
id: 'nvarchar2',
usageLevel: 1,
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'CHAR',
id: 'char',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'NCHAR',
id: 'nchar',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
{ name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
{ name: 'CLOB', id: 'clob', usageLevel: 2 },
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 },
@@ -49,7 +64,12 @@ export const oracleDataTypes: readonly DataTypeData[] = [
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
// Other types
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
{
name: 'RAW',
id: 'raw',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },

View File

@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'integer', id: 'integer', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -11,7 +16,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
@@ -23,18 +44,33 @@ export const postgresDataTypes: readonly DataTypeData[] = [
},
// Less common types
{ name: 'numeric', id: 'numeric' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'real', id: 'real' },
{ name: 'double precision', id: 'double_precision' },
{ name: 'smallserial', id: 'smallserial' },
{ name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' },
{ name: 'smallint', id: 'smallint' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'time', id: 'time' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },

View File

@@ -4,32 +4,93 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bit', id: 'bit', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
usageLevel: 1,
},
{
name: 'nvarchar',
id: 'nvarchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 4000,
},
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types
{ name: 'numeric', id: 'numeric' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'smallint', id: 'smallint' },
{ name: 'smallmoney', id: 'smallmoney' },
{ name: 'tinyint', id: 'tinyint' },
{ name: 'money', id: 'money' },
{ name: 'float', id: 'float' },
{ name: 'real', id: 'real' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'ntext', id: 'ntext' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
},
{ name: 'image', id: 'image' },
{ name: 'datetimeoffset', id: 'datetimeoffset' },
{ name: 'smalldatetime', id: 'smalldatetime' },

View File

@@ -10,25 +10,48 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
// SQLite type aliases and common types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: {
hasCharMaxLength: true,
},
usageLevel: 1,
},
{
name: 'timestamp',
id: 'timestamp',
usageLevel: 1,
},
// Level 2 - Second most common types
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'float', id: 'float', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
},
{ name: 'double', id: 'double', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types (all map to SQLite storage classes)
{ name: 'char', id: 'char', hasCharMaxLength: true },
{
name: 'char',
id: 'char',
fieldAttributes: {
hasCharMaxLength: true,
},
usageLevel: 2,
},
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'smallint', id: 'smallint' },
{ name: 'bigint', id: 'bigint' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility
{ name: 'time', id: 'time' },
{ name: 'date', id: 'date' }, // Added for smartquery compatibility
{ name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility
] as const;

View File

@@ -4,4 +4,5 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
[DatabaseType.POSTGRESQL]: 'public',
[DatabaseType.SQL_SERVER]: 'dbo',
[DatabaseType.CLICKHOUSE]: 'default',
[DatabaseType.COCKROACHDB]: 'public',
};

View File

@@ -0,0 +1,870 @@
import { describe, it, expect, vi } from 'vitest';
import { exportBaseSQL } from '../export-sql-script';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
// Mock the dbml/core importer
vi.mock('@dbml/core', () => ({
importer: {
import: vi.fn((sql: string) => {
// Return a simplified DBML for testing
return sql;
}),
},
}));
describe('DBML Export - SQL Generation Tests', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
const testId = () => `test-id-${++idCounter}`;
const testTime = Date.now();
// Helper to create a field with all required properties
const createField = (overrides: Partial<DBField>): DBField =>
({
id: testId(),
name: 'field',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
createdAt: testTime,
...overrides,
}) as DBField;
// Helper to create a table with all required properties
const createTable = (overrides: Partial<DBTable>): DBTable =>
({
id: testId(),
name: 'table',
fields: [],
indexes: [],
createdAt: testTime,
x: 0,
y: 0,
width: 200,
...overrides,
}) as DBTable;
// Helper to create a diagram with all required properties
const createDiagram = (overrides: Partial<Diagram>): Diagram =>
({
id: testId(),
name: 'diagram',
databaseType: DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: testTime,
updatedAt: testTime,
...overrides,
}) as Diagram;
describe('Composite Primary Keys', () => {
it('should handle tables with composite primary keys correctly', () => {
const tableId = testId();
const field1Id = testId();
const field2Id = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Enchanted Library',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: tableId,
name: 'spell_components',
fields: [
createField({
id: field1Id,
name: 'spell_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field2Id,
name: 'component_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'quantity',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
default: '1',
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain composite primary key syntax
expect(sql).toContain('PRIMARY KEY (spell_id, component_id)');
// Should NOT contain individual PRIMARY KEY constraints
expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
expect(sql).not.toMatch(
/component_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/
);
});
it('should handle single primary keys inline', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Wizard Academy',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'wizards',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#9370DB',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain inline PRIMARY KEY
expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
// Should NOT contain separate PRIMARY KEY constraint
expect(sql).not.toContain('PRIMARY KEY (id)');
});
});
describe('Default Value Handling', () => {
it('should skip invalid default values like "has default"', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Potion Shop',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'potions',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'is_active',
type: { id: 'boolean', name: 'boolean' },
primaryKey: false,
nullable: true,
unique: false,
default: 'has default',
}),
createField({
id: testId(),
name: 'stock_count',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
default: 'DEFAULT has default',
}),
],
indexes: [],
color: '#98FB98',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should not contain invalid default values
expect(sql).not.toContain('DEFAULT has default');
expect(sql).not.toContain('DEFAULT DEFAULT has default');
// The fields should still be in the table
expect(sql).toContain('is_active boolean');
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Treasure Vault',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'treasures',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'gold_value',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: false,
unique: false,
default: '100.50',
precision: 10,
scale: 2,
}),
createField({
id: testId(),
name: 'created_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: 'now()',
}),
createField({
id: testId(),
name: 'currency',
type: { id: 'char', name: 'char' },
characterMaximumLength: '3',
primaryKey: false,
nullable: false,
unique: false,
default: 'EUR',
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain valid defaults
expect(sql).toContain('DEFAULT 100.50');
expect(sql).toContain('DEFAULT now()');
expect(sql).toContain('DEFAULT EUR');
});
it('should handle NOW and similar default values', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Quest Log',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'quests',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'created_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: 'NOW',
}),
createField({
id: testId(),
name: 'updated_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: "('now')",
}),
],
indexes: [],
color: '#4169E1',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should convert NOW to NOW() and ('now') to now()
expect(sql).toContain('created_at timestamp DEFAULT NOW');
expect(sql).toContain('updated_at timestamp DEFAULT now()');
});
});
describe('Character Type Handling', () => {
it('should handle char types with and without length correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Dragon Registry',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'dragons',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'element_code',
type: { id: 'char', name: 'char' },
characterMaximumLength: '2',
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'status',
type: { id: 'char', name: 'char' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#FF6347',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should handle char with explicit length
expect(sql).toContain('element_code char(2)');
// Should add default length for char without length
expect(sql).toContain('status char(1)');
});
it('should not have spaces between char and parentheses', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Rune Inscriptions',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'runes',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'symbol',
type: { id: 'char', name: 'char' },
characterMaximumLength: '5',
primaryKey: false,
nullable: false,
unique: true,
}),
],
indexes: [],
color: '#8B4513',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should not contain "char (" with space
expect(sql).not.toContain('char (');
expect(sql).toContain('char(5)');
});
});
describe('Complex Table Structures', () => {
it('should handle tables with no primary key', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Alchemy Log',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'experiment_logs',
fields: [
createField({
id: testId(),
name: 'experiment_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'alchemist_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'result',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
}),
createField({
id: testId(),
name: 'logged_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: false,
unique: false,
default: 'now()',
}),
],
indexes: [],
color: '#32CD32',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should create a valid table without primary key
expect(sql).toContain('CREATE TABLE experiment_logs');
expect(sql).not.toContain('PRIMARY KEY');
});
it('should handle multiple tables with relationships', () => {
const guildTableId = testId();
const memberTableId = testId();
const guildIdFieldId = testId();
const memberGuildIdFieldId = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Adventurer Guild System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: guildTableId,
name: 'guilds',
fields: [
createField({
id: guildIdFieldId,
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: true,
}),
createField({
id: testId(),
name: 'founded_year',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: true,
unique: false,
}),
],
indexes: [],
x: 0,
y: 0,
color: '#4169E1',
}),
createTable({
id: memberTableId,
name: 'guild_members',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: memberGuildIdFieldId,
name: 'guild_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'member_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'rank',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: true,
unique: false,
default: "'Novice'",
}),
],
indexes: [],
x: 250,
y: 0,
color: '#FFD700',
}),
],
relationships: [
{
id: testId(),
name: 'fk_guild_members_guild',
sourceTableId: memberTableId,
targetTableId: guildTableId,
sourceFieldId: memberGuildIdFieldId,
targetFieldId: guildIdFieldId,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should create both tables
expect(sql).toContain('CREATE TABLE guilds');
expect(sql).toContain('CREATE TABLE guild_members');
// Should create foreign key
expect(sql).toContain(
'ALTER TABLE guild_members ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES guilds (id)'
);
});
});
describe('Schema Support', () => {
it('should handle tables with schemas correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Multi-Realm Database',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'portals',
schema: 'transportation',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'destination',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#9370DB',
}),
createTable({
id: testId(),
name: 'spells',
schema: 'magic',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: true,
}),
],
indexes: [],
x: 250,
y: 0,
color: '#FF1493',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should create schemas
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation');
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic');
// Should use schema-qualified table names
expect(sql).toContain('CREATE TABLE transportation.portals');
expect(sql).toContain('CREATE TABLE magic.spells');
});
});
describe('Edge Cases', () => {
it('should handle empty tables array', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Empty Realm',
databaseType: DatabaseType.POSTGRESQL,
tables: [],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toBe('');
});
it('should handle tables with empty fields', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Void Space',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'empty_table',
fields: [],
indexes: [],
color: '#000000',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should still create table structure
expect(sql).toContain('CREATE TABLE empty_table');
expect(sql).toContain('(\n\n)');
});
it('should handle special characters in default values', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Mystic Scrolls',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'scrolls',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'inscription',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
default: "'Ancient\\'s Wisdom'",
}),
],
indexes: [],
color: '#8B4513',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should preserve escaped quotes
expect(sql).toContain("DEFAULT 'Ancient\\'s Wisdom'");
});
it('should handle numeric precision and scale', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Treasury',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'gold_reserves',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'amount',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: false,
unique: false,
precision: 15,
scale: 2,
}),
createField({
id: testId(),
name: 'interest_rate',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: true,
unique: false,
precision: 5,
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should include precision and scale
expect(sql).toContain('amount numeric(15, 2)');
// Should include precision only when scale is not provided
expect(sql).toContain('interest_rate numeric(5)');
});
});
});

View File

@@ -48,6 +48,50 @@ export function exportFieldComment(comment: string): string {
.join('');
}
export function escapeSQLComment(comment: string): string {
if (!comment) {
return '';
}
// Escape single quotes by doubling them
let escaped = comment.replace(/'/g, "''");
// Replace newlines with spaces to prevent breaking SQL syntax
// Some databases support multi-line comments with specific syntax,
// but for maximum compatibility, we'll replace newlines with spaces
escaped = escaped.replace(/[\r\n]+/g, ' ');
// Trim any excessive whitespace
escaped = escaped.replace(/\s+/g, ' ').trim();
return escaped;
}
export function formatTableComment(comment: string): string {
if (!comment) {
return '';
}
// Split by newlines and add -- to each line
return (
comment
.split('\n')
.map((line) => `-- ${line}`)
.join('\n') + '\n'
);
}
export function formatMSSQLTableComment(comment: string): string {
if (!comment) {
return '';
}
// For MSSQL, we use multi-line comment syntax
// Escape */ to prevent breaking the comment block
const escaped = comment.replace(/\*\//g, '* /');
return `/**\n${escaped}\n*/\n`;
}
export function getInlineFK(table: DBTable, diagram: Diagram): string {
if (!diagram.relationships) {
return '';

View File

@@ -1,5 +1,6 @@
import {
exportFieldComment,
formatMSSQLTableComment,
isFunction,
isKeyword,
strHasQuotes,
@@ -72,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string {
return `'${defaultValue}'`;
}
export function exportMSSQL(diagram: Diagram): string {
export function exportMSSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -82,166 +89,254 @@ export function exportMSSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
if (!onlyRelationships) {
const schemas = new Set<string>();
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
});
return `${
table.comments ? `/**\n${table.comments}\n*/\n` : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
return `${
table.comments
? formatMSSQLTableComment(table.comments)
: ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
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})`;
}
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
// 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')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n\n${table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(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')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
})
.filter(Boolean);
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})
.join('')}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
})()}\n`;
})
.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 (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
if (!sourceField || !targetField) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
const sourceTableName = sourceTable.schema
? `[${sourceTable.schema}].[${sourceTable.name}]`
: `[${sourceTable.name}]`;
const targetTableName = targetTable.schema
? `[${targetTable.schema}].[${targetTable.name}]`
: `[${targetTable.name}]`;
if (!sourceField || !targetField) {
return '';
}
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return '';
}
const fkTableName = fkTable.schema
? `[${fkTable.schema}].[${fkTable.name}]`
: `[${fkTable.name}]`;
const refTableName = refTable.schema
? `[${refTable.schema}].[${refTable.name}]`
: `[${refTable.name}]`;
return {
schema: fkTable.schema || 'dbo',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
};
})
.filter(Boolean); // Remove empty objects
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return sqlScript;
}

View File

@@ -1,5 +1,7 @@
import {
exportFieldComment,
escapeSQLComment,
formatTableComment,
isFunction,
isKeyword,
strHasQuotes,
@@ -168,7 +170,13 @@ function mapMySQLType(typeName: string): string {
return typeName;
}
export function exportMySQL(diagram: Diagram): string {
export function exportMySQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -177,224 +185,245 @@ export function exportMySQL(diagram: Diagram): string {
const relationships = diagram.relationships;
// Start SQL script
let sqlScript = '-- MySQL database export\n\n';
let sqlScript = '-- MySQL database export\n';
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n\n';
if (!onlyRelationships) {
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\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 '';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
if (schemas.size > 0) {
sqlScript += '\n';
}
return `${
table.comments ? `-- ${table.comments}\n` : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.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})`;
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
return `${
table.comments ? formatTableComment(table.comments) : ''
}\nCREATE 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})`;
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})`;
}
}
} else if (field.precision) {
// Set a default size for VARCHAR columns if not specified
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
) {
typeWithSize = `${typeName}(${field.precision})`;
typeWithSize = `${typeName}(255)`;
}
}
// 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';
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
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
.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' : '';
// 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)}`
// 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 '${escapeSQLComment(field.comments)}'`
: '';
// 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='${escapeSQLComment(table.comments)}'`
: ''
};\n${
// Add indexes - MySQL creates them separately from the table definition
(() => {
const validIndexes = 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);
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
// 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'
);
}
);
return field ? field : null;
// 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} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: '';
})
.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`
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})
.filter(Boolean)
.join('\n')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate foreign keys
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n\n';
sqlScript += '\n-- Foreign key constraints\n';
sqlScript += relationships
const foreignKeys = relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
@@ -423,25 +452,62 @@ export function exportMySQL(diagram: Diagram): string {
return '';
}
const sourceTableName = sourceTable.schema
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
: `\`${sourceTable.name}\``;
const targetTableName = targetTable.schema
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
: `\`${targetTable.name}\``;
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return '';
}
const fkTableName = fkTable.schema
? `\`${fkTable.schema}\`.\`${fkTable.name}\``
: `\`${fkTable.name}\``;
const refTableName = refTable.schema
? `\`${refTable.schema}\`.\`${refTable.name}\``
: `\`${refTable.name}\``;
// Create a descriptive constraint name
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
const constraintName = `\`fk_${fkTable.name}_${fkField.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`;
return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
})
.filter(Boolean) // Remove empty strings
.join('\n');
.filter(Boolean); // Remove empty strings
sqlScript += foreignKeys.join('\n');
}
// Commit transaction
sqlScript += '\nCOMMIT;\n';
sqlScript += '\n\nCOMMIT;\n';
return sqlScript;
}

View File

@@ -1,5 +1,7 @@
import {
exportFieldComment,
escapeSQLComment,
formatTableComment,
isFunction,
isKeyword,
strHasQuotes,
@@ -140,10 +142,16 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
}
});
return typesSql + '\n';
return typesSql ? typesSql + '\n' : '';
}
export function exportPostgreSQL(diagram: Diagram): string {
export function exportPostgreSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -154,290 +162,391 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
if (!onlyRelationships) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
sqlScript += '\n';
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
// 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]);
}
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
});
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 '';
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
}
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
return `${
table.comments ? `-- ${table.comments}\n` : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Add sequence creation statements
const sequences = new Set<string>();
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
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\('([^']+)'(?:::[^)]+)?\)/
);
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
if (field.increment && !field.nullable) {
if (
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
if (match) {
sequences.add(match[1]);
}
}
});
});
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
if (sequences.size > 0) {
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 ? formatTableComment(table.comments) : ''
}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;
let serialType = null;
if (field.increment && !field.nullable) {
if (
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
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) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
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('[]', '') + '[]';
}
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
const notNull = field.nullable ? '' : ' NOT NULL';
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';
}
// 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' : '';
// 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)}`
: '';
// 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} ${serialType || typeWithSize}${serialType ? '' : 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
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);${
// Add table comments
table.comments
? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
)
.join('')
}${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
(() => {
const validIndexes = 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,
'_'
);
return field ? field : null;
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} ON ${tableName} (${indexFieldNames.join(', ')});`
: '';
})
.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`
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})
.filter(Boolean)
.join('')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
})()
}\n`;
})
.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 (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
if (!sourceField || !targetField) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
const sourceTableName = sourceTable.schema
? `"${sourceTable.schema}"."${sourceTable.name}"`
: `"${sourceTable.name}"`;
const targetTableName = targetTable.schema
? `"${targetTable.schema}"."${targetTable.name}"`
: `"${targetTable.name}"`;
if (!sourceField || !targetField) {
return '';
}
// 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, '_');
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
const constraintName = `"${safeConstraintName}"`;
if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return '';
}
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
const fkTableName = fkTable.schema
? `"${fkTable.schema}"."${fkTable.name}"`
: `"${fkTable.name}"`;
const refTableName = refTable.schema
? `"${refTable.schema}"."${refTable.name}"`
: `"${refTable.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_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.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 {
schema: fkTable.schema || 'public',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");`,
};
})
.filter(Boolean); // Remove empty objects
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return sqlScript;
}

View File

@@ -1,5 +1,6 @@
import {
exportFieldComment,
formatTableComment,
isFunction,
isKeyword,
strHasQuotes,
@@ -66,8 +67,9 @@ function parseSQLiteDefault(field: DBField): string {
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Map problematic types to SQLite compatible types
// Preserve original types for SQLite export (only map when necessary)
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
const originalType = typeName;
typeName = typeName.toLowerCase();
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
@@ -75,59 +77,62 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
}
// Map common types to SQLite's simplified type system
// Preserve original type names that SQLite accepts
switch (typeName) {
// Keep these types as-is
case 'integer':
case 'text':
case 'real':
case 'blob':
case 'numeric':
case 'decimal':
case 'boolean':
case 'date':
case 'datetime':
case 'timestamp':
case 'float':
case 'double':
case 'varchar':
case 'char':
case 'int':
case 'smallint':
case 'tinyint':
case 'mediumint':
case 'bigint':
return 'INTEGER';
case 'json':
return typeName.toUpperCase();
case 'decimal':
case 'numeric':
case 'float':
case 'double':
case 'real':
return 'REAL';
case 'char':
// Only map types that SQLite truly doesn't recognize
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
return 'DATETIME';
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
return 'BOOLEAN';
case 'user-defined':
case 'json':
case 'jsonb':
return 'TEXT'; // Store as JSON text
return 'TEXT';
case 'array':
return 'TEXT'; // Store as serialized array text
return 'TEXT';
case 'geometry':
case 'geography':
return 'BLOB'; // Store spatial data as BLOB in SQLite
return 'BLOB';
case 'mediumint':
return 'INTEGER';
}
// If type has array notation (ends with []), treat as TEXT
@@ -135,11 +140,17 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'TEXT';
}
// For any other types, default to TEXT
return typeName;
// For any other types, preserve the original
return originalType.toUpperCase();
}
export function exportSQLite(diagram: Diagram): string {
export function exportSQLite({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -148,10 +159,15 @@ export function exportSQLite(diagram: Diagram): string {
const relationships = diagram.relationships;
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
let sqlScript = '-- SQLite database export\n\n';
let sqlScript = '-- SQLite database export\n';
// Add PRAGMA foreign_keys = ON if there are relationships
if (relationships && relationships.length > 0) {
sqlScript += 'PRAGMA foreign_keys = ON;\n\n';
}
// Begin transaction for faster import
sqlScript += 'BEGIN TRANSACTION;\n\n';
sqlScript += 'BEGIN TRANSACTION;\n';
// SQLite doesn't have sequences, so we skip sequence creation
@@ -165,191 +181,280 @@ export function exportSQLite(diagram: Diagram): string {
'sqlite_master',
];
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
if (!onlyRelationships) {
// 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`;
}
// 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}"`;
// 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);
// 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');
// 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
// Collect foreign key constraints for this table
const tableForeignKeys: string[] = [];
relationships.forEach((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
// 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'))
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView ||
sqliteSystemTables.includes(
sourceTable.name.toLowerCase()
) ||
sqliteSystemTables.includes(
targetTable.name.toLowerCase()
)
) {
autoIncrement = ' AUTOINCREMENT';
return;
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return;
}
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
// 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')
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// 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)}`;
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return;
}
// If this foreign key belongs to the current table, add it
if (fkTable.id === table.id) {
tableForeignKeys.push(
` FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}")`
);
}
});
return `${schemaComment}${
table.comments ? formatTableComment(table.comments) : ''
}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 baseTypeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// Add size/precision/scale parameters if applicable
let typeWithParams = baseTypeName;
// Add character maximum length for VARCHAR, CHAR, etc.
if (
field.characterMaximumLength &&
['VARCHAR', 'CHAR', 'TEXT'].includes(
baseTypeName.toUpperCase()
)
) {
typeWithParams = `${baseTypeName}(${field.characterMaximumLength})`;
}
// Add precision and scale for DECIMAL, NUMERIC, etc.
else if (
field.precision &&
[
'DECIMAL',
'NUMERIC',
'REAL',
'FLOAT',
'DOUBLE',
].includes(baseTypeName.toUpperCase())
) {
if (field.scale) {
typeWithParams = `${baseTypeName}(${field.precision}, ${field.scale})`;
} else {
typeWithParams = `${baseTypeName}(${field.precision})`;
}
}
}
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
const notNull = field.nullable ? '' : ' NOT NULL';
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;
// 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} ${typeWithParams}${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(', ')})`
: ''
}${
// Add foreign key constraints
tableForeignKeys.length > 0
? ',\n' + tableForeignKeys.join(',\n')
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
(() => {
const validIndexes = 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(', ')});`
: '';
})
.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`
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.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`;
});
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Foreign keys are now included inline in CREATE TABLE statements
// No need for separate ALTER TABLE statements in SQLite
// Commit transaction
sqlScript += '\nCOMMIT;\n';

View File

@@ -11,23 +11,7 @@ import { exportMySQL } from './export-per-type/mysql';
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
const typeMap: Record<string, string> = {
'character varying': 'varchar',
'char varying': 'varchar',
integer: 'int',
int4: 'int',
int8: 'bigint',
serial4: 'serial',
serial8: 'bigserial',
float8: 'double precision',
float4: 'real',
bool: 'boolean',
character: 'char',
'timestamp without time zone': 'timestamp',
'timestamp with time zone': 'timestamptz',
'time without time zone': 'time',
'time with time zone': 'timetz',
};
const typeMap: Record<string, string> = {};
return typeMap[typeName.toLowerCase()] || typeName;
};
@@ -36,10 +20,12 @@ export const exportBaseSQL = ({
diagram,
targetDatabaseType,
isDBMLFlow = false,
onlyRelationships = false,
}: {
diagram: Diagram;
targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean;
onlyRelationships?: boolean;
}): string => {
const { tables, relationships } = diagram;
@@ -50,16 +36,16 @@ export const exportBaseSQL = ({
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
switch (diagram.databaseType) {
case DatabaseType.SQL_SERVER:
return exportMSSQL(diagram);
return exportMSSQL({ diagram, onlyRelationships });
case DatabaseType.POSTGRESQL:
return exportPostgreSQL(diagram);
return exportPostgreSQL({ diagram, onlyRelationships });
case DatabaseType.SQLITE:
return exportSQLite(diagram);
return exportSQLite({ diagram, onlyRelationships });
case DatabaseType.MYSQL:
case DatabaseType.MARIADB:
return exportMySQL(diagram);
return exportMySQL({ diagram, onlyRelationships });
default:
return exportPostgreSQL(diagram);
return exportPostgreSQL({ diagram, onlyRelationships });
}
}
@@ -131,7 +117,23 @@ export const exportBaseSQL = ({
}
}
});
sqlScript += '\n'; // Add a newline if custom types were processed
if (
diagram.customTypes.some(
(ct) =>
(ct.kind === 'enum' &&
ct.values &&
ct.values.length > 0 &&
targetDatabaseType === DatabaseType.POSTGRESQL &&
!isDBMLFlow) ||
(ct.kind === 'composite' &&
ct.fields &&
ct.fields.length > 0 &&
(targetDatabaseType === DatabaseType.POSTGRESQL ||
isDBMLFlow))
)
) {
sqlScript += '\n';
}
}
// Add CREATE SEQUENCE statements
@@ -154,7 +156,9 @@ export const exportBaseSQL = ({
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
sqlScript += '\n';
if (sequences.size > 0) {
sqlScript += '\n';
}
// Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => {
@@ -163,6 +167,12 @@ export const exportBaseSQL = ({
: table.name;
sqlScript += `CREATE TABLE ${tableName} (\n`;
// Check for composite primary keys
const primaryKeyFields = table.fields.filter(
(field) => field.primaryKey
);
const hasCompositePrimaryKey = primaryKeyFields.length > 1;
table.fields.forEach((field, index) => {
let typeName = simplifyDataType(field.type.name);
@@ -214,17 +224,33 @@ export const exportBaseSQL = ({
typeName = 'text[]';
}
// Handle special types
if (
typeName.toLowerCase() === 'char' &&
!field.characterMaximumLength
) {
// Default char without length to char(1)
typeName = 'char';
}
sqlScript += ` ${field.name} ${typeName}`;
// Add size for character types
if (
field.characterMaximumLength &&
parseInt(field.characterMaximumLength) > 0
parseInt(field.characterMaximumLength) > 0 &&
field.type.name.toLowerCase() !== 'decimal'
) {
sqlScript += `(${field.characterMaximumLength})`;
} else if (field.type.name.toLowerCase().includes('varchar')) {
// Keep varchar sizing, but don't apply to TEXT (previously enum)
sqlScript += `(500)`;
} else if (
typeName.toLowerCase() === 'char' &&
!field.characterMaximumLength
) {
// Default char without explicit length to char(1) for compatibility
sqlScript += `(1)`;
}
// Add precision and scale for numeric types
@@ -249,49 +275,63 @@ export const exportBaseSQL = ({
// Temp remove default user-define value when it have it
let fieldDefault = field.default;
// Remove the type cast part after :: if it exists
if (fieldDefault.includes('::')) {
const endedWithParentheses = fieldDefault.endsWith(')');
fieldDefault = fieldDefault.split('::')[0];
// Skip invalid default values for DBML export
if (
fieldDefault === 'has default' ||
fieldDefault === 'DEFAULT has default'
) {
// Skip this default value as it's invalid SQL
} else {
// Remove the type cast part after :: if it exists
if (fieldDefault.includes('::')) {
const endedWithParentheses = fieldDefault.endsWith(')');
fieldDefault = fieldDefault.split('::')[0];
if (
(fieldDefault.startsWith('(') &&
!fieldDefault.endsWith(')')) ||
endedWithParentheses
) {
fieldDefault += ')';
if (
(fieldDefault.startsWith('(') &&
!fieldDefault.endsWith(')')) ||
endedWithParentheses
) {
fieldDefault += ')';
}
}
}
if (fieldDefault === `('now')`) {
fieldDefault = `now()`;
}
if (fieldDefault === `('now')`) {
fieldDefault = `now()`;
}
sqlScript += ` DEFAULT ${fieldDefault}`;
sqlScript += ` DEFAULT ${fieldDefault}`;
}
}
// Handle PRIMARY KEY constraint
if (field.primaryKey) {
// Handle PRIMARY KEY constraint - only add inline if not composite
if (field.primaryKey && !hasCompositePrimaryKey) {
sqlScript += ' PRIMARY KEY';
}
// Add a comma after each field except the last one
if (index < table.fields.length - 1) {
// Add a comma after each field except the last one (or before composite primary key)
if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
sqlScript += ',\n';
}
});
sqlScript += '\n);\n\n';
// Add composite primary key constraint if needed
if (hasCompositePrimaryKey) {
const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
}
sqlScript += '\n);\n';
// Add table comment
if (table.comments) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments}';\n`;
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
}
table.fields.forEach((field) => {
// Add column comment
if (field.comments) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments}';\n`;
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
}
});
@@ -306,16 +346,19 @@ export const exportBaseSQL = ({
.join(', ');
if (fieldNames) {
const indexName = table.schema
? `${table.schema}_${index.name}`
: index.name;
const indexName =
table.schema && !isDBMLFlow
? `${table.schema}_${index.name}`
: index.name;
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
}
});
sqlScript += '\n';
});
if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
sqlScript += '\n';
}
// Handle relationships (foreign keys)
relationships?.forEach((relationship) => {
const sourceTable = nonViewTables.find(
@@ -338,13 +381,52 @@ export const exportBaseSQL = ({
sourceTableField &&
targetTableField
) {
const sourceTableName = sourceTable.schema
? `${sourceTable.schema}.${sourceTable.name}`
: sourceTable.name;
const targetTableName = targetTable.schema
? `${targetTable.schema}.${targetTable.name}`
: targetTable.name;
sqlScript += `ALTER TABLE ${sourceTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${sourceTableField.name}) REFERENCES ${targetTableName} (${targetTableField.name});\n`;
// Determine which table should have the foreign key based on cardinality
// In a 1:many relationship, the foreign key goes on the "many" side
// If source is "one" and target is "many", FK goes on target table
// If source is "many" and target is "one", FK goes on source table
let fkTable, fkField, refTable, refField;
if (
relationship.sourceCardinality === 'one' &&
relationship.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetTableField;
refTable = sourceTable;
refField = sourceTableField;
} else if (
relationship.sourceCardinality === 'many' &&
relationship.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceTableField;
refTable = targetTable;
refField = targetTableField;
} else if (
relationship.sourceCardinality === 'one' &&
relationship.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceTableField;
refTable = targetTable;
refField = targetTableField;
} else {
// Many-to-many relationships need a junction table, skip for now
return;
}
const fkTableName = fkTable.schema
? `${fkTable.schema}.${fkTable.name}`
: fkTable.name;
const refTableName = refTable.schema
? `${refTable.schema}.${refTable.name}`
: refTable.name;
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
}
});

View File

@@ -0,0 +1,126 @@
import type { DatabaseMetadata } from './metadata-types/database-metadata';
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
export interface SelectedTable {
schema?: string | null;
table: string;
type: 'table' | 'view';
}
export function filterMetadataByTables({
metadata,
selectedTables: inputSelectedTables,
}: {
metadata: DatabaseMetadata;
selectedTables: SelectedTable[];
}): DatabaseMetadata {
const selectedTables = inputSelectedTables.map((st) => {
// Normalize schema names to ensure consistent filtering
const schema = schemaNameToDomainSchemaName(st.schema) ?? '';
return {
...st,
schema,
};
});
// Create sets for faster lookup
const selectedTableSet = new Set(
selectedTables
.filter((st) => st.type === 'table')
.map((st) => `${st.schema}.${st.table}`)
);
const selectedViewSet = new Set(
selectedTables
.filter((st) => st.type === 'view')
.map((st) => `${st.schema}.${st.table}`)
);
// Filter tables
const filteredTables = metadata.tables.filter((table) => {
const schema = schemaNameToDomainSchemaName(table.schema) ?? '';
const tableId = `${schema}.${table.table}`;
return selectedTableSet.has(tableId);
});
// Filter views - include views that were explicitly selected
const filteredViews =
metadata.views?.filter((view) => {
const schema = schemaNameToDomainSchemaName(view.schema) ?? '';
const viewName = view.view_name ?? '';
const viewId = `${schema}.${viewName}`;
return selectedViewSet.has(viewId);
}) || [];
// Filter columns - include columns from both tables and views
const filteredColumns = metadata.columns.filter((col) => {
const fromTable = filteredTables.some(
(tb) => tb.schema === col.schema && tb.table === col.table
);
// For views, the column.table field might contain the view name
const fromView = filteredViews.some(
(view) => view.schema === col.schema && view.view_name === col.table
);
return fromTable || fromView;
});
// Filter primary keys
const filteredPrimaryKeys = metadata.pk_info.filter((pk) =>
filteredTables.some(
(tb) => tb.schema === pk.schema && tb.table === pk.table
)
);
// Filter indexes
const filteredIndexes = metadata.indexes.filter((idx) =>
filteredTables.some(
(tb) => tb.schema === idx.schema && tb.table === idx.table
)
);
// Filter foreign keys - include if either source or target table is selected
// This ensures all relationships related to selected tables are preserved
const filteredForeignKeys = metadata.fk_info.filter((fk) => {
// Handle reference_schema and reference_table fields from the JSON
const targetSchema = fk.reference_schema;
const targetTable = (fk.reference_table || '').replace(/^"+|"+$/g, ''); // Remove extra quotes
const sourceIncluded = filteredTables.some(
(tb) => tb.schema === fk.schema && tb.table === fk.table
);
const targetIncluded = filteredTables.some(
(tb) => tb.schema === targetSchema && tb.table === targetTable
);
return sourceIncluded || targetIncluded;
});
const schemasWithTables = new Set(filteredTables.map((tb) => tb.schema));
const schemasWithViews = new Set(filteredViews.map((view) => view.schema));
// Filter custom types if they exist
const filteredCustomTypes =
metadata.custom_types?.filter((customType) => {
// Also check if the type is used by any of the selected tables' columns
const typeUsedInColumns = filteredColumns.some(
(col) =>
col.type === customType.type ||
col.type.includes(customType.type) // Handle array types like "custom_type[]"
);
return (
schemasWithTables.has(customType.schema) ||
schemasWithViews.has(customType.schema) ||
typeUsedInColumns
);
}) || [];
return {
...metadata,
tables: filteredTables,
columns: filteredColumns,
pk_info: filteredPrimaryKeys,
indexes: filteredIndexes,
fk_info: filteredForeignKeys,
views: filteredViews,
custom_types: filteredCustomTypes,
};
}

View File

@@ -1,4 +1,3 @@
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
import type { TableInfo } from './table-info';
import { z } from 'zod';
@@ -33,20 +32,12 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
};
export const createAggregatedIndexes = ({
tableInfo,
tableSchema,
indexes,
tableIndexes,
}: {
tableInfo: TableInfo;
indexes: IndexInfo[];
tableIndexes: IndexInfo[];
tableSchema?: string;
}): AggregatedIndexInfo[] => {
const tableIndexes = indexes.filter((idx) => {
const indexSchema = schemaNameToDomainSchemaName(idx.schema);
return idx.table === tableInfo.table && indexSchema === tableSchema;
});
return Object.values(
tableIndexes.reduce(
(acc, idx) => {

View File

@@ -2,7 +2,8 @@ const withExtras = false;
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
const withoutDefault = `""`;
export const mariaDBQuery = `SELECT CAST(CONCAT(
export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
SELECT CAST(CONCAT(
'{"fk_info": [',
IFNULL((SELECT GROUP_CONCAT(
CONCAT('{"schema":"', cast(fk.table_schema as CHAR),

View File

@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { validatePostgreSQLSyntax } from '../sql-validator';
import { validateSQL } from '../sql-validator';
import { DatabaseType } from '@/lib/domain';
describe('SQL Validator Auto-fix', () => {
it('should provide auto-fix for cast operator errors', () => {
@@ -14,7 +15,7 @@ SELECT id: :text FROM dragons;
SELECT ST_X(lair_location: :geometry) AS longitude FROM dragons;
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
// Should detect errors
expect(result.isValid).toBe(false);
@@ -38,7 +39,7 @@ SELECT AVG(power_level): :DECIMAL(3,
2) FROM enchantments;
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.fixedSQL).toBeDefined();
@@ -56,7 +57,7 @@ CREATE TABLE potions (
3) DEFAULT 0.000
);`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
@@ -92,7 +93,7 @@ SELECT AVG(power_level): :DECIMAL(3,
2) FROM enchantments;
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.fixedSQL).toBeDefined();
@@ -122,7 +123,7 @@ CREATE TABLE wizards (
name VARCHAR(100)
);`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);

View File

@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { validatePostgreSQLSyntax } from '../sql-validator';
import { validateSQL } from '../sql-validator';
import { DatabaseType } from '@/lib/domain';
describe('SQL Validator', () => {
it('should detect cast operator errors (: :)', () => {
@@ -14,7 +15,7 @@ SELECT id: :text FROM wizards;
SELECT COUNT(*): :integer FROM wizards;
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(2);
@@ -33,7 +34,7 @@ CREATE TABLE potions (
2) NOT NULL
);`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(
@@ -50,7 +51,7 @@ CREATE EXTENSION postgis;
CREATE TABLE dragons (id UUID PRIMARY KEY);
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(
result.warnings.some((w) => w.message.includes('CREATE EXTENSION'))
@@ -72,7 +73,7 @@ BEFORE UPDATE ON wizards
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(
result.warnings.some((w) =>
@@ -102,7 +103,7 @@ CREATE TABLE spells (
);
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
@@ -127,7 +128,7 @@ SELECT
FROM towers t;
`;
const result = validatePostgreSQLSyntax(sql);
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
// Should find multiple cast operator errors

View File

@@ -18,11 +18,14 @@ export interface SQLColumn {
nullable: boolean;
primaryKey: boolean;
unique: boolean;
typeArgs?: {
length?: number;
precision?: number;
scale?: number;
};
typeArgs?:
| {
length?: number;
precision?: number;
scale?: number;
}
| number[]
| string;
comment?: string;
default?: string;
increment?: boolean;
@@ -559,6 +562,38 @@ export function convertToChartDBDiagram(
id: column.type.toLowerCase(),
name: column.type,
};
}
// Handle SQL Server types specifically
else if (
sourceDatabaseType === DatabaseType.SQL_SERVER &&
targetDatabaseType === DatabaseType.SQL_SERVER
) {
const normalizedType = column.type.toLowerCase();
// Preserve SQL Server specific types when target is also SQL Server
if (
normalizedType === 'nvarchar' ||
normalizedType === 'nchar' ||
normalizedType === 'ntext' ||
normalizedType === 'uniqueidentifier' ||
normalizedType === 'datetime2' ||
normalizedType === 'datetimeoffset' ||
normalizedType === 'money' ||
normalizedType === 'smallmoney' ||
normalizedType === 'bit' ||
normalizedType === 'xml' ||
normalizedType === 'hierarchyid' ||
normalizedType === 'geography' ||
normalizedType === 'geometry'
) {
mappedType = { id: normalizedType, name: normalizedType };
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
sourceDatabaseType
);
}
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
@@ -581,22 +616,68 @@ export function convertToChartDBDiagram(
// Add type arguments if present
if (column.typeArgs) {
// Transfer length for varchar/char types
if (
column.typeArgs.length !== undefined &&
(field.type.id === 'varchar' || field.type.id === 'char')
) {
field.characterMaximumLength =
column.typeArgs.length.toString();
// Handle string typeArgs (e.g., 'max' for varchar(max))
if (typeof column.typeArgs === 'string') {
if (
(field.type.id === 'varchar' ||
field.type.id === 'nvarchar') &&
column.typeArgs === 'max'
) {
field.characterMaximumLength = 'max';
}
}
// Transfer precision/scale for numeric types
if (
column.typeArgs.precision !== undefined &&
(field.type.id === 'numeric' || field.type.id === 'decimal')
// Handle array typeArgs (SQL Server format)
else if (
Array.isArray(column.typeArgs) &&
column.typeArgs.length > 0
) {
field.precision = column.typeArgs.precision;
field.scale = column.typeArgs.scale;
if (
field.type.id === 'varchar' ||
field.type.id === 'nvarchar' ||
field.type.id === 'char' ||
field.type.id === 'nchar'
) {
field.characterMaximumLength =
column.typeArgs[0].toString();
} else if (
(field.type.id === 'numeric' ||
field.type.id === 'decimal') &&
column.typeArgs.length >= 2
) {
field.precision = column.typeArgs[0];
field.scale = column.typeArgs[1];
}
}
// Handle object typeArgs (standard format)
else if (
typeof column.typeArgs === 'object' &&
!Array.isArray(column.typeArgs)
) {
const typeArgsObj = column.typeArgs as {
length?: number;
precision?: number;
scale?: number;
};
// Transfer length for varchar/char types
if (
typeArgsObj.length !== undefined &&
(field.type.id === 'varchar' ||
field.type.id === 'char')
) {
field.characterMaximumLength =
typeArgsObj.length.toString();
}
// Transfer precision/scale for numeric types
if (
typeArgsObj.precision !== undefined &&
(field.type.id === 'numeric' ||
field.type.id === 'decimal')
) {
field.precision = typeArgsObj.precision;
field.scale = typeArgsObj.scale;
}
}
}
@@ -705,12 +786,6 @@ export function convertToChartDBDiagram(
);
if (!sourceField || !targetField) {
console.log('Relationship refers to non-existent field:', {
sourceTable: rel.sourceTable,
sourceField: rel.sourceColumn,
targetTable: rel.targetTable,
targetField: rel.targetColumn,
});
return;
}

View File

@@ -1,150 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromMySQL } from '../mysql';
import { sqlImportToDiagram, detectDatabaseType } from '../../../index';
import { DatabaseType } from '@/lib/domain/database-type';
describe('MariaDB Integration', () => {
it('should detect MariaDB from SQL dump', () => {
const mariaDbSql = `
-- MariaDB dump 10.19 Distrib 10.11.2-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: fantasy_db
-- ------------------------------------------------------
-- Server version 10.11.2-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE TABLE magic_realms (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`;
const detectedType = detectDatabaseType(mariaDbSql);
expect(detectedType).toBe(DatabaseType.MARIADB);
});
it('should parse MariaDB SQL using MySQL parser', async () => {
const mariaDbSql = `
CREATE TABLE wizards (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
power_level INT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE spells (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
wizard_id INT NOT NULL,
mana_cost INT DEFAULT 10,
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
const result = await fromMySQL(mariaDbSql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const wizards = result.tables.find((t) => t.name === 'wizards');
expect(wizards?.columns).toHaveLength(5);
const fk = result.relationships[0];
expect(fk.sourceTable).toBe('spells');
expect(fk.targetTable).toBe('wizards');
expect(fk.deleteAction).toBe('CASCADE');
});
it('should handle MariaDB-specific storage engines', async () => {
const mariaDbSql = `
-- Using Aria storage engine (MariaDB specific)
CREATE TABLE magical_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=Aria DEFAULT CHARSET=utf8mb4;
-- Using ColumnStore engine (MariaDB specific)
CREATE TABLE spell_analytics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
spell_name VARCHAR(200),
cast_count BIGINT DEFAULT 0,
avg_mana_cost DECIMAL(10,2)
) ENGINE=COLUMNSTORE DEFAULT CHARSET=utf8mb4;`;
const result = await fromMySQL(mariaDbSql);
expect(result.tables).toHaveLength(2);
expect(
result.tables.find((t) => t.name === 'magical_logs')
).toBeDefined();
expect(
result.tables.find((t) => t.name === 'spell_analytics')
).toBeDefined();
});
it('should handle MariaDB-specific data types', async () => {
const mariaDbSql = `
CREATE TABLE advanced_spells (
id INT AUTO_INCREMENT PRIMARY KEY,
spell_id UUID, -- MariaDB has native UUID type
spell_data JSON, -- JSON support
cast_location POINT, -- Geometry type
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;`;
const result = await fromMySQL(mariaDbSql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.columns).toHaveLength(5);
const uuidCol = table.columns.find((c) => c.name === 'spell_id');
expect(uuidCol?.type).toBe('UUID');
const jsonCol = table.columns.find((c) => c.name === 'spell_data');
expect(jsonCol?.type).toBe('JSON');
});
it('should work with sqlImportToDiagram for MariaDB', async () => {
const mariaDbSql = `
/*!100100 SET @@SQL_MODE='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */;
CREATE TABLE dragon_riders (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
dragon_count INT DEFAULT 0
) ENGINE=InnoDB;
CREATE TABLE dragons (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
rider_id INT,
FOREIGN KEY (rider_id) REFERENCES dragon_riders(id)
) ENGINE=InnoDB;`;
const diagram = await sqlImportToDiagram({
sqlContent: mariaDbSql,
sourceDatabaseType: DatabaseType.MARIADB,
targetDatabaseType: DatabaseType.GENERIC,
});
expect(diagram.tables).toHaveLength(2);
expect(diagram.relationships).toHaveLength(1);
// Check that tables are properly sorted
expect(diagram.tables[0].name).toBe('dragon_riders');
expect(diagram.tables[1].name).toBe('dragons');
});
});

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