mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-04 22:13:15 +00:00
Compare commits
3 Commits
v1.14.0
...
jf/fix_sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5533c071 | ||
|
|
5b9a88a8f3 | ||
|
|
2a8714a564 |
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,4 @@ jobs:
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:ci
|
||||
run: npm run build
|
||||
97
.husky/README.md
Normal file
97
.husky/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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
|
||||
@@ -1,2 +1,13 @@
|
||||
#!/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
|
||||
|
||||
214
.husky/smart-test-runner.sh
Executable file
214
.husky/smart-test-runner.sh
Executable file
@@ -0,0 +1,214 @@
|
||||
#!/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
|
||||
95
.husky/test-mapping.json
Normal file
95
.husky/test-mapping.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,68 +1,5 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
174
package-lock.json
generated
174
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"version": "1.14.0",
|
||||
"version": "1.13.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chartdb",
|
||||
"version": "1.14.0",
|
||||
"version": "1.13.2",
|
||||
"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.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -46,9 +46,8 @@
|
||||
"html-to-image": "^1.11.11",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.441.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"motion": "^12.23.6",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-sql-parser": "^5.3.2",
|
||||
"react": "^18.3.1",
|
||||
@@ -2255,24 +2254,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -2986,7 +2967,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
|
||||
"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==",
|
||||
@@ -3004,39 +2985,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -3318,24 +3266,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -4603,12 +4533,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
|
||||
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
|
||||
"integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.66",
|
||||
"@xyflow/system": "0.0.50",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
@@ -4618,18 +4548,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.66",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
|
||||
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
|
||||
"integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
|
||||
"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"
|
||||
}
|
||||
@@ -6839,33 +6767,6 @@
|
||||
"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",
|
||||
@@ -8151,12 +8052,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.525.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
|
||||
"integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
|
||||
"version": "0.441.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz",
|
||||
"integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
@@ -8297,47 +8198,6 @@
|
||||
"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",
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.14.0",
|
||||
"version": "1.13.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,7 +11,6 @@
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest run --reporter=verbose --bail=1",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
@@ -36,14 +35,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.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -54,9 +53,8 @@
|
||||
"html-to-image": "^1.11.11",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.441.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"motion": "^12.23.6",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-sql-parser": "^5.3.2",
|
||||
"react": "^18.3.1",
|
||||
|
||||
166
parser-comparison-analysis.md
Normal file
166
parser-comparison-analysis.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 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.
|
||||
@@ -31,7 +31,6 @@ export interface CodeSnippetAction {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
@@ -44,8 +43,6 @@ 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(
|
||||
@@ -59,8 +56,6 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
isComplete = true,
|
||||
editorProps,
|
||||
actions,
|
||||
actionsTooltipSide,
|
||||
allowCopy = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
@@ -134,37 +129,33 @@ 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">
|
||||
{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}
|
||||
<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>
|
||||
|
||||
{actions &&
|
||||
actions.length > 0 &&
|
||||
@@ -173,10 +164,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className={cn(
|
||||
'h-fit p-1.5',
|
||||
action.className
|
||||
)}
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
@@ -186,9 +174,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={actionsTooltipSide}
|
||||
>
|
||||
<TooltipContent>
|
||||
{action.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -37,28 +37,18 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
const datatypePattern = dataTypesNames.join('|');
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
|
||||
keywords: ['Table', 'Ref', 'Indexes'],
|
||||
datatypes: dataTypesNames,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[
|
||||
/\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',
|
||||
],
|
||||
[/\b(Table|Ref|Indexes)\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'],
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
|
||||
</Label>
|
||||
<Label
|
||||
className={cn(
|
||||
'text-sm text-center font-normal text-muted-foreground',
|
||||
'text-sm font-normal text-muted-foreground',
|
||||
descriptionClassName
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -93,8 +93,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(isOpen: boolean) => {
|
||||
setOpen?.(isOpen);
|
||||
setIsOpen(isOpen);
|
||||
|
||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||
},
|
||||
[setOpen]
|
||||
);
|
||||
@@ -229,7 +227,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value,
|
||||
matches?.map((match) => match?.toString())
|
||||
matches?.map((match) => match.toString())
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Toaster() {
|
||||
description,
|
||||
action,
|
||||
layout = 'row',
|
||||
hideCloseButton = false,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
@@ -39,7 +38,7 @@ export function Toaster() {
|
||||
) : null}
|
||||
</div>
|
||||
{layout === 'row' ? action : null}
|
||||
{!hideCloseButton ? <ToastClose /> : null}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -12,7 +12,6 @@ type ToasterToast = ToastProps & {
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
layout?: 'row' | 'column';
|
||||
hideCloseButton?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,461 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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>>;
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -12,8 +12,6 @@ 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>({
|
||||
@@ -21,6 +19,4 @@ export const canvasContext = createContext<CanvasContext>({
|
||||
fitView: emptyFn,
|
||||
setOverlapGraph: emptyFn,
|
||||
overlapGraph: createGraph(),
|
||||
setShowFilter: emptyFn,
|
||||
showFilter: false,
|
||||
});
|
||||
|
||||
@@ -21,8 +21,6 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
const [overlapGraph, setOverlapGraph] =
|
||||
useState<Graph<string>>(createGraph());
|
||||
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
|
||||
const reorderTables = useCallback(
|
||||
(
|
||||
options: { updateHistory?: boolean } = {
|
||||
@@ -79,8 +77,6 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
fitView,
|
||||
setOverlapGraph,
|
||||
overlapGraph,
|
||||
setShowFilter,
|
||||
showFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -78,9 +78,6 @@ export interface ChartDBContext {
|
||||
events: EventEmitter<ChartDBEvent>;
|
||||
readonly?: boolean;
|
||||
|
||||
highlightedCustomType?: DBCustomType;
|
||||
highlightCustomTypeId: (id?: string) => void;
|
||||
|
||||
filteredSchemas?: string[];
|
||||
filterSchemas: (schemaIds: string[]) => void;
|
||||
|
||||
@@ -95,10 +92,6 @@ 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>;
|
||||
@@ -284,11 +277,6 @@ export interface ChartDBContext {
|
||||
customType: Partial<DBCustomType>,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Filters
|
||||
hiddenTableIds?: string[];
|
||||
addHiddenTableId: (tableId: string) => Promise<void>;
|
||||
removeHiddenTableId: (tableId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const chartDBContext = createContext<ChartDBContext>({
|
||||
@@ -301,7 +289,6 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
areas: [],
|
||||
customTypes: [],
|
||||
schemas: [],
|
||||
highlightCustomTypeId: emptyFn,
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
currentDiagram: {
|
||||
@@ -321,7 +308,6 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
loadDiagramFromData: emptyFn,
|
||||
clearDiagramData: emptyFn,
|
||||
deleteDiagram: emptyFn,
|
||||
updateDiagramData: emptyFn,
|
||||
|
||||
// Database type operations
|
||||
updateDatabaseType: emptyFn,
|
||||
@@ -386,9 +372,4 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
removeCustomType: emptyFn,
|
||||
removeCustomTypes: emptyFn,
|
||||
updateCustomType: emptyFn,
|
||||
|
||||
// Filters
|
||||
hiddenTableIds: [],
|
||||
addHiddenTableId: emptyFn,
|
||||
removeHiddenTableId: emptyFn,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { deepCopy, generateId } from '@/lib/utils';
|
||||
import { randomColor } from '@/lib/colors';
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
DBCustomTypeKind,
|
||||
type DBCustomType,
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
@@ -40,17 +39,11 @@ export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
const dbStorage = useStorage();
|
||||
let db = dbStorage;
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||
useRedoUndoStack();
|
||||
const {
|
||||
getHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
} = useConfig();
|
||||
const [diagramId, setDiagramId] = useState('');
|
||||
const [diagramName, setDiagramName] = useState('');
|
||||
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
||||
@@ -72,12 +65,8 @@ export const ChartDBProvider: React.FC<
|
||||
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||
diagram?.customTypes ?? []
|
||||
);
|
||||
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
|
||||
const { events: diffEvents } = useDiff();
|
||||
|
||||
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
|
||||
useState<string>();
|
||||
|
||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||
setTables((tables) =>
|
||||
@@ -96,14 +85,6 @@ export const ChartDBProvider: React.FC<
|
||||
|
||||
diffEvents.useSubscription(diffCalculatedHandler);
|
||||
|
||||
// Sync hiddenTableIds with config
|
||||
useEffect(() => {
|
||||
if (diagramId) {
|
||||
const hiddenTables = getHiddenTablesForDiagram(diagramId);
|
||||
setHiddenTableIds(hiddenTables);
|
||||
}
|
||||
}, [diagramId, getHiddenTablesForDiagram]);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const readonly = useMemo(
|
||||
@@ -1535,37 +1516,22 @@ 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(
|
||||
(diagram) => {
|
||||
async (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,
|
||||
@@ -1579,23 +1545,10 @@ 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, {
|
||||
@@ -1759,29 +1712,6 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
|
||||
async (tableId: string) => {
|
||||
if (!hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) => [...prev, tableId]);
|
||||
await hideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, hideTableForDiagram]
|
||||
);
|
||||
|
||||
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
|
||||
useCallback(
|
||||
async (tableId: string) => {
|
||||
if (hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) =>
|
||||
prev.filter((id) => id !== tableId)
|
||||
);
|
||||
await unhideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, unhideTableForDiagram]
|
||||
);
|
||||
|
||||
return (
|
||||
<chartDBContext.Provider
|
||||
value={{
|
||||
@@ -1798,7 +1728,6 @@ export const ChartDBProvider: React.FC<
|
||||
events,
|
||||
readonly,
|
||||
filterSchemas,
|
||||
updateDiagramData,
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
@@ -1855,11 +1784,6 @@ export const ChartDBProvider: React.FC<
|
||||
removeCustomType,
|
||||
removeCustomTypes,
|
||||
updateCustomType,
|
||||
hiddenTableIds,
|
||||
addHiddenTableId,
|
||||
removeHiddenTableId,
|
||||
highlightCustomTypeId,
|
||||
highlightedCustomType,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -8,23 +8,9 @@ export interface ConfigContext {
|
||||
config?: Partial<ChartDBConfig>;
|
||||
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
||||
}) => Promise<void>;
|
||||
getHiddenTablesForDiagram: (diagramId: string) => string[];
|
||||
setHiddenTablesForDiagram: (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
) => Promise<void>;
|
||||
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
|
||||
unhideTableForDiagram: (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContext>({
|
||||
config: undefined,
|
||||
updateConfig: emptyFn,
|
||||
getHiddenTablesForDiagram: () => [],
|
||||
setHiddenTablesForDiagram: emptyFn,
|
||||
hideTableForDiagram: emptyFn,
|
||||
unhideTableForDiagram: emptyFn,
|
||||
});
|
||||
|
||||
@@ -44,86 +44,8 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
return promise;
|
||||
};
|
||||
|
||||
const getHiddenTablesForDiagram = (diagramId: string): string[] => {
|
||||
return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
};
|
||||
|
||||
const setHiddenTablesForDiagram = async (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => ({
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: hiddenTableIds,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const hideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
if (currentHiddenTables.includes(tableId)) {
|
||||
return currentConfig; // Already hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: [...currentHiddenTables, tableId],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unhideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
const filteredTables = currentHiddenTables.filter(
|
||||
(id) => id !== tableId
|
||||
);
|
||||
|
||||
if (filteredTables.length === currentHiddenTables.length) {
|
||||
return currentConfig; // Not hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: filteredTables,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
config,
|
||||
updateConfig,
|
||||
getHiddenTablesForDiagram,
|
||||
setHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
}}
|
||||
>
|
||||
<ConfigContext.Provider value={{ config, updateConfig }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
|
||||
@@ -32,20 +32,14 @@ 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;
|
||||
@@ -66,15 +60,6 @@ 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: ({
|
||||
|
||||
@@ -32,7 +32,6 @@ 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>();
|
||||
|
||||
@@ -128,7 +127,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||
({ diagram, newDiagram: newDiagramArg, options }) => {
|
||||
({ diagram, newDiagram: newDiagramArg }) => {
|
||||
const {
|
||||
diffMap: newDiffs,
|
||||
changedTables: newChangedTables,
|
||||
@@ -140,7 +139,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setFieldsChanged(newChangedFields);
|
||||
setNewDiagram(newDiagramArg);
|
||||
setOriginalDiagram(diagram);
|
||||
setIsSummaryOnly(options?.summaryOnly ?? false);
|
||||
|
||||
events.emit({
|
||||
action: 'diff_calculated',
|
||||
@@ -307,117 +305,6 @@ 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']
|
||||
>(
|
||||
@@ -452,15 +339,6 @@ 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={{
|
||||
@@ -468,10 +346,8 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
originalDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
isSummaryOnly,
|
||||
|
||||
calculateDiff,
|
||||
resetDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
@@ -486,11 +362,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
getFieldNewPrimaryKey,
|
||||
getFieldNewNullable,
|
||||
getFieldNewCharacterMaximumLength,
|
||||
getFieldNewScale,
|
||||
getFieldNewPrecision,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
|
||||
@@ -8,7 +8,6 @@ export enum KeyboardShortcutAction {
|
||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||
SHOW_ALL = 'show_all',
|
||||
TOGGLE_THEME = 'toggle_theme',
|
||||
TOGGLE_FILTER = 'toggle_filter',
|
||||
}
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@@ -72,13 +71,6 @@ 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 {
|
||||
|
||||
@@ -19,9 +19,6 @@ export interface LocalConfigContext {
|
||||
showCardinality: boolean;
|
||||
setShowCardinality: (showCardinality: boolean) => void;
|
||||
|
||||
showFieldAttributes: boolean;
|
||||
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
|
||||
|
||||
hideMultiSchemaNotification: boolean;
|
||||
setHideMultiSchemaNotification: (
|
||||
hideMultiSchemaNotification: boolean
|
||||
@@ -53,9 +50,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
showFieldAttributes: true,
|
||||
setShowFieldAttributes: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
setHideMultiSchemaNotification: emptyFn,
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ const themeKey = 'theme';
|
||||
const scrollActionKey = 'scroll_action';
|
||||
const schemasFilterKey = 'schemas_filter';
|
||||
const showCardinalityKey = 'show_cardinality';
|
||||
const showFieldAttributesKey = 'show_field_attributes';
|
||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
@@ -35,11 +34,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [showFieldAttributes, setShowFieldAttributes] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
|
||||
@@ -125,8 +119,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setSchemasFilter,
|
||||
showCardinality,
|
||||
setShowCardinality,
|
||||
showFieldAttributes,
|
||||
setShowFieldAttributes,
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
setGithubRepoOpened,
|
||||
|
||||
@@ -48,7 +48,6 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
handleThemeToggle,
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[handleThemeToggle]
|
||||
);
|
||||
|
||||
@@ -35,23 +35,12 @@ 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 { editor, IDisposable } from 'monaco-editor';
|
||||
import type * as monaco from 'monaco-editor';
|
||||
import { waitFor } from '@/lib/utils';
|
||||
import {
|
||||
validateSQL,
|
||||
type ValidationResult,
|
||||
} from '@/lib/data/sql-import/sql-validator';
|
||||
import { type ValidationResult } from '@/lib/data/sql-import/sql-validator';
|
||||
import { validateSQL } from '@/lib/data/sql-import/unified-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.';
|
||||
|
||||
@@ -131,8 +120,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
}) => {
|
||||
const { effectiveTheme } = useTheme();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
const pasteDisposableRef = useRef<IDisposable | null>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
const pasteDisposableRef = useRef<monaco.IDisposable | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { isSm: isDesktop } = useBreakpoint('sm');
|
||||
@@ -231,15 +220,11 @@ 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!);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsAutoFixing(false);
|
||||
}, 100);
|
||||
setIsAutoFixing(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [sqlValidation, setScriptResult]);
|
||||
@@ -255,16 +240,6 @@ 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')
|
||||
@@ -318,7 +293,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
}, []);
|
||||
|
||||
const handleEditorDidMount = useCallback(
|
||||
(editor: editor.IStandaloneCodeEditor) => {
|
||||
(editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
|
||||
// Cleanup previous disposable if it exists
|
||||
@@ -334,17 +309,14 @@ 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) AND file is not too large
|
||||
if (detectedType === 'query' && !isLargeFile) {
|
||||
// Only format if it's JSON (query mode)
|
||||
if (detectedType === 'query') {
|
||||
// For JSON mode, format after a short delay
|
||||
setTimeout(() => {
|
||||
editor
|
||||
@@ -355,15 +327,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' && !isLargeFile) {
|
||||
// Only format JSON content if not too large
|
||||
if (importMethod === 'query') {
|
||||
// Only format JSON content
|
||||
setTimeout(() => {
|
||||
editor
|
||||
.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode or large files, do NOT format
|
||||
// For DDL mode, do NOT format
|
||||
}
|
||||
});
|
||||
|
||||
@@ -456,12 +428,20 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
</div>
|
||||
|
||||
{errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
|
||||
<SQLValidationStatus
|
||||
validation={sqlValidation}
|
||||
errorMessage={errorMessage}
|
||||
isAutoFixing={isAutoFixing}
|
||||
onErrorClick={handleErrorClick}
|
||||
/>
|
||||
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>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
@@ -566,7 +546,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
variant="secondary"
|
||||
onClick={handleAutoFix}
|
||||
disabled={isAutoFixing}
|
||||
className="bg-sky-600 text-white hover:bg-sky-700"
|
||||
className="bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
{isAutoFixing ? (
|
||||
<Spinner size="small" />
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Lightbulb,
|
||||
} 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;
|
||||
@@ -19,113 +21,71 @@ 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;
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 we have parser errors (errorMessage) after validation
|
||||
if (errorMessage && !hasErrors) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<Alert variant="destructive" className="mt-2">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
{errorMessage}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator className="mb-1 mt-2" />
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
{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}
|
||||
</div>
|
||||
))}
|
||||
{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}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{validation.errors.length > 3 && (
|
||||
<div className="ml-2 text-xs opacity-70">
|
||||
... and {validation.errors.length - 3} more
|
||||
errors
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{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">
|
||||
@@ -133,47 +93,30 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
|
||||
now ready to import.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{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>
|
||||
{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}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : null}
|
||||
))}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!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}
|
||||
</>
|
||||
{!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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const MAX_TABLES_IN_DIAGRAM = 500;
|
||||
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
|
||||
@@ -1,683 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum CreateDiagramDialogStep {
|
||||
SELECT_DATABASE = 'SELECT_DATABASE',
|
||||
IMPORT_DATABASE = 'IMPORT_DATABASE',
|
||||
SELECT_TABLES = 'SELECT_TABLES',
|
||||
}
|
||||
|
||||
@@ -15,13 +15,9 @@ 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 {}
|
||||
|
||||
@@ -46,8 +42,6 @@ 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);
|
||||
@@ -68,72 +62,49 @@ 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 ({
|
||||
selectedTables,
|
||||
databaseMetadata,
|
||||
}: {
|
||||
selectedTables?: SelectedTable[];
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
} = {}) => {
|
||||
let diagram: Diagram | undefined;
|
||||
const importNewDiagram = useCallback(async () => {
|
||||
let diagram: Diagram | undefined;
|
||||
|
||||
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 },
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
},
|
||||
[
|
||||
importMethod,
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
closeCreateDiagramDialog,
|
||||
navigate,
|
||||
updateConfig,
|
||||
scriptResult,
|
||||
diagramNumber,
|
||||
]
|
||||
);
|
||||
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,
|
||||
]);
|
||||
|
||||
const createEmptyDiagram = useCallback(async () => {
|
||||
const diagram: Diagram = {
|
||||
@@ -167,56 +138,10 @@ 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;
|
||||
}
|
||||
@@ -229,8 +154,6 @@ 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
|
||||
@@ -242,9 +165,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||
}
|
||||
/>
|
||||
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
|
||||
) : (
|
||||
<ImportDatabase
|
||||
onImport={importNewDiagramOrFilterTables}
|
||||
onImport={importNewDiagram}
|
||||
onCreateEmptyDiagram={createEmptyDiagram}
|
||||
databaseEdition={databaseEdition}
|
||||
databaseType={databaseType}
|
||||
@@ -257,18 +180,8 @@ 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>
|
||||
);
|
||||
|
||||
@@ -58,13 +58,16 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||
|
||||
const importDatabase = useCallback(async () => {
|
||||
let diagram: Diagram | undefined;
|
||||
let warnings: string[] | undefined;
|
||||
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
const result = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
diagram = result;
|
||||
warnings = result.warnings;
|
||||
} else {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
@@ -319,7 +322,38 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
|
||||
closeImportDatabaseDialog();
|
||||
// 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();
|
||||
}
|
||||
}, [
|
||||
importMethod,
|
||||
databaseEdition,
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
Suspense,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type * as monaco from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,24 +23,53 @@ 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,
|
||||
preprocessDBML,
|
||||
} from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/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';
|
||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import {
|
||||
clearErrorHighlight,
|
||||
highlightErrorLine,
|
||||
} from '@/components/code-snippet/dbml/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;
|
||||
}
|
||||
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||
withCreateEmptyDiagram?: boolean;
|
||||
@@ -116,8 +145,39 @@ 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(() => {
|
||||
clearErrorHighlight(decorationsCollection.current);
|
||||
decorationsCollection.current?.clear();
|
||||
}, []);
|
||||
|
||||
const validateDBML = useCallback(
|
||||
@@ -129,8 +189,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!content.trim()) return;
|
||||
|
||||
try {
|
||||
const preprocessedContent = preprocessDBML(content);
|
||||
const sanitizedContent = sanitizeDBML(preprocessedContent);
|
||||
const sanitizedContent = sanitizeDBML(content);
|
||||
const parser = new Parser();
|
||||
parser.parse(sanitizedContent, 'dbml');
|
||||
} catch (e) {
|
||||
@@ -140,12 +199,7 @@ 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({
|
||||
error: parsedError,
|
||||
model: editorRef.current?.getModel(),
|
||||
editorDecorationsCollection:
|
||||
decorationsCollection.current,
|
||||
});
|
||||
highlightErrorLine(parsedError);
|
||||
} else {
|
||||
setErrorMessage(
|
||||
e instanceof Error ? e.message : JSON.stringify(e)
|
||||
@@ -153,7 +207,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}
|
||||
}
|
||||
},
|
||||
[clearDecorations, t]
|
||||
[clearDecorations, highlightErrorLine, t]
|
||||
);
|
||||
|
||||
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||
@@ -188,11 +242,13 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!dbmlContent.trim() || errorMessage) return;
|
||||
|
||||
try {
|
||||
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||
// Sanitize DBML content before importing
|
||||
const sanitizedContent = sanitizeDBML(dbmlContent);
|
||||
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
|
||||
const tableIdsToRemove = tables
|
||||
.filter((table) =>
|
||||
importedDiagram.tables?.some(
|
||||
(t: DBTable) =>
|
||||
(t) =>
|
||||
t.name === table.name && t.schema === table.schema
|
||||
)
|
||||
)
|
||||
@@ -201,21 +257,19 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
const relationshipIdsToRemove = relationships
|
||||
.filter((relationship) => {
|
||||
const sourceTable = tables.find(
|
||||
(table: DBTable) =>
|
||||
table.id === relationship.sourceTableId
|
||||
(table) => table.id === relationship.sourceTableId
|
||||
);
|
||||
const targetTable = tables.find(
|
||||
(table: DBTable) =>
|
||||
table.id === relationship.targetTableId
|
||||
(table) => table.id === relationship.targetTableId
|
||||
);
|
||||
if (!sourceTable || !targetTable) return true;
|
||||
const replacementSourceTable = importedDiagram.tables?.find(
|
||||
(table: DBTable) =>
|
||||
(table) =>
|
||||
table.name === sourceTable.name &&
|
||||
table.schema === sourceTable.schema
|
||||
);
|
||||
const replacementTargetTable = importedDiagram.tables?.find(
|
||||
(table: DBTable) =>
|
||||
(table) =>
|
||||
table.name === targetTable.name &&
|
||||
table.schema === targetTable.schema
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -17,23 +17,11 @@ 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 }: { schema: DBSchema }) => void;
|
||||
allowSchemaCreation?: boolean;
|
||||
onConfirm: (schema: string) => void;
|
||||
}
|
||||
|
||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
@@ -41,90 +29,27 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
table,
|
||||
schemas,
|
||||
onConfirm,
|
||||
allowSchemaCreation = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
|
||||
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||
const [selectedSchema, setSelectedSchema] = React.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;
|
||||
setSelectedSchemaId(
|
||||
setSelectedSchema(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: (schemas?.[0]?.id ?? '')
|
||||
);
|
||||
setIsCreatingNew(!allowSchemaSelection);
|
||||
setNewSchemaName(
|
||||
allowSchemaCreation && !allowSchemaSelection
|
||||
? (defaultSchemaName ?? '')
|
||||
: ''
|
||||
);
|
||||
}, [
|
||||
defaultSchemaName,
|
||||
dialog.open,
|
||||
schemas,
|
||||
table?.schema,
|
||||
allowSchemaSelection,
|
||||
allowSchemaCreation,
|
||||
]);
|
||||
|
||||
}, [dialog.open, schemas, table?.schema]);
|
||||
const { closeTableSchemaDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
let createdSchemaId: string;
|
||||
if (isCreatingNew && newSchemaName.trim()) {
|
||||
const newSchema: DBSchema = {
|
||||
id: schemaNameToSchemaId(newSchemaName.trim()),
|
||||
name: newSchemaName.trim(),
|
||||
tableCount: 0,
|
||||
};
|
||||
|
||||
createdSchemaId = newSchema.id;
|
||||
|
||||
onConfirm({ schema: newSchema });
|
||||
} else {
|
||||
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||
if (!schema) return;
|
||||
|
||||
createdSchemaId = schema.id;
|
||||
onConfirm({ schema });
|
||||
}
|
||||
|
||||
filterSchemas([
|
||||
...(filteredSchemas ?? schemas.map((s) => s.id)),
|
||||
createdSchemaId,
|
||||
]);
|
||||
}, [
|
||||
onConfirm,
|
||||
selectedSchemaId,
|
||||
schemas,
|
||||
isCreatingNew,
|
||||
newSchemaName,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
]);
|
||||
onConfirm(selectedSchema);
|
||||
}, [onConfirm, selectedSchema]);
|
||||
|
||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
@@ -135,25 +60,6 @@ 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}
|
||||
@@ -161,106 +67,48 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
if (!open) {
|
||||
closeTableSchemaDialog();
|
||||
}
|
||||
|
||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="flex flex-col" showClose>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{!allowSchemaSelection && allowSchemaCreation
|
||||
? t('create_table_schema_dialog.title')
|
||||
: table
|
||||
? t('update_table_schema_dialog.title')
|
||||
: t('new_table_schema_dialog.title')}
|
||||
{table
|
||||
? t('update_table_schema_dialog.title')
|
||||
: t('new_table_schema_dialog.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{!allowSchemaSelection && allowSchemaCreation
|
||||
? t('create_table_schema_dialog.description')
|
||||
: table
|
||||
? t('update_table_schema_dialog.description', {
|
||||
tableName: table.name,
|
||||
})
|
||||
: t('new_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">
|
||||
{!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}
|
||||
<SelectBox
|
||||
options={schemaOptions}
|
||||
multiple={false}
|
||||
value={selectedSchema}
|
||||
onChange={(value) =>
|
||||
setSelectedSchema(value as string)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex gap-1 md:justify-between">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
{isCreatingNew
|
||||
? t('create_table_schema_dialog.cancel')
|
||||
: table
|
||||
? t('update_table_schema_dialog.cancel')
|
||||
: t('new_table_schema_dialog.cancel')}
|
||||
{table
|
||||
? t('update_table_schema_dialog.cancel')
|
||||
: t('new_table_schema_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<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 onClick={handleConfirm}>
|
||||
{table
|
||||
? t('update_table_schema_dialog.confirm')
|
||||
: t('new_table_schema_dialog.confirm')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
.text-editable {
|
||||
@@ -155,29 +154,3 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,25 +23,23 @@ 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,
|
||||
@@ -72,7 +70,6 @@ const resources = {
|
||||
gu,
|
||||
vi,
|
||||
ar,
|
||||
hr,
|
||||
};
|
||||
|
||||
i18n.use(LanguageDetector)
|
||||
|
||||
@@ -26,8 +26,6 @@ export const ar: LanguageTranslation = {
|
||||
hide_sidebar: 'إخفاء الشريط الجانبي',
|
||||
hide_cardinality: 'إخفاء الكاردينالية',
|
||||
show_cardinality: 'إظهار الكاردينالية',
|
||||
hide_field_attributes: 'إخفاء خصائص الحقل',
|
||||
show_field_attributes: 'إظهار خصائص الحقل',
|
||||
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||
theme: 'المظهر',
|
||||
show_dependencies: 'إظهار الاعتمادات',
|
||||
@@ -76,8 +74,8 @@ export const ar: LanguageTranslation = {
|
||||
title: 'مخططات متعددة',
|
||||
description:
|
||||
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'لا تظهره مجدداً',
|
||||
change_schema: 'تغيير',
|
||||
none: 'لا شيء',
|
||||
},
|
||||
|
||||
@@ -153,10 +151,6 @@ 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: 'خصائص الفهرس',
|
||||
@@ -257,12 +251,9 @@ 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',
|
||||
},
|
||||
@@ -278,11 +269,6 @@ 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: {
|
||||
@@ -414,13 +400,6 @@ export const ar: LanguageTranslation = {
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'تغيير',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'إنشاء مخطط جديد',
|
||||
description:
|
||||
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
|
||||
create: 'إنشاء',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '!ساعدنا على التحسن',
|
||||
|
||||
@@ -26,8 +26,6 @@ export const bn: LanguageTranslation = {
|
||||
hide_sidebar: 'সাইডবার লুকান',
|
||||
hide_cardinality: 'কার্ডিনালিটি লুকান',
|
||||
show_cardinality: 'কার্ডিনালিটি দেখান',
|
||||
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
|
||||
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
|
||||
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
||||
theme: 'থিম',
|
||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||
@@ -77,8 +75,8 @@ export const bn: LanguageTranslation = {
|
||||
title: 'বহু স্কিমা',
|
||||
description:
|
||||
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'পুনরায় দেখাবেন না',
|
||||
change_schema: 'পরিবর্তন করুন',
|
||||
none: 'কিছুই না',
|
||||
},
|
||||
|
||||
@@ -153,12 +151,7 @@ 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: 'ইনডেক্স কর্ম',
|
||||
@@ -258,12 +251,9 @@ 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',
|
||||
},
|
||||
@@ -279,12 +269,6 @@ 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: {
|
||||
@@ -416,13 +400,6 @@ export const bn: LanguageTranslation = {
|
||||
cancel: 'বাতিল করুন',
|
||||
confirm: 'পরিবর্তন করুন',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'নতুন স্কিমা তৈরি করুন',
|
||||
description:
|
||||
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
|
||||
create: 'তৈরি করুন',
|
||||
cancel: 'বাতিল করুন',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'আমাদের উন্নত করতে সাহায্য করুন!',
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -77,8 +75,8 @@ export const de: LanguageTranslation = {
|
||||
title: 'Mehrere Schemas',
|
||||
description:
|
||||
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Nicht erneut anzeigen',
|
||||
change_schema: 'Schema ändern',
|
||||
none: 'Keine',
|
||||
},
|
||||
|
||||
@@ -154,12 +152,7 @@ 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',
|
||||
@@ -260,12 +253,9 @@ 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',
|
||||
},
|
||||
@@ -280,14 +270,7 @@ 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: {
|
||||
@@ -420,13 +403,6 @@ 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!',
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -75,7 +73,8 @@ export const en = {
|
||||
title: 'Multiple Schemas',
|
||||
description:
|
||||
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
|
||||
show_me: 'Show me',
|
||||
dont_show_again: "Don't show again",
|
||||
change_schema: 'Change',
|
||||
none: 'none',
|
||||
},
|
||||
|
||||
@@ -145,12 +144,8 @@ 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: {
|
||||
@@ -250,11 +245,8 @@ 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',
|
||||
@@ -271,10 +263,6 @@ 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: {
|
||||
@@ -406,14 +394,6 @@ 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:
|
||||
|
||||
@@ -24,8 +24,6 @@ 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',
|
||||
@@ -143,12 +141,7 @@ 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',
|
||||
@@ -248,12 +241,9 @@ 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',
|
||||
},
|
||||
@@ -268,13 +258,7 @@ 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: {
|
||||
@@ -408,13 +392,6 @@ 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!',
|
||||
@@ -428,8 +405,8 @@ export const es: LanguageTranslation = {
|
||||
title: 'Múltiples Esquemas',
|
||||
description:
|
||||
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'No mostrar de nuevo',
|
||||
change_schema: 'Cambiar',
|
||||
none: 'nada',
|
||||
},
|
||||
// TODO: Translate
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -141,12 +139,7 @@ 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",
|
||||
@@ -246,12 +239,9 @@ 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',
|
||||
},
|
||||
@@ -266,13 +256,7 @@ 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: {
|
||||
@@ -361,8 +345,8 @@ export const fr: LanguageTranslation = {
|
||||
title: 'Schémas Multiples',
|
||||
description:
|
||||
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Ne plus afficher',
|
||||
change_schema: 'Changer',
|
||||
none: 'Aucun',
|
||||
},
|
||||
|
||||
@@ -388,13 +372,6 @@ 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',
|
||||
|
||||
@@ -26,8 +26,6 @@ export const gu: LanguageTranslation = {
|
||||
hide_sidebar: 'સાઇડબાર છુપાવો',
|
||||
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
|
||||
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
|
||||
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
|
||||
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
|
||||
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
||||
theme: 'થિમ',
|
||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||
@@ -77,8 +75,8 @@ export const gu: LanguageTranslation = {
|
||||
title: 'કઈંક વધારે સ્કીમા',
|
||||
description:
|
||||
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'ફરીથી ન બતાવો',
|
||||
change_schema: 'બદલો',
|
||||
none: 'કઈ નહીં',
|
||||
},
|
||||
|
||||
@@ -154,12 +152,7 @@ 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: 'ઇન્ડેક્સ લક્ષણો',
|
||||
@@ -259,12 +252,9 @@ 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',
|
||||
},
|
||||
@@ -279,13 +269,7 @@ 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: {
|
||||
@@ -417,14 +401,6 @@ export const gu: LanguageTranslation = {
|
||||
confirm: 'બદલો',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'નવું સ્કીમા બનાવો',
|
||||
description:
|
||||
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
|
||||
create: 'બનાવો',
|
||||
cancel: 'રદ કરો',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'અમને સુધારવામાં મદદ કરો!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const hi: LanguageTranslation = {
|
||||
hide_sidebar: 'साइडबार छिपाएँ',
|
||||
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
|
||||
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
|
||||
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
|
||||
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
|
||||
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
||||
theme: 'थीम',
|
||||
show_dependencies: 'निर्भरता दिखाएँ',
|
||||
@@ -76,8 +74,8 @@ export const hi: LanguageTranslation = {
|
||||
title: 'एकाधिक स्कीमा',
|
||||
description:
|
||||
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'फिर से न दिखाएँ',
|
||||
change_schema: 'बदलें',
|
||||
none: 'कोई नहीं',
|
||||
},
|
||||
|
||||
@@ -153,12 +151,7 @@ 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: 'सूचकांक विशेषताएँ',
|
||||
@@ -259,12 +252,9 @@ 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',
|
||||
},
|
||||
@@ -279,13 +269,7 @@ 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: {
|
||||
@@ -420,14 +404,6 @@ export const hi: LanguageTranslation = {
|
||||
confirm: 'बदलें',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'नया स्कीमा बनाएं',
|
||||
description:
|
||||
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
|
||||
create: 'बनाएं',
|
||||
cancel: 'रद्द करें',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'हमें सुधारने में मदद करें!',
|
||||
description:
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
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',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Više shema',
|
||||
description:
|
||||
'{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.',
|
||||
show_me: 'Prikaži mi',
|
||||
none: 'nijedna',
|
||||
},
|
||||
|
||||
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: {
|
||||
schema: 'Shema:',
|
||||
filter_by_schema: 'Filtriraj po shemi',
|
||||
search_schema: 'Pretraži shemu...',
|
||||
no_schemas_found: 'Nema pronađenih shema.',
|
||||
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',
|
||||
};
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -76,8 +74,8 @@ export const id_ID: LanguageTranslation = {
|
||||
title: 'Schema Lebih dari satu',
|
||||
description:
|
||||
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Jangan tampilkan lagi',
|
||||
change_schema: 'Ubah',
|
||||
none: 'Tidak ada',
|
||||
},
|
||||
|
||||
@@ -152,12 +150,7 @@ 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',
|
||||
@@ -257,12 +250,9 @@ 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',
|
||||
},
|
||||
@@ -277,13 +267,7 @@ 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: {
|
||||
@@ -415,14 +399,6 @@ 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:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const ja: LanguageTranslation = {
|
||||
hide_sidebar: 'サイドバーを非表示',
|
||||
hide_cardinality: 'カーディナリティを非表示',
|
||||
show_cardinality: 'カーディナリティを表示',
|
||||
hide_field_attributes: 'フィールド属性を非表示',
|
||||
show_field_attributes: 'フィールド属性を表示',
|
||||
zoom_on_scroll: 'スクロールでズーム',
|
||||
theme: 'テーマ',
|
||||
// TODO: Translate
|
||||
@@ -78,8 +76,8 @@ export const ja: LanguageTranslation = {
|
||||
title: '複数のスキーマ',
|
||||
description:
|
||||
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: '再表示しない',
|
||||
change_schema: '変更',
|
||||
none: 'なし',
|
||||
},
|
||||
|
||||
@@ -156,12 +154,7 @@ 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: 'インデックス属性',
|
||||
@@ -263,12 +256,9 @@ 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',
|
||||
},
|
||||
@@ -285,10 +275,6 @@ 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: {
|
||||
@@ -422,14 +408,6 @@ export const ja: LanguageTranslation = {
|
||||
confirm: '変更',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '新しいスキーマを作成',
|
||||
description:
|
||||
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
|
||||
create: '作成',
|
||||
cancel: 'キャンセル',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '改善をサポートしてください!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const ko_KR: LanguageTranslation = {
|
||||
hide_sidebar: '사이드바 숨기기',
|
||||
hide_cardinality: '카디널리티 숨기기',
|
||||
show_cardinality: '카디널리티 보이기',
|
||||
hide_field_attributes: '필드 속성 숨기기',
|
||||
show_field_attributes: '필드 속성 보이기',
|
||||
zoom_on_scroll: '스크롤 시 확대',
|
||||
theme: '테마',
|
||||
show_dependencies: '종속성 보이기',
|
||||
@@ -76,8 +74,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
title: '다중 스키마',
|
||||
description:
|
||||
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: '다시 보여주지 마세요',
|
||||
change_schema: '변경',
|
||||
none: '없음',
|
||||
},
|
||||
|
||||
@@ -152,12 +150,7 @@ 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: '인덱스 속성',
|
||||
@@ -257,12 +250,9 @@ 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',
|
||||
},
|
||||
@@ -277,13 +267,7 @@ 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: {
|
||||
@@ -415,14 +399,6 @@ export const ko_KR: LanguageTranslation = {
|
||||
confirm: '변경',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '새 스키마 생성',
|
||||
description:
|
||||
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
|
||||
create: '생성',
|
||||
cancel: '취소',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '개선할 수 있도록 도와주세요!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const mr: LanguageTranslation = {
|
||||
hide_sidebar: 'साइडबार लपवा',
|
||||
hide_cardinality: 'कार्डिनॅलिटी लपवा',
|
||||
show_cardinality: 'कार्डिनॅलिटी दाखवा',
|
||||
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
|
||||
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
|
||||
zoom_on_scroll: 'स्क्रोलवर झूम करा',
|
||||
theme: 'थीम',
|
||||
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
||||
@@ -77,8 +75,8 @@ export const mr: LanguageTranslation = {
|
||||
title: 'एकाधिक स्कीमा',
|
||||
description:
|
||||
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'पुन्हा दाखवू नका',
|
||||
change_schema: 'बदला',
|
||||
none: 'काहीही नाही',
|
||||
},
|
||||
|
||||
@@ -155,12 +153,7 @@ 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: 'इंडेक्स गुणधर्म',
|
||||
@@ -262,12 +255,9 @@ 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',
|
||||
},
|
||||
@@ -282,13 +272,7 @@ 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: {
|
||||
@@ -423,14 +407,6 @@ export const mr: LanguageTranslation = {
|
||||
confirm: 'बदला',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'नवीन स्कीमा तयार करा',
|
||||
description:
|
||||
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
|
||||
create: 'तयार करा',
|
||||
cancel: 'रद्द करा',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'आम्हाला सुधारण्यास मदत करा!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const ne: LanguageTranslation = {
|
||||
hide_sidebar: 'साइडबार लुकाउनुहोस्',
|
||||
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
|
||||
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
|
||||
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
|
||||
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
|
||||
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
|
||||
theme: 'थिम',
|
||||
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
||||
@@ -77,8 +75,8 @@ export const ne: LanguageTranslation = {
|
||||
title: 'विविध स्कीमहरू',
|
||||
description:
|
||||
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'फेरि देखाउन नदिनुहोस्',
|
||||
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
|
||||
none: 'कुनै पनि छैन',
|
||||
},
|
||||
|
||||
@@ -153,12 +151,7 @@ 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: 'सूचक विशेषताहरू',
|
||||
@@ -259,12 +252,9 @@ 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',
|
||||
},
|
||||
@@ -279,14 +269,8 @@ 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: {
|
||||
@@ -420,14 +404,6 @@ export const ne: LanguageTranslation = {
|
||||
confirm: 'परिवर्तन गर्नुहोस्',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
|
||||
description:
|
||||
'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
|
||||
create: 'सिर्जना गर्नुहोस्',
|
||||
cancel: 'रद्द गर्नुहोस्',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -77,8 +75,8 @@ export const pt_BR: LanguageTranslation = {
|
||||
title: 'Múltiplos Esquemas',
|
||||
description:
|
||||
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Não mostrar novamente',
|
||||
change_schema: 'Alterar',
|
||||
none: 'nenhum',
|
||||
},
|
||||
|
||||
@@ -153,12 +151,7 @@ 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',
|
||||
@@ -258,12 +251,9 @@ 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',
|
||||
},
|
||||
@@ -278,13 +268,7 @@ 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: {
|
||||
@@ -418,14 +402,6 @@ 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:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const ru: LanguageTranslation = {
|
||||
hide_sidebar: 'Скрыть боковую панель',
|
||||
hide_cardinality: 'Скрыть виды связи',
|
||||
show_cardinality: 'Показать виды связи',
|
||||
show_field_attributes: 'Показать атрибуты поля',
|
||||
hide_field_attributes: 'Скрыть атрибуты поля',
|
||||
zoom_on_scroll: 'Увеличение при прокрутке',
|
||||
theme: 'Тема',
|
||||
show_dependencies: 'Показать зависимости',
|
||||
@@ -75,8 +73,8 @@ export const ru: LanguageTranslation = {
|
||||
title: 'Множественные схемы',
|
||||
description:
|
||||
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Больше не показывать',
|
||||
change_schema: 'Изменить',
|
||||
none: 'никто',
|
||||
},
|
||||
|
||||
@@ -149,12 +147,7 @@ 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: 'Атрибуты индекса',
|
||||
@@ -255,12 +248,9 @@ 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',
|
||||
},
|
||||
@@ -275,13 +265,7 @@ 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: {
|
||||
@@ -415,14 +399,6 @@ export const ru: LanguageTranslation = {
|
||||
confirm: 'Изменить',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'Создать новую схему',
|
||||
description:
|
||||
'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
|
||||
create: 'Создать',
|
||||
cancel: 'Отменить',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Помогите нам стать лучше!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const te: LanguageTranslation = {
|
||||
hide_sidebar: 'సైడ్బార్ దాచండి',
|
||||
hide_cardinality: 'కార్డినాలిటీని దాచండి',
|
||||
show_cardinality: 'కార్డినాలిటీని చూపించండి',
|
||||
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
|
||||
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
|
||||
zoom_on_scroll: 'స్క్రోల్పై జూమ్',
|
||||
theme: 'థీమ్',
|
||||
show_dependencies: 'ఆధారాలు చూపించండి',
|
||||
@@ -77,8 +75,8 @@ export const te: LanguageTranslation = {
|
||||
title: 'బహుళ స్కీమాలు',
|
||||
description:
|
||||
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'మరలా చూపించవద్దు',
|
||||
change_schema: 'మార్చు',
|
||||
none: 'ఎదరికాదు',
|
||||
},
|
||||
|
||||
@@ -153,12 +151,7 @@ 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: 'ఇండెక్స్ గుణాలు',
|
||||
@@ -259,12 +252,9 @@ 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',
|
||||
},
|
||||
@@ -279,13 +269,7 @@ 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: {
|
||||
@@ -419,14 +403,6 @@ export const te: LanguageTranslation = {
|
||||
confirm: 'మార్చు',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'కొత్త స్కీమా సృష్టించండి',
|
||||
description:
|
||||
'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
|
||||
create: 'సృష్టించు',
|
||||
cancel: 'రద్దు',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'మా సహాయంతో మెరుగుపరచండి!',
|
||||
description:
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -77,8 +75,8 @@ export const tr: LanguageTranslation = {
|
||||
title: 'Birden Fazla Şema',
|
||||
description:
|
||||
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Tekrar gösterme',
|
||||
change_schema: 'Değiştir',
|
||||
none: 'yok',
|
||||
},
|
||||
|
||||
@@ -152,12 +150,7 @@ 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',
|
||||
@@ -258,12 +251,9 @@ 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',
|
||||
},
|
||||
@@ -277,13 +267,7 @@ 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: {
|
||||
@@ -408,14 +392,6 @@ 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:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const uk: LanguageTranslation = {
|
||||
hide_sidebar: 'Приховати бічну панель',
|
||||
hide_cardinality: 'Приховати потужність',
|
||||
show_cardinality: 'Показати кардинальність',
|
||||
show_field_attributes: 'Показати атрибути полів',
|
||||
hide_field_attributes: 'Приховати атрибути полів',
|
||||
zoom_on_scroll: 'Масштабувати прокручуванням',
|
||||
theme: 'Тема',
|
||||
show_dependencies: 'Показати залежності',
|
||||
@@ -75,8 +73,8 @@ export const uk: LanguageTranslation = {
|
||||
title: 'Кілька схем',
|
||||
description:
|
||||
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Більше не показувати',
|
||||
change_schema: 'Зміна',
|
||||
none: 'немає',
|
||||
},
|
||||
|
||||
@@ -151,12 +149,7 @@ 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: 'Атрибути індексу',
|
||||
@@ -256,12 +249,9 @@ 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',
|
||||
},
|
||||
@@ -276,13 +266,7 @@ 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: {
|
||||
@@ -416,14 +400,6 @@ export const uk: LanguageTranslation = {
|
||||
confirm: 'Змінити',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'Створити нову схему',
|
||||
description:
|
||||
'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
|
||||
create: 'Створити',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Допоможіть нам покращитися!',
|
||||
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
||||
|
||||
@@ -26,8 +26,6 @@ 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',
|
||||
@@ -76,8 +74,8 @@ export const vi: LanguageTranslation = {
|
||||
title: 'Có nhiều lược đồ',
|
||||
description:
|
||||
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: 'Không hiển thị lại',
|
||||
change_schema: 'Thay đổi',
|
||||
none: 'không có',
|
||||
},
|
||||
|
||||
@@ -152,12 +150,7 @@ 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',
|
||||
@@ -257,12 +250,9 @@ 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',
|
||||
},
|
||||
@@ -277,13 +267,7 @@ 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: {
|
||||
@@ -415,14 +399,6 @@ 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:
|
||||
|
||||
@@ -26,8 +26,6 @@ export const zh_CN: LanguageTranslation = {
|
||||
hide_sidebar: '隐藏侧边栏',
|
||||
hide_cardinality: '隐藏基数',
|
||||
show_cardinality: '展示基数',
|
||||
show_field_attributes: '展示字段属性',
|
||||
hide_field_attributes: '隐藏字段属性',
|
||||
zoom_on_scroll: '滚动缩放',
|
||||
theme: '主题',
|
||||
show_dependencies: '展示依赖',
|
||||
@@ -73,8 +71,8 @@ export const zh_CN: LanguageTranslation = {
|
||||
title: '多个模式',
|
||||
description:
|
||||
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: '不再展示',
|
||||
change_schema: '更改',
|
||||
none: '无',
|
||||
},
|
||||
|
||||
@@ -149,12 +147,7 @@ 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: '索引属性',
|
||||
@@ -254,12 +247,9 @@ 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',
|
||||
},
|
||||
@@ -274,13 +264,7 @@ 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: {
|
||||
@@ -411,13 +395,6 @@ export const zh_CN: LanguageTranslation = {
|
||||
confirm: '更改',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '创建新模式',
|
||||
description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
|
||||
create: '创建',
|
||||
cancel: '取消',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '帮助我们改进!',
|
||||
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
|
||||
|
||||
@@ -26,8 +26,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
hide_sidebar: '隱藏側邊欄',
|
||||
hide_cardinality: '隱藏基數',
|
||||
show_cardinality: '顯示基數',
|
||||
hide_field_attributes: '隱藏欄位屬性',
|
||||
show_field_attributes: '顯示欄位屬性',
|
||||
zoom_on_scroll: '滾動縮放',
|
||||
theme: '主題',
|
||||
show_dependencies: '顯示相依性',
|
||||
@@ -73,8 +71,8 @@ export const zh_TW: LanguageTranslation = {
|
||||
title: '多重 Schema',
|
||||
description:
|
||||
'此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
dont_show_again: '不再顯示',
|
||||
change_schema: '變更',
|
||||
none: '無',
|
||||
},
|
||||
|
||||
@@ -149,12 +147,7 @@ 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: '索引屬性',
|
||||
@@ -254,12 +247,9 @@ 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',
|
||||
},
|
||||
@@ -274,13 +264,7 @@ 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: {
|
||||
@@ -410,14 +394,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
confirm: '變更',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '建立新 Schema',
|
||||
description:
|
||||
'尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
|
||||
create: '建立',
|
||||
cancel: '取消',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '協助我們改善!',
|
||||
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -49,10 +48,6 @@ const generateIdsMapFromDiagram = (
|
||||
idsMap.set(area.id, generateId());
|
||||
});
|
||||
|
||||
diagram.customTypes?.forEach((customType) => {
|
||||
idsMap.set(customType.id, generateId());
|
||||
});
|
||||
|
||||
return idsMap;
|
||||
};
|
||||
|
||||
@@ -218,22 +213,6 @@ 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,
|
||||
@@ -242,7 +221,6 @@ export const cloneDiagram = (
|
||||
relationships,
|
||||
tables,
|
||||
areas,
|
||||
customTypes,
|
||||
createdAt: diagram.createdAt
|
||||
? new Date(diagram.createdAt)
|
||||
: new Date(),
|
||||
|
||||
@@ -48,30 +48,18 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{
|
||||
name: 'varchar',
|
||||
id: 'varchar',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'char large object', id: 'char_large_object' },
|
||||
{
|
||||
name: 'char varying',
|
||||
id: 'char_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||
{ name: 'character large object', id: 'character_large_object' },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||
{
|
||||
name: 'nchar varying',
|
||||
id: 'nchar_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'national character large object',
|
||||
id: 'national_character_large_object',
|
||||
@@ -79,34 +67,22 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||
{
|
||||
name: 'national character varying',
|
||||
id: 'national_character_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national char varying',
|
||||
id: 'national_char_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national character',
|
||||
id: 'national_character',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'national char',
|
||||
id: 'national_char',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||
{ name: 'binary large object', id: 'binary_large_object' },
|
||||
{
|
||||
name: 'binary varying',
|
||||
id: 'binary_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'fixedstring',
|
||||
id: 'fixedstring',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||
{ name: 'string', id: 'string' },
|
||||
|
||||
// Date Types
|
||||
|
||||
@@ -14,23 +14,9 @@ export interface DataType {
|
||||
name: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
hasCharMaxLength?: boolean;
|
||||
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
|
||||
fieldAttributes?: FieldAttributes;
|
||||
}
|
||||
|
||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { DataTypeData } from './data-types';
|
||||
|
||||
export const genericDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{
|
||||
name: 'varchar',
|
||||
id: 'varchar',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
usageLevel: 1,
|
||||
},
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
@@ -15,62 +10,23 @@ export const genericDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{
|
||||
name: 'decimal',
|
||||
id: 'decimal',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 999,
|
||||
min: 1,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 999,
|
||||
min: 0,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 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',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'double', id: 'double' },
|
||||
{ name: 'enum', id: 'enum' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{
|
||||
name: 'numeric',
|
||||
id: 'numeric',
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 999,
|
||||
min: 1,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 999,
|
||||
min: 0,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'real', id: 'real' },
|
||||
{ name: 'set', id: 'set' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'time', id: 'time' },
|
||||
{
|
||||
name: 'varbinary',
|
||||
id: 'varbinary',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
] as const;
|
||||
|
||||
@@ -4,32 +4,12 @@ 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,
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 65,
|
||||
min: 1,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 30,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
|
||||
{ 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',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
@@ -40,39 +20,16 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'mediumint', id: 'mediumint' },
|
||||
{
|
||||
name: 'numeric',
|
||||
id: 'numeric',
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 65,
|
||||
min: 1,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 30,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ 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', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{
|
||||
name: 'binary',
|
||||
id: 'binary',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'varbinary',
|
||||
id: 'varbinary',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
|
||||
@@ -3,12 +3,7 @@ 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',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
usageLevel: 1,
|
||||
},
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
@@ -16,23 +11,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||
{
|
||||
name: 'decimal',
|
||||
id: 'decimal',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 65,
|
||||
min: 1,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 30,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
|
||||
@@ -43,7 +22,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'double', id: 'double' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'tinytext', id: 'tinytext' },
|
||||
{ name: 'mediumtext', id: 'mediumtext' },
|
||||
{ name: 'longtext', id: 'longtext' },
|
||||
|
||||
@@ -2,30 +2,15 @@ import type { DataTypeData } from './data-types';
|
||||
|
||||
export const oracleDataTypes: readonly DataTypeData[] = [
|
||||
// Character types
|
||||
{
|
||||
name: 'VARCHAR2',
|
||||
id: 'varchar2',
|
||||
usageLevel: 1,
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
|
||||
{
|
||||
name: 'NVARCHAR2',
|
||||
id: 'nvarchar2',
|
||||
usageLevel: 1,
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'CHAR',
|
||||
id: 'char',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'NCHAR',
|
||||
id: 'nchar',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
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 },
|
||||
|
||||
@@ -64,12 +49,7 @@ export const oracleDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
|
||||
|
||||
// Other types
|
||||
{
|
||||
name: 'RAW',
|
||||
id: 'raw',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
|
||||
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
|
||||
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
|
||||
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },
|
||||
|
||||
@@ -3,12 +3,7 @@ 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',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
usageLevel: 1,
|
||||
},
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
@@ -16,23 +11,7 @@ export const postgresDataTypes: readonly DataTypeData[] = [
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||
{
|
||||
name: 'decimal',
|
||||
id: 'decimal',
|
||||
usageLevel: 2,
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 131072,
|
||||
min: 0,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 16383,
|
||||
min: 0,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'serial', id: 'serial', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
|
||||
@@ -44,33 +23,18 @@ export const postgresDataTypes: readonly DataTypeData[] = [
|
||||
},
|
||||
|
||||
// Less common types
|
||||
{
|
||||
name: 'numeric',
|
||||
id: 'numeric',
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 131072,
|
||||
min: 0,
|
||||
default: 10,
|
||||
},
|
||||
scale: {
|
||||
max: 16383,
|
||||
min: 0,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ 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', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
|
||||
|
||||
@@ -4,93 +4,32 @@ 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',
|
||||
fieldAttributes: {
|
||||
hasCharMaxLength: true,
|
||||
hasCharMaxLengthOption: true,
|
||||
maxLength: 8000,
|
||||
},
|
||||
usageLevel: 1,
|
||||
},
|
||||
{
|
||||
name: 'nvarchar',
|
||||
id: 'nvarchar',
|
||||
fieldAttributes: {
|
||||
hasCharMaxLength: true,
|
||||
hasCharMaxLengthOption: true,
|
||||
maxLength: 4000,
|
||||
},
|
||||
usageLevel: 1,
|
||||
},
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, 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,
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 38,
|
||||
min: 1,
|
||||
default: 18,
|
||||
},
|
||||
scale: {
|
||||
max: 38,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ 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',
|
||||
fieldAttributes: {
|
||||
precision: {
|
||||
max: 38,
|
||||
min: 1,
|
||||
default: 18,
|
||||
},
|
||||
scale: {
|
||||
max: 38,
|
||||
min: 0,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ 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', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||
{ name: 'ntext', id: 'ntext' },
|
||||
{
|
||||
name: 'binary',
|
||||
id: 'binary',
|
||||
fieldAttributes: { hasCharMaxLength: true },
|
||||
},
|
||||
{
|
||||
name: 'varbinary',
|
||||
id: 'varbinary',
|
||||
fieldAttributes: {
|
||||
hasCharMaxLength: true,
|
||||
hasCharMaxLengthOption: true,
|
||||
maxLength: 8000,
|
||||
},
|
||||
},
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'image', id: 'image' },
|
||||
{ name: 'datetimeoffset', id: 'datetimeoffset' },
|
||||
{ name: 'smalldatetime', id: 'smalldatetime' },
|
||||
|
||||
@@ -10,41 +10,21 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||
|
||||
// SQLite type aliases and common types
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{
|
||||
name: 'varchar',
|
||||
id: 'varchar',
|
||||
fieldAttributes: {
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
usageLevel: 1,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
id: 'timestamp',
|
||||
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 },
|
||||
|
||||
// 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',
|
||||
fieldAttributes: {
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
usageLevel: 2,
|
||||
},
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
|
||||
@@ -4,5 +4,4 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
|
||||
[DatabaseType.POSTGRESQL]: 'public',
|
||||
[DatabaseType.SQL_SERVER]: 'dbo',
|
||||
[DatabaseType.CLICKHOUSE]: 'default',
|
||||
[DatabaseType.COCKROACHDB]: 'public',
|
||||
};
|
||||
|
||||
@@ -1,870 +0,0 @@
|
||||
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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,50 +48,6 @@ 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 '';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
formatMSSQLTableComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
@@ -73,13 +72,7 @@ function parseMSSQLDefault(field: DBField): string {
|
||||
return `'${defaultValue}'`;
|
||||
}
|
||||
|
||||
export function exportMSSQL({
|
||||
diagram,
|
||||
onlyRelationships = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
onlyRelationships?: boolean;
|
||||
}): string {
|
||||
export function exportMSSQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
@@ -89,254 +82,166 @@ export function exportMSSQL({
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
// 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 '';
|
||||
}
|
||||
});
|
||||
|
||||
// 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`;
|
||||
});
|
||||
const tableName = table.schema
|
||||
? `[${table.schema}].[${table.name}]`
|
||||
: `[${table.name}]`;
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
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;
|
||||
|
||||
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})`;
|
||||
}
|
||||
// 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})`;
|
||||
}
|
||||
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 && 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})`;
|
||||
}
|
||||
}
|
||||
|
||||
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)'
|
||||
// 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)}`
|
||||
: '';
|
||||
|
||||
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(', ')});`
|
||||
: '';
|
||||
// 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}]` : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return validIndexes.length > 0
|
||||
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||
// 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`
|
||||
: '';
|
||||
})()}\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
}
|
||||
})
|
||||
.join('')}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript += `\n${relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
// 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
|
||||
);
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
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}]`;
|
||||
|
||||
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 `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n')}`;
|
||||
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
escapeSQLComment,
|
||||
formatTableComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
@@ -170,13 +168,7 @@ function mapMySQLType(typeName: string): string {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
export function exportMySQL({
|
||||
diagram,
|
||||
onlyRelationships = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
onlyRelationships?: boolean;
|
||||
}): string {
|
||||
export function exportMySQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
@@ -185,245 +177,224 @@ export function exportMySQL({
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Start SQL script
|
||||
let sqlScript = '-- MySQL database export\n';
|
||||
let sqlScript = '-- MySQL database export\n\n';
|
||||
|
||||
if (!onlyRelationships) {
|
||||
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
||||
sqlScript += 'START TRANSACTION;\n';
|
||||
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
||||
sqlScript += 'START TRANSACTION;\n\n';
|
||||
|
||||
// Create databases (schemas) if they don't exist
|
||||
const schemas = new Set<string>();
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
|
||||
});
|
||||
|
||||
if (schemas.size > 0) {
|
||||
sqlScript += '\n';
|
||||
// Create databases (schemas) if they don't exist
|
||||
const schemas = new Set<string>();
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
|
||||
});
|
||||
|
||||
// Use schema prefix if available
|
||||
const tableName = table.schema
|
||||
? `\`${table.schema}\`.\`${table.name}\``
|
||||
: `\`${table.name}\``;
|
||||
if (schemas.size > 0) {
|
||||
sqlScript += '\n';
|
||||
}
|
||||
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter(
|
||||
(f) => f.primaryKey
|
||||
);
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${
|
||||
table.comments ? formatTableComment(table.comments) : ''
|
||||
}\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `\`${field.name}\``;
|
||||
// Use schema prefix if available
|
||||
const tableName = table.schema
|
||||
? `\`${table.schema}\`.\`${table.name}\``
|
||||
: `\`${table.name}\``;
|
||||
|
||||
// Handle type name - map to MySQL compatible types
|
||||
const typeName = mapMySQLType(field.type.name);
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||
|
||||
// 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})`;
|
||||
}
|
||||
}
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
return `${
|
||||
table.comments ? `-- ${table.comments}\n` : ''
|
||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `\`${field.name}\``;
|
||||
|
||||
// Set a default size for VARCHAR columns if not specified
|
||||
// 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' &&
|
||||
!field.characterMaximumLength
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'varbinary'
|
||||
) {
|
||||
typeWithSize = `${typeName}(255)`;
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
||||
let autoIncrement = '';
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
field.primaryKey &&
|
||||
(field.default
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Set a default size for VARCHAR columns if not specified
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' &&
|
||||
!field.characterMaximumLength
|
||||
) {
|
||||
typeWithSize = `${typeName}(255)`;
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
||||
let autoIncrement = '';
|
||||
if (
|
||||
field.primaryKey &&
|
||||
(field.default?.toLowerCase().includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('autoincrement') ||
|
||||
field.default?.includes('nextval'))
|
||||
) {
|
||||
autoIncrement = ' AUTO_INCREMENT';
|
||||
}
|
||||
.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)}`
|
||||
: '';
|
||||
|
||||
// MySQL supports inline comments
|
||||
const comment = field.comments
|
||||
? ` COMMENT '${escapeSQLComment(field.comments)}'`
|
||||
// Handle default value
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity') &&
|
||||
!field.default
|
||||
.toLowerCase()
|
||||
.includes('autoincrement') &&
|
||||
!field.default.includes('nextval')
|
||||
? ` DEFAULT ${parseMySQLDefault(field)}`
|
||||
: '';
|
||||
|
||||
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);
|
||||
// MySQL supports inline comments
|
||||
const comment = field.comments
|
||||
? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
|
||||
: '';
|
||||
|
||||
// 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 `${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
|
||||
);
|
||||
|
||||
// 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(', ')});`
|
||||
: '';
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return validIndexes.length > 0
|
||||
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||
// 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`
|
||||
: '';
|
||||
})()
|
||||
}\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript += '\n-- Foreign key constraints\n\n';
|
||||
|
||||
const foreignKeys = relationships
|
||||
sqlScript += relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find(
|
||||
(t) => t.id === r.sourceTableId
|
||||
@@ -452,62 +423,25 @@ export function exportMySQL({
|
||||
return '';
|
||||
}
|
||||
|
||||
// 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}\``;
|
||||
const sourceTableName = sourceTable.schema
|
||||
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
|
||||
: `\`${sourceTable.name}\``;
|
||||
const targetTableName = targetTable.schema
|
||||
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
|
||||
: `\`${targetTable.name}\``;
|
||||
|
||||
// Create a descriptive constraint name
|
||||
const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
|
||||
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
|
||||
|
||||
// MySQL supports ON DELETE and ON UPDATE actions
|
||||
return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
|
||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
|
||||
})
|
||||
.filter(Boolean); // Remove empty strings
|
||||
|
||||
sqlScript += foreignKeys.join('\n');
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
sqlScript += '\n\nCOMMIT;\n';
|
||||
sqlScript += '\nCOMMIT;\n';
|
||||
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
escapeSQLComment,
|
||||
formatTableComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
@@ -142,16 +140,10 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
|
||||
}
|
||||
});
|
||||
|
||||
return typesSql ? typesSql + '\n' : '';
|
||||
return typesSql + '\n';
|
||||
}
|
||||
|
||||
export function exportPostgreSQL({
|
||||
diagram,
|
||||
onlyRelationships = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
onlyRelationships?: boolean;
|
||||
}): string {
|
||||
export function exportPostgreSQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
@@ -162,391 +154,290 @@ export function exportPostgreSQL({
|
||||
|
||||
// Create CREATE SCHEMA statements for all schemas
|
||||
let sqlScript = '';
|
||||
if (!onlyRelationships) {
|
||||
const schemas = new Set<string>();
|
||||
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`;
|
||||
});
|
||||
if (schemas.size > 0) {
|
||||
sqlScript += '\n';
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sequences.forEach((sequence) => {
|
||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||
});
|
||||
if (sequences.size > 0) {
|
||||
sqlScript += '\n';
|
||||
// Also collect schemas from custom types
|
||||
customTypes.forEach((customType) => {
|
||||
if (customType.schema) {
|
||||
schemas.add(customType.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
// Add schema creation statements
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
||||
});
|
||||
sqlScript += '\n';
|
||||
|
||||
const tableName = table.schema
|
||||
? `"${table.schema}"."${table.name}"`
|
||||
: `"${table.name}"`;
|
||||
// Add custom types (enums and composite types)
|
||||
sqlScript += exportCustomTypes(customTypes);
|
||||
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter(
|
||||
(f) => f.primaryKey
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return `${
|
||||
table.comments ? formatTableComment(table.comments) : ''
|
||||
}CREATE TABLE ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `"${field.name}"`;
|
||||
sequences.forEach((sequence) => {
|
||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||
});
|
||||
sqlScript += '\n';
|
||||
|
||||
// Handle type name - map problematic types to PostgreSQL compatible types
|
||||
const typeName = mapPostgresType(
|
||||
field.type.name,
|
||||
field.name
|
||||
);
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Handle PostgreSQL specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
let serialType = null;
|
||||
const tableName = table.schema
|
||||
? `"${table.schema}"."${table.name}"`
|
||||
: `"${table.name}"`;
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() ===
|
||||
'character varying' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'character'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
}
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
return `${
|
||||
table.comments ? `-- ${table.comments}\n` : ''
|
||||
}CREATE TABLE ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `"${field.name}"`;
|
||||
|
||||
// Handle array types (check if the type name ends with '[]')
|
||||
if (typeName.endsWith('[]')) {
|
||||
typeWithSize =
|
||||
typeWithSize.replace('[]', '') + '[]';
|
||||
}
|
||||
// Handle type name - map problematic types to PostgreSQL compatible types
|
||||
const typeName = mapPostgresType(
|
||||
field.type.name,
|
||||
field.name
|
||||
);
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
// Handle PostgreSQL specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
let serialType = null;
|
||||
|
||||
// Handle identity generation
|
||||
let identity = '';
|
||||
if (field.increment && !field.nullable) {
|
||||
if (
|
||||
field.default &&
|
||||
field.default.includes('nextval')
|
||||
typeName.toLowerCase() === 'integer' ||
|
||||
typeName.toLowerCase() === 'int'
|
||||
) {
|
||||
// PostgreSQL already handles this with DEFAULT nextval()
|
||||
} else if (
|
||||
field.default &&
|
||||
field.default.toLowerCase().includes('identity')
|
||||
) {
|
||||
identity = ' GENERATED BY DEFAULT AS IDENTITY';
|
||||
serialType = 'SERIAL';
|
||||
} else if (typeName.toLowerCase() === 'bigint') {
|
||||
serialType = 'BIGSERIAL';
|
||||
} else if (typeName.toLowerCase() === 'smallint') {
|
||||
serialType = 'SMALLSERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
// 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' : '';
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'character varying' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'character'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default value using PostgreSQL specific parser
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity')
|
||||
? ` DEFAULT ${parsePostgresDefault(field)}`
|
||||
: '';
|
||||
// Handle array types (check if the type name ends with '[]')
|
||||
if (typeName.endsWith('[]')) {
|
||||
typeWithSize = typeWithSize.replace('[]', '') + '[]';
|
||||
}
|
||||
|
||||
// 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);
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// 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 '';
|
||||
}
|
||||
// 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';
|
||||
}
|
||||
|
||||
// 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,
|
||||
'_'
|
||||
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||
// This avoids redundant uniqueness constraints
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value using PostgreSQL specific parser
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity')
|
||||
? ` DEFAULT ${parsePostgresDefault(field)}`
|
||||
: '';
|
||||
|
||||
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${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
|
||||
);
|
||||
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(', ')});`
|
||||
: '';
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return validIndexes.length > 0
|
||||
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||
// 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`
|
||||
: '';
|
||||
})()
|
||||
}\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript += `\n${relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
// 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
|
||||
);
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
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;
|
||||
// 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, '_');
|
||||
|
||||
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 constraintName = `"${safeConstraintName}"`;
|
||||
|
||||
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 `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n')}`;
|
||||
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
formatTableComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
@@ -140,13 +139,7 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
export function exportSQLite({
|
||||
diagram,
|
||||
onlyRelationships = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
onlyRelationships?: boolean;
|
||||
}): string {
|
||||
export function exportSQLite(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
@@ -155,10 +148,10 @@ export function exportSQLite({
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
||||
let sqlScript = '-- SQLite database export\n';
|
||||
let sqlScript = '-- SQLite database export\n\n';
|
||||
|
||||
// Begin transaction for faster import
|
||||
sqlScript += 'BEGIN TRANSACTION;\n';
|
||||
sqlScript += 'BEGIN TRANSACTION;\n\n';
|
||||
|
||||
// SQLite doesn't have sequences, so we skip sequence creation
|
||||
|
||||
@@ -172,167 +165,151 @@ export function exportSQLite({
|
||||
'sqlite_master',
|
||||
];
|
||||
|
||||
if (!onlyRelationships) {
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
// 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 ? formatTableComment(table.comments) : ''
|
||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `"${field.name}"`;
|
||||
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
|
||||
);
|
||||
// Handle type name - map to SQLite compatible types
|
||||
const typeName = mapSQLiteType(
|
||||
field.type.name,
|
||||
field.primaryKey
|
||||
);
|
||||
|
||||
// SQLite ignores length specifiers, so we don't add them
|
||||
// We'll keep this simple without size info
|
||||
const typeWithoutSize = typeName;
|
||||
// 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';
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle autoincrement - only works with INTEGER PRIMARY KEY
|
||||
let autoIncrement = '';
|
||||
if (
|
||||
field.primaryKey &&
|
||||
singleIntegerPrimaryKey &&
|
||||
(field.default
|
||||
// Handle autoincrement - only works with INTEGER PRIMARY KEY
|
||||
let autoIncrement = '';
|
||||
if (
|
||||
field.primaryKey &&
|
||||
singleIntegerPrimaryKey &&
|
||||
(field.default?.toLowerCase().includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('autoincrement') ||
|
||||
field.default?.includes('nextval'))
|
||||
) {
|
||||
autoIncrement = ' AUTOINCREMENT';
|
||||
.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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
// Add PRIMARY KEY inline only for single INTEGER primary key
|
||||
const primaryKey =
|
||||
field.primaryKey && singleIntegerPrimaryKey
|
||||
? ' PRIMARY KEY' + autoIncrement
|
||||
: '';
|
||||
|
||||
// Handle default value - Special handling for datetime() function
|
||||
let defaultValue = '';
|
||||
if (
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity') &&
|
||||
!field.default
|
||||
.toLowerCase()
|
||||
.includes('autoincrement') &&
|
||||
!field.default.includes('nextval')
|
||||
) {
|
||||
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
|
||||
if (field.default.includes("datetime(''now'')")) {
|
||||
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
|
||||
} else {
|
||||
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add PRIMARY KEY inline only for single INTEGER primary key
|
||||
const primaryKey =
|
||||
field.primaryKey && singleIntegerPrimaryKey
|
||||
? ' PRIMARY KEY' + autoIncrement
|
||||
: '';
|
||||
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
|
||||
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
|
||||
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||
.map((f) => `"${f.name}"`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n${
|
||||
// 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(', ')});`
|
||||
: '';
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
|
||||
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
|
||||
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||
.map((f) => `"${f.name}"`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n\n${
|
||||
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
||||
table.indexes
|
||||
.map((index) => {
|
||||
// Skip indexes that exactly match the primary key
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return validIndexes.length > 0
|
||||
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||
// 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`
|
||||
: '';
|
||||
})()
|
||||
}\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.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
|
||||
@@ -341,7 +318,7 @@ export function exportSQLite({
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript +=
|
||||
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
||||
sqlScript += '-- PRAGMA foreign_keys = ON;\n';
|
||||
sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
|
||||
|
||||
relationships.forEach((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
@@ -369,44 +346,8 @@ export function exportSQLite({
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Create commented out version of what would be ALTER TABLE statement
|
||||
sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`;
|
||||
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,23 @@ import { exportMySQL } from './export-per-type/mysql';
|
||||
|
||||
// Function to simplify verbose data type names
|
||||
const simplifyDataType = (typeName: string): string => {
|
||||
const typeMap: Record<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',
|
||||
};
|
||||
|
||||
return typeMap[typeName.toLowerCase()] || typeName;
|
||||
};
|
||||
@@ -20,12 +36,10 @@ export const exportBaseSQL = ({
|
||||
diagram,
|
||||
targetDatabaseType,
|
||||
isDBMLFlow = false,
|
||||
onlyRelationships = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
targetDatabaseType: DatabaseType;
|
||||
isDBMLFlow?: boolean;
|
||||
onlyRelationships?: boolean;
|
||||
}): string => {
|
||||
const { tables, relationships } = diagram;
|
||||
|
||||
@@ -36,16 +50,16 @@ export const exportBaseSQL = ({
|
||||
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
|
||||
switch (diagram.databaseType) {
|
||||
case DatabaseType.SQL_SERVER:
|
||||
return exportMSSQL({ diagram, onlyRelationships });
|
||||
return exportMSSQL(diagram);
|
||||
case DatabaseType.POSTGRESQL:
|
||||
return exportPostgreSQL({ diagram, onlyRelationships });
|
||||
return exportPostgreSQL(diagram);
|
||||
case DatabaseType.SQLITE:
|
||||
return exportSQLite({ diagram, onlyRelationships });
|
||||
return exportSQLite(diagram);
|
||||
case DatabaseType.MYSQL:
|
||||
case DatabaseType.MARIADB:
|
||||
return exportMySQL({ diagram, onlyRelationships });
|
||||
return exportMySQL(diagram);
|
||||
default:
|
||||
return exportPostgreSQL({ diagram, onlyRelationships });
|
||||
return exportPostgreSQL(diagram);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,23 +131,7 @@ export const exportBaseSQL = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
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';
|
||||
}
|
||||
sqlScript += '\n'; // Add a newline if custom types were processed
|
||||
}
|
||||
|
||||
// Add CREATE SEQUENCE statements
|
||||
@@ -156,9 +154,7 @@ export const exportBaseSQL = ({
|
||||
sequences.forEach((sequence) => {
|
||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||
});
|
||||
if (sequences.size > 0) {
|
||||
sqlScript += '\n';
|
||||
}
|
||||
sqlScript += '\n';
|
||||
|
||||
// Loop through each non-view table to generate the SQL statements
|
||||
nonViewTables.forEach((table) => {
|
||||
@@ -167,12 +163,6 @@ 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);
|
||||
|
||||
@@ -224,33 +214,17 @@ 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 &&
|
||||
field.type.name.toLowerCase() !== 'decimal'
|
||||
parseInt(field.characterMaximumLength) > 0
|
||||
) {
|
||||
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
|
||||
@@ -275,63 +249,49 @@ export const exportBaseSQL = ({
|
||||
// Temp remove default user-define value when it have it
|
||||
let fieldDefault = field.default;
|
||||
|
||||
// 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];
|
||||
// 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()`;
|
||||
}
|
||||
|
||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||
}
|
||||
|
||||
if (fieldDefault === `('now')`) {
|
||||
fieldDefault = `now()`;
|
||||
}
|
||||
|
||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||
}
|
||||
|
||||
// Handle PRIMARY KEY constraint - only add inline if not composite
|
||||
if (field.primaryKey && !hasCompositePrimaryKey) {
|
||||
// Handle PRIMARY KEY constraint
|
||||
if (field.primaryKey) {
|
||||
sqlScript += ' PRIMARY KEY';
|
||||
}
|
||||
|
||||
// Add a comma after each field except the last one (or before composite primary key)
|
||||
if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
|
||||
// Add a comma after each field except the last one
|
||||
if (index < table.fields.length - 1) {
|
||||
sqlScript += ',\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';
|
||||
sqlScript += '\n);\n\n';
|
||||
|
||||
// Add table comment
|
||||
if (table.comments) {
|
||||
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
|
||||
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments}';\n`;
|
||||
}
|
||||
|
||||
table.fields.forEach((field) => {
|
||||
// Add column comment
|
||||
if (field.comments) {
|
||||
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
|
||||
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments}';\n`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -346,18 +306,15 @@ export const exportBaseSQL = ({
|
||||
.join(', ');
|
||||
|
||||
if (fieldNames) {
|
||||
const indexName =
|
||||
table.schema && !isDBMLFlow
|
||||
? `${table.schema}_${index.name}`
|
||||
: index.name;
|
||||
const indexName = table.schema
|
||||
? `${table.schema}_${index.name}`
|
||||
: index.name;
|
||||
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
|
||||
sqlScript += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle relationships (foreign keys)
|
||||
relationships?.forEach((relationship) => {
|
||||
@@ -381,52 +338,13 @@ export const exportBaseSQL = ({
|
||||
sourceTableField &&
|
||||
targetTableField
|
||||
) {
|
||||
// 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`;
|
||||
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`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||
import type { TableInfo } from './table-info';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -32,12 +33,20 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
|
||||
};
|
||||
|
||||
export const createAggregatedIndexes = ({
|
||||
tableIndexes,
|
||||
tableInfo,
|
||||
tableSchema,
|
||||
indexes,
|
||||
}: {
|
||||
tableInfo: TableInfo;
|
||||
tableIndexes: IndexInfo[];
|
||||
indexes: 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) => {
|
||||
|
||||
@@ -2,8 +2,7 @@ const withExtras = false;
|
||||
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
|
||||
const withoutDefault = `""`;
|
||||
|
||||
export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
|
||||
SELECT CAST(CONCAT(
|
||||
export const mariaDBQuery = `SELECT CAST(CONCAT(
|
||||
'{"fk_info": [',
|
||||
IFNULL((SELECT GROUP_CONCAT(
|
||||
CONCAT('{"schema":"', cast(fk.table_schema as CHAR),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateSQL } from '../sql-validator';
|
||||
import { DatabaseType } from '@/lib/domain';
|
||||
import { validatePostgreSQLSyntax } from '../sql-validator';
|
||||
|
||||
describe('SQL Validator Auto-fix', () => {
|
||||
it('should provide auto-fix for cast operator errors', () => {
|
||||
@@ -15,7 +14,7 @@ SELECT id: :text FROM dragons;
|
||||
SELECT ST_X(lair_location: :geometry) AS longitude FROM dragons;
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
// Should detect errors
|
||||
expect(result.isValid).toBe(false);
|
||||
@@ -39,7 +38,7 @@ SELECT AVG(power_level): :DECIMAL(3,
|
||||
2) FROM enchantments;
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.fixedSQL).toBeDefined();
|
||||
@@ -57,7 +56,7 @@ CREATE TABLE potions (
|
||||
3) DEFAULT 0.000
|
||||
);`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
@@ -93,7 +92,7 @@ SELECT AVG(power_level): :DECIMAL(3,
|
||||
2) FROM enchantments;
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.fixedSQL).toBeDefined();
|
||||
@@ -123,7 +122,7 @@ CREATE TABLE wizards (
|
||||
name VARCHAR(100)
|
||||
);`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateSQL } from '../sql-validator';
|
||||
import { DatabaseType } from '@/lib/domain';
|
||||
import { validatePostgreSQLSyntax } from '../sql-validator';
|
||||
|
||||
describe('SQL Validator', () => {
|
||||
it('should detect cast operator errors (: :)', () => {
|
||||
@@ -15,7 +14,7 @@ SELECT id: :text FROM wizards;
|
||||
SELECT COUNT(*): :integer FROM wizards;
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
@@ -34,7 +33,7 @@ CREATE TABLE potions (
|
||||
2) NOT NULL
|
||||
);`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(
|
||||
@@ -51,7 +50,7 @@ CREATE EXTENSION postgis;
|
||||
CREATE TABLE dragons (id UUID PRIMARY KEY);
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(
|
||||
result.warnings.some((w) => w.message.includes('CREATE EXTENSION'))
|
||||
@@ -73,7 +72,7 @@ BEFORE UPDATE ON wizards
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(
|
||||
result.warnings.some((w) =>
|
||||
@@ -103,7 +102,7 @@ CREATE TABLE spells (
|
||||
);
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
@@ -128,7 +127,7 @@ SELECT
|
||||
FROM towers t;
|
||||
`;
|
||||
|
||||
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
|
||||
const result = validatePostgreSQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
// Should find multiple cast operator errors
|
||||
|
||||
@@ -18,14 +18,11 @@ export interface SQLColumn {
|
||||
nullable: boolean;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
typeArgs?:
|
||||
| {
|
||||
length?: number;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
}
|
||||
| number[]
|
||||
| string;
|
||||
typeArgs?: {
|
||||
length?: number;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
};
|
||||
comment?: string;
|
||||
default?: string;
|
||||
increment?: boolean;
|
||||
@@ -562,38 +559,6 @@ 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(
|
||||
@@ -616,68 +581,22 @@ export function convertToChartDBDiagram(
|
||||
|
||||
// Add type arguments if present
|
||||
if (column.typeArgs) {
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
// Handle array typeArgs (SQL Server format)
|
||||
else if (
|
||||
Array.isArray(column.typeArgs) &&
|
||||
column.typeArgs.length > 0
|
||||
// Transfer length for varchar/char types
|
||||
if (
|
||||
column.typeArgs.length !== undefined &&
|
||||
(field.type.id === 'varchar' || field.type.id === 'char')
|
||||
) {
|
||||
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];
|
||||
}
|
||||
field.characterMaximumLength =
|
||||
column.typeArgs.length.toString();
|
||||
}
|
||||
// Handle object typeArgs (standard format)
|
||||
else if (
|
||||
typeof column.typeArgs === 'object' &&
|
||||
!Array.isArray(column.typeArgs)
|
||||
|
||||
// Transfer precision/scale for numeric types
|
||||
if (
|
||||
column.typeArgs.precision !== undefined &&
|
||||
(field.type.id === 'numeric' || field.type.id === 'decimal')
|
||||
) {
|
||||
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;
|
||||
}
|
||||
field.precision = column.typeArgs.precision;
|
||||
field.scale = column.typeArgs.scale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,487 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
import { fromMySQL } from '../mysql';
|
||||
|
||||
describe('MySQL Core Functionality', () => {
|
||||
describe('Basic Table Parsing', () => {
|
||||
it('should parse a simple table', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
expect(result.tables[0].name).toBe('users');
|
||||
expect(result.tables[0].columns).toHaveLength(4);
|
||||
|
||||
const idColumn = result.tables[0].columns.find(
|
||||
(c) => c.name === 'id'
|
||||
);
|
||||
expect(idColumn?.primaryKey).toBe(true);
|
||||
expect(idColumn?.increment).toBe(true);
|
||||
|
||||
const emailColumn = result.tables[0].columns.find(
|
||||
(c) => c.name === 'email'
|
||||
);
|
||||
expect(emailColumn?.unique).toBe(true);
|
||||
expect(emailColumn?.nullable).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse tables with backticks', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE \`user-profiles\` (
|
||||
\`user-id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`full-name\` VARCHAR(255) NOT NULL,
|
||||
\`bio-text\` TEXT
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
expect(result.tables[0].name).toBe('user-profiles');
|
||||
expect(
|
||||
result.tables[0].columns.some((c) => c.name === 'user-id')
|
||||
).toBe(true);
|
||||
expect(
|
||||
result.tables[0].columns.some((c) => c.name === 'full-name')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle IF NOT EXISTS clause', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
expect(result.tables[0].name).toBe('products');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Types', () => {
|
||||
it('should parse various MySQL data types', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE data_types_test (
|
||||
col_tinyint TINYINT,
|
||||
col_smallint SMALLINT,
|
||||
col_mediumint MEDIUMINT,
|
||||
col_int INT(11),
|
||||
col_bigint BIGINT,
|
||||
col_decimal DECIMAL(10,2),
|
||||
col_float FLOAT,
|
||||
col_double DOUBLE,
|
||||
col_bit BIT(8),
|
||||
col_char CHAR(10),
|
||||
col_varchar VARCHAR(255),
|
||||
col_binary BINARY(16),
|
||||
col_varbinary VARBINARY(255),
|
||||
col_tinytext TINYTEXT,
|
||||
col_text TEXT,
|
||||
col_mediumtext MEDIUMTEXT,
|
||||
col_longtext LONGTEXT,
|
||||
col_tinyblob TINYBLOB,
|
||||
col_blob BLOB,
|
||||
col_mediumblob MEDIUMBLOB,
|
||||
col_longblob LONGBLOB,
|
||||
col_date DATE,
|
||||
col_datetime DATETIME,
|
||||
col_timestamp TIMESTAMP,
|
||||
col_time TIME,
|
||||
col_year YEAR,
|
||||
col_enum ENUM('small', 'medium', 'large'),
|
||||
col_set SET('read', 'write', 'execute'),
|
||||
col_json JSON,
|
||||
col_geometry GEOMETRY,
|
||||
col_point POINT
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
const table = result.tables[0];
|
||||
|
||||
expect(table.columns.find((c) => c.name === 'col_int')?.type).toBe(
|
||||
'INT'
|
||||
);
|
||||
expect(
|
||||
table.columns.find((c) => c.name === 'col_varchar')?.type
|
||||
).toBe('VARCHAR');
|
||||
expect(
|
||||
table.columns.find((c) => c.name === 'col_decimal')?.type
|
||||
).toBe('DECIMAL');
|
||||
expect(table.columns.find((c) => c.name === 'col_enum')?.type).toBe(
|
||||
'ENUM'
|
||||
);
|
||||
expect(table.columns.find((c) => c.name === 'col_json')?.type).toBe(
|
||||
'JSON'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constraints', () => {
|
||||
it('should parse PRIMARY KEY constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE pk_test (
|
||||
id INT,
|
||||
code VARCHAR(10),
|
||||
PRIMARY KEY (id, code)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
const table = result.tables[0];
|
||||
expect(table.columns.find((c) => c.name === 'id')?.primaryKey).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
table.columns.find((c) => c.name === 'code')?.primaryKey
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) =>
|
||||
idx.name === 'pk_pk_test' &&
|
||||
idx.columns.includes('id') &&
|
||||
idx.columns.includes('code')
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse UNIQUE constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE unique_test (
|
||||
id INT PRIMARY KEY,
|
||||
email VARCHAR(255),
|
||||
username VARCHAR(100),
|
||||
UNIQUE KEY uk_email (email),
|
||||
UNIQUE KEY uk_username_email (username, email)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
const table = result.tables[0];
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) => idx.name === 'uk_email' && idx.unique === true
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) =>
|
||||
idx.name === 'uk_username_email' &&
|
||||
idx.unique === true &&
|
||||
idx.columns.length === 2
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse CHECK constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE check_test (
|
||||
id INT PRIMARY KEY,
|
||||
age INT CHECK (age >= 18),
|
||||
price DECIMAL(10,2) CHECK (price > 0)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
expect(result.tables[0].columns).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Foreign Keys', () => {
|
||||
it('should parse inline FOREIGN KEY constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE departments (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE employees (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
department_id INT,
|
||||
FOREIGN KEY (department_id) REFERENCES departments(id)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.relationships).toHaveLength(1);
|
||||
|
||||
const fk = result.relationships[0];
|
||||
expect(fk.sourceTable).toBe('employees');
|
||||
expect(fk.sourceColumn).toBe('department_id');
|
||||
expect(fk.targetTable).toBe('departments');
|
||||
expect(fk.targetColumn).toBe('id');
|
||||
});
|
||||
|
||||
it('should parse named FOREIGN KEY constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE orders (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
customer_id INT NOT NULL,
|
||||
CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE customers (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.relationships).toHaveLength(1);
|
||||
|
||||
const fk = result.relationships[0];
|
||||
expect(fk.name).toBe('fk_order_customer');
|
||||
expect(fk.deleteAction).toBe('CASCADE');
|
||||
expect(fk.updateAction).toBe('CASCADE');
|
||||
});
|
||||
|
||||
it('should handle ALTER TABLE ADD FOREIGN KEY', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE reviews (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_id INT NOT NULL,
|
||||
rating INT NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE reviews
|
||||
ADD CONSTRAINT fk_review_product
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.relationships).toHaveLength(1);
|
||||
|
||||
const fk = result.relationships[0];
|
||||
expect(fk.sourceTable).toBe('reviews');
|
||||
expect(fk.targetTable).toBe('products');
|
||||
expect(fk.deleteAction).toBe('CASCADE');
|
||||
});
|
||||
|
||||
it('should handle composite foreign keys', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE tenants (
|
||||
id INT NOT NULL,
|
||||
region VARCHAR(10) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (id, region)
|
||||
);
|
||||
|
||||
CREATE TABLE tenant_settings (
|
||||
tenant_id INT NOT NULL,
|
||||
tenant_region VARCHAR(10) NOT NULL,
|
||||
setting_key VARCHAR(100) NOT NULL,
|
||||
setting_value TEXT,
|
||||
PRIMARY KEY (tenant_id, tenant_region, setting_key),
|
||||
FOREIGN KEY (tenant_id, tenant_region) REFERENCES tenants(id, region)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.relationships).toHaveLength(2);
|
||||
|
||||
const fk1 = result.relationships.find(
|
||||
(r) => r.sourceColumn === 'tenant_id'
|
||||
);
|
||||
const fk2 = result.relationships.find(
|
||||
(r) => r.sourceColumn === 'tenant_region'
|
||||
);
|
||||
|
||||
expect(fk1?.targetColumn).toBe('id');
|
||||
expect(fk2?.targetColumn).toBe('region');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Indexes', () => {
|
||||
it('should parse CREATE INDEX statements', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category VARCHAR(100),
|
||||
price DECIMAL(10,2)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category ON products(category);
|
||||
CREATE UNIQUE INDEX idx_name ON products(name);
|
||||
CREATE INDEX idx_category_price ON products(category, price);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
const table = result.tables[0];
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) => idx.name === 'idx_category' && !idx.unique
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) => idx.name === 'idx_name' && idx.unique
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
table.indexes.some(
|
||||
(idx) =>
|
||||
idx.name === 'idx_category_price' &&
|
||||
idx.columns.length === 2
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse inline INDEX definitions', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_email (email),
|
||||
UNIQUE INDEX uk_username (username),
|
||||
INDEX idx_created (created_at DESC)
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
const table = result.tables[0];
|
||||
expect(table.indexes.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table Options', () => {
|
||||
it('should handle ENGINE and CHARSET options', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE logs (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
message TEXT
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.tables[0].name).toBe('products');
|
||||
expect(result.tables[1].name).toBe('logs');
|
||||
});
|
||||
|
||||
it('should handle AUTO_INCREMENT initial value', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE orders (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_number VARCHAR(50) NOT NULL
|
||||
) AUTO_INCREMENT=1000;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
const idColumn = result.tables[0].columns.find(
|
||||
(c) => c.name === 'id'
|
||||
);
|
||||
expect(idColumn?.increment).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should reject inline REFERENCES (PostgreSQL style)', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY,
|
||||
department_id INT REFERENCES departments(id)
|
||||
);
|
||||
`;
|
||||
|
||||
// Using the original parser which checks for inline REFERENCES
|
||||
await expect(fromMySQL(sql)).rejects.toThrow(
|
||||
/MySQL\/MariaDB does not support inline REFERENCES/
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle malformed SQL gracefully', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE test (
|
||||
id INT PRIMARY KEY
|
||||
name VARCHAR(255) -- missing comma
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
// Should still create a table with fallback parsing
|
||||
expect(result.tables.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comments and Special Cases', () => {
|
||||
it('should handle SQL comments', async () => {
|
||||
const sql = `
|
||||
-- This is a comment
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY, -- user identifier
|
||||
/* Multi-line comment
|
||||
spanning multiple lines */
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
# MySQL-style comment
|
||||
CREATE TABLE posts (
|
||||
id INT PRIMARY KEY
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.tables.map((t) => t.name).sort()).toEqual([
|
||||
'posts',
|
||||
'users',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty or whitespace-only input', async () => {
|
||||
const result1 = await fromMySQLImproved('');
|
||||
expect(result1.tables).toHaveLength(0);
|
||||
expect(result1.relationships).toHaveLength(0);
|
||||
|
||||
const result2 = await fromMySQLImproved(' \n\n ');
|
||||
expect(result2.tables).toHaveLength(0);
|
||||
expect(result2.relationships).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,498 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
|
||||
describe('MySQL Real-World Examples', () => {
|
||||
describe('Magical Academy Example', () => {
|
||||
it('should parse the magical academy example with all 16 tables', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE schools(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE towers(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE ranks(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE spell_permissions(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
spell_type VARCHAR(100) NOT NULL,
|
||||
casting_level VARCHAR(50) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE rank_spell_permissions(
|
||||
rank_id INT NOT NULL,
|
||||
spell_permission_id INT NOT NULL,
|
||||
PRIMARY KEY (rank_id, spell_permission_id),
|
||||
FOREIGN KEY (rank_id) REFERENCES ranks(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (spell_permission_id) REFERENCES spell_permissions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE grimoire_types(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE wizards(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
wizard_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
UNIQUE KEY school_wizard_unique (school_id, wizard_name),
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE wizard_ranks(
|
||||
wizard_id INT NOT NULL,
|
||||
rank_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (wizard_id, rank_id, tower_id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (rank_id) REFERENCES ranks(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE apprentices(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
enrollment_date DATE NOT NULL,
|
||||
primary_mentor INT,
|
||||
sponsoring_wizard INT,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (primary_mentor) REFERENCES wizards(id),
|
||||
FOREIGN KEY (sponsoring_wizard) REFERENCES wizards(id)
|
||||
);
|
||||
|
||||
CREATE TABLE spell_lessons(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
apprentice_id INT NOT NULL,
|
||||
instructor_id INT NOT NULL,
|
||||
lesson_date DATETIME NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (apprentice_id) REFERENCES apprentices(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (instructor_id) REFERENCES wizards(id)
|
||||
);
|
||||
|
||||
CREATE TABLE grimoires(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
apprentice_id INT NOT NULL,
|
||||
grimoire_type_id INT NOT NULL,
|
||||
author_wizard_id INT NOT NULL,
|
||||
content JSON NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (apprentice_id) REFERENCES apprentices(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (grimoire_type_id) REFERENCES grimoire_types(id),
|
||||
FOREIGN KEY (author_wizard_id) REFERENCES wizards(id)
|
||||
);
|
||||
|
||||
CREATE TABLE tuition_scrolls(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT NOT NULL,
|
||||
tower_id INT NOT NULL,
|
||||
apprentice_id INT NOT NULL,
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (apprentice_id) REFERENCES apprentices(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE tuition_items(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
tuition_scroll_id INT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
FOREIGN KEY (tuition_scroll_id) REFERENCES tuition_scrolls(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE patron_sponsorships(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
tuition_scroll_id INT NOT NULL,
|
||||
patron_house VARCHAR(255) NOT NULL,
|
||||
sponsorship_code VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
FOREIGN KEY (tuition_scroll_id) REFERENCES tuition_scrolls(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE gold_payments(
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
tuition_scroll_id INT NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
payment_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tuition_scroll_id) REFERENCES tuition_scrolls(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE arcane_logs(
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
school_id INT,
|
||||
wizard_id INT,
|
||||
tower_id INT,
|
||||
table_name VARCHAR(100) NOT NULL,
|
||||
operation VARCHAR(50) NOT NULL,
|
||||
record_id INT,
|
||||
changes JSON,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (tower_id) REFERENCES towers(id) ON DELETE SET NULL
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
// Should find all 16 tables
|
||||
const expectedTables = [
|
||||
'apprentices',
|
||||
'arcane_logs',
|
||||
'gold_payments',
|
||||
'grimoire_types',
|
||||
'grimoires',
|
||||
'patron_sponsorships',
|
||||
'rank_spell_permissions',
|
||||
'ranks',
|
||||
'schools',
|
||||
'spell_lessons',
|
||||
'spell_permissions',
|
||||
'towers',
|
||||
'tuition_items',
|
||||
'tuition_scrolls',
|
||||
'wizard_ranks',
|
||||
'wizards',
|
||||
];
|
||||
|
||||
expect(result.tables).toHaveLength(16);
|
||||
expect(result.tables.map((t) => t.name).sort()).toEqual(
|
||||
expectedTables
|
||||
);
|
||||
|
||||
// Verify key relationships exist
|
||||
const relationships = result.relationships;
|
||||
|
||||
// Check some critical relationships
|
||||
expect(
|
||||
relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'wizards' &&
|
||||
r.targetTable === 'schools' &&
|
||||
r.sourceColumn === 'school_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'wizard_ranks' &&
|
||||
r.targetTable === 'wizards' &&
|
||||
r.sourceColumn === 'wizard_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'apprentices' &&
|
||||
r.targetTable === 'wizards' &&
|
||||
r.sourceColumn === 'primary_mentor'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enchanted Bazaar Example', () => {
|
||||
it('should parse the enchanted bazaar example with triggers and procedures', async () => {
|
||||
const sql = `
|
||||
-- Enchanted Bazaar tables with complex features
|
||||
CREATE TABLE merchants(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE artifacts(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
merchant_id INT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
|
||||
enchantment_charges INT DEFAULT 0 CHECK (enchantment_charges >= 0),
|
||||
FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Stored procedure that should be skipped
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE consume_charges(IN artifact_id INT, IN charges_used INT)
|
||||
BEGIN
|
||||
UPDATE artifacts SET enchantment_charges = enchantment_charges - charges_used WHERE id = artifact_id;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CREATE TABLE trades(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(50) DEFAULT 'negotiating'
|
||||
);
|
||||
|
||||
CREATE TABLE trade_items(
|
||||
trade_id INT,
|
||||
artifact_id INT,
|
||||
quantity INT NOT NULL CHECK (quantity > 0),
|
||||
agreed_price DECIMAL(10, 2) NOT NULL,
|
||||
PRIMARY KEY (trade_id, artifact_id),
|
||||
FOREIGN KEY (trade_id) REFERENCES trades(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (artifact_id) REFERENCES artifacts(id)
|
||||
);
|
||||
|
||||
-- Create trigger
|
||||
CREATE TRIGGER charge_consumption_trigger
|
||||
AFTER INSERT ON trade_items
|
||||
FOR EACH ROW
|
||||
CALL consume_charges(NEW.artifact_id, NEW.quantity);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql, {
|
||||
includeWarnings: true,
|
||||
});
|
||||
|
||||
// Should parse all tables despite procedures and triggers
|
||||
expect(result.tables.length).toBeGreaterThanOrEqual(4);
|
||||
|
||||
// Check for specific tables
|
||||
const tableNames = result.tables.map((t) => t.name);
|
||||
expect(tableNames).toContain('merchants');
|
||||
expect(tableNames).toContain('artifacts');
|
||||
expect(tableNames).toContain('trades');
|
||||
expect(tableNames).toContain('trade_items');
|
||||
|
||||
// Check relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'artifacts' &&
|
||||
r.targetTable === 'merchants'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'trade_items' &&
|
||||
r.targetTable === 'trades'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Should have warnings about unsupported features
|
||||
if (result.warnings) {
|
||||
expect(
|
||||
result.warnings.some(
|
||||
(w) =>
|
||||
w.includes('procedure') ||
|
||||
w.includes('function') ||
|
||||
w.includes('Trigger') ||
|
||||
w.includes('trigger')
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dragon Registry Example', () => {
|
||||
it('should parse dragon registry with mixed constraint styles', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE dragon_species (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
species_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
breath_type ENUM('fire', 'ice', 'lightning', 'acid', 'poison') NOT NULL,
|
||||
max_wingspan DECIMAL(5,2),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE dragon_habitats (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
habitat_name VARCHAR(200) NOT NULL,
|
||||
location_type VARCHAR(50) NOT NULL,
|
||||
climate VARCHAR(50),
|
||||
INDEX idx_location (location_type)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE dragons (
|
||||
dragon_id INT NOT NULL AUTO_INCREMENT,
|
||||
dragon_name VARCHAR(255) NOT NULL,
|
||||
species_id INT NOT NULL,
|
||||
habitat_id INT,
|
||||
birth_year INT,
|
||||
treasure_value DECIMAL(15,2) DEFAULT 0.00,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
PRIMARY KEY (dragon_id),
|
||||
CONSTRAINT fk_dragon_species FOREIGN KEY (species_id) REFERENCES dragon_species(id),
|
||||
CONSTRAINT fk_dragon_habitat FOREIGN KEY (habitat_id) REFERENCES dragon_habitats(id) ON DELETE SET NULL,
|
||||
INDEX idx_species (species_id),
|
||||
INDEX idx_active_dragons (is_active, species_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE dragon_riders (
|
||||
rider_id INT NOT NULL AUTO_INCREMENT,
|
||||
rider_name VARCHAR(255) NOT NULL,
|
||||
guild_membership VARCHAR(100),
|
||||
years_experience INT DEFAULT 0,
|
||||
PRIMARY KEY (rider_id),
|
||||
UNIQUE KEY uk_rider_name (rider_name)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE dragon_bonds (
|
||||
bond_id INT NOT NULL AUTO_INCREMENT,
|
||||
dragon_id INT NOT NULL,
|
||||
rider_id INT NOT NULL,
|
||||
bond_date DATE NOT NULL,
|
||||
bond_strength ENUM('weak', 'moderate', 'strong', 'unbreakable') DEFAULT 'weak',
|
||||
PRIMARY KEY (bond_id),
|
||||
UNIQUE KEY unique_dragon_rider (dragon_id, rider_id),
|
||||
FOREIGN KEY (dragon_id) REFERENCES dragons(dragon_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (rider_id) REFERENCES dragon_riders(rider_id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(5);
|
||||
|
||||
const tableNames = result.tables.map((t) => t.name).sort();
|
||||
expect(tableNames).toEqual([
|
||||
'dragon_bonds',
|
||||
'dragon_habitats',
|
||||
'dragon_riders',
|
||||
'dragon_species',
|
||||
'dragons',
|
||||
]);
|
||||
|
||||
// Check that ENUMs were parsed correctly
|
||||
const dragonSpecies = result.tables.find(
|
||||
(t) => t.name === 'dragon_species'
|
||||
);
|
||||
const breathTypeColumn = dragonSpecies?.columns.find(
|
||||
(c) => c.name === 'breath_type'
|
||||
);
|
||||
expect(breathTypeColumn?.type).toBe('ENUM');
|
||||
|
||||
// Check indexes
|
||||
const dragonsTable = result.tables.find(
|
||||
(t) => t.name === 'dragons'
|
||||
);
|
||||
expect(dragonsTable?.indexes.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
dragonsTable?.indexes.some((idx) => idx.name === 'idx_species')
|
||||
).toBe(true);
|
||||
|
||||
// Check relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'dragons' &&
|
||||
r.targetTable === 'dragon_species' &&
|
||||
r.sourceColumn === 'species_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'dragon_bonds' &&
|
||||
r.targetTable === 'dragons' &&
|
||||
r.deleteAction === 'CASCADE'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mystic Marketplace Example with Backticks', () => {
|
||||
it('should handle tables with backticks and special characters', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE \`marketplace-vendors\` (
|
||||
\`vendor-id\` INT NOT NULL AUTO_INCREMENT,
|
||||
\`vendor name\` VARCHAR(255) NOT NULL,
|
||||
\`shop.location\` VARCHAR(500),
|
||||
\`rating%\` DECIMAL(3,2),
|
||||
PRIMARY KEY (\`vendor-id\`)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE \`item_categories\` (
|
||||
\`category-id\` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
\`category@name\` VARCHAR(100) NOT NULL UNIQUE,
|
||||
\`parent_category\` INT,
|
||||
FOREIGN KEY (\`parent_category\`) REFERENCES \`item_categories\`(\`category-id\`)
|
||||
);
|
||||
|
||||
CREATE TABLE \`magical.items\` (
|
||||
\`item#id\` INT NOT NULL AUTO_INCREMENT,
|
||||
\`item-name\` VARCHAR(255) NOT NULL,
|
||||
\`vendor-id\` INT NOT NULL,
|
||||
\`category-id\` INT NOT NULL,
|
||||
\`price$gold\` DECIMAL(10,2) NOT NULL,
|
||||
PRIMARY KEY (\`item#id\`),
|
||||
CONSTRAINT \`fk_item_vendor\` FOREIGN KEY (\`vendor-id\`) REFERENCES \`marketplace-vendors\`(\`vendor-id\`),
|
||||
CONSTRAINT \`fk_item_category\` FOREIGN KEY (\`category-id\`) REFERENCES \`item_categories\`(\`category-id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(3);
|
||||
|
||||
// Check that backtick-wrapped names are preserved
|
||||
const vendor = result.tables.find(
|
||||
(t) => t.name === 'marketplace-vendors'
|
||||
);
|
||||
expect(vendor).toBeDefined();
|
||||
expect(vendor?.columns.some((c) => c.name === 'vendor-id')).toBe(
|
||||
true
|
||||
);
|
||||
expect(vendor?.columns.some((c) => c.name === 'vendor name')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Check self-referencing foreign key
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'item_categories' &&
|
||||
r.targetTable === 'item_categories' &&
|
||||
r.sourceColumn === 'parent_category'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Check cross-table relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'magical.items' &&
|
||||
r.targetTable === 'marketplace-vendors'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQL } from '../mysql';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
import { validateMySQLSyntax } from '../mysql-validator';
|
||||
|
||||
describe('MySQL Fantasy World Integration', () => {
|
||||
const fantasyWorldSQL = `
|
||||
-- Fantasy World Database Schema
|
||||
-- A magical realm management system
|
||||
|
||||
-- Realm Management
|
||||
CREATE TABLE realms (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
magic_level ENUM('low', 'medium', 'high', 'legendary') DEFAULT 'medium',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_magic_level (magic_level)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Magical Creatures Registry
|
||||
CREATE TABLE creature_types (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
classification ENUM('beast', 'dragon', 'elemental', 'undead', 'fey', 'construct') NOT NULL,
|
||||
danger_level INT CHECK (danger_level BETWEEN 1 AND 10),
|
||||
is_sentient BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE creatures (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
creature_type_id INT NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
health_points INT DEFAULT 100,
|
||||
magic_points INT DEFAULT 50,
|
||||
special_abilities JSON, -- ["fire_breath", "invisibility", "teleportation"]
|
||||
last_sighted DATETIME,
|
||||
status ENUM('active', 'dormant', 'banished', 'deceased') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (creature_type_id) REFERENCES creature_types(id),
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id) ON DELETE CASCADE,
|
||||
INDEX idx_realm_status (realm_id, status),
|
||||
INDEX idx_type (creature_type_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Wizard Registry
|
||||
CREATE TABLE wizard_ranks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
rank_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
min_power_level INT NOT NULL,
|
||||
permissions JSON, -- ["cast_forbidden_spells", "access_restricted_library", "mentor_apprentices"]
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE wizards (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
wizard_rank_id INT NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
power_level INT DEFAULT 1,
|
||||
specialization ENUM('elemental', 'necromancy', 'illusion', 'healing', 'divination') NOT NULL,
|
||||
familiar_creature_id INT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (wizard_rank_id) REFERENCES wizard_ranks(id),
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id),
|
||||
FOREIGN KEY (familiar_creature_id) REFERENCES creatures(id) ON DELETE SET NULL,
|
||||
INDEX idx_rank (wizard_rank_id),
|
||||
INDEX idx_realm (realm_id),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Spell Library
|
||||
CREATE TABLE spell_schools (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
forbidden BOOLEAN DEFAULT FALSE,
|
||||
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,
|
||||
incantation TEXT,
|
||||
spell_school_id INT NOT NULL,
|
||||
mana_cost INT DEFAULT 10,
|
||||
cast_time_seconds INT DEFAULT 3,
|
||||
range_meters INT DEFAULT 10,
|
||||
components JSON, -- ["verbal", "somatic", "material:dragon_scale"]
|
||||
effects JSON, -- {"damage": 50, "duration": 300, "area": "cone"}
|
||||
min_wizard_rank_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (spell_school_id) REFERENCES spell_schools(id),
|
||||
FOREIGN KEY (min_wizard_rank_id) REFERENCES wizard_ranks(id),
|
||||
INDEX idx_school (spell_school_id),
|
||||
FULLTEXT idx_search (name, incantation)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Wizard Spellbooks (many-to-many)
|
||||
CREATE TABLE wizard_spellbooks (
|
||||
wizard_id INT NOT NULL,
|
||||
spell_id INT NOT NULL,
|
||||
learned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
mastery_level INT DEFAULT 1 CHECK (mastery_level BETWEEN 1 AND 5),
|
||||
times_cast INT DEFAULT 0,
|
||||
PRIMARY KEY (wizard_id, spell_id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (spell_id) REFERENCES spells(id) ON DELETE CASCADE,
|
||||
INDEX idx_spell (spell_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Magical Items
|
||||
CREATE TABLE item_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE magical_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
item_category_id INT NOT NULL,
|
||||
rarity ENUM('common', 'uncommon', 'rare', 'epic', 'legendary', 'artifact') NOT NULL,
|
||||
power_level INT DEFAULT 1,
|
||||
enchantments JSON, -- ["strength+5", "fire_resistance", "invisibility_on_use"]
|
||||
curse_effects JSON, -- ["bound_to_owner", "drains_life", "attracts_monsters"]
|
||||
created_by_wizard_id INT,
|
||||
found_in_realm_id INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (item_category_id) REFERENCES item_categories(id),
|
||||
FOREIGN KEY (created_by_wizard_id) REFERENCES wizards(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (found_in_realm_id) REFERENCES realms(id) ON DELETE SET NULL,
|
||||
INDEX idx_category (item_category_id),
|
||||
INDEX idx_rarity (rarity)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Wizard Inventory
|
||||
CREATE TABLE wizard_inventory (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
wizard_id INT NOT NULL,
|
||||
item_id INT NOT NULL,
|
||||
quantity INT DEFAULT 1,
|
||||
equipped BOOLEAN DEFAULT FALSE,
|
||||
acquired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (item_id) REFERENCES magical_items(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_wizard_item (wizard_id, item_id),
|
||||
INDEX idx_wizard (wizard_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Quests and Adventures
|
||||
CREATE TABLE quests (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
realm_id INT NOT NULL,
|
||||
difficulty ENUM('novice', 'adept', 'expert', 'master', 'legendary') NOT NULL,
|
||||
reward_gold INT DEFAULT 0,
|
||||
reward_experience INT DEFAULT 0,
|
||||
reward_items JSON, -- [{"item_id": 1, "quantity": 1}]
|
||||
status ENUM('available', 'in_progress', 'completed', 'failed') DEFAULT 'available',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id),
|
||||
INDEX idx_realm_status (realm_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Quest Participants
|
||||
CREATE TABLE quest_participants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
quest_id INT NOT NULL,
|
||||
wizard_id INT NOT NULL,
|
||||
role ENUM('leader', 'member', 'guide', 'support') DEFAULT 'member',
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (quest_id) REFERENCES quests(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_quest_wizard (quest_id, wizard_id),
|
||||
INDEX idx_wizard (wizard_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Magical Events Log
|
||||
CREATE TABLE magical_events (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_type ENUM('spell_cast', 'item_created', 'creature_summoned', 'realm_shift', 'quest_completed') NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
wizard_id INT,
|
||||
creature_id INT,
|
||||
description TEXT,
|
||||
magic_fluctuation INT DEFAULT 0, -- Positive or negative impact on realm magic
|
||||
event_data JSON, -- Additional event-specific data
|
||||
occurred_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (creature_id) REFERENCES creatures(id) ON DELETE SET NULL,
|
||||
INDEX idx_realm_time (realm_id, occurred_at),
|
||||
INDEX idx_event_type (event_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Wizard Guilds
|
||||
CREATE TABLE guilds (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) UNIQUE NOT NULL,
|
||||
motto TEXT,
|
||||
realm_id INT NOT NULL,
|
||||
founded_by_wizard_id INT,
|
||||
member_count INT DEFAULT 0,
|
||||
guild_hall_location VARCHAR(500),
|
||||
treasury_gold INT DEFAULT 0,
|
||||
founded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id),
|
||||
FOREIGN KEY (founded_by_wizard_id) REFERENCES wizards(id) ON DELETE SET NULL,
|
||||
INDEX idx_realm (realm_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Guild Memberships
|
||||
CREATE TABLE guild_memberships (
|
||||
wizard_id INT NOT NULL,
|
||||
guild_id INT NOT NULL,
|
||||
rank ENUM('apprentice', 'member', 'officer', 'leader') DEFAULT 'member',
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
contribution_points INT DEFAULT 0,
|
||||
PRIMARY KEY (wizard_id, guild_id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||
INDEX idx_guild (guild_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Enchantment Recipes
|
||||
CREATE TABLE enchantment_recipes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
required_spell_ids JSON NOT NULL, -- [1, 5, 12]
|
||||
required_items JSON NOT NULL, -- [{"item_id": 3, "quantity": 2}]
|
||||
result_enchantment VARCHAR(200) NOT NULL,
|
||||
success_rate DECIMAL(5,2) DEFAULT 75.00,
|
||||
created_by_wizard_id INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by_wizard_id) REFERENCES wizards(id) ON DELETE SET NULL,
|
||||
INDEX idx_creator (created_by_wizard_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ALTER TABLE for additional constraints
|
||||
ALTER TABLE creatures ADD CONSTRAINT fk_creature_realm
|
||||
FOREIGN KEY (realm_id) REFERENCES realms(id) ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE magical_items ADD CONSTRAINT unique_artifact_name
|
||||
UNIQUE KEY (name, rarity);
|
||||
`;
|
||||
|
||||
describe('Full Fantasy World Schema', () => {
|
||||
it('should parse the complete fantasy world database', async () => {
|
||||
const result = await fromMySQL(fantasyWorldSQL);
|
||||
|
||||
// Verify all tables are parsed
|
||||
expect(result.tables).toHaveLength(17);
|
||||
|
||||
const expectedTables = [
|
||||
'creature_types',
|
||||
'creatures',
|
||||
'enchantment_recipes',
|
||||
'guild_memberships',
|
||||
'guilds',
|
||||
'item_categories',
|
||||
'magical_events',
|
||||
'magical_items',
|
||||
'quest_participants',
|
||||
'quests',
|
||||
'realms',
|
||||
'spell_schools',
|
||||
'spells',
|
||||
'wizard_inventory',
|
||||
'wizard_ranks',
|
||||
'wizard_spellbooks',
|
||||
'wizards',
|
||||
];
|
||||
|
||||
expect(result.tables.map((t) => t.name).sort()).toEqual(
|
||||
expectedTables
|
||||
);
|
||||
|
||||
// Verify key relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'wizards' &&
|
||||
r.targetTable === 'wizard_ranks' &&
|
||||
r.sourceColumn === 'wizard_rank_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'creatures' &&
|
||||
r.targetTable === 'realms' &&
|
||||
r.deleteAction === 'CASCADE'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Verify JSON columns
|
||||
const creatures = result.tables.find((t) => t.name === 'creatures');
|
||||
const abilitiesCol = creatures?.columns.find(
|
||||
(c) => c.name === 'special_abilities'
|
||||
);
|
||||
expect(abilitiesCol?.type).toBe('JSON');
|
||||
|
||||
// Verify ENUM columns
|
||||
const magicLevel = result.tables
|
||||
.find((t) => t.name === 'realms')
|
||||
?.columns.find((c) => c.name === 'magic_level');
|
||||
expect(magicLevel?.type).toBe('ENUM');
|
||||
|
||||
// Verify indexes
|
||||
const wizards = result.tables.find((t) => t.name === 'wizards');
|
||||
expect(
|
||||
wizards?.indexes.some((idx) => idx.name === 'idx_email')
|
||||
).toBe(true);
|
||||
|
||||
// Verify many-to-many relationship table
|
||||
const spellbooks = result.tables.find(
|
||||
(t) => t.name === 'wizard_spellbooks'
|
||||
);
|
||||
expect(
|
||||
spellbooks?.columns.filter((c) => c.primaryKey)
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle the schema with skipValidation', async () => {
|
||||
const result = await fromMySQLImproved(fantasyWorldSQL, {
|
||||
skipValidation: true,
|
||||
includeWarnings: true,
|
||||
});
|
||||
|
||||
expect(result.tables).toHaveLength(17);
|
||||
expect(result.relationships.length).toBeGreaterThan(20);
|
||||
|
||||
// Check for CASCADE actions
|
||||
const cascadeRelations = result.relationships.filter(
|
||||
(r) =>
|
||||
r.deleteAction === 'CASCADE' || r.updateAction === 'CASCADE'
|
||||
);
|
||||
expect(cascadeRelations.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('should validate the fantasy schema', () => {
|
||||
const validation = validateMySQLSyntax(fantasyWorldSQL);
|
||||
|
||||
// Should be valid (no multi-line comment issues)
|
||||
expect(validation.isValid).toBe(true);
|
||||
expect(validation.errors).toHaveLength(0);
|
||||
|
||||
// May have some warnings but should be minimal
|
||||
expect(validation.warnings.length).toBeLessThan(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fantasy Schema with Validation Issues', () => {
|
||||
it('should handle SQL that becomes invalid after comment removal', async () => {
|
||||
const problematicSQL = `
|
||||
CREATE TABLE spell_components (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
rarity VARCHAR(50), -- "Common",
|
||||
"Rare", "Legendary" -- This will cause issues
|
||||
properties JSON -- [
|
||||
"magical_essence",
|
||||
"dragon_scale"
|
||||
] -- This JSON example will also cause issues
|
||||
);`;
|
||||
|
||||
// After comment removal, this SQL becomes malformed
|
||||
// The parser should handle this gracefully
|
||||
try {
|
||||
await fromMySQL(problematicSQL);
|
||||
// If it parses, that's OK - the sanitizer may have cleaned it up
|
||||
} catch (error) {
|
||||
// If it fails, that's also OK - the SQL was problematic
|
||||
expect(error.message).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should detect inline REFERENCES in fantasy schema', async () => {
|
||||
const invalidSQL = `
|
||||
CREATE TABLE wizard_familiars (
|
||||
id INT PRIMARY KEY,
|
||||
wizard_id INT REFERENCES wizards(id), -- PostgreSQL style, not MySQL
|
||||
familiar_name VARCHAR(100)
|
||||
);`;
|
||||
|
||||
await expect(fromMySQL(invalidSQL)).rejects.toThrow(
|
||||
'inline REFERENCES'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQL } from '../mysql';
|
||||
|
||||
describe('MySQL Fantasy Schema Test', () => {
|
||||
it('should parse a complete fantasy realm management schema', async () => {
|
||||
const fantasySQL = `
|
||||
-- Enchanted Realms Database
|
||||
CREATE TABLE magical_realms (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
realm_name VARCHAR(100) UNIQUE NOT NULL,
|
||||
magic_density DECIMAL(5,2) DEFAULT 100.00,
|
||||
portal_coordinates VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_realm_name (realm_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE creature_classes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
class_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
base_health INT DEFAULT 100,
|
||||
base_mana INT DEFAULT 50,
|
||||
special_traits JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE magical_creatures (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
creature_name VARCHAR(200) NOT NULL,
|
||||
class_id INT NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
level INT DEFAULT 1,
|
||||
experience_points INT DEFAULT 0,
|
||||
is_legendary BOOLEAN DEFAULT FALSE,
|
||||
abilities TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (class_id) REFERENCES creature_classes(id),
|
||||
FOREIGN KEY (realm_id) REFERENCES magical_realms(id) ON DELETE CASCADE,
|
||||
INDEX idx_realm_creatures (realm_id),
|
||||
INDEX idx_legendary (is_legendary)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE wizard_towers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tower_name VARCHAR(100) NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
height_meters INT DEFAULT 50,
|
||||
defensive_wards JSON,
|
||||
library_size ENUM('small', 'medium', 'large', 'grand') DEFAULT 'medium',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (realm_id) REFERENCES magical_realms(id),
|
||||
INDEX idx_realm_towers (realm_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE arcane_wizards (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
wizard_name VARCHAR(200) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
tower_id INT,
|
||||
specialization VARCHAR(50) NOT NULL,
|
||||
mana_capacity INT DEFAULT 1000,
|
||||
spell_slots INT DEFAULT 10,
|
||||
familiar_creature_id INT,
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tower_id) REFERENCES wizard_towers(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (familiar_creature_id) REFERENCES magical_creatures(id) ON DELETE SET NULL,
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_tower (tower_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE spell_tomes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
spell_name VARCHAR(200) NOT NULL,
|
||||
mana_cost INT DEFAULT 50,
|
||||
cast_time_seconds DECIMAL(4,2) DEFAULT 1.5,
|
||||
damage_type ENUM('fire', 'ice', 'lightning', 'arcane', 'nature', 'shadow') NOT NULL,
|
||||
spell_description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FULLTEXT idx_spell_search (spell_name, spell_description)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE wizard_spellbooks (
|
||||
wizard_id INT NOT NULL,
|
||||
spell_id INT NOT NULL,
|
||||
mastery_level INT DEFAULT 1,
|
||||
times_cast INT DEFAULT 0,
|
||||
learned_date DATE,
|
||||
PRIMARY KEY (wizard_id, spell_id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES arcane_wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (spell_id) REFERENCES spell_tomes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE enchanted_artifacts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
artifact_name VARCHAR(200) UNIQUE NOT NULL,
|
||||
power_level INT CHECK (power_level BETWEEN 1 AND 100),
|
||||
curse_type VARCHAR(100),
|
||||
owner_wizard_id INT,
|
||||
found_in_realm_id INT,
|
||||
enchantments JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (owner_wizard_id) REFERENCES arcane_wizards(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (found_in_realm_id) REFERENCES magical_realms(id),
|
||||
INDEX idx_power (power_level),
|
||||
INDEX idx_owner (owner_wizard_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE portal_network (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
portal_name VARCHAR(100) NOT NULL,
|
||||
source_realm_id INT NOT NULL,
|
||||
destination_realm_id INT NOT NULL,
|
||||
stability_percentage DECIMAL(5,2) DEFAULT 95.00,
|
||||
mana_cost_per_use INT DEFAULT 100,
|
||||
is_bidirectional BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_realm_id) REFERENCES magical_realms(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (destination_realm_id) REFERENCES magical_realms(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_portal_connection (source_realm_id, destination_realm_id),
|
||||
INDEX idx_source (source_realm_id),
|
||||
INDEX idx_destination (destination_realm_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE magical_guilds (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
guild_name VARCHAR(200) UNIQUE NOT NULL,
|
||||
founding_wizard_id INT,
|
||||
headquarters_tower_id INT,
|
||||
guild_treasury INT DEFAULT 0,
|
||||
member_limit INT DEFAULT 50,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (founding_wizard_id) REFERENCES arcane_wizards(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (headquarters_tower_id) REFERENCES wizard_towers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE guild_memberships (
|
||||
wizard_id INT NOT NULL,
|
||||
guild_id INT NOT NULL,
|
||||
joined_date DATE NOT NULL,
|
||||
guild_rank ENUM('apprentice', 'member', 'elder', 'master') DEFAULT 'apprentice',
|
||||
contribution_points INT DEFAULT 0,
|
||||
PRIMARY KEY (wizard_id, guild_id),
|
||||
FOREIGN KEY (wizard_id) REFERENCES arcane_wizards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (guild_id) REFERENCES magical_guilds(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE realm_events (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
realm_id INT NOT NULL,
|
||||
description TEXT,
|
||||
magic_fluctuation INT DEFAULT 0,
|
||||
participants JSON,
|
||||
event_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (realm_id) REFERENCES magical_realms(id) ON DELETE CASCADE,
|
||||
INDEX idx_realm_time (realm_id, event_timestamp),
|
||||
INDEX idx_event_type (event_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Additional constraints via ALTER TABLE
|
||||
ALTER TABLE magical_creatures
|
||||
ADD CONSTRAINT chk_level CHECK (level BETWEEN 1 AND 100);
|
||||
|
||||
ALTER TABLE wizard_spellbooks
|
||||
ADD CONSTRAINT chk_mastery CHECK (mastery_level BETWEEN 1 AND 10);
|
||||
`;
|
||||
|
||||
console.log('Parsing fantasy realm schema...');
|
||||
const result = await fromMySQL(fantasySQL);
|
||||
|
||||
// Expected structure
|
||||
const expectedTables = [
|
||||
'arcane_wizards',
|
||||
'creature_classes',
|
||||
'enchanted_artifacts',
|
||||
'guild_memberships',
|
||||
'magical_creatures',
|
||||
'magical_guilds',
|
||||
'magical_realms',
|
||||
'portal_network',
|
||||
'realm_events',
|
||||
'spell_tomes',
|
||||
'wizard_spellbooks',
|
||||
'wizard_towers',
|
||||
];
|
||||
|
||||
console.log('Found tables:', result.tables.map((t) => t.name).sort());
|
||||
|
||||
expect(result.tables).toHaveLength(12);
|
||||
expect(result.tables.map((t) => t.name).sort()).toEqual(expectedTables);
|
||||
|
||||
// Verify relationships
|
||||
console.log(
|
||||
`\nTotal relationships found: ${result.relationships.length}`
|
||||
);
|
||||
|
||||
// Check some key relationships
|
||||
const creatureRelations = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'magical_creatures'
|
||||
);
|
||||
expect(creatureRelations).toHaveLength(2); // class_id and realm_id
|
||||
|
||||
const wizardRelations = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'arcane_wizards'
|
||||
);
|
||||
expect(wizardRelations).toHaveLength(2); // tower_id and familiar_creature_id
|
||||
|
||||
// Check CASCADE relationships
|
||||
const cascadeRelations = result.relationships.filter(
|
||||
(r) => r.deleteAction === 'CASCADE'
|
||||
);
|
||||
console.log(
|
||||
`\nRelationships with CASCADE delete: ${cascadeRelations.length}`
|
||||
);
|
||||
expect(cascadeRelations.length).toBeGreaterThan(5);
|
||||
|
||||
// Verify special columns
|
||||
const realms = result.tables.find((t) => t.name === 'magical_realms');
|
||||
const magicDensity = realms?.columns.find(
|
||||
(c) => c.name === 'magic_density'
|
||||
);
|
||||
expect(magicDensity?.type).toBe('DECIMAL');
|
||||
|
||||
const spells = result.tables.find((t) => t.name === 'spell_tomes');
|
||||
const damageType = spells?.columns.find(
|
||||
(c) => c.name === 'damage_type'
|
||||
);
|
||||
expect(damageType?.type).toBe('ENUM');
|
||||
|
||||
// Check indexes
|
||||
const wizards = result.tables.find((t) => t.name === 'arcane_wizards');
|
||||
expect(wizards?.indexes.some((idx) => idx.name === 'idx_email')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Check unique constraints
|
||||
const portals = result.tables.find((t) => t.name === 'portal_network');
|
||||
expect(
|
||||
portals?.indexes.some(
|
||||
(idx) =>
|
||||
idx.name === 'uk_portal_connection' && idx.unique === true
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
console.log('\n=== Parsing Summary ===');
|
||||
console.log(`Tables parsed: ${result.tables.length}`);
|
||||
console.log(`Relationships found: ${result.relationships.length}`);
|
||||
console.log(
|
||||
`Tables with indexes: ${result.tables.filter((t) => t.indexes.length > 0).length}`
|
||||
);
|
||||
console.log(
|
||||
`Tables with primary keys: ${
|
||||
result.tables.filter((t) => t.columns.some((c) => c.primaryKey))
|
||||
.length
|
||||
}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQL } from '../mysql';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
|
||||
describe('MySQL Final Integration', () => {
|
||||
it('should use the improved parser from fromMySQL', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(100)
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id INT PRIMARY KEY,
|
||||
user_id INT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);`;
|
||||
|
||||
const result = await fromMySQL(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.relationships).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should reject inline REFERENCES', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE posts (
|
||||
id INT PRIMARY KEY,
|
||||
user_id INT REFERENCES users(id)
|
||||
);`;
|
||||
|
||||
await expect(fromMySQL(sql)).rejects.toThrow(
|
||||
'MySQL/MariaDB does not support inline REFERENCES'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a large fantasy schema with skipValidation', async () => {
|
||||
const fantasySQL = `
|
||||
-- Dragon Registry System
|
||||
CREATE TABLE dragon_species (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
species_name VARCHAR(100) UNIQUE NOT NULL,
|
||||
element_affinity ENUM('fire', 'ice', 'lightning', 'earth', 'shadow', 'light') NOT NULL,
|
||||
average_wingspan_meters DECIMAL(6,2),
|
||||
is_ancient BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE dragon_lairs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
location_name VARCHAR(200) NOT NULL,
|
||||
coordinates JSON, -- {"x": 1000, "y": 2000, "z": 500}
|
||||
treasure_value INT DEFAULT 0,
|
||||
trap_level INT DEFAULT 1 CHECK (trap_level BETWEEN 1 AND 10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_treasure (treasure_value)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE dragons (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
dragon_name VARCHAR(200) NOT NULL,
|
||||
species_id INT NOT NULL,
|
||||
lair_id INT,
|
||||
age_years INT DEFAULT 0,
|
||||
hoard_size INT DEFAULT 0,
|
||||
breath_weapon_power INT DEFAULT 100,
|
||||
is_sleeping BOOLEAN DEFAULT FALSE,
|
||||
last_seen_at DATETIME,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (species_id) REFERENCES dragon_species(id),
|
||||
FOREIGN KEY (lair_id) REFERENCES dragon_lairs(id) ON DELETE SET NULL,
|
||||
INDEX idx_species (species_id),
|
||||
INDEX idx_lair (lair_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Adventurer's Guild
|
||||
CREATE TABLE adventurer_classes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
class_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
primary_stat ENUM('strength', 'dexterity', 'intelligence', 'wisdom', 'charisma') NOT NULL,
|
||||
hit_dice INT DEFAULT 6,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE adventurers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
adventurer_name VARCHAR(200) NOT NULL,
|
||||
class_id INT NOT NULL,
|
||||
level INT DEFAULT 1,
|
||||
experience_points INT DEFAULT 0,
|
||||
gold_pieces INT DEFAULT 100,
|
||||
is_alive BOOLEAN DEFAULT TRUE,
|
||||
last_quest_date DATE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (class_id) REFERENCES adventurer_classes(id),
|
||||
INDEX idx_class (class_id),
|
||||
INDEX idx_level (level)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE dragon_encounters (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
dragon_id INT NOT NULL,
|
||||
adventurer_id INT NOT NULL,
|
||||
encounter_date DATETIME NOT NULL,
|
||||
outcome ENUM('fled', 'negotiated', 'fought', 'befriended') NOT NULL,
|
||||
gold_stolen INT DEFAULT 0,
|
||||
survived BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (dragon_id) REFERENCES dragons(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (adventurer_id) REFERENCES adventurers(id) ON DELETE CASCADE,
|
||||
INDEX idx_dragon (dragon_id),
|
||||
INDEX idx_adventurer (adventurer_id),
|
||||
INDEX idx_date (encounter_date)
|
||||
) ENGINE=InnoDB;
|
||||
`;
|
||||
|
||||
// First, let's try with skipValidation
|
||||
const result = await fromMySQLImproved(fantasySQL, {
|
||||
skipValidation: true,
|
||||
includeWarnings: true,
|
||||
});
|
||||
|
||||
console.log('\n=== Results with skipValidation ===');
|
||||
console.log('Tables:', result.tables.length);
|
||||
console.log('Relationships:', result.relationships.length);
|
||||
console.log('Warnings:', result.warnings?.length || 0);
|
||||
|
||||
expect(result.tables.length).toBe(6);
|
||||
expect(result.relationships.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
// Verify key tables
|
||||
const dragons = result.tables.find((t) => t.name === 'dragons');
|
||||
expect(dragons).toBeDefined();
|
||||
expect(
|
||||
dragons?.columns.find((c) => c.name === 'breath_weapon_power')
|
||||
).toBeDefined();
|
||||
|
||||
// Check relationships
|
||||
const dragonRelations = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'dragons'
|
||||
);
|
||||
expect(dragonRelations).toHaveLength(2); // species_id and lair_id
|
||||
});
|
||||
|
||||
it('should handle SQL with comments like PostgreSQL', async () => {
|
||||
// Use properly formatted SQL that won't break when comments are removed
|
||||
const sqlWithComments = `
|
||||
CREATE TABLE test (
|
||||
id INT PRIMARY KEY,
|
||||
data JSON, -- Example: ["value1", "value2"]
|
||||
status VARCHAR(50), -- Can be "active" or "inactive"
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Auto-set timestamp
|
||||
);`;
|
||||
|
||||
// This should work because comments are removed first
|
||||
const result = await fromMySQL(sqlWithComments);
|
||||
|
||||
console.log('\n=== Result ===');
|
||||
console.log('Tables:', result.tables.length);
|
||||
console.log('Columns:', result.tables[0]?.columns.length);
|
||||
|
||||
expect(result.tables).toHaveLength(1);
|
||||
expect(result.tables[0].name).toBe('test');
|
||||
expect(result.tables[0].columns).toHaveLength(4);
|
||||
|
||||
// Verify columns were parsed correctly
|
||||
const columns = result.tables[0].columns.map((c) => c.name);
|
||||
expect(columns).toContain('id');
|
||||
expect(columns).toContain('data');
|
||||
expect(columns).toContain('status');
|
||||
expect(columns).toContain('created_at');
|
||||
});
|
||||
|
||||
it('should handle SQL that may become problematic after comment removal', async () => {
|
||||
// This SQL is problematic because removing comments leaves invalid syntax
|
||||
const problematicSql = `
|
||||
CREATE TABLE test (
|
||||
id INT PRIMARY KEY,
|
||||
data JSON, -- [
|
||||
"value1",
|
||||
"value2"
|
||||
] -- This leaves broken syntax
|
||||
);`;
|
||||
|
||||
// The parser might handle this in different ways
|
||||
try {
|
||||
const result = await fromMySQL(problematicSql);
|
||||
// If it succeeds, it might have parsed partially
|
||||
expect(result.tables.length).toBeGreaterThanOrEqual(0);
|
||||
} catch (error) {
|
||||
// If it fails, that's also acceptable
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
|
||||
describe('MySQL Fix Test', () => {
|
||||
it('should parse foreign keys with comments containing commas', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE product_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE packages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
category_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
badge_text VARCHAR(50), -- "Beliebt", "Empfohlen", etc.
|
||||
color_code VARCHAR(7), -- Hex-Farbe für UI
|
||||
FOREIGN KEY (category_id) REFERENCES product_categories(id)
|
||||
);`;
|
||||
|
||||
const result = await fromMySQLImproved(sql, { skipValidation: true });
|
||||
|
||||
console.log(
|
||||
'Tables:',
|
||||
result.tables.map((t) => t.name)
|
||||
);
|
||||
console.log('Relationships:', result.relationships.length);
|
||||
result.relationships.forEach((r) => {
|
||||
console.log(
|
||||
` ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.tables).toHaveLength(2);
|
||||
expect(result.relationships).toHaveLength(1);
|
||||
expect(result.relationships[0]).toMatchObject({
|
||||
sourceTable: 'packages',
|
||||
sourceColumn: 'category_id',
|
||||
targetTable: 'product_categories',
|
||||
targetColumn: 'id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse the actual packages table from the file', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE product_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(255),
|
||||
sort_order INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Pakete (für VServer, Game-Server, Web-Hosting)
|
||||
CREATE TABLE packages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
category_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Paket-Eigenschaften
|
||||
is_popular BOOLEAN DEFAULT FALSE,
|
||||
badge_text VARCHAR(50), -- Examples: "Beliebt", "Empfohlen", etc.
|
||||
color_code VARCHAR(7), -- Hex color for UI
|
||||
|
||||
sort_order INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (category_id) REFERENCES product_categories(id),
|
||||
|
||||
-- Only categories with packages: VServer(2), Game-Server(1), Web-Hosting(4)
|
||||
CHECK (category_id IN (1, 2, 4))
|
||||
);`;
|
||||
|
||||
const result = await fromMySQLImproved(sql, { skipValidation: true });
|
||||
|
||||
console.log('\nActual packages table test:');
|
||||
console.log(
|
||||
'Tables:',
|
||||
result.tables.map((t) => t.name)
|
||||
);
|
||||
console.log('Relationships:', result.relationships.length);
|
||||
result.relationships.forEach((r) => {
|
||||
console.log(
|
||||
` ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}`
|
||||
);
|
||||
});
|
||||
|
||||
const packagesRelationships = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'packages'
|
||||
);
|
||||
expect(packagesRelationships).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,601 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fromMySQLImproved } from '../mysql-improved';
|
||||
|
||||
describe('MySQL Integration Tests', () => {
|
||||
describe('E-Commerce Database Schema', () => {
|
||||
it('should parse a complete e-commerce database', async () => {
|
||||
const sql = `
|
||||
-- E-commerce database schema
|
||||
CREATE TABLE categories (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE CASCADE,
|
||||
INDEX idx_parent (parent_id),
|
||||
INDEX idx_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE brands (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
logo_url VARCHAR(500),
|
||||
website VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
sku VARCHAR(50) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
brand_id INT,
|
||||
category_id INT NOT NULL,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
compare_at_price DECIMAL(10,2),
|
||||
cost DECIMAL(10,2),
|
||||
quantity INT DEFAULT 0,
|
||||
weight DECIMAL(8,3),
|
||||
status ENUM('active', 'draft', 'archived') DEFAULT 'draft',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (brand_id) REFERENCES brands(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id),
|
||||
INDEX idx_sku (sku),
|
||||
INDEX idx_category (category_id),
|
||||
INDEX idx_brand (brand_id),
|
||||
INDEX idx_status (status),
|
||||
FULLTEXT idx_search (name, description)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE product_images (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_id INT NOT NULL,
|
||||
image_url VARCHAR(500) NOT NULL,
|
||||
alt_text VARCHAR(255),
|
||||
position INT DEFAULT 0,
|
||||
is_primary BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||
INDEX idx_product (product_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE customers (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE addresses (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
customer_id INT NOT NULL,
|
||||
type ENUM('billing', 'shipping', 'both') DEFAULT 'both',
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
company VARCHAR(100),
|
||||
address_line1 VARCHAR(255) NOT NULL,
|
||||
address_line2 VARCHAR(255),
|
||||
city VARCHAR(100) NOT NULL,
|
||||
state_province VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
country_code CHAR(2) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||
INDEX idx_customer (customer_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE carts (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
customer_id INT,
|
||||
session_id VARCHAR(128),
|
||||
status ENUM('active', 'abandoned', 'converted') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL,
|
||||
INDEX idx_customer (customer_id),
|
||||
INDEX idx_session (session_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE cart_items (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
cart_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
UNIQUE KEY uk_cart_product (cart_id, product_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE orders (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_number VARCHAR(50) UNIQUE NOT NULL,
|
||||
customer_id INT NOT NULL,
|
||||
billing_address_id INT NOT NULL,
|
||||
shipping_address_id INT NOT NULL,
|
||||
status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded') DEFAULT 'pending',
|
||||
subtotal DECIMAL(10,2) NOT NULL,
|
||||
tax_amount DECIMAL(10,2) DEFAULT 0.00,
|
||||
shipping_amount DECIMAL(10,2) DEFAULT 0.00,
|
||||
discount_amount DECIMAL(10,2) DEFAULT 0.00,
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
currency_code CHAR(3) DEFAULT 'USD',
|
||||
payment_status ENUM('pending', 'paid', 'partially_paid', 'refunded', 'failed') DEFAULT 'pending',
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id),
|
||||
FOREIGN KEY (billing_address_id) REFERENCES addresses(id),
|
||||
FOREIGN KEY (shipping_address_id) REFERENCES addresses(id),
|
||||
INDEX idx_order_number (order_number),
|
||||
INDEX idx_customer (customer_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE order_items (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
product_name VARCHAR(255) NOT NULL,
|
||||
product_sku VARCHAR(50) NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
total DECIMAL(10,2) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
INDEX idx_order (order_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE payments (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id INT NOT NULL,
|
||||
payment_method ENUM('credit_card', 'debit_card', 'paypal', 'stripe', 'bank_transfer') NOT NULL,
|
||||
transaction_id VARCHAR(255) UNIQUE,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency_code CHAR(3) DEFAULT 'USD',
|
||||
status ENUM('pending', 'processing', 'completed', 'failed', 'refunded') DEFAULT 'pending',
|
||||
gateway_response JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||
INDEX idx_order (order_id),
|
||||
INDEX idx_transaction (transaction_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE reviews (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_id INT NOT NULL,
|
||||
customer_id INT NOT NULL,
|
||||
order_id INT,
|
||||
rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||
title VARCHAR(255),
|
||||
comment TEXT,
|
||||
is_verified_purchase BOOLEAN DEFAULT FALSE,
|
||||
is_featured BOOLEAN DEFAULT FALSE,
|
||||
helpful_count INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL,
|
||||
UNIQUE KEY uk_product_customer (product_id, customer_id),
|
||||
INDEX idx_product (product_id),
|
||||
INDEX idx_rating (rating),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE coupons (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
code VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
discount_type ENUM('fixed', 'percentage') NOT NULL,
|
||||
discount_amount DECIMAL(10,2) NOT NULL,
|
||||
minimum_amount DECIMAL(10,2),
|
||||
usage_limit INT,
|
||||
usage_count INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
valid_from DATETIME NOT NULL,
|
||||
valid_until DATETIME,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_code (code),
|
||||
INDEX idx_active (is_active),
|
||||
INDEX idx_valid_dates (valid_from, valid_until)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE order_coupons (
|
||||
order_id INT NOT NULL,
|
||||
coupon_id INT NOT NULL,
|
||||
discount_amount DECIMAL(10,2) NOT NULL,
|
||||
PRIMARY KEY (order_id, coupon_id),
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (coupon_id) REFERENCES coupons(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
// Verify all tables are parsed
|
||||
expect(result.tables).toHaveLength(14);
|
||||
|
||||
const expectedTables = [
|
||||
'addresses',
|
||||
'brands',
|
||||
'cart_items',
|
||||
'carts',
|
||||
'categories',
|
||||
'coupons',
|
||||
'customers',
|
||||
'order_coupons',
|
||||
'order_items',
|
||||
'orders',
|
||||
'payments',
|
||||
'product_images',
|
||||
'products',
|
||||
'reviews',
|
||||
];
|
||||
|
||||
expect(result.tables.map((t) => t.name).sort()).toEqual(
|
||||
expectedTables
|
||||
);
|
||||
|
||||
// Verify key relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'products' &&
|
||||
r.targetTable === 'categories'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'cart_items' &&
|
||||
r.targetTable === 'products'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'orders' &&
|
||||
r.targetTable === 'customers'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Check self-referencing relationship
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'categories' &&
|
||||
r.targetTable === 'categories' &&
|
||||
r.sourceColumn === 'parent_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Verify ENUMs are parsed
|
||||
const products = result.tables.find((t) => t.name === 'products');
|
||||
const statusColumn = products?.columns.find(
|
||||
(c) => c.name === 'status'
|
||||
);
|
||||
expect(statusColumn?.type).toBe('ENUM');
|
||||
|
||||
// Verify indexes
|
||||
expect(
|
||||
products?.indexes.some((idx) => idx.name === 'idx_sku')
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Social Media Platform Schema', () => {
|
||||
it('should parse a social media database with complex relationships', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(100),
|
||||
bio TEXT,
|
||||
avatar_url VARCHAR(500),
|
||||
cover_image_url VARCHAR(500),
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
is_private BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE posts (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
visibility ENUM('public', 'followers', 'private') DEFAULT 'public',
|
||||
reply_to_id BIGINT,
|
||||
repost_of_id BIGINT,
|
||||
like_count INT DEFAULT 0,
|
||||
reply_count INT DEFAULT 0,
|
||||
repost_count INT DEFAULT 0,
|
||||
view_count INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (reply_to_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (repost_of_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_created (created_at),
|
||||
FULLTEXT idx_content (content)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE follows (
|
||||
follower_id BIGINT NOT NULL,
|
||||
following_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (follower_id, following_id),
|
||||
FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (following_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_following (following_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE likes (
|
||||
user_id BIGINT NOT NULL,
|
||||
post_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, post_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
INDEX idx_post (post_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE hashtags (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
tag VARCHAR(100) UNIQUE NOT NULL,
|
||||
post_count INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_tag (tag)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE post_hashtags (
|
||||
post_id BIGINT NOT NULL,
|
||||
hashtag_id INT NOT NULL,
|
||||
PRIMARY KEY (post_id, hashtag_id),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (hashtag_id) REFERENCES hashtags(id) ON DELETE CASCADE,
|
||||
INDEX idx_hashtag (hashtag_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE messages (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
sender_id BIGINT NOT NULL,
|
||||
recipient_id BIGINT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_recipient (recipient_id, is_read),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE notifications (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
type ENUM('like', 'follow', 'reply', 'repost', 'mention') NOT NULL,
|
||||
actor_id BIGINT NOT NULL,
|
||||
post_id BIGINT,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_unread (user_id, is_read),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(8);
|
||||
|
||||
// Check self-referencing relationships in posts
|
||||
const postRelationships = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'posts'
|
||||
);
|
||||
expect(
|
||||
postRelationships.some(
|
||||
(r) =>
|
||||
r.targetTable === 'posts' &&
|
||||
r.sourceColumn === 'reply_to_id'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
postRelationships.some(
|
||||
(r) =>
|
||||
r.targetTable === 'posts' &&
|
||||
r.sourceColumn === 'repost_of_id'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Check many-to-many relationships
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'follows' &&
|
||||
r.sourceColumn === 'follower_id' &&
|
||||
r.targetTable === 'users'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
result.relationships.some(
|
||||
(r) =>
|
||||
r.sourceTable === 'follows' &&
|
||||
r.sourceColumn === 'following_id' &&
|
||||
r.targetTable === 'users'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Verify composite primary keys
|
||||
const follows = result.tables.find((t) => t.name === 'follows');
|
||||
const followerCol = follows?.columns.find(
|
||||
(c) => c.name === 'follower_id'
|
||||
);
|
||||
const followingCol = follows?.columns.find(
|
||||
(c) => c.name === 'following_id'
|
||||
);
|
||||
expect(followerCol?.primaryKey).toBe(true);
|
||||
expect(followingCol?.primaryKey).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Financial System Schema', () => {
|
||||
it('should parse a financial system with decimal precision and constraints', async () => {
|
||||
const sql = `
|
||||
CREATE TABLE currencies (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
code CHAR(3) UNIQUE NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
symbol VARCHAR(5),
|
||||
decimal_places TINYINT DEFAULT 2,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE accounts (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
account_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
account_type ENUM('checking', 'savings', 'investment', 'credit') NOT NULL,
|
||||
currency_id INT NOT NULL,
|
||||
balance DECIMAL(19,4) DEFAULT 0.0000,
|
||||
available_balance DECIMAL(19,4) DEFAULT 0.0000,
|
||||
credit_limit DECIMAL(19,4),
|
||||
interest_rate DECIMAL(5,4),
|
||||
status ENUM('active', 'frozen', 'closed') DEFAULT 'active',
|
||||
opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (currency_id) REFERENCES currencies(id),
|
||||
INDEX idx_account_number (account_number),
|
||||
INDEX idx_status (status),
|
||||
CHECK (balance >= 0 OR account_type = 'credit'),
|
||||
CHECK (available_balance <= balance OR account_type = 'credit')
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE transactions (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
transaction_ref VARCHAR(50) UNIQUE NOT NULL,
|
||||
from_account_id BIGINT,
|
||||
to_account_id BIGINT,
|
||||
amount DECIMAL(19,4) NOT NULL,
|
||||
currency_id INT NOT NULL,
|
||||
type ENUM('deposit', 'withdrawal', 'transfer', 'fee', 'interest') NOT NULL,
|
||||
status ENUM('pending', 'processing', 'completed', 'failed', 'reversed') DEFAULT 'pending',
|
||||
description TEXT,
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (from_account_id) REFERENCES accounts(id),
|
||||
FOREIGN KEY (to_account_id) REFERENCES accounts(id),
|
||||
FOREIGN KEY (currency_id) REFERENCES currencies(id),
|
||||
INDEX idx_ref (transaction_ref),
|
||||
INDEX idx_from_account (from_account_id),
|
||||
INDEX idx_to_account (to_account_id),
|
||||
INDEX idx_created (created_at),
|
||||
INDEX idx_status (status),
|
||||
CHECK (from_account_id IS NOT NULL OR to_account_id IS NOT NULL)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE exchange_rates (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
from_currency_id INT NOT NULL,
|
||||
to_currency_id INT NOT NULL,
|
||||
rate DECIMAL(19,10) NOT NULL,
|
||||
effective_date DATE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (from_currency_id) REFERENCES currencies(id),
|
||||
FOREIGN KEY (to_currency_id) REFERENCES currencies(id),
|
||||
UNIQUE KEY uk_currency_pair_date (from_currency_id, to_currency_id, effective_date),
|
||||
INDEX idx_effective_date (effective_date)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
user_id BIGINT,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
old_values JSON,
|
||||
new_values JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_entity (entity_type, entity_id),
|
||||
INDEX idx_created (created_at),
|
||||
INDEX idx_action (action)
|
||||
) ENGINE=InnoDB;
|
||||
`;
|
||||
|
||||
const result = await fromMySQLImproved(sql);
|
||||
|
||||
expect(result.tables).toHaveLength(5);
|
||||
|
||||
// Check decimal precision is preserved
|
||||
const accounts = result.tables.find((t) => t.name === 'accounts');
|
||||
const balanceCol = accounts?.columns.find(
|
||||
(c) => c.name === 'balance'
|
||||
);
|
||||
expect(balanceCol?.type).toBe('DECIMAL');
|
||||
|
||||
const transactionFKs = result.relationships.filter(
|
||||
(r) => r.sourceTable === 'transactions'
|
||||
);
|
||||
|
||||
expect(
|
||||
transactionFKs.some(
|
||||
(r) =>
|
||||
r.sourceColumn === 'from_account_id' &&
|
||||
r.targetTable === 'accounts'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
transactionFKs.some(
|
||||
(r) =>
|
||||
r.sourceColumn === 'to_account_id' &&
|
||||
r.targetTable === 'accounts'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
// Check composite unique constraint
|
||||
const exchangeRates = result.tables.find(
|
||||
(t) => t.name === 'exchange_rates'
|
||||
);
|
||||
expect(
|
||||
exchangeRates?.indexes.some(
|
||||
(idx) =>
|
||||
idx.name === 'uk_currency_pair_date' &&
|
||||
idx.unique === true
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateMySQLSyntax,
|
||||
formatValidationMessage,
|
||||
} from '../mysql-validator';
|
||||
|
||||
describe('MySQL Validator', () => {
|
||||
it('should pass valid MySQL after comments are removed', () => {
|
||||
// In the new flow, comments are removed before validation
|
||||
// So this SQL would have comments stripped and be valid
|
||||
const sql = `CREATE TABLE packages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
badge_text VARCHAR(50),
|
||||
color_code VARCHAR(7)
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should validate clean SQL without comments', () => {
|
||||
// Comments would be removed before validation
|
||||
const sql = `CREATE TABLE product_vserver (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
available_os JSON
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect inline REFERENCES', () => {
|
||||
const sql = `CREATE TABLE users (
|
||||
id INT PRIMARY KEY,
|
||||
profile_id INT REFERENCES profiles(id)
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].code).toBe('INLINE_REFERENCES');
|
||||
expect(result.errors[0].line).toBe(3);
|
||||
});
|
||||
|
||||
it('should pass valid MySQL', () => {
|
||||
const sql = `CREATE TABLE users (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id INT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
title VARCHAR(200),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should validate a fantasy-themed MySQL schema', () => {
|
||||
// Test with already sanitized SQL (comments removed)
|
||||
const sql = `
|
||||
CREATE TABLE magic_schools (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
element_type VARCHAR(50),
|
||||
forbidden_spells JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE wizards (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
magic_school_id INT REFERENCES magic_schools(id), -- Inline REFERENCES (PostgreSQL style)
|
||||
power_level INT DEFAULT 1
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
|
||||
console.log('\n=== Fantasy Schema Validation ===');
|
||||
console.log(`Valid: ${result.isValid}`);
|
||||
console.log(`Errors: ${result.errors.length}`);
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
|
||||
// Should only have inline REFERENCES error now
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors.length).toBe(1);
|
||||
expect(result.errors[0].code).toBe('INLINE_REFERENCES');
|
||||
});
|
||||
|
||||
it('should format validation messages nicely', () => {
|
||||
const sql = `CREATE TABLE test (
|
||||
id INT PRIMARY KEY,
|
||||
ref_id INT REFERENCES other(id)
|
||||
);`;
|
||||
|
||||
const result = validateMySQLSyntax(sql);
|
||||
const message = formatValidationMessage(result);
|
||||
|
||||
console.log('\nFormatted validation message:');
|
||||
console.log(message);
|
||||
|
||||
expect(message).toContain('❌ MySQL/MariaDB syntax validation failed');
|
||||
expect(message).toContain('Error at line 3');
|
||||
expect(message).toContain('💡 Suggestion');
|
||||
});
|
||||
});
|
||||
1165
src/lib/data/sql-import/dialect-importers/mysql/mysql-improved.ts
Normal file
1165
src/lib/data/sql-import/dialect-importers/mysql/mysql-improved.ts
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user