mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 14:03:15 +00:00 
			
		
		
		
	Compare commits
	
		
			65 Commits
		
	
	
		
			v1.13.0
			...
			feat/table
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8874cb552d | ||
| 
						 | 
					32b2c2fa7a | ||
| 
						 | 
					63e8c82b24 | ||
| 
						 | 
					06cb0b5161 | ||
| 
						 | 
					8ffde62c1a | ||
| 
						 | 
					39247b77a2 | ||
| 
						 | 
					984b2aeee2 | ||
| 
						 | 
					eed104be5b | ||
| 
						 | 
					00bd535b3c | ||
| 
						 | 
					18e914242f | ||
| 
						 | 
					e68837a34a | ||
| 
						 | 
					b30162d98b | ||
| 
						 | 
					dba372d25a | ||
| 
						 | 
					2eb48e75d3 | ||
| 
						 | 
					867903cd5f | ||
| 
						 | 
					8aeb1df0ad | ||
| 
						 | 
					6bea827293 | ||
| 
						 | 
					a119854da7 | ||
| 
						 | 
					bfbfd7b843 | ||
| 
						 | 
					0ca7008735 | ||
| 
						 | 
					4bc71c52ff | ||
| 
						 | 
					8f27f10dec | ||
| 
						 | 
					a93ec2cab9 | ||
| 
						 | 
					386e40a0bf | ||
| 
						 | 
					bda150d4b6 | ||
| 
						 | 
					87836e53d1 | ||
| 
						 | 
					7e0483f1a5 | ||
| 
						 | 
					309ee9cb0f | ||
| 
						 | 
					79b885502e | ||
| 
						 | 
					745bdee86d | ||
| 
						 | 
					08eb9cc55f | ||
| 
						 | 
					778f85d492 | ||
| 
						 | 
					fb92be7d3e | ||
| 
						 | 
					6df588f40e | ||
| 
						 | 
					b46ed58dff | ||
| 
						 | 
					0d9f57a9c9 | ||
| 
						 | 
					b7dbe54c83 | ||
| 
						 | 
					43d1dfff71 | ||
| 
						 | 
					9949a46ee3 | ||
| 
						 | 
					dfbcf05b2f | ||
| 
						 | 
					f56fab9876 | ||
| 
						 | 
					c9ea7da092 | ||
| 
						 | 
					22d46e1e90 | ||
| 
						 | 
					6af94afc56 | ||
| 
						 | 
					f7f92903de | ||
| 
						 | 
					b35e17526b | ||
| 
						 | 
					bf32c08d37 | ||
| 
						 | 
					5d337409d6 | ||
| 
						 | 
					67f5ac303e | ||
| 
						 | 
					578546a171 | ||
| 
						 | 
					aa0b629a3e | ||
| 
						 | 
					69beaa0a83 | ||
| 
						 | 
					4fcc49d49a | ||
| 
						 | 
					d15985e399 | ||
| 
						 | 
					d429128e65 | ||
| 
						 | 
					2fce8326b6 | ||
| 
						 | 
					433c68a33d | ||
| 
						 | 
					58acb65f12 | ||
| 
						 | 
					7978955819 | ||
| 
						 | 
					c6118e0cdb | ||
| 
						 | 
					7d063b905f | ||
| 
						 | 
					e0ff198c3f | ||
| 
						 | 
					8b86e1c229 | ||
| 
						 | 
					24be28a662 | ||
| 
						 | 
					c6788b4917 | 
							
								
								
									
										5
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -24,4 +24,7 @@ jobs:
 | 
			
		||||
        run: npm run lint
 | 
			
		||||
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: npm run build
 | 
			
		||||
        run: npm run build
 | 
			
		||||
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: npm run test:ci
 | 
			
		||||
							
								
								
									
										23
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,28 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
 | 
			
		||||
 | 
			
		||||
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
 | 
			
		||||
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
 | 
			
		||||
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
 | 
			
		||||
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
 | 
			
		||||
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
 | 
			
		||||
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
 | 
			
		||||
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
 | 
			
		||||
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
 | 
			
		||||
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
 | 
			
		||||
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
 | 
			
		||||
 | 
			
		||||
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ FROM node:22-alpine AS builder
 | 
			
		||||
ARG VITE_OPENAI_API_KEY
 | 
			
		||||
ARG VITE_OPENAI_API_ENDPOINT
 | 
			
		||||
ARG VITE_LLM_MODEL_NAME
 | 
			
		||||
ARG VITE_HIDE_BUCKLE_DOT_DEV
 | 
			
		||||
ARG VITE_HIDE_CHARTDB_CLOUD
 | 
			
		||||
ARG VITE_DISABLE_ANALYTICS
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +17,8 @@ COPY . .
 | 
			
		||||
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
 | 
			
		||||
    echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
 | 
			
		||||
    echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
 | 
			
		||||
    echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env 
 | 
			
		||||
    echo "VITE_HIDE_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
 | 
			
		||||
    echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env
 | 
			
		||||
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -125,6 +125,8 @@ docker run \
 | 
			
		||||
  -p 8080:80 chartdb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
 | 
			
		||||
 | 
			
		||||
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
 | 
			
		||||
 | 
			
		||||
Open your browser and navigate to `http://localhost:8080`.
 | 
			
		||||
 
 | 
			
		||||
@@ -10,11 +10,12 @@ server {
 | 
			
		||||
 | 
			
		||||
    location /config.js {
 | 
			
		||||
        default_type application/javascript;
 | 
			
		||||
        return 200 "window.env = { 
 | 
			
		||||
        return 200 "window.env = {
 | 
			
		||||
            OPENAI_API_KEY: \"$OPENAI_API_KEY\",
 | 
			
		||||
            OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
 | 
			
		||||
            LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
 | 
			
		||||
            HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
 | 
			
		||||
            HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
 | 
			
		||||
            DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
 | 
			
		||||
        };";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
# Replace placeholders in nginx.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
 | 
			
		||||
# Start Nginx
 | 
			
		||||
nginx -g "daemon off;"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								index.html
									
									
									
									
									
								
							@@ -13,11 +13,21 @@
 | 
			
		||||
            rel="stylesheet"
 | 
			
		||||
        />
 | 
			
		||||
        <script src="/config.js"></script>
 | 
			
		||||
        <script
 | 
			
		||||
            src="https://cdn.usefathom.com/script.js"
 | 
			
		||||
            data-site="PRHIVBNN"
 | 
			
		||||
            defer
 | 
			
		||||
        ></script>
 | 
			
		||||
        <script>
 | 
			
		||||
            // Load analytics only if not disabled
 | 
			
		||||
            (function() {
 | 
			
		||||
                const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
 | 
			
		||||
                                        (typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
 | 
			
		||||
 | 
			
		||||
                if (!disableAnalytics) {
 | 
			
		||||
                    const script = document.createElement('script');
 | 
			
		||||
                    script.src = 'https://cdn.usefathom.com/script.js';
 | 
			
		||||
                    script.setAttribute('data-site', 'PRHIVBNN');
 | 
			
		||||
                    script.defer = true;
 | 
			
		||||
                    document.head.appendChild(script);
 | 
			
		||||
                }
 | 
			
		||||
            })();
 | 
			
		||||
        </script>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div id="root"></div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1052
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1052
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.13.0",
 | 
			
		||||
    "version": "1.13.2",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
@@ -9,7 +9,11 @@
 | 
			
		||||
        "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
 | 
			
		||||
        "lint:fix": "npm run lint -- --fix",
 | 
			
		||||
        "preview": "vite preview",
 | 
			
		||||
        "prepare": "husky"
 | 
			
		||||
        "prepare": "husky",
 | 
			
		||||
        "test": "vitest",
 | 
			
		||||
        "test:ci": "vitest run --reporter=verbose --bail=1",
 | 
			
		||||
        "test:ui": "vitest --ui",
 | 
			
		||||
        "test:coverage": "vitest --coverage"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
@@ -32,7 +36,7 @@
 | 
			
		||||
        "@radix-ui/react-scroll-area": "1.2.0",
 | 
			
		||||
        "@radix-ui/react-select": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.2",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.1.2",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
        "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toast": "^1.2.1",
 | 
			
		||||
        "@radix-ui/react-toggle": "^1.1.0",
 | 
			
		||||
@@ -50,8 +54,9 @@
 | 
			
		||||
        "html-to-image": "^1.11.11",
 | 
			
		||||
        "i18next": "^23.14.0",
 | 
			
		||||
        "i18next-browser-languagedetector": "^8.0.0",
 | 
			
		||||
        "lucide-react": "^0.441.0",
 | 
			
		||||
        "lucide-react": "^0.525.0",
 | 
			
		||||
        "monaco-editor": "^0.52.0",
 | 
			
		||||
        "motion": "^12.23.6",
 | 
			
		||||
        "nanoid": "^5.0.7",
 | 
			
		||||
        "node-sql-parser": "^5.3.2",
 | 
			
		||||
        "react": "^18.3.1",
 | 
			
		||||
@@ -73,12 +78,16 @@
 | 
			
		||||
        "@eslint/compat": "^1.2.4",
 | 
			
		||||
        "@eslint/eslintrc": "^3.2.0",
 | 
			
		||||
        "@eslint/js": "^9.16.0",
 | 
			
		||||
        "@testing-library/jest-dom": "^6.6.3",
 | 
			
		||||
        "@testing-library/react": "^16.3.0",
 | 
			
		||||
        "@testing-library/user-event": "^14.6.1",
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
        "@types/react": "^18.3.3",
 | 
			
		||||
        "@types/react-dom": "^18.3.0",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^8.18.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.18.0",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.3.1",
 | 
			
		||||
        "@vitest/ui": "^3.2.4",
 | 
			
		||||
        "autoprefixer": "^10.4.20",
 | 
			
		||||
        "eslint": "^9.16.0",
 | 
			
		||||
        "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
@@ -90,6 +99,7 @@
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.7",
 | 
			
		||||
        "eslint-plugin-tailwindcss": "^3.17.4",
 | 
			
		||||
        "globals": "^15.13.0",
 | 
			
		||||
        "happy-dom": "^18.0.1",
 | 
			
		||||
        "husky": "^9.1.5",
 | 
			
		||||
        "postcss": "^8.4.40",
 | 
			
		||||
        "prettier": "^3.3.3",
 | 
			
		||||
@@ -97,6 +107,7 @@
 | 
			
		||||
        "tailwindcss": "^3.4.7",
 | 
			
		||||
        "typescript": "^5.2.2",
 | 
			
		||||
        "unplugin-inject-preload": "^3.0.0",
 | 
			
		||||
        "vite": "^5.3.4"
 | 
			
		||||
        "vite": "^5.3.4",
 | 
			
		||||
        "vitest": "^3.2.4"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,8 @@ export interface CodeSnippetProps {
 | 
			
		||||
    isComplete?: boolean;
 | 
			
		||||
    editorProps?: React.ComponentProps<EditorType>;
 | 
			
		||||
    actions?: CodeSnippetAction[];
 | 
			
		||||
    actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
 | 
			
		||||
    allowCopy?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
@@ -56,6 +58,8 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
        isComplete = true,
 | 
			
		||||
        editorProps,
 | 
			
		||||
        actions,
 | 
			
		||||
        actionsTooltipSide,
 | 
			
		||||
        allowCopy = true,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const monaco = useMonaco();
 | 
			
		||||
@@ -129,33 +133,37 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        {isComplete ? (
 | 
			
		||||
                            <div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
 | 
			
		||||
                                <Tooltip
 | 
			
		||||
                                    onOpenChange={setTooltipOpen}
 | 
			
		||||
                                    open={isCopied || tooltipOpen}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <span>
 | 
			
		||||
                                            <Button
 | 
			
		||||
                                                className="h-fit p-1.5"
 | 
			
		||||
                                                variant="outline"
 | 
			
		||||
                                                onClick={copyToClipboard}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {isCopied ? (
 | 
			
		||||
                                                    <CopyCheck size={16} />
 | 
			
		||||
                                                ) : (
 | 
			
		||||
                                                    <Copy size={16} />
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        {t(
 | 
			
		||||
                                            isCopied
 | 
			
		||||
                                                ? 'copied'
 | 
			
		||||
                                                : 'copy_to_clipboard'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                                {allowCopy ? (
 | 
			
		||||
                                    <Tooltip
 | 
			
		||||
                                        onOpenChange={setTooltipOpen}
 | 
			
		||||
                                        open={isCopied || tooltipOpen}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <TooltipTrigger asChild>
 | 
			
		||||
                                            <span>
 | 
			
		||||
                                                <Button
 | 
			
		||||
                                                    className="h-fit p-1.5"
 | 
			
		||||
                                                    variant="outline"
 | 
			
		||||
                                                    onClick={copyToClipboard}
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {isCopied ? (
 | 
			
		||||
                                                        <CopyCheck size={16} />
 | 
			
		||||
                                                    ) : (
 | 
			
		||||
                                                        <Copy size={16} />
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </Button>
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </TooltipTrigger>
 | 
			
		||||
                                        <TooltipContent
 | 
			
		||||
                                            side={actionsTooltipSide}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {t(
 | 
			
		||||
                                                isCopied
 | 
			
		||||
                                                    ? 'copied'
 | 
			
		||||
                                                    : 'copy_to_clipboard'
 | 
			
		||||
                                            )}
 | 
			
		||||
                                        </TooltipContent>
 | 
			
		||||
                                    </Tooltip>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
 | 
			
		||||
                                {actions &&
 | 
			
		||||
                                    actions.length > 0 &&
 | 
			
		||||
@@ -174,7 +182,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                                                    </Button>
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                            </TooltipTrigger>
 | 
			
		||||
                                            <TooltipContent>
 | 
			
		||||
                                            <TooltipContent
 | 
			
		||||
                                                side={actionsTooltipSide}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {action.label}
 | 
			
		||||
                                            </TooltipContent>
 | 
			
		||||
                                        </Tooltip>
 | 
			
		||||
 
 | 
			
		||||
@@ -43,12 +43,19 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
            root: [
 | 
			
		||||
                [/\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 font-normal text-muted-foreground',
 | 
			
		||||
                        'text-sm text-center font-normal text-muted-foreground',
 | 
			
		||||
                        descriptionClassName
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import type { ButtonProps } from '../button/button';
 | 
			
		||||
import { buttonVariants } from '../button/button-variants';
 | 
			
		||||
import {
 | 
			
		||||
    ChevronLeftIcon,
 | 
			
		||||
    ChevronRightIcon,
 | 
			
		||||
    DotsHorizontalIcon,
 | 
			
		||||
} from '@radix-ui/react-icons';
 | 
			
		||||
 | 
			
		||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
 | 
			
		||||
    <nav
 | 
			
		||||
        role="navigation"
 | 
			
		||||
        aria-label="pagination"
 | 
			
		||||
        className={cn('mx-auto flex w-full justify-center', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
);
 | 
			
		||||
Pagination.displayName = 'Pagination';
 | 
			
		||||
 | 
			
		||||
const PaginationContent = React.forwardRef<
 | 
			
		||||
    HTMLUListElement,
 | 
			
		||||
    React.ComponentProps<'ul'>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <ul
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn('flex flex-row items-center gap-1', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
PaginationContent.displayName = 'PaginationContent';
 | 
			
		||||
 | 
			
		||||
const PaginationItem = React.forwardRef<
 | 
			
		||||
    HTMLLIElement,
 | 
			
		||||
    React.ComponentProps<'li'>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <li ref={ref} className={cn('', className)} {...props} />
 | 
			
		||||
));
 | 
			
		||||
PaginationItem.displayName = 'PaginationItem';
 | 
			
		||||
 | 
			
		||||
type PaginationLinkProps = {
 | 
			
		||||
    isActive?: boolean;
 | 
			
		||||
} & Pick<ButtonProps, 'size'> &
 | 
			
		||||
    React.ComponentProps<'a'>;
 | 
			
		||||
 | 
			
		||||
const PaginationLink = ({
 | 
			
		||||
    className,
 | 
			
		||||
    isActive,
 | 
			
		||||
    size = 'icon',
 | 
			
		||||
    ...props
 | 
			
		||||
}: PaginationLinkProps) => (
 | 
			
		||||
    <a
 | 
			
		||||
        aria-current={isActive ? 'page' : undefined}
 | 
			
		||||
        className={cn(
 | 
			
		||||
            buttonVariants({
 | 
			
		||||
                variant: isActive ? 'outline' : 'ghost',
 | 
			
		||||
                size,
 | 
			
		||||
            }),
 | 
			
		||||
            className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
);
 | 
			
		||||
PaginationLink.displayName = 'PaginationLink';
 | 
			
		||||
 | 
			
		||||
const PaginationPrevious = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
    <PaginationLink
 | 
			
		||||
        aria-label="Go to previous page"
 | 
			
		||||
        size="default"
 | 
			
		||||
        className={cn('gap-1 pl-2.5', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <ChevronLeftIcon className="size-4" />
 | 
			
		||||
        <span>Previous</span>
 | 
			
		||||
    </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
PaginationPrevious.displayName = 'PaginationPrevious';
 | 
			
		||||
 | 
			
		||||
const PaginationNext = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
    <PaginationLink
 | 
			
		||||
        aria-label="Go to next page"
 | 
			
		||||
        size="default"
 | 
			
		||||
        className={cn('gap-1 pr-2.5', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <span>Next</span>
 | 
			
		||||
        <ChevronRightIcon className="size-4" />
 | 
			
		||||
    </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
PaginationNext.displayName = 'PaginationNext';
 | 
			
		||||
 | 
			
		||||
const PaginationEllipsis = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<'span'>) => (
 | 
			
		||||
    <span
 | 
			
		||||
        aria-hidden
 | 
			
		||||
        className={cn('flex h-9 w-9 items-center justify-center', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <DotsHorizontalIcon className="size-4" />
 | 
			
		||||
        <span className="sr-only">More pages</span>
 | 
			
		||||
    </span>
 | 
			
		||||
);
 | 
			
		||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
    Pagination,
 | 
			
		||||
    PaginationContent,
 | 
			
		||||
    PaginationLink,
 | 
			
		||||
    PaginationItem,
 | 
			
		||||
    PaginationPrevious,
 | 
			
		||||
    PaginationNext,
 | 
			
		||||
    PaginationEllipsis,
 | 
			
		||||
};
 | 
			
		||||
@@ -93,6 +93,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            (isOpen: boolean) => {
 | 
			
		||||
                setOpen?.(isOpen);
 | 
			
		||||
                setIsOpen(isOpen);
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => (document.body.style.pointerEvents = ''), 500);
 | 
			
		||||
            },
 | 
			
		||||
            [setOpen]
 | 
			
		||||
        );
 | 
			
		||||
@@ -227,7 +229,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                        onSelect={() =>
 | 
			
		||||
                            handleSelect(
 | 
			
		||||
                                option.value,
 | 
			
		||||
                                matches?.map((match) => match.toString())
 | 
			
		||||
                                matches?.map((match) => match?.toString())
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    >
 | 
			
		||||
@@ -418,27 +420,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
 | 
			
		||||
                        <ScrollArea>
 | 
			
		||||
                            <div className="max-h-64 w-full">
 | 
			
		||||
                                <CommandGroup>
 | 
			
		||||
                                    <CommandList className="max-h-fit w-full">
 | 
			
		||||
                                        {hasGroups
 | 
			
		||||
                                            ? Object.entries(groups).map(
 | 
			
		||||
                                                  ([
 | 
			
		||||
                                                      groupName,
 | 
			
		||||
                                                      groupOptions,
 | 
			
		||||
                                                  ]) => (
 | 
			
		||||
                                                      <CommandGroup
 | 
			
		||||
                                                          key={groupName}
 | 
			
		||||
                                                          heading={groupName}
 | 
			
		||||
                                                      >
 | 
			
		||||
                                                          {groupOptions.map(
 | 
			
		||||
                                                              renderOption
 | 
			
		||||
                                                          )}
 | 
			
		||||
                                                      </CommandGroup>
 | 
			
		||||
                                                  )
 | 
			
		||||
                                <CommandList className="max-h-fit w-full">
 | 
			
		||||
                                    {hasGroups
 | 
			
		||||
                                        ? Object.entries(groups).map(
 | 
			
		||||
                                              ([groupName, groupOptions]) => (
 | 
			
		||||
                                                  <CommandGroup
 | 
			
		||||
                                                      key={groupName}
 | 
			
		||||
                                                      heading={groupName}
 | 
			
		||||
                                                  >
 | 
			
		||||
                                                      {groupOptions.map(
 | 
			
		||||
                                                          renderOption
 | 
			
		||||
                                                      )}
 | 
			
		||||
                                                  </CommandGroup>
 | 
			
		||||
                                              )
 | 
			
		||||
                                            : options.map(renderOption)}
 | 
			
		||||
                                    </CommandList>
 | 
			
		||||
                                </CommandGroup>
 | 
			
		||||
                                          )
 | 
			
		||||
                                        : options.map(renderOption)}
 | 
			
		||||
                                </CommandList>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </ScrollArea>
 | 
			
		||||
                    </Command>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ export function Toaster() {
 | 
			
		||||
                description,
 | 
			
		||||
                action,
 | 
			
		||||
                layout = 'row',
 | 
			
		||||
                hideCloseButton = false,
 | 
			
		||||
                ...props
 | 
			
		||||
            }) {
 | 
			
		||||
                return (
 | 
			
		||||
@@ -38,7 +39,7 @@ export function Toaster() {
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {layout === 'row' ? action : null}
 | 
			
		||||
                        <ToastClose />
 | 
			
		||||
                        {!hideCloseButton ? <ToastClose /> : null}
 | 
			
		||||
                    </Toast>
 | 
			
		||||
                );
 | 
			
		||||
            })}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
 | 
			
		||||
    description?: React.ReactNode;
 | 
			
		||||
    action?: ToastActionElement;
 | 
			
		||||
    layout?: 'row' | 'column';
 | 
			
		||||
    hideCloseButton?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Skeleton } from '../skeleton/skeleton';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface TreeItemSkeletonProps
 | 
			
		||||
    extends React.HTMLAttributes<HTMLDivElement> {}
 | 
			
		||||
 | 
			
		||||
export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
 | 
			
		||||
    className,
 | 
			
		||||
    style,
 | 
			
		||||
}) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn('px-2 py-1', className)} style={style}>
 | 
			
		||||
            <Skeleton className="h-3.5 w-full rounded-sm" />
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,461 @@
 | 
			
		||||
import {
 | 
			
		||||
    ChevronRight,
 | 
			
		||||
    File,
 | 
			
		||||
    Folder,
 | 
			
		||||
    Loader2,
 | 
			
		||||
    type LucideIcon,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { motion, AnimatePresence } from 'framer-motion';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import type {
 | 
			
		||||
    TreeNode,
 | 
			
		||||
    FetchChildrenFunction,
 | 
			
		||||
    SelectableTreeProps,
 | 
			
		||||
} from './tree';
 | 
			
		||||
import type { ExpandedState } from './use-tree';
 | 
			
		||||
import { useTree } from './use-tree';
 | 
			
		||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { TreeItemSkeleton } from './tree-item-skeleton';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
 | 
			
		||||
interface TreeViewProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    data: TreeNode<Type, Context>[];
 | 
			
		||||
    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
			
		||||
    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    defaultIcon?: LucideIcon;
 | 
			
		||||
    defaultFolderIcon?: LucideIcon;
 | 
			
		||||
    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    selectable?: SelectableTreeProps<Type, Context>;
 | 
			
		||||
    expanded?: ExpandedState;
 | 
			
		||||
    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
			
		||||
    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    loadingNodeIds?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function TreeView<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>({
 | 
			
		||||
    data,
 | 
			
		||||
    fetchChildren,
 | 
			
		||||
    onNodeClick,
 | 
			
		||||
    className,
 | 
			
		||||
    defaultIcon = File,
 | 
			
		||||
    defaultFolderIcon = Folder,
 | 
			
		||||
    defaultIconProps,
 | 
			
		||||
    defaultFolderIconProps,
 | 
			
		||||
    selectable,
 | 
			
		||||
    expanded: expandedProp,
 | 
			
		||||
    setExpanded: setExpandedProp,
 | 
			
		||||
    renderHoverComponent,
 | 
			
		||||
    renderActionsComponent,
 | 
			
		||||
    loadingNodeIds,
 | 
			
		||||
}: TreeViewProps<Type, Context>) {
 | 
			
		||||
    const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
 | 
			
		||||
        useTree({
 | 
			
		||||
            fetchChildren,
 | 
			
		||||
            expanded: expandedProp,
 | 
			
		||||
            setExpanded: setExpandedProp,
 | 
			
		||||
        });
 | 
			
		||||
    const [selectedIdInternal, setSelectedIdInternal] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >(selectable?.defaultSelectedId);
 | 
			
		||||
 | 
			
		||||
    const selectedId = useMemo(() => {
 | 
			
		||||
        return selectable?.selectedId ?? selectedIdInternal;
 | 
			
		||||
    }, [selectable?.selectedId, selectedIdInternal]);
 | 
			
		||||
 | 
			
		||||
    const setSelectedId = useCallback(
 | 
			
		||||
        (value: SetStateAction<string | undefined>) => {
 | 
			
		||||
            if (selectable?.setSelectedId) {
 | 
			
		||||
                selectable.setSelectedId(value);
 | 
			
		||||
            } else {
 | 
			
		||||
                setSelectedIdInternal(value);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [selectable, setSelectedIdInternal]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (selectable?.enabled && selectable.defaultSelectedId) {
 | 
			
		||||
            if (selectable.defaultSelectedId === selectedId) return;
 | 
			
		||||
            setSelectedId(selectable.defaultSelectedId);
 | 
			
		||||
            const { node, path } = findNodeById(
 | 
			
		||||
                data,
 | 
			
		||||
                selectable.defaultSelectedId
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (node) {
 | 
			
		||||
                selectable.onSelectedChange?.(node);
 | 
			
		||||
 | 
			
		||||
                // Expand all parent nodes
 | 
			
		||||
                for (const parent of path) {
 | 
			
		||||
                    if (expanded[parent.id]) continue;
 | 
			
		||||
                    toggleNode(
 | 
			
		||||
                        parent.id,
 | 
			
		||||
                        parent.type,
 | 
			
		||||
                        parent.context,
 | 
			
		||||
                        parent.children
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
 | 
			
		||||
 | 
			
		||||
    const handleNodeSelect = (node: TreeNode<Type, Context>) => {
 | 
			
		||||
        if (selectable?.enabled) {
 | 
			
		||||
            setSelectedId(node.id);
 | 
			
		||||
            selectable.onSelectedChange?.(node);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn('w-full', className)}>
 | 
			
		||||
            {data.map((node, index) => (
 | 
			
		||||
                <TreeNode
 | 
			
		||||
                    key={node.id}
 | 
			
		||||
                    node={node}
 | 
			
		||||
                    level={0}
 | 
			
		||||
                    expanded={expanded}
 | 
			
		||||
                    loading={loading}
 | 
			
		||||
                    loadedChildren={loadedChildren}
 | 
			
		||||
                    hasMoreChildren={hasMoreChildren}
 | 
			
		||||
                    onToggle={toggleNode}
 | 
			
		||||
                    onNodeClick={onNodeClick}
 | 
			
		||||
                    defaultIcon={defaultIcon}
 | 
			
		||||
                    defaultFolderIcon={defaultFolderIcon}
 | 
			
		||||
                    defaultIconProps={defaultIconProps}
 | 
			
		||||
                    defaultFolderIconProps={defaultFolderIconProps}
 | 
			
		||||
                    selectable={selectable?.enabled}
 | 
			
		||||
                    selectedId={selectedId}
 | 
			
		||||
                    onSelect={handleNodeSelect}
 | 
			
		||||
                    className={index > 0 ? 'mt-0.5' : ''}
 | 
			
		||||
                    renderHoverComponent={renderHoverComponent}
 | 
			
		||||
                    renderActionsComponent={renderActionsComponent}
 | 
			
		||||
                    loadingNodeIds={loadingNodeIds}
 | 
			
		||||
                />
 | 
			
		||||
            ))}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TreeNodeProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    node: TreeNode<Type, Context>;
 | 
			
		||||
    level: number;
 | 
			
		||||
    expanded: Record<string, boolean>;
 | 
			
		||||
    loading: Record<string, boolean>;
 | 
			
		||||
    loadedChildren: Record<string, TreeNode<Type, Context>[]>;
 | 
			
		||||
    hasMoreChildren: Record<string, boolean>;
 | 
			
		||||
    onToggle: (
 | 
			
		||||
        nodeId: string,
 | 
			
		||||
        nodeType: Type,
 | 
			
		||||
        nodeContext: Context[Type],
 | 
			
		||||
        staticChildren?: TreeNode<Type, Context>[]
 | 
			
		||||
    ) => void;
 | 
			
		||||
    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    defaultIcon: LucideIcon;
 | 
			
		||||
    defaultFolderIcon: LucideIcon;
 | 
			
		||||
    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    selectable?: boolean;
 | 
			
		||||
    selectedId?: string;
 | 
			
		||||
    onSelect: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    loadingNodeIds?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
 | 
			
		||||
    node,
 | 
			
		||||
    level,
 | 
			
		||||
    expanded,
 | 
			
		||||
    loading,
 | 
			
		||||
    loadedChildren,
 | 
			
		||||
    hasMoreChildren,
 | 
			
		||||
    onToggle,
 | 
			
		||||
    onNodeClick,
 | 
			
		||||
    defaultIcon: DefaultIcon,
 | 
			
		||||
    defaultFolderIcon: DefaultFolderIcon,
 | 
			
		||||
    defaultIconProps,
 | 
			
		||||
    defaultFolderIconProps,
 | 
			
		||||
    selectable,
 | 
			
		||||
    selectedId,
 | 
			
		||||
    onSelect,
 | 
			
		||||
    className,
 | 
			
		||||
    renderHoverComponent,
 | 
			
		||||
    renderActionsComponent,
 | 
			
		||||
    loadingNodeIds,
 | 
			
		||||
}: TreeNodeProps<Type, Context>) {
 | 
			
		||||
    const [isHovered, setIsHovered] = useState(false);
 | 
			
		||||
    const isExpanded = expanded[node.id];
 | 
			
		||||
    const isLoading = loading[node.id];
 | 
			
		||||
    const children = loadedChildren[node.id] || node.children;
 | 
			
		||||
    const isSelected = selectedId === node.id;
 | 
			
		||||
 | 
			
		||||
    const IconComponent =
 | 
			
		||||
        node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
 | 
			
		||||
    const iconProps: React.ComponentProps<LucideIcon> = {
 | 
			
		||||
        strokeWidth: isSelected ? 2.5 : 2,
 | 
			
		||||
        ...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
 | 
			
		||||
        ...node.iconProps,
 | 
			
		||||
        className: cn(
 | 
			
		||||
            'h-3.5 w-3.5 text-muted-foreground flex-none',
 | 
			
		||||
            isSelected && 'text-primary text-white',
 | 
			
		||||
            node.iconProps?.className
 | 
			
		||||
        ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn(className)}>
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
 | 
			
		||||
                    'transition-colors duration-200',
 | 
			
		||||
                    isSelected
 | 
			
		||||
                        ? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
 | 
			
		||||
                        : 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
 | 
			
		||||
                    node.className
 | 
			
		||||
                )}
 | 
			
		||||
                {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                style={{ paddingLeft: `${level * 16 + 8}px` }}
 | 
			
		||||
                onMouseEnter={() => setIsHovered(true)}
 | 
			
		||||
                onMouseLeave={() => setIsHovered(false)}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    if (selectable && !node.unselectable) {
 | 
			
		||||
                        onSelect(node);
 | 
			
		||||
                    }
 | 
			
		||||
                    // if (node.isFolder) {
 | 
			
		||||
                    //     onToggle(node.id, node.children);
 | 
			
		||||
                    // }
 | 
			
		||||
 | 
			
		||||
                    // called only once in case of double click
 | 
			
		||||
                    if (e.detail !== 2) {
 | 
			
		||||
                        onNodeClick?.(node);
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
                onDoubleClick={(e) => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    if (node.isFolder) {
 | 
			
		||||
                        onToggle(
 | 
			
		||||
                            node.id,
 | 
			
		||||
                            node.type,
 | 
			
		||||
                            node.context,
 | 
			
		||||
                            node.children
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <div className="flex flex-none items-center gap-1.5">
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="ghost"
 | 
			
		||||
                        size="icon"
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
 | 
			
		||||
                            isExpanded && 'rotate-90',
 | 
			
		||||
                            'transition-transform duration-200'
 | 
			
		||||
                        )}
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                            e.stopPropagation();
 | 
			
		||||
                            if (node.isFolder) {
 | 
			
		||||
                                onToggle(
 | 
			
		||||
                                    node.id,
 | 
			
		||||
                                    node.type,
 | 
			
		||||
                                    node.context,
 | 
			
		||||
                                    node.children
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {node.isFolder &&
 | 
			
		||||
                            (isLoading ? (
 | 
			
		||||
                                <Loader2
 | 
			
		||||
                                    className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                        'text-white': isSelected,
 | 
			
		||||
                                    })}
 | 
			
		||||
                                />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <ChevronRight
 | 
			
		||||
                                    className={cn('size-3.5', {
 | 
			
		||||
                                        'text-white': isSelected,
 | 
			
		||||
                                    })}
 | 
			
		||||
                                    strokeWidth={2}
 | 
			
		||||
                                />
 | 
			
		||||
                            ))}
 | 
			
		||||
                    </Button>
 | 
			
		||||
 | 
			
		||||
                    {node.tooltip ? (
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger asChild>
 | 
			
		||||
                                {loadingNodeIds?.includes(node.id) ? (
 | 
			
		||||
                                    <Loader2
 | 
			
		||||
                                        className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                            'text-white': isSelected,
 | 
			
		||||
                                        })}
 | 
			
		||||
                                    />
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <IconComponent
 | 
			
		||||
                                        {...(isSelected
 | 
			
		||||
                                            ? { 'data-selected': true }
 | 
			
		||||
                                            : {})}
 | 
			
		||||
                                        {...iconProps}
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent
 | 
			
		||||
                                align="center"
 | 
			
		||||
                                className="max-w-[400px]"
 | 
			
		||||
                            >
 | 
			
		||||
                                {node.tooltip}
 | 
			
		||||
                            </TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                    ) : node.empty ? null : loadingNodeIds?.includes(
 | 
			
		||||
                          node.id
 | 
			
		||||
                      ) ? (
 | 
			
		||||
                        <Loader2
 | 
			
		||||
                            className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                // 'text-white': isSelected,
 | 
			
		||||
                            })}
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <IconComponent
 | 
			
		||||
                            {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                            {...iconProps}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <span
 | 
			
		||||
                    {...node.labelProps}
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'text-xs truncate min-w-0 flex-1 w-0',
 | 
			
		||||
                        isSelected && 'font-medium text-primary text-white',
 | 
			
		||||
                        node.labelProps?.className
 | 
			
		||||
                    )}
 | 
			
		||||
                    {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                >
 | 
			
		||||
                    {node.empty ? '' : node.name}
 | 
			
		||||
                </span>
 | 
			
		||||
                {renderActionsComponent && renderActionsComponent(node)}
 | 
			
		||||
                {isHovered && renderHoverComponent
 | 
			
		||||
                    ? renderHoverComponent(node)
 | 
			
		||||
                    : null}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <AnimatePresence initial={false}>
 | 
			
		||||
                {isExpanded && children && (
 | 
			
		||||
                    <motion.div
 | 
			
		||||
                        initial={{ height: 0, opacity: 0 }}
 | 
			
		||||
                        animate={{
 | 
			
		||||
                            height: 'auto',
 | 
			
		||||
                            opacity: 1,
 | 
			
		||||
                            transition: {
 | 
			
		||||
                                height: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.3 + children.length * 0.018,
 | 
			
		||||
                                        0.7
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                                opacity: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.2 + children.length * 0.012,
 | 
			
		||||
                                        0.4
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                            },
 | 
			
		||||
                        }}
 | 
			
		||||
                        exit={{
 | 
			
		||||
                            height: 0,
 | 
			
		||||
                            opacity: 0,
 | 
			
		||||
                            transition: {
 | 
			
		||||
                                height: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.2 + children.length * 0.01,
 | 
			
		||||
                                        0.45
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                                opacity: {
 | 
			
		||||
                                    duration: 0.1,
 | 
			
		||||
                                    ease: 'easeOut',
 | 
			
		||||
                                },
 | 
			
		||||
                            },
 | 
			
		||||
                        }}
 | 
			
		||||
                        style={{ overflow: 'hidden' }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {children.map((child) => (
 | 
			
		||||
                            <TreeNode
 | 
			
		||||
                                key={child.id}
 | 
			
		||||
                                node={child}
 | 
			
		||||
                                level={level + 1}
 | 
			
		||||
                                expanded={expanded}
 | 
			
		||||
                                loading={loading}
 | 
			
		||||
                                loadedChildren={loadedChildren}
 | 
			
		||||
                                hasMoreChildren={hasMoreChildren}
 | 
			
		||||
                                onToggle={onToggle}
 | 
			
		||||
                                onNodeClick={onNodeClick}
 | 
			
		||||
                                defaultIcon={DefaultIcon}
 | 
			
		||||
                                defaultFolderIcon={DefaultFolderIcon}
 | 
			
		||||
                                defaultIconProps={defaultIconProps}
 | 
			
		||||
                                defaultFolderIconProps={defaultFolderIconProps}
 | 
			
		||||
                                selectable={selectable}
 | 
			
		||||
                                selectedId={selectedId}
 | 
			
		||||
                                onSelect={onSelect}
 | 
			
		||||
                                className="mt-0.5"
 | 
			
		||||
                                renderHoverComponent={renderHoverComponent}
 | 
			
		||||
                                renderActionsComponent={renderActionsComponent}
 | 
			
		||||
                                loadingNodeIds={loadingNodeIds}
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {isLoading ? (
 | 
			
		||||
                            <TreeItemSkeleton
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    paddingLeft: `${level + 2 * 16 + 8}px`,
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </motion.div>
 | 
			
		||||
                )}
 | 
			
		||||
            </AnimatePresence>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function findNodeById<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>(
 | 
			
		||||
    nodes: TreeNode<Type, Context>[],
 | 
			
		||||
    id: string,
 | 
			
		||||
    initialPath: TreeNode<Type, Context>[] = []
 | 
			
		||||
): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
 | 
			
		||||
    const path: TreeNode<Type, Context>[] = [...initialPath];
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
        if (node.id === id) return { node, path };
 | 
			
		||||
        if (node.children) {
 | 
			
		||||
            const found = findNodeById(node.children, id, [...path, node]);
 | 
			
		||||
            if (found.node) {
 | 
			
		||||
                return found;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return { node: null, path };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import type { LucideIcon } from 'lucide-react';
 | 
			
		||||
import type React from 'react';
 | 
			
		||||
 | 
			
		||||
export interface TreeNode<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
    isFolder?: boolean;
 | 
			
		||||
    children?: TreeNode<Type, Context>[];
 | 
			
		||||
    icon?: LucideIcon;
 | 
			
		||||
    iconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    labelProps?: React.ComponentProps<'span'>;
 | 
			
		||||
    type: Type;
 | 
			
		||||
    unselectable?: boolean;
 | 
			
		||||
    tooltip?: string;
 | 
			
		||||
    context: Context[Type];
 | 
			
		||||
    empty?: boolean;
 | 
			
		||||
    className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FetchChildrenFunction<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> = (
 | 
			
		||||
    nodeId: string,
 | 
			
		||||
    nodeType: Type,
 | 
			
		||||
    nodeContext: Context[Type]
 | 
			
		||||
) => Promise<TreeNode<Type, Context>[]>;
 | 
			
		||||
 | 
			
		||||
export interface SelectableTreeProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    defaultSelectedId?: string;
 | 
			
		||||
    onSelectedChange?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    selectedId?: string;
 | 
			
		||||
    setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
			
		||||
import type { Dispatch, SetStateAction } from 'react';
 | 
			
		||||
import { useState, useCallback, useMemo } from 'react';
 | 
			
		||||
import type { TreeNode, FetchChildrenFunction } from './tree';
 | 
			
		||||
 | 
			
		||||
export interface ExpandedState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoadingState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoadedChildren<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    [key: string]: TreeNode<Type, Context>[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface HasMoreChildrenState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTree<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>({
 | 
			
		||||
    fetchChildren,
 | 
			
		||||
    expanded: expandedProp,
 | 
			
		||||
    setExpanded: setExpandedProp,
 | 
			
		||||
}: {
 | 
			
		||||
    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
			
		||||
    expanded?: ExpandedState;
 | 
			
		||||
    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
			
		||||
}) {
 | 
			
		||||
    const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
 | 
			
		||||
 | 
			
		||||
    const expanded = useMemo(
 | 
			
		||||
        () => expandedProp ?? expandedInternal,
 | 
			
		||||
        [expandedProp, expandedInternal]
 | 
			
		||||
    );
 | 
			
		||||
    const setExpanded = useCallback(
 | 
			
		||||
        (value: SetStateAction<ExpandedState>) => {
 | 
			
		||||
            if (setExpandedProp) {
 | 
			
		||||
                setExpandedProp(value);
 | 
			
		||||
            } else {
 | 
			
		||||
                setExpandedInternal(value);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [setExpandedProp, setExpandedInternal]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [loading, setLoading] = useState<LoadingState>({});
 | 
			
		||||
    const [loadedChildren, setLoadedChildren] = useState<
 | 
			
		||||
        LoadedChildren<Type, Context>
 | 
			
		||||
    >({});
 | 
			
		||||
    const [hasMoreChildren, setHasMoreChildren] =
 | 
			
		||||
        useState<HasMoreChildrenState>({});
 | 
			
		||||
 | 
			
		||||
    const mergeChildren = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
            staticChildren: TreeNode<Type, Context>[] = [],
 | 
			
		||||
            fetchedChildren: TreeNode<Type, Context>[] = []
 | 
			
		||||
        ) => {
 | 
			
		||||
            const fetchedChildrenIds = new Set(
 | 
			
		||||
                fetchedChildren.map((child) => child.id)
 | 
			
		||||
            );
 | 
			
		||||
            const uniqueStaticChildren = staticChildren.filter(
 | 
			
		||||
                (child) => !fetchedChildrenIds.has(child.id)
 | 
			
		||||
            );
 | 
			
		||||
            return [...uniqueStaticChildren, ...fetchedChildren];
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const toggleNode = useCallback(
 | 
			
		||||
        async (
 | 
			
		||||
            nodeId: string,
 | 
			
		||||
            nodeType: Type,
 | 
			
		||||
            nodeContext: Context[Type],
 | 
			
		||||
            staticChildren?: TreeNode<Type, Context>[]
 | 
			
		||||
        ) => {
 | 
			
		||||
            if (expanded[nodeId]) {
 | 
			
		||||
                // If we're collapsing, just update expanded state
 | 
			
		||||
                setExpanded((prev) => ({ ...prev, [nodeId]: false }));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get any previously fetched children
 | 
			
		||||
            const previouslyFetchedChildren = loadedChildren[nodeId] || [];
 | 
			
		||||
 | 
			
		||||
            // If we have static children, merge them with any previously fetched children
 | 
			
		||||
            if (staticChildren?.length) {
 | 
			
		||||
                const mergedChildren = mergeChildren(
 | 
			
		||||
                    staticChildren,
 | 
			
		||||
                    previouslyFetchedChildren
 | 
			
		||||
                );
 | 
			
		||||
                setLoadedChildren((prev) => ({
 | 
			
		||||
                    ...prev,
 | 
			
		||||
                    [nodeId]: mergedChildren,
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                // Only show "more loading" if we haven't fetched children before
 | 
			
		||||
                setHasMoreChildren((prev) => ({
 | 
			
		||||
                    ...prev,
 | 
			
		||||
                    [nodeId]: !previouslyFetchedChildren.length,
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Set expanded state immediately to show static/previously fetched children
 | 
			
		||||
            setExpanded((prev) => ({ ...prev, [nodeId]: true }));
 | 
			
		||||
 | 
			
		||||
            // If we haven't loaded dynamic children yet
 | 
			
		||||
            if (!previouslyFetchedChildren.length) {
 | 
			
		||||
                setLoading((prev) => ({ ...prev, [nodeId]: true }));
 | 
			
		||||
                try {
 | 
			
		||||
                    const fetchedChildren = await fetchChildren?.(
 | 
			
		||||
                        nodeId,
 | 
			
		||||
                        nodeType,
 | 
			
		||||
                        nodeContext
 | 
			
		||||
                    );
 | 
			
		||||
                    // Merge static and newly fetched children
 | 
			
		||||
                    const allChildren = mergeChildren(
 | 
			
		||||
                        staticChildren || [],
 | 
			
		||||
                        fetchedChildren
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    setLoadedChildren((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [nodeId]: allChildren,
 | 
			
		||||
                    }));
 | 
			
		||||
                    setHasMoreChildren((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [nodeId]: false,
 | 
			
		||||
                    }));
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('Error loading children:', error);
 | 
			
		||||
                } finally {
 | 
			
		||||
                    setLoading((prev) => ({ ...prev, [nodeId]: false }));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        expanded,
 | 
			
		||||
        loading,
 | 
			
		||||
        loadedChildren,
 | 
			
		||||
        hasMoreChildren,
 | 
			
		||||
        toggleNode,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,8 @@ export interface CanvasContext {
 | 
			
		||||
    }) => void;
 | 
			
		||||
    setOverlapGraph: (graph: Graph<string>) => void;
 | 
			
		||||
    overlapGraph: Graph<string>;
 | 
			
		||||
    setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
    showFilter: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
@@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
    fitView: emptyFn,
 | 
			
		||||
    setOverlapGraph: emptyFn,
 | 
			
		||||
    overlapGraph: createGraph(),
 | 
			
		||||
    setShowFilter: emptyFn,
 | 
			
		||||
    showFilter: false,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
    const [overlapGraph, setOverlapGraph] =
 | 
			
		||||
        useState<Graph<string>>(createGraph());
 | 
			
		||||
 | 
			
		||||
    const [showFilter, setShowFilter] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const reorderTables = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
            options: { updateHistory?: boolean } = {
 | 
			
		||||
@@ -77,6 +79,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
                fitView,
 | 
			
		||||
                setOverlapGraph,
 | 
			
		||||
                overlapGraph,
 | 
			
		||||
                setShowFilter,
 | 
			
		||||
                showFilter,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,9 @@ export interface ChartDBContext {
 | 
			
		||||
    events: EventEmitter<ChartDBEvent>;
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
 | 
			
		||||
    highlightedCustomType?: DBCustomType;
 | 
			
		||||
    highlightCustomTypeId: (id?: string) => void;
 | 
			
		||||
 | 
			
		||||
    filteredSchemas?: string[];
 | 
			
		||||
    filterSchemas: (schemaIds: string[]) => void;
 | 
			
		||||
 | 
			
		||||
@@ -277,6 +280,11 @@ 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>({
 | 
			
		||||
@@ -289,6 +297,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    areas: [],
 | 
			
		||||
    customTypes: [],
 | 
			
		||||
    schemas: [],
 | 
			
		||||
    highlightCustomTypeId: emptyFn,
 | 
			
		||||
    filteredSchemas: [],
 | 
			
		||||
    filterSchemas: emptyFn,
 | 
			
		||||
    currentDiagram: {
 | 
			
		||||
@@ -372,4 +381,9 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    removeCustomType: emptyFn,
 | 
			
		||||
    removeCustomTypes: emptyFn,
 | 
			
		||||
    updateCustomType: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Filters
 | 
			
		||||
    hiddenTableIds: [],
 | 
			
		||||
    addHiddenTableId: emptyFn,
 | 
			
		||||
    removeHiddenTableId: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { deepCopy, generateId } from '@/lib/utils';
 | 
			
		||||
import { randomColor } from '@/lib/colors';
 | 
			
		||||
@@ -29,6 +29,7 @@ import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '@/lib/domain/db-custom-type';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
 | 
			
		||||
export interface ChartDBProviderProps {
 | 
			
		||||
    diagram?: Diagram;
 | 
			
		||||
@@ -44,6 +45,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    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());
 | 
			
		||||
@@ -65,8 +71,12 @@ 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) =>
 | 
			
		||||
@@ -85,6 +95,14 @@ 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(
 | 
			
		||||
@@ -304,22 +322,27 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const addTables: ChartDBContext['addTables'] = useCallback(
 | 
			
		||||
        async (tables: DBTable[], options = { updateHistory: true }) => {
 | 
			
		||||
            setTables((currentTables) => [...currentTables, ...tables]);
 | 
			
		||||
        async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
 | 
			
		||||
            setTables((currentTables) => [...currentTables, ...tablesToAdd]);
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                ...tables.map((table) => db.addTable({ diagramId, table })),
 | 
			
		||||
                ...tablesToAdd.map((table) =>
 | 
			
		||||
                    db.addTable({ diagramId, table })
 | 
			
		||||
                ),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            events.emit({ action: 'add_tables', data: { tables } });
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'add_tables',
 | 
			
		||||
                data: { tables: tablesToAdd },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'addTables',
 | 
			
		||||
                    redoData: { tables },
 | 
			
		||||
                    undoData: { tableIds: tables.map((t) => t.id) },
 | 
			
		||||
                    redoData: { tables: tablesToAdd },
 | 
			
		||||
                    undoData: { tableIds: tablesToAdd.map((t) => t.id) },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
@@ -778,13 +801,23 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const fields = getTable(tableId)?.fields ?? [];
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? { ...table, fields: [...table.fields, field] }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
            setTables((tables) => {
 | 
			
		||||
                return tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        db.updateTable({
 | 
			
		||||
                            id: tableId,
 | 
			
		||||
                            attributes: {
 | 
			
		||||
                                ...table,
 | 
			
		||||
                                fields: [...table.fields, field],
 | 
			
		||||
                            },
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        return { ...table, fields: [...table.fields, field] };
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'add_field',
 | 
			
		||||
@@ -805,13 +838,6 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: [...table.fields, field],
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
@@ -1508,22 +1534,37 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        [db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const highlightCustomTypeId = useCallback(
 | 
			
		||||
        (id?: string) => setHighlightedCustomTypeId(id),
 | 
			
		||||
        [setHighlightedCustomTypeId]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const highlightedCustomType = useMemo(() => {
 | 
			
		||||
        return highlightedCustomTypeId
 | 
			
		||||
            ? customTypes.find((type) => type.id === highlightedCustomTypeId)
 | 
			
		||||
            : undefined;
 | 
			
		||||
    }, [highlightedCustomTypeId, customTypes]);
 | 
			
		||||
 | 
			
		||||
    const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            async (diagram) => {
 | 
			
		||||
            (diagram) => {
 | 
			
		||||
                setDiagramId(diagram.id);
 | 
			
		||||
                setDiagramName(diagram.name);
 | 
			
		||||
                setDatabaseType(diagram.databaseType);
 | 
			
		||||
                setDatabaseEdition(diagram.databaseEdition);
 | 
			
		||||
                setTables(diagram?.tables ?? []);
 | 
			
		||||
                setRelationships(diagram?.relationships ?? []);
 | 
			
		||||
                setDependencies(diagram?.dependencies ?? []);
 | 
			
		||||
                setAreas(diagram?.areas ?? []);
 | 
			
		||||
                setCustomTypes(diagram?.customTypes ?? []);
 | 
			
		||||
                setTables(diagram.tables ?? []);
 | 
			
		||||
                setRelationships(diagram.relationships ?? []);
 | 
			
		||||
                setDependencies(diagram.dependencies ?? []);
 | 
			
		||||
                setAreas(diagram.areas ?? []);
 | 
			
		||||
                setCustomTypes(diagram.customTypes ?? []);
 | 
			
		||||
                setDiagramCreatedAt(diagram.createdAt);
 | 
			
		||||
                setDiagramUpdatedAt(diagram.updatedAt);
 | 
			
		||||
                setHighlightedCustomTypeId(undefined);
 | 
			
		||||
 | 
			
		||||
                events.emit({ action: 'load_diagram', data: { diagram } });
 | 
			
		||||
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
                resetUndoStack();
 | 
			
		||||
            },
 | 
			
		||||
            [
 | 
			
		||||
                setDiagramId,
 | 
			
		||||
@@ -1537,7 +1578,10 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                setCustomTypes,
 | 
			
		||||
                setDiagramCreatedAt,
 | 
			
		||||
                setDiagramUpdatedAt,
 | 
			
		||||
                setHighlightedCustomTypeId,
 | 
			
		||||
                events,
 | 
			
		||||
                resetRedoStack,
 | 
			
		||||
                resetUndoStack,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -1704,6 +1748,29 @@ 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={{
 | 
			
		||||
@@ -1776,6 +1843,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                removeCustomType,
 | 
			
		||||
                removeCustomTypes,
 | 
			
		||||
                updateCustomType,
 | 
			
		||||
                hiddenTableIds,
 | 
			
		||||
                addHiddenTableId,
 | 
			
		||||
                removeHiddenTableId,
 | 
			
		||||
                highlightCustomTypeId,
 | 
			
		||||
                highlightedCustomType,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,23 @@ 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,8 +44,86 @@ 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 }}>
 | 
			
		||||
        <ConfigContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                config,
 | 
			
		||||
                updateConfig,
 | 
			
		||||
                getHiddenTablesForDiagram,
 | 
			
		||||
                setHiddenTablesForDiagram,
 | 
			
		||||
                hideTableForDiagram,
 | 
			
		||||
                unhideTableForDiagram,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
        </ConfigContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,10 @@ import type {
 | 
			
		||||
} from './diff-context';
 | 
			
		||||
import { diffContext } from './diff-context';
 | 
			
		||||
 | 
			
		||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
 | 
			
		||||
import {
 | 
			
		||||
    generateDiff,
 | 
			
		||||
    getDiffMapKey,
 | 
			
		||||
} from '@/lib/domain/diff/diff-check/diff-check';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useEventEmitter } from 'ahooks';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
 | 
			
		||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
			
		||||
    SHOW_ALL = 'show_all',
 | 
			
		||||
    TOGGLE_THEME = 'toggle_theme',
 | 
			
		||||
    TOGGLE_FILTER = 'toggle_filter',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcut {
 | 
			
		||||
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
 | 
			
		||||
        keyCombinationMac: 'meta+m',
 | 
			
		||||
        keyCombinationWin: 'ctrl+m',
 | 
			
		||||
    },
 | 
			
		||||
    [KeyboardShortcutAction.TOGGLE_FILTER]: {
 | 
			
		||||
        action: KeyboardShortcutAction.TOGGLE_FILTER,
 | 
			
		||||
        keyCombinationLabelMac: '⌘F',
 | 
			
		||||
        keyCombinationLabelWin: 'Ctrl+F',
 | 
			
		||||
        keyCombinationMac: 'meta+f',
 | 
			
		||||
        keyCombinationWin: 'ctrl+f',
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutForOS {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,9 @@ export interface LocalConfigContext {
 | 
			
		||||
    showCardinality: boolean;
 | 
			
		||||
    setShowCardinality: (showCardinality: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    showFieldAttributes: boolean;
 | 
			
		||||
    setShowFieldAttributes: (showFieldAttributes: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    hideMultiSchemaNotification: boolean;
 | 
			
		||||
    setHideMultiSchemaNotification: (
 | 
			
		||||
        hideMultiSchemaNotification: boolean
 | 
			
		||||
@@ -50,6 +53,9 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    showCardinality: true,
 | 
			
		||||
    setShowCardinality: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showFieldAttributes: true,
 | 
			
		||||
    setShowFieldAttributes: emptyFn,
 | 
			
		||||
 | 
			
		||||
    hideMultiSchemaNotification: false,
 | 
			
		||||
    setHideMultiSchemaNotification: emptyFn,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ 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';
 | 
			
		||||
@@ -34,6 +35,11 @@ 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) ||
 | 
			
		||||
@@ -119,6 +125,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                setSchemasFilter,
 | 
			
		||||
                showCardinality,
 | 
			
		||||
                setShowCardinality,
 | 
			
		||||
                showFieldAttributes,
 | 
			
		||||
                setShowFieldAttributes,
 | 
			
		||||
                hideMultiSchemaNotification,
 | 
			
		||||
                setHideMultiSchemaNotification,
 | 
			
		||||
                setGithubRepoOpened,
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,8 +1,7 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export type Theme = 'light' | 'dark' | 'system';
 | 
			
		||||
export type EffectiveTheme = Exclude<Theme, 'system'>;
 | 
			
		||||
import type { Theme, EffectiveTheme } from '@/lib/types';
 | 
			
		||||
export type { Theme, EffectiveTheme };
 | 
			
		||||
 | 
			
		||||
export interface ThemeContext {
 | 
			
		||||
    theme: Theme;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        handleThemeToggle,
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
            enableOnFormTags: true,
 | 
			
		||||
        },
 | 
			
		||||
        [handleThemeToggle]
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,22 @@ 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 } from 'monaco-editor';
 | 
			
		||||
import type { editor, IDisposable } from 'monaco-editor';
 | 
			
		||||
import { waitFor } from '@/lib/utils';
 | 
			
		||||
import {
 | 
			
		||||
    validateSQL,
 | 
			
		||||
    type ValidationResult,
 | 
			
		||||
} from '@/lib/data/sql-import/sql-validator';
 | 
			
		||||
import { SQLValidationStatus } from './sql-validation-status';
 | 
			
		||||
 | 
			
		||||
const calculateContentSizeMB = (content: string): number => {
 | 
			
		||||
    return content.length / (1024 * 1024); // Convert to MB
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const calculateIsLargeFile = (content: string): boolean => {
 | 
			
		||||
    const contentSizeMB = calculateContentSizeMB(content);
 | 
			
		||||
    return contentSizeMB > 2; // Consider large if over 2MB
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const errorScriptOutputMessage =
 | 
			
		||||
    'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
 | 
			
		||||
@@ -117,6 +132,7 @@ 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 { t } = useTranslation();
 | 
			
		||||
    const { isSm: isDesktop } = useBreakpoint('sm');
 | 
			
		||||
@@ -124,6 +140,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
 | 
			
		||||
    const [isCheckingJson, setIsCheckingJson] = useState(false);
 | 
			
		||||
    const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
 | 
			
		||||
    const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
    const [isAutoFixing, setIsAutoFixing] = useState(false);
 | 
			
		||||
    const [showAutoFixButton, setShowAutoFixButton] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setScriptResult('');
 | 
			
		||||
@@ -134,11 +155,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    // Check if the ddl is valid
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (importMethod !== 'ddl') {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!scriptResult.trim()) return;
 | 
			
		||||
        if (!scriptResult.trim()) {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // First run our validation based on database type
 | 
			
		||||
        const validation = validateSQL(scriptResult, databaseType);
 | 
			
		||||
        setSqlValidation(validation);
 | 
			
		||||
 | 
			
		||||
        // If we have auto-fixable errors, show the auto-fix button
 | 
			
		||||
        if (validation.fixedSQL && validation.errors.length > 0) {
 | 
			
		||||
            setShowAutoFixButton(true);
 | 
			
		||||
            // Don't try to parse invalid SQL
 | 
			
		||||
            setErrorMessage('SQL contains syntax errors');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Hide auto-fix button if no fixes available
 | 
			
		||||
        setShowAutoFixButton(false);
 | 
			
		||||
 | 
			
		||||
        // Validate the SQL (either original or already fixed)
 | 
			
		||||
        parseSQLError({
 | 
			
		||||
            sqlContent: scriptResult,
 | 
			
		||||
            sourceDatabaseType: databaseType,
 | 
			
		||||
@@ -184,8 +227,44 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        }
 | 
			
		||||
    }, [errorMessage.length, onImport, scriptResult]);
 | 
			
		||||
 | 
			
		||||
    const handleAutoFix = useCallback(() => {
 | 
			
		||||
        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);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        }
 | 
			
		||||
    }, [sqlValidation, setScriptResult]);
 | 
			
		||||
 | 
			
		||||
    const handleErrorClick = useCallback((line: number) => {
 | 
			
		||||
        if (editorRef.current) {
 | 
			
		||||
            // Set cursor to the error line
 | 
			
		||||
            editorRef.current.setPosition({ lineNumber: line, column: 1 });
 | 
			
		||||
            editorRef.current.revealLineInCenter(line);
 | 
			
		||||
            editorRef.current.focus();
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    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')
 | 
			
		||||
@@ -211,7 +290,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const handleCheckJson = useCallback(async () => {
 | 
			
		||||
        setIsCheckingJson(true);
 | 
			
		||||
 | 
			
		||||
        const fixedJson = await fixMetadataJson(scriptResult);
 | 
			
		||||
        await waitFor(1000);
 | 
			
		||||
        const fixedJson = fixMetadataJson(scriptResult);
 | 
			
		||||
 | 
			
		||||
        if (isStringMetadataJson(fixedJson)) {
 | 
			
		||||
            setScriptResult(fixedJson);
 | 
			
		||||
@@ -227,37 +307,69 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        setIsCheckingJson(false);
 | 
			
		||||
    }, [scriptResult, setScriptResult, formatEditor]);
 | 
			
		||||
 | 
			
		||||
    const detectAndSetImportMethod = useCallback(() => {
 | 
			
		||||
        const content = editorRef.current?.getValue();
 | 
			
		||||
        if (content && content.trim()) {
 | 
			
		||||
            const detectedType = detectContentType(content);
 | 
			
		||||
            if (detectedType && detectedType !== importMethod) {
 | 
			
		||||
                setImportMethod(detectedType);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }, [setImportMethod, importMethod]);
 | 
			
		||||
 | 
			
		||||
    const [editorDidMount, setEditorDidMount] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (editorRef.current && editorDidMount) {
 | 
			
		||||
            editorRef.current.onDidPaste(() => {
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    editorRef.current
 | 
			
		||||
                        ?.getAction('editor.action.formatDocument')
 | 
			
		||||
                        ?.run();
 | 
			
		||||
                }, 0);
 | 
			
		||||
                setTimeout(detectAndSetImportMethod, 0);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [detectAndSetImportMethod, editorDidMount]);
 | 
			
		||||
        // Cleanup paste handler on unmount
 | 
			
		||||
        return () => {
 | 
			
		||||
            if (pasteDisposableRef.current) {
 | 
			
		||||
                pasteDisposableRef.current.dispose();
 | 
			
		||||
                pasteDisposableRef.current = null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleEditorDidMount = useCallback(
 | 
			
		||||
        (editor: editor.IStandaloneCodeEditor) => {
 | 
			
		||||
            editorRef.current = editor;
 | 
			
		||||
            setEditorDidMount(true);
 | 
			
		||||
 | 
			
		||||
            // Cleanup previous disposable if it exists
 | 
			
		||||
            if (pasteDisposableRef.current) {
 | 
			
		||||
                pasteDisposableRef.current.dispose();
 | 
			
		||||
                pasteDisposableRef.current = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add paste handler for all modes
 | 
			
		||||
            const disposable = editor.onDidPaste(() => {
 | 
			
		||||
                const model = editor.getModel();
 | 
			
		||||
                if (!model) return;
 | 
			
		||||
 | 
			
		||||
                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) {
 | 
			
		||||
                        // For JSON mode, format after a short delay
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            editor
 | 
			
		||||
                                .getAction('editor.action.formatDocument')
 | 
			
		||||
                                ?.run();
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                    // 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
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            editor
 | 
			
		||||
                                .getAction('editor.action.formatDocument')
 | 
			
		||||
                                ?.run();
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                    // For DDL mode or large files, do NOT format
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            pasteDisposableRef.current = disposable;
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
        [importMethod, setImportMethod]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderHeader = useCallback(() => {
 | 
			
		||||
@@ -314,7 +426,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                    : 'dbml-light'
 | 
			
		||||
                            }
 | 
			
		||||
                            options={{
 | 
			
		||||
                                formatOnPaste: true,
 | 
			
		||||
                                formatOnPaste: false, // Never format on paste - we handle it manually
 | 
			
		||||
                                minimap: { enabled: false },
 | 
			
		||||
                                scrollBeyondLastLine: false,
 | 
			
		||||
                                automaticLayout: true,
 | 
			
		||||
@@ -343,10 +455,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                    </Suspense>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {errorMessage ? (
 | 
			
		||||
                    <div className="mt-2 flex shrink-0 items-center gap-2">
 | 
			
		||||
                        <p className="text-xs text-red-700">{errorMessage}</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
 | 
			
		||||
                    <SQLValidationStatus
 | 
			
		||||
                        validation={sqlValidation}
 | 
			
		||||
                        errorMessage={errorMessage}
 | 
			
		||||
                        isAutoFixing={isAutoFixing}
 | 
			
		||||
                        onErrorClick={handleErrorClick}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </div>
 | 
			
		||||
        ),
 | 
			
		||||
@@ -357,6 +472,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
            effectiveTheme,
 | 
			
		||||
            debouncedHandleInputChange,
 | 
			
		||||
            handleEditorDidMount,
 | 
			
		||||
            sqlValidation,
 | 
			
		||||
            isAutoFixing,
 | 
			
		||||
            handleErrorClick,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@@ -442,13 +560,28 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                )
 | 
			
		||||
                            )}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    ) : showAutoFixButton && importMethod === 'ddl' ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            variant="secondary"
 | 
			
		||||
                            onClick={handleAutoFix}
 | 
			
		||||
                            disabled={isAutoFixing}
 | 
			
		||||
                            className="bg-sky-600 text-white hover:bg-sky-700"
 | 
			
		||||
                        >
 | 
			
		||||
                            {isAutoFixing ? (
 | 
			
		||||
                                <Spinner size="small" />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                'Try auto-fix'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    ) : keepDialogAfterImport ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            variant="default"
 | 
			
		||||
                            disabled={
 | 
			
		||||
                                scriptResult.trim().length === 0 ||
 | 
			
		||||
                                errorMessage.length > 0
 | 
			
		||||
                                errorMessage.length > 0 ||
 | 
			
		||||
                                isAutoFixing
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={handleImport}
 | 
			
		||||
                        >
 | 
			
		||||
@@ -461,7 +594,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                variant="default"
 | 
			
		||||
                                disabled={
 | 
			
		||||
                                    scriptResult.trim().length === 0 ||
 | 
			
		||||
                                    errorMessage.length > 0
 | 
			
		||||
                                    errorMessage.length > 0 ||
 | 
			
		||||
                                    isAutoFixing
 | 
			
		||||
                                }
 | 
			
		||||
                                onClick={handleImport}
 | 
			
		||||
                            >
 | 
			
		||||
@@ -494,6 +628,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        handleCheckJson,
 | 
			
		||||
        goBack,
 | 
			
		||||
        t,
 | 
			
		||||
        importMethod,
 | 
			
		||||
        isAutoFixing,
 | 
			
		||||
        showAutoFixButton,
 | 
			
		||||
        handleAutoFix,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
 | 
			
		||||
import { Alert, AlertDescription } from '@/components/alert/alert';
 | 
			
		||||
import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
 | 
			
		||||
interface SQLValidationStatusProps {
 | 
			
		||||
    validation?: ValidationResult | null;
 | 
			
		||||
    errorMessage: string;
 | 
			
		||||
    isAutoFixing?: boolean;
 | 
			
		||||
    onErrorClick?: (line: number) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
 | 
			
		||||
    validation,
 | 
			
		||||
    errorMessage,
 | 
			
		||||
    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>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Separator className="mb-1 mt-2" />
 | 
			
		||||
 | 
			
		||||
            {hasErrors ? (
 | 
			
		||||
                <div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
 | 
			
		||||
                    <ScrollArea className="h-24">
 | 
			
		||||
                        <div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
 | 
			
		||||
                            {validation?.errors
 | 
			
		||||
                                .slice(0, 3)
 | 
			
		||||
                                .map((error, idx) => (
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        key={idx}
 | 
			
		||||
                                        className="flex items-start gap-2"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
 | 
			
		||||
                                        <div className="flex-1 text-sm text-red-700 dark:text-red-300">
 | 
			
		||||
                                            <button
 | 
			
		||||
                                                onClick={() =>
 | 
			
		||||
                                                    onErrorClick?.(error.line)
 | 
			
		||||
                                                }
 | 
			
		||||
                                                className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200"
 | 
			
		||||
                                                type="button"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                Line {error.line}
 | 
			
		||||
                                            </button>
 | 
			
		||||
                                            <span className="mx-1">:</span>
 | 
			
		||||
                                            <span className="text-xs">
 | 
			
		||||
                                                {error.message}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                            {error.suggestion && (
 | 
			
		||||
                                                <div className="mt-1 flex items-start gap-2">
 | 
			
		||||
                                                    <span className="text-xs font-medium ">
 | 
			
		||||
                                                        {error.suggestion}
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            )}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                ))}
 | 
			
		||||
                            {validation?.errors &&
 | 
			
		||||
                            validation?.errors.length > 3 ? (
 | 
			
		||||
                                <div className="flex items-center gap-2">
 | 
			
		||||
                                    <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
 | 
			
		||||
                                    <span className="text-xs font-medium">
 | 
			
		||||
                                        {validation.errors.length - 3} more
 | 
			
		||||
                                        error
 | 
			
		||||
                                        {validation.errors.length - 3 > 1
 | 
			
		||||
                                            ? 's'
 | 
			
		||||
                                            : ''}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {wasAutoFixed && !hasErrors ? (
 | 
			
		||||
                <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">
 | 
			
		||||
                        SQL syntax errors were automatically fixed. Your SQL is
 | 
			
		||||
                        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>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {!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}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
export const MAX_TABLES_IN_DIAGRAM = 500;
 | 
			
		||||
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
 | 
			
		||||
							
								
								
									
										683
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										683
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,683 @@
 | 
			
		||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
 | 
			
		||||
import { Checkbox } from '@/components/checkbox/checkbox';
 | 
			
		||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { generateTableKey } from '@/lib/domain';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import {
 | 
			
		||||
    Pagination,
 | 
			
		||||
    PaginationContent,
 | 
			
		||||
    PaginationItem,
 | 
			
		||||
    PaginationPrevious,
 | 
			
		||||
    PaginationNext,
 | 
			
		||||
} from '@/components/pagination/pagination';
 | 
			
		||||
import { MAX_TABLES_IN_DIAGRAM } from './constants';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
export interface SelectTablesProps {
 | 
			
		||||
    databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
    onImport: ({
 | 
			
		||||
        selectedTables,
 | 
			
		||||
        databaseMetadata,
 | 
			
		||||
    }: {
 | 
			
		||||
        selectedTables?: SelectedTable[];
 | 
			
		||||
        databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    onBack: () => void;
 | 
			
		||||
    isLoading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TABLES_PER_PAGE = 10;
 | 
			
		||||
 | 
			
		||||
interface TableInfo {
 | 
			
		||||
    key: string;
 | 
			
		||||
    schema?: string;
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    fullName: string;
 | 
			
		||||
    type: 'table' | 'view';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SelectTables: React.FC<SelectTablesProps> = ({
 | 
			
		||||
    databaseMetadata,
 | 
			
		||||
    onImport,
 | 
			
		||||
    onBack,
 | 
			
		||||
    isLoading = false,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [searchTerm, setSearchTerm] = useState('');
 | 
			
		||||
    const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
    const [showTables, setShowTables] = useState(true);
 | 
			
		||||
    const [showViews, setShowViews] = useState(false);
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [isImporting, setIsImporting] = useState(false);
 | 
			
		||||
 | 
			
		||||
    // Prepare all tables and views with their metadata
 | 
			
		||||
    const allTables = useMemo(() => {
 | 
			
		||||
        const tables: TableInfo[] = [];
 | 
			
		||||
 | 
			
		||||
        // Add regular tables
 | 
			
		||||
        databaseMetadata?.tables.forEach((table) => {
 | 
			
		||||
            const schema = schemaNameToDomainSchemaName(table.schema);
 | 
			
		||||
            const tableName = table.table;
 | 
			
		||||
 | 
			
		||||
            const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
 | 
			
		||||
 | 
			
		||||
            tables.push({
 | 
			
		||||
                key,
 | 
			
		||||
                schema,
 | 
			
		||||
                tableName,
 | 
			
		||||
                fullName: schema ? `${schema}.${tableName}` : tableName,
 | 
			
		||||
                type: 'table',
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add views
 | 
			
		||||
        databaseMetadata?.views?.forEach((view) => {
 | 
			
		||||
            const schema = schemaNameToDomainSchemaName(view.schema);
 | 
			
		||||
            const viewName = view.view_name;
 | 
			
		||||
 | 
			
		||||
            if (!viewName) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const key = `view:${generateTableKey({
 | 
			
		||||
                tableName: viewName,
 | 
			
		||||
                schemaName: schema,
 | 
			
		||||
            })}`;
 | 
			
		||||
 | 
			
		||||
            tables.push({
 | 
			
		||||
                key,
 | 
			
		||||
                schema,
 | 
			
		||||
                tableName: viewName,
 | 
			
		||||
                fullName:
 | 
			
		||||
                    schema === 'default' ? viewName : `${schema}.${viewName}`,
 | 
			
		||||
                type: 'view',
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
 | 
			
		||||
    }, [databaseMetadata?.tables, databaseMetadata?.views]);
 | 
			
		||||
 | 
			
		||||
    // Count tables and views separately
 | 
			
		||||
    const tableCount = useMemo(
 | 
			
		||||
        () => allTables.filter((t) => t.type === 'table').length,
 | 
			
		||||
        [allTables]
 | 
			
		||||
    );
 | 
			
		||||
    const viewCount = useMemo(
 | 
			
		||||
        () => allTables.filter((t) => t.type === 'view').length,
 | 
			
		||||
        [allTables]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Initialize selectedTables with all tables (not views) if less than 100 tables
 | 
			
		||||
    const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
 | 
			
		||||
        const tables = allTables.filter((t) => t.type === 'table');
 | 
			
		||||
        if (tables.length < MAX_TABLES_IN_DIAGRAM) {
 | 
			
		||||
            return new Set(tables.map((t) => t.key));
 | 
			
		||||
        }
 | 
			
		||||
        return new Set();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Filter tables based on search term and type filters
 | 
			
		||||
    const filteredTables = useMemo(() => {
 | 
			
		||||
        let filtered = allTables;
 | 
			
		||||
 | 
			
		||||
        // Filter by type
 | 
			
		||||
        filtered = filtered.filter((table) => {
 | 
			
		||||
            if (table.type === 'table' && !showTables) return false;
 | 
			
		||||
            if (table.type === 'view' && !showViews) return false;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Filter by search term
 | 
			
		||||
        if (searchTerm.trim()) {
 | 
			
		||||
            const searchLower = searchTerm.toLowerCase();
 | 
			
		||||
            filtered = filtered.filter(
 | 
			
		||||
                (table) =>
 | 
			
		||||
                    table.tableName.toLowerCase().includes(searchLower) ||
 | 
			
		||||
                    table.schema?.toLowerCase().includes(searchLower) ||
 | 
			
		||||
                    table.fullName.toLowerCase().includes(searchLower)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return filtered;
 | 
			
		||||
    }, [allTables, searchTerm, showTables, showViews]);
 | 
			
		||||
 | 
			
		||||
    // Calculate pagination
 | 
			
		||||
    const totalPages = useMemo(
 | 
			
		||||
        () => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
 | 
			
		||||
        [filteredTables.length]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const paginatedTables = useMemo(() => {
 | 
			
		||||
        const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
 | 
			
		||||
        const endIndex = startIndex + TABLES_PER_PAGE;
 | 
			
		||||
        return filteredTables.slice(startIndex, endIndex);
 | 
			
		||||
    }, [filteredTables, currentPage]);
 | 
			
		||||
 | 
			
		||||
    // Get currently visible selected tables
 | 
			
		||||
    const visibleSelectedTables = useMemo(() => {
 | 
			
		||||
        return paginatedTables.filter((table) => selectedTables.has(table.key));
 | 
			
		||||
    }, [paginatedTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const canAddMore = useMemo(
 | 
			
		||||
        () => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
 | 
			
		||||
        [selectedTables.size]
 | 
			
		||||
    );
 | 
			
		||||
    const hasSearchResults = useMemo(
 | 
			
		||||
        () => filteredTables.length > 0,
 | 
			
		||||
        [filteredTables.length]
 | 
			
		||||
    );
 | 
			
		||||
    const allVisibleSelected = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            visibleSelectedTables.length === paginatedTables.length &&
 | 
			
		||||
            paginatedTables.length > 0,
 | 
			
		||||
        [visibleSelectedTables.length, paginatedTables.length]
 | 
			
		||||
    );
 | 
			
		||||
    const canSelectAllFiltered = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            filteredTables.length > 0 &&
 | 
			
		||||
            filteredTables.some((table) => !selectedTables.has(table.key)) &&
 | 
			
		||||
            canAddMore,
 | 
			
		||||
        [filteredTables, selectedTables, canAddMore]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Reset to first page when search changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setCurrentPage(1);
 | 
			
		||||
    }, [searchTerm]);
 | 
			
		||||
 | 
			
		||||
    const handleTableToggle = useCallback(
 | 
			
		||||
        (tableKey: string) => {
 | 
			
		||||
            const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
            if (newSelected.has(tableKey)) {
 | 
			
		||||
                newSelected.delete(tableKey);
 | 
			
		||||
            } else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
 | 
			
		||||
                newSelected.add(tableKey);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setSelectedTables(newSelected);
 | 
			
		||||
        },
 | 
			
		||||
        [selectedTables]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleTogglePageSelection = useCallback(() => {
 | 
			
		||||
        const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
        if (allVisibleSelected) {
 | 
			
		||||
            // Deselect all on current page
 | 
			
		||||
            for (const table of paginatedTables) {
 | 
			
		||||
                newSelected.delete(table.key);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Select all on current page
 | 
			
		||||
            for (const table of paginatedTables) {
 | 
			
		||||
                if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
			
		||||
                newSelected.add(table.key);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedTables(newSelected);
 | 
			
		||||
    }, [allVisibleSelected, paginatedTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const handleSelectAllFiltered = useCallback(() => {
 | 
			
		||||
        const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
        for (const table of filteredTables) {
 | 
			
		||||
            if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
			
		||||
            newSelected.add(table.key);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedTables(newSelected);
 | 
			
		||||
    }, [filteredTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const handleNextPage = useCallback(() => {
 | 
			
		||||
        if (currentPage < totalPages) {
 | 
			
		||||
            setCurrentPage(currentPage + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentPage, totalPages]);
 | 
			
		||||
 | 
			
		||||
    const handlePrevPage = useCallback(() => {
 | 
			
		||||
        if (currentPage > 1) {
 | 
			
		||||
            setCurrentPage(currentPage - 1);
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentPage]);
 | 
			
		||||
 | 
			
		||||
    const handleClearSelection = useCallback(() => {
 | 
			
		||||
        setSelectedTables(new Set());
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(async () => {
 | 
			
		||||
        if (isImporting) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setIsImporting(true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const selectedTableObjects: SelectedTable[] = Array.from(
 | 
			
		||||
                selectedTables
 | 
			
		||||
            )
 | 
			
		||||
                .map((key): SelectedTable | null => {
 | 
			
		||||
                    const table = allTables.find((t) => t.key === key);
 | 
			
		||||
                    if (!table) return null;
 | 
			
		||||
 | 
			
		||||
                    return {
 | 
			
		||||
                        schema: table.schema,
 | 
			
		||||
                        table: table.tableName,
 | 
			
		||||
                        type: table.type,
 | 
			
		||||
                    } satisfies SelectedTable;
 | 
			
		||||
                })
 | 
			
		||||
                .filter((t): t is SelectedTable => t !== null);
 | 
			
		||||
 | 
			
		||||
            await onImport({
 | 
			
		||||
                selectedTables: selectedTableObjects,
 | 
			
		||||
                databaseMetadata,
 | 
			
		||||
            });
 | 
			
		||||
        } finally {
 | 
			
		||||
            setIsImporting(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
 | 
			
		||||
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const renderPagination = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <Pagination>
 | 
			
		||||
                <PaginationContent>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <PaginationPrevious
 | 
			
		||||
                            onClick={handlePrevPage}
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'cursor-pointer',
 | 
			
		||||
                                currentPage === 1 &&
 | 
			
		||||
                                    'pointer-events-none opacity-50'
 | 
			
		||||
                            )}
 | 
			
		||||
                        />
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <span className="px-3 text-sm text-muted-foreground">
 | 
			
		||||
                            Page {currentPage} of {totalPages}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <PaginationNext
 | 
			
		||||
                            onClick={handleNextPage}
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'cursor-pointer',
 | 
			
		||||
                                (currentPage >= totalPages ||
 | 
			
		||||
                                    filteredTables.length === 0) &&
 | 
			
		||||
                                    'pointer-events-none opacity-50'
 | 
			
		||||
                            )}
 | 
			
		||||
                        />
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                </PaginationContent>
 | 
			
		||||
            </Pagination>
 | 
			
		||||
        ),
 | 
			
		||||
        [
 | 
			
		||||
            currentPage,
 | 
			
		||||
            totalPages,
 | 
			
		||||
            handlePrevPage,
 | 
			
		||||
            handleNextPage,
 | 
			
		||||
            filteredTables.length,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (isLoading) {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="flex h-[400px] items-center justify-center">
 | 
			
		||||
                <div className="text-center">
 | 
			
		||||
                    <Spinner className="mb-4" />
 | 
			
		||||
                    <p className="text-sm text-muted-foreground">
 | 
			
		||||
                        Parsing database metadata...
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <DialogHeader>
 | 
			
		||||
                <DialogTitle>Select Tables to Import</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                    {tableCount} {tableCount === 1 ? 'table' : 'tables'}
 | 
			
		||||
                    {viewCount > 0 && (
 | 
			
		||||
                        <>
 | 
			
		||||
                            {' and '}
 | 
			
		||||
                            {viewCount} {viewCount === 1 ? 'view' : 'views'}
 | 
			
		||||
                        </>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {' found. '}
 | 
			
		||||
                    {allTables.length > MAX_TABLES_IN_DIAGRAM
 | 
			
		||||
                        ? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
 | 
			
		||||
                        : 'Choose which ones to import.'}
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
            <DialogInternalContent>
 | 
			
		||||
                <div className="flex h-full flex-col space-y-4">
 | 
			
		||||
                    {/* Warning/Info Banner */}
 | 
			
		||||
                    {allTables.length > MAX_TABLES_IN_DIAGRAM ? (
 | 
			
		||||
                        <div
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'flex items-center gap-2 rounded-lg p-3 text-sm',
 | 
			
		||||
                                'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
 | 
			
		||||
                            )}
 | 
			
		||||
                        >
 | 
			
		||||
                            <AlertCircle className="size-4 shrink-0" />
 | 
			
		||||
                            <span>
 | 
			
		||||
                                Due to performance limitations, you can import a
 | 
			
		||||
                                maximum of {MAX_TABLES_IN_DIAGRAM} tables.
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {/* Search Input */}
 | 
			
		||||
                    <div className="relative">
 | 
			
		||||
                        <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
 | 
			
		||||
                        <Input
 | 
			
		||||
                            placeholder="Search tables..."
 | 
			
		||||
                            value={searchTerm}
 | 
			
		||||
                            onChange={(e) => setSearchTerm(e.target.value)}
 | 
			
		||||
                            className="px-9"
 | 
			
		||||
                        />
 | 
			
		||||
                        {searchTerm && (
 | 
			
		||||
                            <button
 | 
			
		||||
                                onClick={() => setSearchTerm('')}
 | 
			
		||||
                                className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
 | 
			
		||||
                            >
 | 
			
		||||
                                <X className="size-4" />
 | 
			
		||||
                            </button>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Selection Status and Actions - Responsive layout */}
 | 
			
		||||
                    <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
 | 
			
		||||
                        {/* Left side: selection count -> checkboxes -> results found */}
 | 
			
		||||
                        <div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
 | 
			
		||||
                            <div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
 | 
			
		||||
                                <span className="text-center font-medium">
 | 
			
		||||
                                    {selectedTables.size} /{' '}
 | 
			
		||||
                                    {Math.min(
 | 
			
		||||
                                        MAX_TABLES_IN_DIAGRAM,
 | 
			
		||||
                                        allTables.length
 | 
			
		||||
                                    )}{' '}
 | 
			
		||||
                                    items selected
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div className="flex items-center gap-3 sm:border-x sm:px-4">
 | 
			
		||||
                                <div className="flex items-center gap-2">
 | 
			
		||||
                                    <Checkbox
 | 
			
		||||
                                        checked={showTables}
 | 
			
		||||
                                        onCheckedChange={(checked) => {
 | 
			
		||||
                                            // Prevent unchecking if it's the only one checked
 | 
			
		||||
                                            if (!checked && !showViews) return;
 | 
			
		||||
                                            setShowTables(!!checked);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Table
 | 
			
		||||
                                        className="size-4"
 | 
			
		||||
                                        strokeWidth={1.5}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <span>tables</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div className="flex items-center gap-2">
 | 
			
		||||
                                    <Checkbox
 | 
			
		||||
                                        checked={showViews}
 | 
			
		||||
                                        onCheckedChange={(checked) => {
 | 
			
		||||
                                            // Prevent unchecking if it's the only one checked
 | 
			
		||||
                                            if (!checked && !showTables) return;
 | 
			
		||||
                                            setShowViews(!!checked);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <View
 | 
			
		||||
                                        className="size-4"
 | 
			
		||||
                                        strokeWidth={1.5}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <span>views</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <span className="hidden text-muted-foreground sm:inline">
 | 
			
		||||
                                {filteredTables.length}{' '}
 | 
			
		||||
                                {filteredTables.length === 1
 | 
			
		||||
                                    ? 'result'
 | 
			
		||||
                                    : 'results'}{' '}
 | 
			
		||||
                                found
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        {/* Right side: action buttons */}
 | 
			
		||||
                        <div className="flex flex-wrap items-center justify-center gap-2">
 | 
			
		||||
                            {hasSearchResults && (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {/* Show page selection button when not searching and no selection */}
 | 
			
		||||
                                    {!searchTerm &&
 | 
			
		||||
                                        selectedTables.size === 0 && (
 | 
			
		||||
                                            <Button
 | 
			
		||||
                                                variant="outline"
 | 
			
		||||
                                                size="sm"
 | 
			
		||||
                                                onClick={
 | 
			
		||||
                                                    handleTogglePageSelection
 | 
			
		||||
                                                }
 | 
			
		||||
                                                disabled={
 | 
			
		||||
                                                    paginatedTables.length === 0
 | 
			
		||||
                                                }
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {allVisibleSelected
 | 
			
		||||
                                                    ? 'Deselect'
 | 
			
		||||
                                                    : 'Select'}{' '}
 | 
			
		||||
                                                page
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    {/* Show Select all button when there are unselected tables */}
 | 
			
		||||
                                    {canSelectAllFiltered &&
 | 
			
		||||
                                        selectedTables.size === 0 && (
 | 
			
		||||
                                            <Button
 | 
			
		||||
                                                variant="outline"
 | 
			
		||||
                                                size="sm"
 | 
			
		||||
                                                onClick={
 | 
			
		||||
                                                    handleSelectAllFiltered
 | 
			
		||||
                                                }
 | 
			
		||||
                                                disabled={!canSelectAllFiltered}
 | 
			
		||||
                                                title={(() => {
 | 
			
		||||
                                                    const unselectedCount =
 | 
			
		||||
                                                        filteredTables.filter(
 | 
			
		||||
                                                            (table) =>
 | 
			
		||||
                                                                !selectedTables.has(
 | 
			
		||||
                                                                    table.key
 | 
			
		||||
                                                                )
 | 
			
		||||
                                                        ).length;
 | 
			
		||||
                                                    const remainingCapacity =
 | 
			
		||||
                                                        MAX_TABLES_IN_DIAGRAM -
 | 
			
		||||
                                                        selectedTables.size;
 | 
			
		||||
                                                    if (
 | 
			
		||||
                                                        unselectedCount >
 | 
			
		||||
                                                        remainingCapacity
 | 
			
		||||
                                                    ) {
 | 
			
		||||
                                                        return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    return undefined;
 | 
			
		||||
                                                })()}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {(() => {
 | 
			
		||||
                                                    const unselectedCount =
 | 
			
		||||
                                                        filteredTables.filter(
 | 
			
		||||
                                                            (table) =>
 | 
			
		||||
                                                                !selectedTables.has(
 | 
			
		||||
                                                                    table.key
 | 
			
		||||
                                                                )
 | 
			
		||||
                                                        ).length;
 | 
			
		||||
                                                    const remainingCapacity =
 | 
			
		||||
                                                        MAX_TABLES_IN_DIAGRAM -
 | 
			
		||||
                                                        selectedTables.size;
 | 
			
		||||
                                                    if (
 | 
			
		||||
                                                        unselectedCount >
 | 
			
		||||
                                                        remainingCapacity
 | 
			
		||||
                                                    ) {
 | 
			
		||||
                                                        return `Select ${remainingCapacity} of ${unselectedCount}`;
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    return `Select all ${unselectedCount}`;
 | 
			
		||||
                                                })()}
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        )}
 | 
			
		||||
                                </>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {selectedTables.size > 0 && (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {/* Show page selection/deselection button when user has selections */}
 | 
			
		||||
                                    {paginatedTables.length > 0 && (
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            size="sm"
 | 
			
		||||
                                            onClick={handleTogglePageSelection}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {allVisibleSelected
 | 
			
		||||
                                                ? 'Deselect'
 | 
			
		||||
                                                : 'Select'}{' '}
 | 
			
		||||
                                            page
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        size="sm"
 | 
			
		||||
                                        onClick={handleClearSelection}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Clear selection
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {/* Table List */}
 | 
			
		||||
                <div className="flex min-h-[428px] flex-1 flex-col">
 | 
			
		||||
                    {hasSearchResults ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <div className="flex-1 py-4">
 | 
			
		||||
                                <div className="space-y-1">
 | 
			
		||||
                                    {paginatedTables.map((table) => {
 | 
			
		||||
                                        const isSelected = selectedTables.has(
 | 
			
		||||
                                            table.key
 | 
			
		||||
                                        );
 | 
			
		||||
                                        const isDisabled =
 | 
			
		||||
                                            !isSelected &&
 | 
			
		||||
                                            selectedTables.size >=
 | 
			
		||||
                                                MAX_TABLES_IN_DIAGRAM;
 | 
			
		||||
 | 
			
		||||
                                        return (
 | 
			
		||||
                                            <div
 | 
			
		||||
                                                key={table.key}
 | 
			
		||||
                                                className={cn(
 | 
			
		||||
                                                    'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
 | 
			
		||||
                                                    {
 | 
			
		||||
                                                        'cursor-not-allowed':
 | 
			
		||||
                                                            isDisabled,
 | 
			
		||||
 | 
			
		||||
                                                        'bg-muted hover:bg-muted/80':
 | 
			
		||||
                                                            isSelected,
 | 
			
		||||
                                                        'hover:bg-accent':
 | 
			
		||||
                                                            !isSelected &&
 | 
			
		||||
                                                            !isDisabled,
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                <Checkbox
 | 
			
		||||
                                                    checked={isSelected}
 | 
			
		||||
                                                    disabled={isDisabled}
 | 
			
		||||
                                                    onCheckedChange={() =>
 | 
			
		||||
                                                        handleTableToggle(
 | 
			
		||||
                                                            table.key
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                />
 | 
			
		||||
                                                {table.type === 'view' ? (
 | 
			
		||||
                                                    <View
 | 
			
		||||
                                                        className="size-4"
 | 
			
		||||
                                                        strokeWidth={1.5}
 | 
			
		||||
                                                    />
 | 
			
		||||
                                                ) : (
 | 
			
		||||
                                                    <Table
 | 
			
		||||
                                                        className="size-4"
 | 
			
		||||
                                                        strokeWidth={1.5}
 | 
			
		||||
                                                    />
 | 
			
		||||
                                                )}
 | 
			
		||||
                                                <span className="flex-1">
 | 
			
		||||
                                                    {table.schema ? (
 | 
			
		||||
                                                        <span className="text-muted-foreground">
 | 
			
		||||
                                                            {table.schema}.
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                    ) : null}
 | 
			
		||||
                                                    <span className="font-medium">
 | 
			
		||||
                                                        {table.tableName}
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                    {table.type === 'view' && (
 | 
			
		||||
                                                        <span className="ml-2 text-xs text-muted-foreground">
 | 
			
		||||
                                                            (view)
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                                {isSelected && (
 | 
			
		||||
                                                    <Check className="size-4 text-pink-600" />
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        );
 | 
			
		||||
                                    })}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <div className="flex h-full items-center justify-center py-4">
 | 
			
		||||
                            <p className="text-sm text-muted-foreground">
 | 
			
		||||
                                {searchTerm
 | 
			
		||||
                                    ? 'No tables found matching your search.'
 | 
			
		||||
                                    : 'Start typing to search for tables...'}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
                {isDesktop ? renderPagination() : null}
 | 
			
		||||
            </DialogInternalContent>
 | 
			
		||||
            <DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
 | 
			
		||||
                <Button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    variant="secondary"
 | 
			
		||||
                    onClick={onBack}
 | 
			
		||||
                    disabled={isImporting}
 | 
			
		||||
                >
 | 
			
		||||
                    {t('new_diagram_dialog.back')}
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    onClick={handleConfirm}
 | 
			
		||||
                    disabled={selectedTables.size === 0 || isImporting}
 | 
			
		||||
                    className="bg-pink-500 text-white hover:bg-pink-600"
 | 
			
		||||
                >
 | 
			
		||||
                    {isImporting ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Spinner className="mr-2 size-4 text-white" />
 | 
			
		||||
                            Importing...
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        `Import ${selectedTables.size} Tables`
 | 
			
		||||
                    )}
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                {!isDesktop ? renderPagination() : null}
 | 
			
		||||
            </DialogFooter>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
export enum CreateDiagramDialogStep {
 | 
			
		||||
    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
			
		||||
    IMPORT_DATABASE = 'IMPORT_DATABASE',
 | 
			
		||||
    SELECT_TABLES = 'SELECT_TABLES',
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { SelectDatabase } from './select-database/select-database';
 | 
			
		||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
			
		||||
import { ImportDatabase } from '../common/import-database/import-database';
 | 
			
		||||
import { SelectTables } from '../common/select-tables/select-tables';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
			
		||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
 | 
			
		||||
 | 
			
		||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
    const { listDiagrams, addDiagram } = useStorage();
 | 
			
		||||
    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
 | 
			
		||||
    const [isParsingMetadata, setIsParsingMetadata] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setDatabaseEdition(undefined);
 | 
			
		||||
@@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        setDatabaseEdition(undefined);
 | 
			
		||||
        setScriptResult('');
 | 
			
		||||
        setImportMethod('query');
 | 
			
		||||
        setParsedMetadata(undefined);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
 | 
			
		||||
    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
			
		||||
 | 
			
		||||
    const importNewDiagram = useCallback(async () => {
 | 
			
		||||
        let diagram: Diagram | undefined;
 | 
			
		||||
    const importNewDiagram = useCallback(
 | 
			
		||||
        async ({
 | 
			
		||||
            selectedTables,
 | 
			
		||||
            databaseMetadata,
 | 
			
		||||
        }: {
 | 
			
		||||
            selectedTables?: SelectedTable[];
 | 
			
		||||
            databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
        } = {}) => {
 | 
			
		||||
            let diagram: Diagram | undefined;
 | 
			
		||||
 | 
			
		||||
        if (importMethod === 'ddl') {
 | 
			
		||||
            diagram = await sqlImportToDiagram({
 | 
			
		||||
                sqlContent: scriptResult,
 | 
			
		||||
                sourceDatabaseType: databaseType,
 | 
			
		||||
                targetDatabaseType: databaseType,
 | 
			
		||||
            if (importMethod === 'ddl') {
 | 
			
		||||
                diagram = await sqlImportToDiagram({
 | 
			
		||||
                    sqlContent: scriptResult,
 | 
			
		||||
                    sourceDatabaseType: databaseType,
 | 
			
		||||
                    targetDatabaseType: databaseType,
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                let metadata: DatabaseMetadata | undefined = databaseMetadata;
 | 
			
		||||
 | 
			
		||||
                if (!metadata) {
 | 
			
		||||
                    metadata = loadDatabaseMetadata(scriptResult);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (selectedTables && selectedTables.length > 0) {
 | 
			
		||||
                    metadata = filterMetadataByTables({
 | 
			
		||||
                        metadata,
 | 
			
		||||
                        selectedTables,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
                    databaseType,
 | 
			
		||||
                    databaseMetadata: metadata,
 | 
			
		||||
                    diagramNumber,
 | 
			
		||||
                    databaseEdition:
 | 
			
		||||
                        databaseEdition?.trim().length === 0
 | 
			
		||||
                            ? undefined
 | 
			
		||||
                            : databaseEdition,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await addDiagram({ diagram });
 | 
			
		||||
            await updateConfig({
 | 
			
		||||
                config: { defaultDiagramId: diagram.id },
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            const databaseMetadata: DatabaseMetadata =
 | 
			
		||||
                loadDatabaseMetadata(scriptResult);
 | 
			
		||||
 | 
			
		||||
            diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
                databaseType,
 | 
			
		||||
                databaseMetadata,
 | 
			
		||||
                diagramNumber,
 | 
			
		||||
                databaseEdition:
 | 
			
		||||
                    databaseEdition?.trim().length === 0
 | 
			
		||||
                        ? undefined
 | 
			
		||||
                        : databaseEdition,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await addDiagram({ diagram });
 | 
			
		||||
        await updateConfig({ config: { defaultDiagramId: diagram.id } });
 | 
			
		||||
        closeCreateDiagramDialog();
 | 
			
		||||
        navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
    }, [
 | 
			
		||||
        importMethod,
 | 
			
		||||
        databaseType,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
        databaseEdition,
 | 
			
		||||
        closeCreateDiagramDialog,
 | 
			
		||||
        navigate,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
        scriptResult,
 | 
			
		||||
        diagramNumber,
 | 
			
		||||
    ]);
 | 
			
		||||
            closeCreateDiagramDialog();
 | 
			
		||||
            navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            importMethod,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            addDiagram,
 | 
			
		||||
            databaseEdition,
 | 
			
		||||
            closeCreateDiagramDialog,
 | 
			
		||||
            navigate,
 | 
			
		||||
            updateConfig,
 | 
			
		||||
            scriptResult,
 | 
			
		||||
            diagramNumber,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createEmptyDiagram = useCallback(async () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const importNewDiagramOrFilterTables = useCallback(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            setIsParsingMetadata(true);
 | 
			
		||||
 | 
			
		||||
            if (importMethod === 'ddl') {
 | 
			
		||||
                await importNewDiagram();
 | 
			
		||||
            } else {
 | 
			
		||||
                // Parse metadata asynchronously to avoid blocking the UI
 | 
			
		||||
                const metadata = await new Promise<DatabaseMetadata>(
 | 
			
		||||
                    (resolve, reject) => {
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            try {
 | 
			
		||||
                                const result =
 | 
			
		||||
                                    loadDatabaseMetadata(scriptResult);
 | 
			
		||||
                                resolve(result);
 | 
			
		||||
                            } catch (err) {
 | 
			
		||||
                                reject(err);
 | 
			
		||||
                            }
 | 
			
		||||
                        }, 0);
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const totalTablesAndViews =
 | 
			
		||||
                    metadata.tables.length + (metadata.views?.length || 0);
 | 
			
		||||
 | 
			
		||||
                setParsedMetadata(metadata);
 | 
			
		||||
 | 
			
		||||
                // Check if it's a large database that needs table selection
 | 
			
		||||
                if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
 | 
			
		||||
                    setStep(CreateDiagramDialogStep.SELECT_TABLES);
 | 
			
		||||
                } else {
 | 
			
		||||
                    await importNewDiagram({
 | 
			
		||||
                        databaseMetadata: metadata,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            setIsParsingMetadata(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [importMethod, scriptResult, importNewDiagram]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                // Don't allow closing while parsing metadata
 | 
			
		||||
                if (isParsingMetadata) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!hasExistingDiagram) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
 | 
			
		||||
                showClose={hasExistingDiagram}
 | 
			
		||||
                onInteractOutside={(e) => e.preventDefault()}
 | 
			
		||||
                onEscapeKeyDown={(e) => e.preventDefault()}
 | 
			
		||||
            >
 | 
			
		||||
                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
			
		||||
                    <SelectDatabase
 | 
			
		||||
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
 | 
			
		||||
                    <ImportDatabase
 | 
			
		||||
                        onImport={importNewDiagram}
 | 
			
		||||
                        onImport={importNewDiagramOrFilterTables}
 | 
			
		||||
                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
                        title={t('new_diagram_dialog.import_database.title')}
 | 
			
		||||
                        importMethod={importMethod}
 | 
			
		||||
                        setImportMethod={setImportMethod}
 | 
			
		||||
                        keepDialogAfterImport={true}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
                ) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
 | 
			
		||||
                    <SelectTables
 | 
			
		||||
                        isLoading={isParsingMetadata || !parsedMetadata}
 | 
			
		||||
                        databaseMetadata={parsedMetadata}
 | 
			
		||||
                        onImport={importNewDiagram}
 | 
			
		||||
                        onBack={() =>
 | 
			
		||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,16 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Editor } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { AlertCircle } from 'lucide-react';
 | 
			
		||||
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
 | 
			
		||||
import {
 | 
			
		||||
    importDBMLToDiagram,
 | 
			
		||||
    sanitizeDBML,
 | 
			
		||||
    preprocessDBML,
 | 
			
		||||
} from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { Parser } from '@dbml/core';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { debounce } from '@/lib/utils';
 | 
			
		||||
@@ -189,7 +194,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
            if (!content.trim()) return;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const sanitizedContent = sanitizeDBML(content);
 | 
			
		||||
                const preprocessedContent = preprocessDBML(content);
 | 
			
		||||
                const sanitizedContent = sanitizeDBML(preprocessedContent);
 | 
			
		||||
                const parser = new Parser();
 | 
			
		||||
                parser.parse(sanitizedContent, 'dbml');
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
@@ -242,13 +248,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
        if (!dbmlContent.trim() || errorMessage) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Sanitize DBML content before importing
 | 
			
		||||
            const sanitizedContent = sanitizeDBML(dbmlContent);
 | 
			
		||||
            const importedDiagram = await importDBMLToDiagram(sanitizedContent);
 | 
			
		||||
            const importedDiagram = await importDBMLToDiagram(dbmlContent);
 | 
			
		||||
            const tableIdsToRemove = tables
 | 
			
		||||
                .filter((table) =>
 | 
			
		||||
                    importedDiagram.tables?.some(
 | 
			
		||||
                        (t) =>
 | 
			
		||||
                        (t: DBTable) =>
 | 
			
		||||
                            t.name === table.name && t.schema === table.schema
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
@@ -257,19 +261,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
            const relationshipIdsToRemove = relationships
 | 
			
		||||
                .filter((relationship) => {
 | 
			
		||||
                    const sourceTable = tables.find(
 | 
			
		||||
                        (table) => table.id === relationship.sourceTableId
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.id === relationship.sourceTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    const targetTable = tables.find(
 | 
			
		||||
                        (table) => table.id === relationship.targetTableId
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.id === relationship.targetTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    if (!sourceTable || !targetTable) return true;
 | 
			
		||||
                    const replacementSourceTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table) =>
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.name === sourceTable.name &&
 | 
			
		||||
                            table.schema === sourceTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
                    const replacementTargetTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table) =>
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.name === targetTable.name &&
 | 
			
		||||
                            table.schema === targetTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { Group, SquarePlus } from 'lucide-react';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
 | 
			
		||||
export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
			
		||||
    table?: DBTable;
 | 
			
		||||
    schemas: DBSchema[];
 | 
			
		||||
    onConfirm: (schema: string) => void;
 | 
			
		||||
    onConfirm: ({ schema }: { schema: DBSchema }) => void;
 | 
			
		||||
    allowSchemaCreation?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
@@ -29,27 +41,90 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
    table,
 | 
			
		||||
    schemas,
 | 
			
		||||
    onConfirm,
 | 
			
		||||
    allowSchemaCreation = false,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [selectedSchema, setSelectedSchema] = React.useState<string>(
 | 
			
		||||
    const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
 | 
			
		||||
    const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
 | 
			
		||||
        table?.schema
 | 
			
		||||
            ? schemaNameToSchemaId(table.schema)
 | 
			
		||||
            : (schemas?.[0]?.id ?? '')
 | 
			
		||||
    );
 | 
			
		||||
    const allowSchemaSelection = useMemo(
 | 
			
		||||
        () => schemas && schemas.length > 0,
 | 
			
		||||
        [schemas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const defaultSchemaName = useMemo(
 | 
			
		||||
        () => defaultSchemas?.[databaseType],
 | 
			
		||||
        [databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [isCreatingNew, setIsCreatingNew] =
 | 
			
		||||
        useState<boolean>(!allowSchemaSelection);
 | 
			
		||||
    const [newSchemaName, setNewSchemaName] = useState<string>(
 | 
			
		||||
        allowSchemaCreation && !allowSchemaSelection
 | 
			
		||||
            ? (defaultSchemaName ?? '')
 | 
			
		||||
            : ''
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        setSelectedSchema(
 | 
			
		||||
        setSelectedSchemaId(
 | 
			
		||||
            table?.schema
 | 
			
		||||
                ? schemaNameToSchemaId(table.schema)
 | 
			
		||||
                : (schemas?.[0]?.id ?? '')
 | 
			
		||||
        );
 | 
			
		||||
    }, [dialog.open, schemas, table?.schema]);
 | 
			
		||||
        setIsCreatingNew(!allowSchemaSelection);
 | 
			
		||||
        setNewSchemaName(
 | 
			
		||||
            allowSchemaCreation && !allowSchemaSelection
 | 
			
		||||
                ? (defaultSchemaName ?? '')
 | 
			
		||||
                : ''
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        defaultSchemaName,
 | 
			
		||||
        dialog.open,
 | 
			
		||||
        schemas,
 | 
			
		||||
        table?.schema,
 | 
			
		||||
        allowSchemaSelection,
 | 
			
		||||
        allowSchemaCreation,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const { closeTableSchemaDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(() => {
 | 
			
		||||
        onConfirm(selectedSchema);
 | 
			
		||||
    }, [onConfirm, selectedSchema]);
 | 
			
		||||
        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,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
@@ -60,6 +135,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
        [schemas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderSwitchCreateOrSelectButton = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <Button
 | 
			
		||||
                variant="outline"
 | 
			
		||||
                className="w-full justify-start"
 | 
			
		||||
                onClick={() => setIsCreatingNew(!isCreatingNew)}
 | 
			
		||||
                disabled={!allowSchemaSelection || !allowSchemaCreation}
 | 
			
		||||
            >
 | 
			
		||||
                {!isCreatingNew ? (
 | 
			
		||||
                    <SquarePlus className="mr-2 size-4 " />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <Group className="mr-2 size-4 " />
 | 
			
		||||
                )}
 | 
			
		||||
                {isCreatingNew ? 'Select existing schema' : 'Create new schema'}
 | 
			
		||||
            </Button>
 | 
			
		||||
        ),
 | 
			
		||||
        [isCreatingNew, allowSchemaSelection, allowSchemaCreation]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
@@ -67,48 +161,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeTableSchemaDialog();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => (document.body.style.pointerEvents = ''), 500);
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent className="flex flex-col" showClose>
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {table
 | 
			
		||||
                            ? t('update_table_schema_dialog.title')
 | 
			
		||||
                            : t('new_table_schema_dialog.title')}
 | 
			
		||||
                        {!allowSchemaSelection && allowSchemaCreation
 | 
			
		||||
                            ? t('create_table_schema_dialog.title')
 | 
			
		||||
                            : table
 | 
			
		||||
                              ? t('update_table_schema_dialog.title')
 | 
			
		||||
                              : t('new_table_schema_dialog.title')}
 | 
			
		||||
                    </DialogTitle>
 | 
			
		||||
                    <DialogDescription>
 | 
			
		||||
                        {table
 | 
			
		||||
                            ? t('update_table_schema_dialog.description', {
 | 
			
		||||
                                  tableName: table.name,
 | 
			
		||||
                              })
 | 
			
		||||
                            : t('new_table_schema_dialog.description')}
 | 
			
		||||
                        {!allowSchemaSelection && allowSchemaCreation
 | 
			
		||||
                            ? t('create_table_schema_dialog.description')
 | 
			
		||||
                            : table
 | 
			
		||||
                              ? t('update_table_schema_dialog.description', {
 | 
			
		||||
                                    tableName: table.name,
 | 
			
		||||
                                })
 | 
			
		||||
                              : t('new_table_schema_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="grid gap-4 py-1">
 | 
			
		||||
                    <div className="grid w-full items-center gap-4">
 | 
			
		||||
                        <SelectBox
 | 
			
		||||
                            options={schemaOptions}
 | 
			
		||||
                            multiple={false}
 | 
			
		||||
                            value={selectedSchema}
 | 
			
		||||
                            onChange={(value) =>
 | 
			
		||||
                                setSelectedSchema(value as string)
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                        {!isCreatingNew ? (
 | 
			
		||||
                            <SelectBox
 | 
			
		||||
                                options={schemaOptions}
 | 
			
		||||
                                multiple={false}
 | 
			
		||||
                                value={selectedSchemaId}
 | 
			
		||||
                                onChange={(value) =>
 | 
			
		||||
                                    setSelectedSchemaId(value as string)
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <div className="flex flex-col gap-2">
 | 
			
		||||
                                {allowSchemaCreation &&
 | 
			
		||||
                                !allowSchemaSelection ? (
 | 
			
		||||
                                    <Label htmlFor="new-schema-name">
 | 
			
		||||
                                        Schema Name
 | 
			
		||||
                                    </Label>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    id="new-schema-name"
 | 
			
		||||
                                    value={newSchemaName}
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        setNewSchemaName(e.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
 | 
			
		||||
                                    autoFocus
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
 | 
			
		||||
                        {allowSchemaCreation && allowSchemaSelection ? (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <div className="relative">
 | 
			
		||||
                                    <Separator className="my-2" />
 | 
			
		||||
                                    <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
 | 
			
		||||
                                        or
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {allowSchemaSelection ? (
 | 
			
		||||
                                    renderSwitchCreateOrSelectButton()
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <Tooltip>
 | 
			
		||||
                                        <TooltipTrigger asChild>
 | 
			
		||||
                                            <span>
 | 
			
		||||
                                                {renderSwitchCreateOrSelectButton()}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </TooltipTrigger>
 | 
			
		||||
                                        <TooltipContent>
 | 
			
		||||
                                            <p>No existing schemas available</p>
 | 
			
		||||
                                        </TooltipContent>
 | 
			
		||||
                                    </Tooltip>
 | 
			
		||||
                                )}
 | 
			
		||||
                            </>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">
 | 
			
		||||
                            {table
 | 
			
		||||
                                ? t('update_table_schema_dialog.cancel')
 | 
			
		||||
                                : t('new_table_schema_dialog.cancel')}
 | 
			
		||||
                            {isCreatingNew
 | 
			
		||||
                                ? t('create_table_schema_dialog.cancel')
 | 
			
		||||
                                : table
 | 
			
		||||
                                  ? t('update_table_schema_dialog.cancel')
 | 
			
		||||
                                  : t('new_table_schema_dialog.cancel')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button onClick={handleConfirm}>
 | 
			
		||||
                            {table
 | 
			
		||||
                                ? t('update_table_schema_dialog.confirm')
 | 
			
		||||
                                : t('new_table_schema_dialog.confirm')}
 | 
			
		||||
                        <Button
 | 
			
		||||
                            onClick={handleConfirm}
 | 
			
		||||
                            disabled={isCreatingNew && !newSchemaName.trim()}
 | 
			
		||||
                        >
 | 
			
		||||
                            {isCreatingNew
 | 
			
		||||
                                ? t('create_table_schema_dialog.create')
 | 
			
		||||
                                : table
 | 
			
		||||
                                  ? t('update_table_schema_dialog.confirm')
 | 
			
		||||
                                  : t('new_table_schema_dialog.confirm')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,7 @@
 | 
			
		||||
    }
 | 
			
		||||
    body {
 | 
			
		||||
        @apply bg-background text-foreground;
 | 
			
		||||
        overscroll-behavior-x: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .text-editable {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn';
 | 
			
		||||
import { gu, guMetadata } from './locales/gu';
 | 
			
		||||
import { vi, viMetadata } from './locales/vi';
 | 
			
		||||
import { ar, arMetadata } from './locales/ar';
 | 
			
		||||
import { hr, hrMetadata } from './locales/hr';
 | 
			
		||||
 | 
			
		||||
export const languages: LanguageMetadata[] = [
 | 
			
		||||
    enMetadata,
 | 
			
		||||
    esMetadata,
 | 
			
		||||
    frMetadata,
 | 
			
		||||
    deMetadata,
 | 
			
		||||
    esMetadata,
 | 
			
		||||
    ukMetadata,
 | 
			
		||||
    ruMetadata,
 | 
			
		||||
    trMetadata,
 | 
			
		||||
    hrMetadata,
 | 
			
		||||
    pt_BRMetadata,
 | 
			
		||||
    hiMetadata,
 | 
			
		||||
    jaMetadata,
 | 
			
		||||
    ko_KRMetadata,
 | 
			
		||||
    pt_BRMetadata,
 | 
			
		||||
    ukMetadata,
 | 
			
		||||
    ruMetadata,
 | 
			
		||||
    zh_CNMetadata,
 | 
			
		||||
    zh_TWMetadata,
 | 
			
		||||
    neMetadata,
 | 
			
		||||
    mrMetadata,
 | 
			
		||||
    trMetadata,
 | 
			
		||||
    id_IDMetadata,
 | 
			
		||||
    teMetadata,
 | 
			
		||||
    bnMetadata,
 | 
			
		||||
@@ -70,6 +72,7 @@ const resources = {
 | 
			
		||||
    gu,
 | 
			
		||||
    vi,
 | 
			
		||||
    ar,
 | 
			
		||||
    hr,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
i18n.use(LanguageDetector)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'إخفاء الشريط الجانبي',
 | 
			
		||||
                hide_cardinality: 'إخفاء الكاردينالية',
 | 
			
		||||
                show_cardinality: 'إظهار الكاردينالية',
 | 
			
		||||
                hide_field_attributes: 'إخفاء خصائص الحقل',
 | 
			
		||||
                show_field_attributes: 'إظهار خصائص الحقل',
 | 
			
		||||
                zoom_on_scroll: 'تكبير/تصغير عند التمرير',
 | 
			
		||||
                theme: 'المظهر',
 | 
			
		||||
                show_dependencies: 'إظهار الاعتمادات',
 | 
			
		||||
@@ -74,8 +76,8 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
            title: 'مخططات متعددة',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
 | 
			
		||||
            dont_show_again: 'لا تظهره مجدداً',
 | 
			
		||||
            change_schema: 'تغيير',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'لا شيء',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,6 +153,10 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
                        delete_field: 'حذف الحقل',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'الدقة',
 | 
			
		||||
                        scale: 'النطاق',
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'خصائص الفهرس',
 | 
			
		||||
@@ -251,9 +257,12 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,6 +278,11 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
            redo: 'إعادة',
 | 
			
		||||
            reorder_diagram: 'إعادة ترتيب الرسم البياني',
 | 
			
		||||
            highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -400,6 +414,13 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            confirm: 'تغيير',
 | 
			
		||||
        },
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'إنشاء مخطط جديد',
 | 
			
		||||
            description:
 | 
			
		||||
                'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
 | 
			
		||||
            create: 'إنشاء',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '!ساعدنا على التحسن',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'সাইডবার লুকান',
 | 
			
		||||
                hide_cardinality: 'কার্ডিনালিটি লুকান',
 | 
			
		||||
                show_cardinality: 'কার্ডিনালিটি দেখান',
 | 
			
		||||
                hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
 | 
			
		||||
                show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
 | 
			
		||||
                zoom_on_scroll: 'স্ক্রলে জুম করুন',
 | 
			
		||||
                theme: 'থিম',
 | 
			
		||||
                show_dependencies: 'নির্ভরতাগুলি দেখান',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
            title: 'বহু স্কিমা',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
 | 
			
		||||
            dont_show_again: 'পুনরায় দেখাবেন না',
 | 
			
		||||
            change_schema: 'পরিবর্তন করুন',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'কিছুই না',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +153,12 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'কোনো মন্তব্য নেই',
 | 
			
		||||
                        delete_field: 'ফিল্ড মুছুন',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'নির্ভুলতা',
 | 
			
		||||
                        scale: 'স্কেল',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ইনডেক্স কর্ম',
 | 
			
		||||
@@ -251,9 +258,12 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,6 +279,12 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
            redo: 'পুনরায় করুন',
 | 
			
		||||
            reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
 | 
			
		||||
            highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
 | 
			
		||||
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -400,6 +416,13 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
            cancel: 'বাতিল করুন',
 | 
			
		||||
            confirm: 'পরিবর্তন করুন',
 | 
			
		||||
        },
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'নতুন স্কিমা তৈরি করুন',
 | 
			
		||||
            description:
 | 
			
		||||
                'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
 | 
			
		||||
            create: 'তৈরি করুন',
 | 
			
		||||
            cancel: 'বাতিল করুন',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'আমাদের উন্নত করতে সাহায্য করুন!',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Seitenleiste ausblenden',
 | 
			
		||||
                hide_cardinality: 'Kardinalität ausblenden',
 | 
			
		||||
                show_cardinality: 'Kardinalität anzeigen',
 | 
			
		||||
                hide_field_attributes: 'Feldattribute ausblenden',
 | 
			
		||||
                show_field_attributes: 'Feldattribute anzeigen',
 | 
			
		||||
                zoom_on_scroll: 'Zoom beim Scrollen',
 | 
			
		||||
                theme: 'Stil',
 | 
			
		||||
                show_dependencies: 'Abhängigkeiten anzeigen',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const de: LanguageTranslation = {
 | 
			
		||||
            title: 'Mehrere Schemas',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Nicht erneut anzeigen',
 | 
			
		||||
            change_schema: 'Schema ändern',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'Keine',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -152,7 +154,12 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Keine Kommentare',
 | 
			
		||||
                        delete_field: 'Feld löschen',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Präzision',
 | 
			
		||||
                        scale: 'Skalierung',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Indexattribute',
 | 
			
		||||
@@ -253,9 +260,12 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -270,7 +280,14 @@ export const de: LanguageTranslation = {
 | 
			
		||||
            undo: 'Rückgängig',
 | 
			
		||||
            redo: 'Wiederholen',
 | 
			
		||||
            reorder_diagram: 'Diagramm neu anordnen',
 | 
			
		||||
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -403,6 +420,13 @@ export const de: LanguageTranslation = {
 | 
			
		||||
            cancel: 'Abbrechen',
 | 
			
		||||
            confirm: 'Ändern',
 | 
			
		||||
        },
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Neues Schema erstellen',
 | 
			
		||||
            description:
 | 
			
		||||
                'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
 | 
			
		||||
            create: 'Erstellen',
 | 
			
		||||
            cancel: 'Abbrechen',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Hilf uns, uns zu verbessern!',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const en = {
 | 
			
		||||
                hide_sidebar: 'Hide Sidebar',
 | 
			
		||||
                hide_cardinality: 'Hide Cardinality',
 | 
			
		||||
                show_cardinality: 'Show Cardinality',
 | 
			
		||||
                hide_field_attributes: 'Hide Field Attributes',
 | 
			
		||||
                show_field_attributes: 'Show Field Attributes',
 | 
			
		||||
                zoom_on_scroll: 'Zoom on Scroll',
 | 
			
		||||
                theme: 'Theme',
 | 
			
		||||
                show_dependencies: 'Show Dependencies',
 | 
			
		||||
@@ -73,8 +75,7 @@ export const en = {
 | 
			
		||||
            title: 'Multiple Schemas',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: "Don't show again",
 | 
			
		||||
            change_schema: 'Change',
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'none',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -126,6 +127,10 @@ export const en = {
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
                default_grouping: 'Default View',
 | 
			
		||||
                group_by_schema: 'Group by Schema',
 | 
			
		||||
                group_by_area: 'Group by Area',
 | 
			
		||||
                no_area: 'No Area',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Fields',
 | 
			
		||||
@@ -144,8 +149,12 @@ export const en = {
 | 
			
		||||
                        title: 'Field Attributes',
 | 
			
		||||
                        unique: 'Unique',
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Precision',
 | 
			
		||||
                        scale: 'Scale',
 | 
			
		||||
                        comments: 'Comments',
 | 
			
		||||
                        no_comments: 'No comments',
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        delete_field: 'Delete Field',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
@@ -245,8 +254,11 @@ export const en = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
@@ -254,6 +266,15 @@ export const en = {
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        canvas_filter: {
 | 
			
		||||
            title: 'Filter Tables',
 | 
			
		||||
            search_placeholder: 'Search tables...',
 | 
			
		||||
            default_grouping: 'Default View',
 | 
			
		||||
            group_by_schema: 'Group by Schema',
 | 
			
		||||
            group_by_area: 'Group by Area',
 | 
			
		||||
            no_area: 'No Area',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        toolbar: {
 | 
			
		||||
            zoom_in: 'Zoom In',
 | 
			
		||||
            zoom_out: 'Zoom Out',
 | 
			
		||||
@@ -263,6 +284,10 @@ export const en = {
 | 
			
		||||
            redo: 'Redo',
 | 
			
		||||
            reorder_diagram: 'Reorder Diagram',
 | 
			
		||||
            highlight_overlapping_tables: 'Highlight Overlapping Tables',
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -394,6 +419,14 @@ export const en = {
 | 
			
		||||
            confirm: 'Change',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Create New Schema',
 | 
			
		||||
            description:
 | 
			
		||||
                'No schemas exist yet. Create your first schema to organize your tables.',
 | 
			
		||||
            create: 'Create',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Help us improve!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,8 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                view: 'Ver',
 | 
			
		||||
                hide_cardinality: 'Ocultar Cardinalidad',
 | 
			
		||||
                show_cardinality: 'Mostrar Cardinalidad',
 | 
			
		||||
                show_field_attributes: 'Mostrar Atributos de Campo',
 | 
			
		||||
                hide_field_attributes: 'Ocultar Atributos de Campo',
 | 
			
		||||
                show_sidebar: 'Mostrar Barra Lateral',
 | 
			
		||||
                hide_sidebar: 'Ocultar Barra Lateral',
 | 
			
		||||
                zoom_on_scroll: 'Zoom al Desplazarse',
 | 
			
		||||
@@ -141,7 +143,12 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Sin comentarios',
 | 
			
		||||
                        delete_field: 'Eliminar Campo',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Precisión',
 | 
			
		||||
                        scale: 'Escala',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atributos del Índice',
 | 
			
		||||
@@ -241,9 +248,12 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -258,7 +268,13 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            undo: 'Deshacer',
 | 
			
		||||
            redo: 'Rehacer',
 | 
			
		||||
            reorder_diagram: 'Reordenar Diagrama',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Resaltar tablas superpuestas',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -392,6 +408,13 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            cancel: 'Cancelar',
 | 
			
		||||
            confirm: 'Cambiar',
 | 
			
		||||
        },
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Crear Nuevo Esquema',
 | 
			
		||||
            description:
 | 
			
		||||
                'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
 | 
			
		||||
            create: 'Crear',
 | 
			
		||||
            cancel: 'Cancelar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '¡Ayúdanos a mejorar!',
 | 
			
		||||
@@ -405,8 +428,8 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            title: 'Múltiples Esquemas',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'No mostrar de nuevo',
 | 
			
		||||
            change_schema: 'Cambiar',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'nada',
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Cacher la Barre Latérale',
 | 
			
		||||
                hide_cardinality: 'Cacher la Cardinalité',
 | 
			
		||||
                show_cardinality: 'Afficher la Cardinalité',
 | 
			
		||||
                hide_field_attributes: 'Masquer les Attributs de Champ',
 | 
			
		||||
                show_field_attributes: 'Afficher les Attributs de Champ',
 | 
			
		||||
                zoom_on_scroll: 'Zoom sur le Défilement',
 | 
			
		||||
                theme: 'Thème',
 | 
			
		||||
                show_dependencies: 'Afficher les Dépendances',
 | 
			
		||||
@@ -139,7 +141,12 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Pas de commentaires',
 | 
			
		||||
                        delete_field: 'Supprimer le Champ',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Précision',
 | 
			
		||||
                        scale: 'Échelle',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: "Attributs de l'Index",
 | 
			
		||||
@@ -239,9 +246,12 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -256,7 +266,13 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
            undo: 'Annuler',
 | 
			
		||||
            redo: 'Rétablir',
 | 
			
		||||
            reorder_diagram: 'Réorganiser le Diagramme',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Surligner les tables chevauchées',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -345,8 +361,8 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
            title: 'Schémas Multiples',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Ne plus afficher',
 | 
			
		||||
            change_schema: 'Changer',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'Aucun',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -372,6 +388,13 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
            confirm: 'Modifier',
 | 
			
		||||
        },
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Créer un Nouveau Schéma',
 | 
			
		||||
            description:
 | 
			
		||||
                "Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
 | 
			
		||||
            create: 'Créer',
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_relationship_dialog: {
 | 
			
		||||
            title: 'Créer une Relation',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'સાઇડબાર છુપાવો',
 | 
			
		||||
                hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
 | 
			
		||||
                show_cardinality: 'કાર્ડિનાલિટી બતાવો',
 | 
			
		||||
                hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
 | 
			
		||||
                show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
 | 
			
		||||
                zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
 | 
			
		||||
                theme: 'થિમ',
 | 
			
		||||
                show_dependencies: 'નિર્ભરતાઓ બતાવો',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
            title: 'કઈંક વધારે સ્કીમા',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'ફરીથી ન બતાવો',
 | 
			
		||||
            change_schema: 'બદલો',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'કઈ નહીં',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -152,7 +154,12 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'કોઈ ટિપ્પણીઓ નથી',
 | 
			
		||||
                        delete_field: 'ફીલ્ડ કાઢી નાખો',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'ચોકસાઈ',
 | 
			
		||||
                        scale: 'માપ',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ઇન્ડેક્સ લક્ષણો',
 | 
			
		||||
@@ -252,9 +259,12 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,7 +279,13 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
            undo: 'અનડુ',
 | 
			
		||||
            redo: 'રીડુ',
 | 
			
		||||
            reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -401,6 +417,14 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
            confirm: 'બદલો',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'નવું સ્કીમા બનાવો',
 | 
			
		||||
            description:
 | 
			
		||||
                'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
 | 
			
		||||
            create: 'બનાવો',
 | 
			
		||||
            cancel: 'રદ કરો',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'અમને સુધારવામાં મદદ કરો!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'साइडबार छिपाएँ',
 | 
			
		||||
                hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
 | 
			
		||||
                show_cardinality: 'कार्डिनैलिटी दिखाएँ',
 | 
			
		||||
                hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
 | 
			
		||||
                show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
 | 
			
		||||
                zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
 | 
			
		||||
                theme: 'थीम',
 | 
			
		||||
                show_dependencies: 'निर्भरता दिखाएँ',
 | 
			
		||||
@@ -74,8 +76,8 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
            title: 'एकाधिक स्कीमा',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
 | 
			
		||||
            dont_show_again: 'फिर से न दिखाएँ',
 | 
			
		||||
            change_schema: 'बदलें',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'कोई नहीं',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +153,12 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'कोई टिप्पणी नहीं',
 | 
			
		||||
                        delete_field: 'फ़ील्ड हटाएँ',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Precision',
 | 
			
		||||
                        scale: 'Scale',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'सूचकांक विशेषताएँ',
 | 
			
		||||
@@ -252,9 +259,12 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,7 +279,13 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
            undo: 'पूर्ववत करें',
 | 
			
		||||
            redo: 'पुनः करें',
 | 
			
		||||
            reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -404,6 +420,14 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
            confirm: 'बदलें',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'नया स्कीमा बनाएं',
 | 
			
		||||
            description:
 | 
			
		||||
                'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
 | 
			
		||||
            create: 'बनाएं',
 | 
			
		||||
            cancel: 'रद्द करें',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'हमें सुधारने में मदद करें!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										503
									
								
								src/i18n/locales/hr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										503
									
								
								src/i18n/locales/hr.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,503 @@
 | 
			
		||||
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,6 +26,8 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Sembunyikan Sidebar',
 | 
			
		||||
                hide_cardinality: 'Sembunyikan Kardinalitas',
 | 
			
		||||
                show_cardinality: 'Tampilkan Kardinalitas',
 | 
			
		||||
                hide_field_attributes: 'Sembunyikan Atribut Kolom',
 | 
			
		||||
                show_field_attributes: 'Tampilkan Atribut Kolom',
 | 
			
		||||
                zoom_on_scroll: 'Perbesar saat Scroll',
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Tampilkan Dependensi',
 | 
			
		||||
@@ -74,8 +76,8 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
            title: 'Schema Lebih dari satu',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Jangan tampilkan lagi',
 | 
			
		||||
            change_schema: 'Ubah',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'Tidak ada',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +152,12 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Tidak ada komentar',
 | 
			
		||||
                        delete_field: 'Hapus Kolom',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Presisi',
 | 
			
		||||
                        scale: 'Skala',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atribut Indeks',
 | 
			
		||||
@@ -250,9 +257,12 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -267,7 +277,13 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
            undo: 'Undo',
 | 
			
		||||
            redo: 'Redo',
 | 
			
		||||
            reorder_diagram: 'Atur Ulang Diagram',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -399,6 +415,14 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
            confirm: 'Ubah',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Buat Skema Baru',
 | 
			
		||||
            description:
 | 
			
		||||
                'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
 | 
			
		||||
            create: 'Buat',
 | 
			
		||||
            cancel: 'Batal',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Bantu kami meningkatkan!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'サイドバーを非表示',
 | 
			
		||||
                hide_cardinality: 'カーディナリティを非表示',
 | 
			
		||||
                show_cardinality: 'カーディナリティを表示',
 | 
			
		||||
                hide_field_attributes: 'フィールド属性を非表示',
 | 
			
		||||
                show_field_attributes: 'フィールド属性を表示',
 | 
			
		||||
                zoom_on_scroll: 'スクロールでズーム',
 | 
			
		||||
                theme: 'テーマ',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
@@ -76,8 +78,8 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
            title: '複数のスキーマ',
 | 
			
		||||
            description:
 | 
			
		||||
                'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
 | 
			
		||||
            dont_show_again: '再表示しない',
 | 
			
		||||
            change_schema: '変更',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'なし',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -154,7 +156,12 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'コメントがありません',
 | 
			
		||||
                        delete_field: 'フィールドを削除',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: '精度',
 | 
			
		||||
                        scale: '小数点以下桁数',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'インデックス属性',
 | 
			
		||||
@@ -256,9 +263,12 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -275,6 +285,10 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
            reorder_diagram: 'ダイアグラムを並べ替え',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            highlight_overlapping_tables: 'Highlight Overlapping Tables',
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -408,6 +422,14 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
            confirm: '変更',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: '新しいスキーマを作成',
 | 
			
		||||
            description:
 | 
			
		||||
                'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
 | 
			
		||||
            create: '作成',
 | 
			
		||||
            cancel: 'キャンセル',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '改善をサポートしてください!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: '사이드바 숨기기',
 | 
			
		||||
                hide_cardinality: '카디널리티 숨기기',
 | 
			
		||||
                show_cardinality: '카디널리티 보이기',
 | 
			
		||||
                hide_field_attributes: '필드 속성 숨기기',
 | 
			
		||||
                show_field_attributes: '필드 속성 보이기',
 | 
			
		||||
                zoom_on_scroll: '스크롤 시 확대',
 | 
			
		||||
                theme: '테마',
 | 
			
		||||
                show_dependencies: '종속성 보이기',
 | 
			
		||||
@@ -74,8 +76,8 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
            title: '다중 스키마',
 | 
			
		||||
            description:
 | 
			
		||||
                '현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: '다시 보여주지 마세요',
 | 
			
		||||
            change_schema: '변경',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: '없음',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +152,12 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                        no_comments: '주석 없음',
 | 
			
		||||
                        delete_field: '필드 삭제',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: '정밀도',
 | 
			
		||||
                        scale: '소수점 자릿수',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '인덱스 속성',
 | 
			
		||||
@@ -250,9 +257,12 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -267,7 +277,13 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
            undo: '실행 취소',
 | 
			
		||||
            redo: '다시 실행',
 | 
			
		||||
            reorder_diagram: '다이어그램 재정렬',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: '겹치는 테이블 강조 표시',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -399,6 +415,14 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
            confirm: '변경',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: '새 스키마 생성',
 | 
			
		||||
            description:
 | 
			
		||||
                '아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
 | 
			
		||||
            create: '생성',
 | 
			
		||||
            cancel: '취소',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '개선할 수 있도록 도와주세요!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'साइडबार लपवा',
 | 
			
		||||
                hide_cardinality: 'कार्डिनॅलिटी लपवा',
 | 
			
		||||
                show_cardinality: 'कार्डिनॅलिटी दाखवा',
 | 
			
		||||
                hide_field_attributes: 'फील्ड गुणधर्म लपवा',
 | 
			
		||||
                show_field_attributes: 'फील्ड गुणधर्म दाखवा',
 | 
			
		||||
                zoom_on_scroll: 'स्क्रोलवर झूम करा',
 | 
			
		||||
                theme: 'थीम',
 | 
			
		||||
                show_dependencies: 'डिपेंडेन्सि दाखवा',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
            title: 'एकाधिक स्कीमा',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'पुन्हा दाखवू नका',
 | 
			
		||||
            change_schema: 'बदला',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'काहीही नाही',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -153,7 +155,12 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'कोणत्याही टिप्पणी नाहीत',
 | 
			
		||||
                        delete_field: 'फील्ड हटवा',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'अचूकता',
 | 
			
		||||
                        scale: 'प्रमाण',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'इंडेक्स गुणधर्म',
 | 
			
		||||
@@ -255,9 +262,12 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -272,7 +282,13 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
            undo: 'पूर्ववत करा',
 | 
			
		||||
            redo: 'पुन्हा करा',
 | 
			
		||||
            reorder_diagram: 'आरेख पुनःक्रमित करा',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -407,6 +423,14 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
            confirm: 'बदला',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'नवीन स्कीमा तयार करा',
 | 
			
		||||
            description:
 | 
			
		||||
                'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
 | 
			
		||||
            create: 'तयार करा',
 | 
			
		||||
            cancel: 'रद्द करा',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'आम्हाला सुधारण्यास मदत करा!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'साइडबार लुकाउनुहोस्',
 | 
			
		||||
                hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
 | 
			
		||||
                show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
 | 
			
		||||
                hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
 | 
			
		||||
                show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
 | 
			
		||||
                zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
 | 
			
		||||
                theme: 'थिम',
 | 
			
		||||
                show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
            title: 'विविध स्कीमहरू',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
 | 
			
		||||
            dont_show_again: 'फेरि देखाउन नदिनुहोस्',
 | 
			
		||||
            change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'कुनै पनि छैन',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +153,12 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'कुनै टिप्पणीहरू छैनन्',
 | 
			
		||||
                        delete_field: 'क्षेत्र हटाउनुहोस्',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'परिशुद्धता',
 | 
			
		||||
                        scale: 'स्केल',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'सूचक विशेषताहरू',
 | 
			
		||||
@@ -252,9 +259,12 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,8 +279,14 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
            undo: 'पूर्ववत',
 | 
			
		||||
            redo: 'पुनः गर्नुहोस्',
 | 
			
		||||
            reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables:
 | 
			
		||||
                'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -404,6 +420,14 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
            confirm: 'परिवर्तन गर्नुहोस्',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
 | 
			
		||||
            description:
 | 
			
		||||
                'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
 | 
			
		||||
            create: 'सिर्जना गर्नुहोस्',
 | 
			
		||||
            cancel: 'रद्द गर्नुहोस्',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Ocultar Barra Lateral',
 | 
			
		||||
                hide_cardinality: 'Ocultar Cardinalidade',
 | 
			
		||||
                show_cardinality: 'Mostrar Cardinalidade',
 | 
			
		||||
                hide_field_attributes: 'Ocultar Atributos de Campo',
 | 
			
		||||
                show_field_attributes: 'Mostrar Atributos de Campo',
 | 
			
		||||
                zoom_on_scroll: 'Zoom ao Rolar',
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Mostrar Dependências',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
            title: 'Múltiplos Esquemas',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Não mostrar novamente',
 | 
			
		||||
            change_schema: 'Alterar',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'nenhum',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +153,12 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Sem comentários',
 | 
			
		||||
                        delete_field: 'Excluir Campo',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Precisão',
 | 
			
		||||
                        scale: 'Escala',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atributos do Índice',
 | 
			
		||||
@@ -251,9 +258,12 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -268,7 +278,13 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
            undo: 'Desfazer',
 | 
			
		||||
            redo: 'Refazer',
 | 
			
		||||
            reorder_diagram: 'Reordenar Diagrama',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -402,6 +418,14 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
            confirm: 'Alterar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Criar Novo Esquema',
 | 
			
		||||
            description:
 | 
			
		||||
                'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.',
 | 
			
		||||
            create: 'Criar',
 | 
			
		||||
            cancel: 'Cancelar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Ajude-nos a melhorar!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Скрыть боковую панель',
 | 
			
		||||
                hide_cardinality: 'Скрыть виды связи',
 | 
			
		||||
                show_cardinality: 'Показать виды связи',
 | 
			
		||||
                show_field_attributes: 'Показать атрибуты поля',
 | 
			
		||||
                hide_field_attributes: 'Скрыть атрибуты поля',
 | 
			
		||||
                zoom_on_scroll: 'Увеличение при прокрутке',
 | 
			
		||||
                theme: 'Тема',
 | 
			
		||||
                show_dependencies: 'Показать зависимости',
 | 
			
		||||
@@ -73,8 +75,8 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
            title: 'Множественные схемы',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Больше не показывать',
 | 
			
		||||
            change_schema: 'Изменить',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'никто',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -147,7 +149,12 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Комментарии',
 | 
			
		||||
                        no_comments: 'Нет комментария',
 | 
			
		||||
                        delete_field: 'Удалить поле',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        character_length: 'Макс. длина',
 | 
			
		||||
                        precision: 'Точность',
 | 
			
		||||
                        scale: 'Масштаб',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Атрибуты индекса',
 | 
			
		||||
@@ -248,9 +255,12 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -265,7 +275,13 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
            undo: 'Отменить',
 | 
			
		||||
            redo: 'Вернуть',
 | 
			
		||||
            reorder_diagram: 'Переупорядочить диаграмму',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -399,6 +415,14 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
            confirm: 'Изменить',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Создать новую схему',
 | 
			
		||||
            description:
 | 
			
		||||
                'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
 | 
			
		||||
            create: 'Создать',
 | 
			
		||||
            cancel: 'Отменить',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Помогите нам стать лучше!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'సైడ్బార్ దాచండి',
 | 
			
		||||
                hide_cardinality: 'కార్డినాలిటీని దాచండి',
 | 
			
		||||
                show_cardinality: 'కార్డినాలిటీని చూపించండి',
 | 
			
		||||
                show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
 | 
			
		||||
                hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
 | 
			
		||||
                zoom_on_scroll: 'స్క్రోల్పై జూమ్',
 | 
			
		||||
                theme: 'థీమ్',
 | 
			
		||||
                show_dependencies: 'ఆధారాలు చూపించండి',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const te: LanguageTranslation = {
 | 
			
		||||
            title: 'బహుళ స్కీమాలు',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'మరలా చూపించవద్దు',
 | 
			
		||||
            change_schema: 'మార్చు',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'ఎదరికాదు',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +153,12 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'వ్యాఖ్యలు లేవు',
 | 
			
		||||
                        delete_field: 'ఫీల్డ్ తొలగించు',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'సూక్ష్మత',
 | 
			
		||||
                        scale: 'స్కేల్',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ఇండెక్స్ గుణాలు',
 | 
			
		||||
@@ -252,9 +259,12 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -269,7 +279,13 @@ export const te: LanguageTranslation = {
 | 
			
		||||
            undo: 'తిరిగి చేయు',
 | 
			
		||||
            redo: 'మరలా చేయు',
 | 
			
		||||
            reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -403,6 +419,14 @@ export const te: LanguageTranslation = {
 | 
			
		||||
            confirm: 'మార్చు',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'కొత్త స్కీమా సృష్టించండి',
 | 
			
		||||
            description:
 | 
			
		||||
                'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
 | 
			
		||||
            create: 'సృష్టించు',
 | 
			
		||||
            cancel: 'రద్దు',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'మా సహాయంతో మెరుగుపరచండి!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Kenar Çubuğunu Gizle',
 | 
			
		||||
                hide_cardinality: 'Kardinaliteyi Gizle',
 | 
			
		||||
                show_cardinality: 'Kardinaliteyi Göster',
 | 
			
		||||
                show_field_attributes: 'Alan Özelliklerini Göster',
 | 
			
		||||
                hide_field_attributes: 'Alan Özelliklerini Gizle',
 | 
			
		||||
                zoom_on_scroll: 'Kaydırarak Yakınlaştır',
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Bağımlılıkları Göster',
 | 
			
		||||
@@ -75,8 +77,8 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
            title: 'Birden Fazla Şema',
 | 
			
		||||
            description:
 | 
			
		||||
                'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Tekrar gösterme',
 | 
			
		||||
            change_schema: 'Değiştir',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'yok',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +152,12 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Yorum yok',
 | 
			
		||||
                        delete_field: 'Alanı Sil',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Hassasiyet',
 | 
			
		||||
                        scale: 'Ölçek',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'İndeks Özellikleri',
 | 
			
		||||
@@ -251,9 +258,12 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -267,7 +277,13 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
            undo: 'Geri Al',
 | 
			
		||||
            redo: 'Yinele',
 | 
			
		||||
            reorder_diagram: 'Diyagramı Yeniden Sırala',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
            database_selection: {
 | 
			
		||||
@@ -392,6 +408,14 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
            cancel: 'İptal',
 | 
			
		||||
            confirm: 'Değiştir',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Yeni Şema Oluştur',
 | 
			
		||||
            description:
 | 
			
		||||
                'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.',
 | 
			
		||||
            create: 'Oluştur',
 | 
			
		||||
            cancel: 'İptal',
 | 
			
		||||
        },
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Bize yardım et!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Приховати бічну панель',
 | 
			
		||||
                hide_cardinality: 'Приховати потужність',
 | 
			
		||||
                show_cardinality: 'Показати кардинальність',
 | 
			
		||||
                show_field_attributes: 'Показати атрибути полів',
 | 
			
		||||
                hide_field_attributes: 'Приховати атрибути полів',
 | 
			
		||||
                zoom_on_scroll: 'Масштабувати прокручуванням',
 | 
			
		||||
                theme: 'Тема',
 | 
			
		||||
                show_dependencies: 'Показати залежності',
 | 
			
		||||
@@ -73,8 +75,8 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
            title: 'Кілька схем',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
 | 
			
		||||
            dont_show_again: 'Більше не показувати',
 | 
			
		||||
            change_schema: 'Зміна',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'немає',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -149,7 +151,12 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Немає коментарів',
 | 
			
		||||
                        delete_field: 'Видалити поле',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Точність',
 | 
			
		||||
                        scale: 'Масштаб',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Атрибути індексу',
 | 
			
		||||
@@ -249,9 +256,12 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -266,7 +276,13 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
            undo: 'Скасувати',
 | 
			
		||||
            redo: 'Повторити',
 | 
			
		||||
            reorder_diagram: 'Перевпорядкувати діаграму',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -400,6 +416,14 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
            confirm: 'Змінити',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Створити нову схему',
 | 
			
		||||
            description:
 | 
			
		||||
                'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
 | 
			
		||||
            create: 'Створити',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Допоможіть нам покращитися!',
 | 
			
		||||
            description: 'Поставне на зірку на GitHub? Це лише один клік!',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Ẩn thanh bên',
 | 
			
		||||
                hide_cardinality: 'Ẩn số lượng',
 | 
			
		||||
                show_cardinality: 'Hiển thị số lượng',
 | 
			
		||||
                show_field_attributes: 'Hiển thị thuộc tính trường',
 | 
			
		||||
                hide_field_attributes: 'Ẩn thuộc tính trường',
 | 
			
		||||
                zoom_on_scroll: 'Thu phóng khi cuộn',
 | 
			
		||||
                theme: 'Chủ đề',
 | 
			
		||||
                show_dependencies: 'Hiển thị các phụ thuộc',
 | 
			
		||||
@@ -74,8 +76,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}}.',
 | 
			
		||||
            dont_show_again: 'Không hiển thị lại',
 | 
			
		||||
            change_schema: 'Thay đổi',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: 'không có',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +152,12 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                        no_comments: 'Không có bình luận',
 | 
			
		||||
                        delete_field: 'Xóa trường',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: 'Độ chính xác',
 | 
			
		||||
                        scale: 'Tỷ lệ',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Thuộc tính chỉ mục',
 | 
			
		||||
@@ -250,9 +257,12 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -267,7 +277,13 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
            undo: 'Hoàn tác',
 | 
			
		||||
            redo: 'Làm lại',
 | 
			
		||||
            reorder_diagram: 'Sắp xếp lại sơ đồ',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -399,6 +415,14 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
            confirm: 'Xác nhận',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: 'Tạo lược đồ mới',
 | 
			
		||||
            description:
 | 
			
		||||
                'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.',
 | 
			
		||||
            create: 'Tạo',
 | 
			
		||||
            cancel: 'Hủy',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Hãy giúp chúng tôi cải thiện!',
 | 
			
		||||
            description:
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: '隐藏侧边栏',
 | 
			
		||||
                hide_cardinality: '隐藏基数',
 | 
			
		||||
                show_cardinality: '展示基数',
 | 
			
		||||
                show_field_attributes: '展示字段属性',
 | 
			
		||||
                hide_field_attributes: '隐藏字段属性',
 | 
			
		||||
                zoom_on_scroll: '滚动缩放',
 | 
			
		||||
                theme: '主题',
 | 
			
		||||
                show_dependencies: '展示依赖',
 | 
			
		||||
@@ -71,8 +73,8 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
            title: '多个模式',
 | 
			
		||||
            description:
 | 
			
		||||
                '此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
 | 
			
		||||
            dont_show_again: '不再展示',
 | 
			
		||||
            change_schema: '更改',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: '无',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -147,7 +149,12 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                        no_comments: '空',
 | 
			
		||||
                        delete_field: '删除字段',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: '精度',
 | 
			
		||||
                        scale: '小数位',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '索引属性',
 | 
			
		||||
@@ -247,9 +254,12 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -264,7 +274,13 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
            undo: '撤销',
 | 
			
		||||
            redo: '重做',
 | 
			
		||||
            reorder_diagram: '重新排列关系图',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: '突出显示重叠的表',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -395,6 +411,13 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
            confirm: '更改',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: '创建新模式',
 | 
			
		||||
            description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
 | 
			
		||||
            create: '创建',
 | 
			
		||||
            cancel: '取消',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '帮助我们改进!',
 | 
			
		||||
            description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: '隱藏側邊欄',
 | 
			
		||||
                hide_cardinality: '隱藏基數',
 | 
			
		||||
                show_cardinality: '顯示基數',
 | 
			
		||||
                hide_field_attributes: '隱藏欄位屬性',
 | 
			
		||||
                show_field_attributes: '顯示欄位屬性',
 | 
			
		||||
                zoom_on_scroll: '滾動縮放',
 | 
			
		||||
                theme: '主題',
 | 
			
		||||
                show_dependencies: '顯示相依性',
 | 
			
		||||
@@ -71,8 +73,8 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
            title: '多重 Schema',
 | 
			
		||||
            description:
 | 
			
		||||
                '此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。',
 | 
			
		||||
            dont_show_again: '不再顯示',
 | 
			
		||||
            change_schema: '變更',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            show_me: 'Show me',
 | 
			
		||||
            none: '無',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -147,7 +149,12 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                        no_comments: '無註解',
 | 
			
		||||
                        delete_field: '刪除欄位',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        default_value: 'Default Value',
 | 
			
		||||
                        no_default: 'No default',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        precision: '精度',
 | 
			
		||||
                        scale: '小數位',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '索引屬性',
 | 
			
		||||
@@ -247,9 +254,12 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                    field_name_placeholder: 'Field name',
 | 
			
		||||
                    field_type_placeholder: 'Select type',
 | 
			
		||||
                    add_field: 'Add Field',
 | 
			
		||||
                    no_fields_tooltip: 'No fields defined for this custom type',
 | 
			
		||||
                    custom_type_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        highlight_fields: 'Highlight Fields',
 | 
			
		||||
                        delete_custom_type: 'Delete',
 | 
			
		||||
                        clear_field_highlight: 'Clear Highlight',
 | 
			
		||||
                    },
 | 
			
		||||
                    delete_custom_type: 'Delete Type',
 | 
			
		||||
                },
 | 
			
		||||
@@ -264,7 +274,13 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
            undo: '復原',
 | 
			
		||||
            redo: '重做',
 | 
			
		||||
            reorder_diagram: '重新排列圖表',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
 | 
			
		||||
            custom_type_highlight_tooltip:
 | 
			
		||||
                'Highlighting "{{typeName}}" - Click to clear',
 | 
			
		||||
            highlight_overlapping_tables: '突出顯示重疊表格',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            filter: 'Filter Tables',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -394,6 +410,14 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
            confirm: '變更',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_table_schema_dialog: {
 | 
			
		||||
            title: '建立新 Schema',
 | 
			
		||||
            description:
 | 
			
		||||
                '尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
 | 
			
		||||
            create: '建立',
 | 
			
		||||
            cancel: '取消',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '協助我們改善!',
 | 
			
		||||
            description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import type { DBCustomType } from './domain';
 | 
			
		||||
import type { Area } from './domain/area';
 | 
			
		||||
import type { DBDependency } from './domain/db-dependency';
 | 
			
		||||
import type { DBField } from './domain/db-field';
 | 
			
		||||
@@ -48,6 +49,10 @@ const generateIdsMapFromDiagram = (
 | 
			
		||||
        idsMap.set(area.id, generateId());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    diagram.customTypes?.forEach((customType) => {
 | 
			
		||||
        idsMap.set(customType.id, generateId());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return idsMap;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -124,7 +129,7 @@ export const cloneDiagram = (
 | 
			
		||||
    } = {
 | 
			
		||||
        generateId: defaultGenerateId,
 | 
			
		||||
    }
 | 
			
		||||
): Diagram => {
 | 
			
		||||
): { diagram: Diagram; idsMap: Map<string, string> } => {
 | 
			
		||||
    const { generateId } = options;
 | 
			
		||||
    const diagramId = generateId();
 | 
			
		||||
 | 
			
		||||
@@ -213,14 +218,38 @@ 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,
 | 
			
		||||
        id: diagramId,
 | 
			
		||||
        dependencies,
 | 
			
		||||
        relationships,
 | 
			
		||||
        tables,
 | 
			
		||||
        areas,
 | 
			
		||||
        createdAt: new Date(),
 | 
			
		||||
        updatedAt: new Date(),
 | 
			
		||||
        diagram: {
 | 
			
		||||
            ...diagram,
 | 
			
		||||
            id: diagramId,
 | 
			
		||||
            dependencies,
 | 
			
		||||
            relationships,
 | 
			
		||||
            tables,
 | 
			
		||||
            areas,
 | 
			
		||||
            customTypes,
 | 
			
		||||
            createdAt: diagram.createdAt
 | 
			
		||||
                ? new Date(diagram.createdAt)
 | 
			
		||||
                : new Date(),
 | 
			
		||||
            updatedAt: diagram.updatedAt
 | 
			
		||||
                ? new Date(diagram.updatedAt)
 | 
			
		||||
                : new Date(),
 | 
			
		||||
        },
 | 
			
		||||
        idsMap,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -48,18 +48,30 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'mediumblob', id: 'mediumblob' },
 | 
			
		||||
    { name: 'tinyblob', id: 'tinyblob' },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    { name: 'char large object', id: 'char_large_object' },
 | 
			
		||||
    { name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'char varying',
 | 
			
		||||
        id: 'char_varying',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'character large object', id: 'character_large_object' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'character varying',
 | 
			
		||||
        id: 'character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'nchar large object', id: 'nchar_large_object' },
 | 
			
		||||
    { name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'nchar varying',
 | 
			
		||||
        id: 'nchar_varying',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character large object',
 | 
			
		||||
        id: 'national_character_large_object',
 | 
			
		||||
@@ -67,22 +79,34 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character varying',
 | 
			
		||||
        id: 'national_character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national char varying',
 | 
			
		||||
        id: 'national_char_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character',
 | 
			
		||||
        id: 'national_character',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national char',
 | 
			
		||||
        id: 'national_char',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'national char', id: 'national_char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'binary large object', id: 'binary_large_object' },
 | 
			
		||||
    { name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'binary varying',
 | 
			
		||||
        id: 'binary_varying',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'fixedstring',
 | 
			
		||||
        id: 'fixedstring',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'string', id: 'string' },
 | 
			
		||||
 | 
			
		||||
    // Date Types
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,23 @@ export interface DataType {
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DataTypeData extends DataType {
 | 
			
		||||
export interface FieldAttributeRange {
 | 
			
		||||
    max: number;
 | 
			
		||||
    min: number;
 | 
			
		||||
    default: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FieldAttributes {
 | 
			
		||||
    hasCharMaxLength?: boolean;
 | 
			
		||||
    hasCharMaxLengthOption?: boolean;
 | 
			
		||||
    precision?: FieldAttributeRange;
 | 
			
		||||
    scale?: FieldAttributeRange;
 | 
			
		||||
    maxLength?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DataTypeData extends DataType {
 | 
			
		||||
    usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
 | 
			
		||||
    fieldAttributes?: FieldAttributes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,12 @@ import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const genericDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Level 1 - Most commonly used types
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 1 },
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
@@ -10,23 +15,62 @@ export const genericDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 2 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 999,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 999,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 2,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'datetime', id: 'datetime', usageLevel: 2 },
 | 
			
		||||
    { name: 'json', id: 'json', usageLevel: 2 },
 | 
			
		||||
    { name: 'uuid', id: 'uuid', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
    // Less common types
 | 
			
		||||
    { name: 'bigint', id: 'bigint' },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'binary',
 | 
			
		||||
        id: 'binary',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    { name: 'double', id: 'double' },
 | 
			
		||||
    { name: 'enum', id: 'enum' },
 | 
			
		||||
    { name: 'float', id: 'float' },
 | 
			
		||||
    { name: 'numeric', id: 'numeric' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'numeric',
 | 
			
		||||
        id: 'numeric',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 999,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 999,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 2,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'real', id: 'real' },
 | 
			
		||||
    { name: 'set', id: 'set' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
    { name: 'time', id: 'time' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varbinary',
 | 
			
		||||
        id: 'varbinary',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
] as const;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,32 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Level 1 - Most commonly used types
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 1 },
 | 
			
		||||
    { name: 'bigint', id: 'bigint', usageLevel: 1 },
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 65,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 30,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 0,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
    { name: 'datetime', id: 'datetime', usageLevel: 1 },
 | 
			
		||||
    { name: 'date', id: 'date', usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
@@ -20,16 +40,39 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'tinyint', id: 'tinyint' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
    { name: 'mediumint', id: 'mediumint' },
 | 
			
		||||
    { name: 'numeric', id: 'numeric' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'numeric',
 | 
			
		||||
        id: 'numeric',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 65,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 30,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 0,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'float', id: 'float' },
 | 
			
		||||
    { name: 'double', id: 'double' },
 | 
			
		||||
    { name: 'bit', id: 'bit' },
 | 
			
		||||
    { name: 'bool', id: 'bool' },
 | 
			
		||||
    { name: 'time', id: 'time' },
 | 
			
		||||
    { name: 'year', id: 'year' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'binary',
 | 
			
		||||
        id: 'binary',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varbinary',
 | 
			
		||||
        id: 'varbinary',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'tinyblob', id: 'tinyblob' },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'mediumblob', id: 'mediumblob' },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
 | 
			
		||||
export const mysqlDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Level 1 - Most commonly used types
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 1 },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
@@ -11,7 +16,23 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
    { name: 'bigint', id: 'bigint', usageLevel: 2 },
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 2 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 65,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 30,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 0,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'datetime', id: 'datetime', usageLevel: 2 },
 | 
			
		||||
    { name: 'json', id: 'json', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
@@ -22,7 +43,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'float', id: 'float' },
 | 
			
		||||
    { name: 'double', id: 'double' },
 | 
			
		||||
    { name: 'bit', id: 'bit' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    { name: 'tinytext', id: 'tinytext' },
 | 
			
		||||
    { name: 'mediumtext', id: 'mediumtext' },
 | 
			
		||||
    { name: 'longtext', id: 'longtext' },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,30 @@ import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const oracleDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Character types
 | 
			
		||||
    { name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'VARCHAR2',
 | 
			
		||||
        id: 'varchar2',
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'NVARCHAR2',
 | 
			
		||||
        id: 'nvarchar2',
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'CHAR',
 | 
			
		||||
        id: 'char',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'NCHAR',
 | 
			
		||||
        id: 'nchar',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
 | 
			
		||||
    { name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
 | 
			
		||||
    { name: 'CLOB', id: 'clob', usageLevel: 2 },
 | 
			
		||||
    { name: 'NCLOB', id: 'nclob', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +64,12 @@ export const oracleDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'BFILE', id: 'bfile', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
    // Other types
 | 
			
		||||
    { name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'RAW',
 | 
			
		||||
        id: 'raw',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
 | 
			
		||||
    { name: 'ROWID', id: 'rowid', usageLevel: 2 },
 | 
			
		||||
    { name: 'UROWID', id: 'urowid', usageLevel: 2 },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
 | 
			
		||||
export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Level 1 - Most commonly used types
 | 
			
		||||
    { name: 'integer', id: 'integer', usageLevel: 1 },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
@@ -11,7 +16,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
    { name: 'bigint', id: 'bigint', usageLevel: 2 },
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 2 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 131072,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 16383,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 2,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'serial', id: 'serial', usageLevel: 2 },
 | 
			
		||||
    { name: 'json', id: 'json', usageLevel: 2 },
 | 
			
		||||
    { name: 'jsonb', id: 'jsonb', usageLevel: 2 },
 | 
			
		||||
@@ -23,18 +44,33 @@ export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Less common types
 | 
			
		||||
    { name: 'numeric', id: 'numeric' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'numeric',
 | 
			
		||||
        id: 'numeric',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 131072,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 10,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 16383,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 2,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'real', id: 'real' },
 | 
			
		||||
    { name: 'double precision', id: 'double_precision' },
 | 
			
		||||
    { name: 'smallserial', id: 'smallserial' },
 | 
			
		||||
    { name: 'bigserial', id: 'bigserial' },
 | 
			
		||||
    { name: 'money', id: 'money' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'character varying',
 | 
			
		||||
        id: 'character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'time', id: 'time' },
 | 
			
		||||
    { name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
 | 
			
		||||
 
 | 
			
		||||
@@ -4,32 +4,93 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Level 1 - Most commonly used types
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 1 },
 | 
			
		||||
    { name: 'bit', id: 'bit', usageLevel: 1 },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    { name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            hasCharMaxLength: true,
 | 
			
		||||
            hasCharMaxLengthOption: true,
 | 
			
		||||
            maxLength: 8000,
 | 
			
		||||
        },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'nvarchar',
 | 
			
		||||
        id: 'nvarchar',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            hasCharMaxLength: true,
 | 
			
		||||
            hasCharMaxLengthOption: true,
 | 
			
		||||
            maxLength: 4000,
 | 
			
		||||
        },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
    { name: 'datetime', id: 'datetime', usageLevel: 1 },
 | 
			
		||||
    { name: 'date', id: 'date', usageLevel: 1 },
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
    { name: 'bigint', id: 'bigint', usageLevel: 2 },
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 2 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 38,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 18,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 38,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 0,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'datetime2', id: 'datetime2', usageLevel: 2 },
 | 
			
		||||
    { name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
 | 
			
		||||
    { name: 'json', id: 'json', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
    // Less common types
 | 
			
		||||
    { name: 'numeric', id: 'numeric' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'numeric',
 | 
			
		||||
        id: 'numeric',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            precision: {
 | 
			
		||||
                max: 38,
 | 
			
		||||
                min: 1,
 | 
			
		||||
                default: 18,
 | 
			
		||||
            },
 | 
			
		||||
            scale: {
 | 
			
		||||
                max: 38,
 | 
			
		||||
                min: 0,
 | 
			
		||||
                default: 0,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
    { name: 'smallmoney', id: 'smallmoney' },
 | 
			
		||||
    { name: 'tinyint', id: 'tinyint' },
 | 
			
		||||
    { name: 'money', id: 'money' },
 | 
			
		||||
    { name: 'float', id: 'float' },
 | 
			
		||||
    { name: 'real', id: 'real' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'nchar', id: 'nchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    { name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
 | 
			
		||||
    { name: 'ntext', id: 'ntext' },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'binary',
 | 
			
		||||
        id: 'binary',
 | 
			
		||||
        fieldAttributes: { hasCharMaxLength: true },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varbinary',
 | 
			
		||||
        id: 'varbinary',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            hasCharMaxLength: true,
 | 
			
		||||
            hasCharMaxLengthOption: true,
 | 
			
		||||
            maxLength: 8000,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'image', id: 'image' },
 | 
			
		||||
    { name: 'datetimeoffset', id: 'datetimeoffset' },
 | 
			
		||||
    { name: 'smalldatetime', id: 'smalldatetime' },
 | 
			
		||||
 
 | 
			
		||||
@@ -10,21 +10,41 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
 | 
			
		||||
    // SQLite type aliases and common types
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 1 },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
    { name: 'date', id: 'date', usageLevel: 1 },
 | 
			
		||||
    { name: 'datetime', id: 'datetime', usageLevel: 1 },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'varchar',
 | 
			
		||||
        id: 'varchar',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            hasCharMaxLength: true,
 | 
			
		||||
        },
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'timestamp',
 | 
			
		||||
        id: 'timestamp',
 | 
			
		||||
        usageLevel: 1,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
    { name: 'numeric', id: 'numeric', usageLevel: 2 },
 | 
			
		||||
    { name: 'decimal', id: 'decimal', usageLevel: 2 },
 | 
			
		||||
    { name: 'float', id: 'float', usageLevel: 2 },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'decimal',
 | 
			
		||||
        id: 'decimal',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'double', id: 'double', usageLevel: 2 },
 | 
			
		||||
    { name: 'json', id: 'json', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
    // Less common types (all map to SQLite storage classes)
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'char',
 | 
			
		||||
        id: 'char',
 | 
			
		||||
        fieldAttributes: {
 | 
			
		||||
            hasCharMaxLength: true,
 | 
			
		||||
        },
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'binary', id: 'binary' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
 
 | 
			
		||||
@@ -4,4 +4,5 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
 | 
			
		||||
    [DatabaseType.POSTGRESQL]: 'public',
 | 
			
		||||
    [DatabaseType.SQL_SERVER]: 'dbo',
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: 'default',
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: 'public',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										870
									
								
								src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										870
									
								
								src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,870 @@
 | 
			
		||||
import { describe, it, expect, vi } from 'vitest';
 | 
			
		||||
import { exportBaseSQL } from '../export-sql-script';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
 | 
			
		||||
// Mock the dbml/core importer
 | 
			
		||||
vi.mock('@dbml/core', () => ({
 | 
			
		||||
    importer: {
 | 
			
		||||
        import: vi.fn((sql: string) => {
 | 
			
		||||
            // Return a simplified DBML for testing
 | 
			
		||||
            return sql;
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
describe('DBML Export - SQL Generation Tests', () => {
 | 
			
		||||
    // Helper to generate test IDs and timestamps
 | 
			
		||||
    let idCounter = 0;
 | 
			
		||||
    const testId = () => `test-id-${++idCounter}`;
 | 
			
		||||
    const testTime = Date.now();
 | 
			
		||||
 | 
			
		||||
    // Helper to create a field with all required properties
 | 
			
		||||
    const createField = (overrides: Partial<DBField>): DBField =>
 | 
			
		||||
        ({
 | 
			
		||||
            id: testId(),
 | 
			
		||||
            name: 'field',
 | 
			
		||||
            type: { id: 'text', name: 'text' },
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
            unique: false,
 | 
			
		||||
            createdAt: testTime,
 | 
			
		||||
            ...overrides,
 | 
			
		||||
        }) as DBField;
 | 
			
		||||
 | 
			
		||||
    // Helper to create a table with all required properties
 | 
			
		||||
    const createTable = (overrides: Partial<DBTable>): DBTable =>
 | 
			
		||||
        ({
 | 
			
		||||
            id: testId(),
 | 
			
		||||
            name: 'table',
 | 
			
		||||
            fields: [],
 | 
			
		||||
            indexes: [],
 | 
			
		||||
            createdAt: testTime,
 | 
			
		||||
            x: 0,
 | 
			
		||||
            y: 0,
 | 
			
		||||
            width: 200,
 | 
			
		||||
            ...overrides,
 | 
			
		||||
        }) as DBTable;
 | 
			
		||||
 | 
			
		||||
    // Helper to create a diagram with all required properties
 | 
			
		||||
    const createDiagram = (overrides: Partial<Diagram>): Diagram =>
 | 
			
		||||
        ({
 | 
			
		||||
            id: testId(),
 | 
			
		||||
            name: 'diagram',
 | 
			
		||||
            databaseType: DatabaseType.GENERIC,
 | 
			
		||||
            tables: [],
 | 
			
		||||
            relationships: [],
 | 
			
		||||
            createdAt: testTime,
 | 
			
		||||
            updatedAt: testTime,
 | 
			
		||||
            ...overrides,
 | 
			
		||||
        }) as Diagram;
 | 
			
		||||
 | 
			
		||||
    describe('Composite Primary Keys', () => {
 | 
			
		||||
        it('should handle tables with composite primary keys correctly', () => {
 | 
			
		||||
            const tableId = testId();
 | 
			
		||||
            const field1Id = testId();
 | 
			
		||||
            const field2Id = testId();
 | 
			
		||||
 | 
			
		||||
            const diagram: Diagram = createDiagram({
 | 
			
		||||
                id: testId(),
 | 
			
		||||
                name: 'Enchanted Library',
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: tableId,
 | 
			
		||||
                        name: 'spell_components',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: field1Id,
 | 
			
		||||
                                name: 'spell_id',
 | 
			
		||||
                                type: { id: 'uuid', name: 'uuid' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: field2Id,
 | 
			
		||||
                                name: 'component_id',
 | 
			
		||||
                                type: { id: 'uuid', name: 'uuid' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'quantity',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                                default: '1',
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [],
 | 
			
		||||
                        color: '#FFD700',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const sql = exportBaseSQL({
 | 
			
		||||
                diagram,
 | 
			
		||||
                targetDatabaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                isDBMLFlow: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should contain composite primary key syntax
 | 
			
		||||
            expect(sql).toContain('PRIMARY KEY (spell_id, component_id)');
 | 
			
		||||
            // Should NOT contain individual PRIMARY KEY constraints
 | 
			
		||||
            expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
 | 
			
		||||
            expect(sql).not.toMatch(
 | 
			
		||||
                /component_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle single primary keys inline', () => {
 | 
			
		||||
            const diagram: Diagram = createDiagram({
 | 
			
		||||
                id: testId(),
 | 
			
		||||
                name: 'Wizard Academy',
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: testId(),
 | 
			
		||||
                        name: 'wizards',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'uuid', name: 'uuid' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'name',
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [],
 | 
			
		||||
                        color: '#9370DB',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const sql = exportBaseSQL({
 | 
			
		||||
                diagram,
 | 
			
		||||
                targetDatabaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                isDBMLFlow: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should contain inline PRIMARY KEY
 | 
			
		||||
            expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
 | 
			
		||||
            // Should NOT contain separate PRIMARY KEY constraint
 | 
			
		||||
            expect(sql).not.toContain('PRIMARY KEY (id)');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Default Value Handling', () => {
 | 
			
		||||
        it('should skip invalid default values like "has default"', () => {
 | 
			
		||||
            const diagram: Diagram = createDiagram({
 | 
			
		||||
                id: testId(),
 | 
			
		||||
                name: 'Potion Shop',
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: testId(),
 | 
			
		||||
                        name: 'potions',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'uuid', name: 'uuid' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'is_active',
 | 
			
		||||
                                type: { id: 'boolean', name: 'boolean' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: true,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                                default: 'has default',
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: testId(),
 | 
			
		||||
                                name: 'stock_count',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                                default: 'DEFAULT has default',
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [],
 | 
			
		||||
                        color: '#98FB98',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const sql = exportBaseSQL({
 | 
			
		||||
                diagram,
 | 
			
		||||
                targetDatabaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                isDBMLFlow: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should not contain invalid default values
 | 
			
		||||
            expect(sql).not.toContain('DEFAULT has default');
 | 
			
		||||
            expect(sql).not.toContain('DEFAULT DEFAULT has default');
 | 
			
		||||
            // The fields should still be in the table
 | 
			
		||||
            expect(sql).toContain('is_active boolean');
 | 
			
		||||
            expect(sql).toContain('stock_count int 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,6 +48,50 @@ export function exportFieldComment(comment: string): string {
 | 
			
		||||
        .join('');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function escapeSQLComment(comment: string): string {
 | 
			
		||||
    if (!comment) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Escape single quotes by doubling them
 | 
			
		||||
    let escaped = comment.replace(/'/g, "''");
 | 
			
		||||
 | 
			
		||||
    // Replace newlines with spaces to prevent breaking SQL syntax
 | 
			
		||||
    // Some databases support multi-line comments with specific syntax,
 | 
			
		||||
    // but for maximum compatibility, we'll replace newlines with spaces
 | 
			
		||||
    escaped = escaped.replace(/[\r\n]+/g, ' ');
 | 
			
		||||
 | 
			
		||||
    // Trim any excessive whitespace
 | 
			
		||||
    escaped = escaped.replace(/\s+/g, ' ').trim();
 | 
			
		||||
 | 
			
		||||
    return escaped;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatTableComment(comment: string): string {
 | 
			
		||||
    if (!comment) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Split by newlines and add -- to each line
 | 
			
		||||
    return (
 | 
			
		||||
        comment
 | 
			
		||||
            .split('\n')
 | 
			
		||||
            .map((line) => `-- ${line}`)
 | 
			
		||||
            .join('\n') + '\n'
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatMSSQLTableComment(comment: string): string {
 | 
			
		||||
    if (!comment) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For MSSQL, we use multi-line comment syntax
 | 
			
		||||
    // Escape */ to prevent breaking the comment block
 | 
			
		||||
    const escaped = comment.replace(/\*\//g, '* /');
 | 
			
		||||
    return `/**\n${escaped}\n*/\n`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getInlineFK(table: DBTable, diagram: Diagram): string {
 | 
			
		||||
    if (!diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
    exportFieldComment,
 | 
			
		||||
    formatMSSQLTableComment,
 | 
			
		||||
    isFunction,
 | 
			
		||||
    isKeyword,
 | 
			
		||||
    strHasQuotes,
 | 
			
		||||
@@ -72,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string {
 | 
			
		||||
    return `'${defaultValue}'`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportMSSQL(diagram: Diagram): string {
 | 
			
		||||
export function exportMSSQL({
 | 
			
		||||
    diagram,
 | 
			
		||||
    onlyRelationships = false,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    onlyRelationships?: boolean;
 | 
			
		||||
}): string {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
@@ -82,166 +89,254 @@ export function exportMSSQL(diagram: Diagram): string {
 | 
			
		||||
 | 
			
		||||
    // Create CREATE SCHEMA statements for all schemas
 | 
			
		||||
    let sqlScript = '';
 | 
			
		||||
    const schemas = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        if (table.schema) {
 | 
			
		||||
            schemas.add(table.schema);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    if (!onlyRelationships) {
 | 
			
		||||
        const schemas = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    // Add schema creation statements
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n    EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Generate table creation SQL
 | 
			
		||||
    sqlScript += tables
 | 
			
		||||
        .map((table: DBTable) => {
 | 
			
		||||
            // Skip views
 | 
			
		||||
            if (table.isView) {
 | 
			
		||||
                return '';
 | 
			
		||||
        tables.forEach((table) => {
 | 
			
		||||
            if (table.schema) {
 | 
			
		||||
                schemas.add(table.schema);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            const tableName = table.schema
 | 
			
		||||
                ? `[${table.schema}].[${table.name}]`
 | 
			
		||||
                : `[${table.name}]`;
 | 
			
		||||
        // Add schema creation statements
 | 
			
		||||
        schemas.forEach((schema) => {
 | 
			
		||||
            sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n    EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            return `${
 | 
			
		||||
                table.comments ? `/**\n${table.comments}\n*/\n` : ''
 | 
			
		||||
            }CREATE TABLE ${tableName} (\n${table.fields
 | 
			
		||||
                .map((field: DBField) => {
 | 
			
		||||
                    const fieldName = `[${field.name}]`;
 | 
			
		||||
                    const typeName = field.type.name;
 | 
			
		||||
        // Generate table creation SQL
 | 
			
		||||
        sqlScript += tables
 | 
			
		||||
            .map((table: DBTable) => {
 | 
			
		||||
                // Skip views
 | 
			
		||||
                if (table.isView) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    // Handle SQL Server specific type formatting
 | 
			
		||||
                    let typeWithSize = typeName;
 | 
			
		||||
                    if (field.characterMaximumLength) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'nvarchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'nchar'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                const tableName = table.schema
 | 
			
		||||
                    ? `[${table.schema}].[${table.name}]`
 | 
			
		||||
                    : `[${table.name}]`;
 | 
			
		||||
 | 
			
		||||
                return `${
 | 
			
		||||
                    table.comments
 | 
			
		||||
                        ? formatMSSQLTableComment(table.comments)
 | 
			
		||||
                        : ''
 | 
			
		||||
                }CREATE TABLE ${tableName} (\n${table.fields
 | 
			
		||||
                    .map((field: DBField) => {
 | 
			
		||||
                        const fieldName = `[${field.name}]`;
 | 
			
		||||
                        const typeName = field.type.name;
 | 
			
		||||
 | 
			
		||||
                        // Handle SQL Server specific type formatting
 | 
			
		||||
                        let typeWithSize = typeName;
 | 
			
		||||
                        if (field.characterMaximumLength) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'nvarchar' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'nchar'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision && field.scale) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                        if (field.precision && field.scale) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (field.precision) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
                        const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
 | 
			
		||||
                    // Check if identity column
 | 
			
		||||
                    const identity = field.default
 | 
			
		||||
                        ?.toLowerCase()
 | 
			
		||||
                        .includes('identity')
 | 
			
		||||
                        ? ' IDENTITY(1,1)'
 | 
			
		||||
                        : '';
 | 
			
		||||
 | 
			
		||||
                    const unique =
 | 
			
		||||
                        !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    // Handle default value using SQL Server specific parser
 | 
			
		||||
                    const defaultValue =
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        !field.default.toLowerCase().includes('identity')
 | 
			
		||||
                            ? ` DEFAULT ${parseMSSQLDefault(field)}`
 | 
			
		||||
                        // Check if identity column
 | 
			
		||||
                        const identity = field.default
 | 
			
		||||
                            ?.toLowerCase()
 | 
			
		||||
                            .includes('identity')
 | 
			
		||||
                            ? ' IDENTITY(1,1)'
 | 
			
		||||
                            : '';
 | 
			
		||||
 | 
			
		||||
                    // Do not add PRIMARY KEY as a column constraint - will add as table constraint
 | 
			
		||||
                    return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
 | 
			
		||||
                })
 | 
			
		||||
                .join(',\n')}${
 | 
			
		||||
                table.fields.filter((f) => f.primaryKey).length > 0
 | 
			
		||||
                    ? `,\n    PRIMARY KEY (${table.fields
 | 
			
		||||
                          .filter((f) => f.primaryKey)
 | 
			
		||||
                          .map((f) => `[${f.name}]`)
 | 
			
		||||
                          .join(', ')})`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }\n);\n\n${table.indexes
 | 
			
		||||
                .map((index) => {
 | 
			
		||||
                    const indexName = table.schema
 | 
			
		||||
                        ? `[${table.schema}_${index.name}]`
 | 
			
		||||
                        : `[${index.name}]`;
 | 
			
		||||
                    const indexFields = index.fieldIds
 | 
			
		||||
                        .map((fieldId) => {
 | 
			
		||||
                            const field = table.fields.find(
 | 
			
		||||
                                (f) => f.id === fieldId
 | 
			
		||||
                            );
 | 
			
		||||
                            return field ? `[${field.name}]` : '';
 | 
			
		||||
                        const unique =
 | 
			
		||||
                            !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                        // Handle default value using SQL Server specific parser
 | 
			
		||||
                        const defaultValue =
 | 
			
		||||
                            field.default &&
 | 
			
		||||
                            !field.default.toLowerCase().includes('identity')
 | 
			
		||||
                                ? ` DEFAULT ${parseMSSQLDefault(field)}`
 | 
			
		||||
                                : '';
 | 
			
		||||
 | 
			
		||||
                        // Do not add PRIMARY KEY as a column constraint - will add as table constraint
 | 
			
		||||
                        return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
 | 
			
		||||
                    })
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    table.fields.filter((f) => f.primaryKey).length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${table.fields
 | 
			
		||||
                              .filter((f) => f.primaryKey)
 | 
			
		||||
                              .map((f) => `[${f.name}]`)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
                        : ''
 | 
			
		||||
                }\n);\n${(() => {
 | 
			
		||||
                    const validIndexes = table.indexes
 | 
			
		||||
                        .map((index) => {
 | 
			
		||||
                            const indexName = table.schema
 | 
			
		||||
                                ? `[${table.schema}_${index.name}]`
 | 
			
		||||
                                : `[${index.name}]`;
 | 
			
		||||
                            const indexFields = index.fieldIds
 | 
			
		||||
                                .map((fieldId) => {
 | 
			
		||||
                                    const field = table.fields.find(
 | 
			
		||||
                                        (f) => f.id === fieldId
 | 
			
		||||
                                    );
 | 
			
		||||
                                    return field ? `[${field.name}]` : '';
 | 
			
		||||
                                })
 | 
			
		||||
                                .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                            // SQL Server has a limit of 32 columns in an index
 | 
			
		||||
                            if (indexFields.length > 32) {
 | 
			
		||||
                                const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
 | 
			
		||||
                                console.warn(
 | 
			
		||||
                                    `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
 | 
			
		||||
                                );
 | 
			
		||||
                                indexFields.length = 32;
 | 
			
		||||
                                return indexFields.length > 0
 | 
			
		||||
                                    ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
 | 
			
		||||
                                    : '';
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return indexFields.length > 0
 | 
			
		||||
                                ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
 | 
			
		||||
                                : '';
 | 
			
		||||
                        })
 | 
			
		||||
                        .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                    // SQL Server has a limit of 32 columns in an index
 | 
			
		||||
                    if (indexFields.length > 32) {
 | 
			
		||||
                        const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
 | 
			
		||||
                        console.warn(
 | 
			
		||||
                            `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
 | 
			
		||||
                        );
 | 
			
		||||
                        indexFields.length = 32;
 | 
			
		||||
                        return indexFields.length > 0
 | 
			
		||||
                            ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
 | 
			
		||||
                            : '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return indexFields.length > 0
 | 
			
		||||
                        ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
 | 
			
		||||
                    return validIndexes.length > 0
 | 
			
		||||
                        ? `\n-- Indexes\n${validIndexes.join('\n')}`
 | 
			
		||||
                        : '';
 | 
			
		||||
                })
 | 
			
		||||
                .join('')}`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
        .join('\n');
 | 
			
		||||
                })()}\n`;
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
            .join('\n');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate foreign keys
 | 
			
		||||
    sqlScript += `\n${relationships
 | 
			
		||||
        .map((r: DBRelationship) => {
 | 
			
		||||
            const sourceTable = tables.find((t) => t.id === r.sourceTableId);
 | 
			
		||||
            const targetTable = tables.find((t) => t.id === r.targetTableId);
 | 
			
		||||
    if (relationships.length > 0) {
 | 
			
		||||
        sqlScript += '\n-- Foreign key constraints\n';
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                !sourceTable ||
 | 
			
		||||
                !targetTable ||
 | 
			
		||||
                sourceTable.isView ||
 | 
			
		||||
                targetTable.isView
 | 
			
		||||
            ) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
        // Process all relationships and create FK objects with schema info
 | 
			
		||||
        const foreignKeys = relationships
 | 
			
		||||
            .map((r: DBRelationship) => {
 | 
			
		||||
                const sourceTable = tables.find(
 | 
			
		||||
                    (t) => t.id === r.sourceTableId
 | 
			
		||||
                );
 | 
			
		||||
                const targetTable = tables.find(
 | 
			
		||||
                    (t) => t.id === r.targetTableId
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            const sourceField = sourceTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.sourceFieldId
 | 
			
		||||
            );
 | 
			
		||||
            const targetField = targetTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.targetFieldId
 | 
			
		||||
            );
 | 
			
		||||
                if (
 | 
			
		||||
                    !sourceTable ||
 | 
			
		||||
                    !targetTable ||
 | 
			
		||||
                    sourceTable.isView ||
 | 
			
		||||
                    targetTable.isView
 | 
			
		||||
                ) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if (!sourceField || !targetField) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
                const sourceField = sourceTable.fields.find(
 | 
			
		||||
                    (f) => f.id === r.sourceFieldId
 | 
			
		||||
                );
 | 
			
		||||
                const targetField = targetTable.fields.find(
 | 
			
		||||
                    (f) => f.id === r.targetFieldId
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            const sourceTableName = sourceTable.schema
 | 
			
		||||
                ? `[${sourceTable.schema}].[${sourceTable.name}]`
 | 
			
		||||
                : `[${sourceTable.name}]`;
 | 
			
		||||
            const targetTableName = targetTable.schema
 | 
			
		||||
                ? `[${targetTable.schema}].[${targetTable.name}]`
 | 
			
		||||
                : `[${targetTable.name}]`;
 | 
			
		||||
                if (!sourceField || !targetField) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings
 | 
			
		||||
        .join('\n')}`;
 | 
			
		||||
                // Determine which table should have the foreign key based on cardinality
 | 
			
		||||
                let fkTable, fkField, refTable, refField;
 | 
			
		||||
 | 
			
		||||
                if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'many'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on target table
 | 
			
		||||
                    fkTable = targetTable;
 | 
			
		||||
                    fkField = targetField;
 | 
			
		||||
                    refTable = sourceTable;
 | 
			
		||||
                    refField = sourceField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'many' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on source table
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | 
			
		||||
                    // We'll keep the current behavior for 1:1
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Many-to-many relationships need a junction table, skip for now
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const fkTableName = fkTable.schema
 | 
			
		||||
                    ? `[${fkTable.schema}].[${fkTable.name}]`
 | 
			
		||||
                    : `[${fkTable.name}]`;
 | 
			
		||||
                const refTableName = refTable.schema
 | 
			
		||||
                    ? `[${refTable.schema}].[${refTable.name}]`
 | 
			
		||||
                    : `[${refTable.name}]`;
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    schema: fkTable.schema || 'dbo',
 | 
			
		||||
                    sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
 | 
			
		||||
                };
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean); // Remove empty objects
 | 
			
		||||
 | 
			
		||||
        // Group foreign keys by schema
 | 
			
		||||
        const fksBySchema = foreignKeys.reduce(
 | 
			
		||||
            (acc, fk) => {
 | 
			
		||||
                if (!fk) return acc;
 | 
			
		||||
                const schema = fk.schema;
 | 
			
		||||
                if (!acc[schema]) {
 | 
			
		||||
                    acc[schema] = [];
 | 
			
		||||
                }
 | 
			
		||||
                acc[schema].push(fk.sql);
 | 
			
		||||
                return acc;
 | 
			
		||||
            },
 | 
			
		||||
            {} as Record<string, string[]>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Sort schemas and generate SQL with separators
 | 
			
		||||
        const sortedSchemas = Object.keys(fksBySchema).sort();
 | 
			
		||||
        const fkSql = sortedSchemas
 | 
			
		||||
            .map((schema, index) => {
 | 
			
		||||
                const schemaFks = fksBySchema[schema].join('\n');
 | 
			
		||||
                if (index === 0) {
 | 
			
		||||
                    return `-- Schema: ${schema}\n${schemaFks}`;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return `\n-- Schema: ${schema}\n${schemaFks}`;
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .join('\n');
 | 
			
		||||
 | 
			
		||||
        sqlScript += fkSql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return sqlScript;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
    exportFieldComment,
 | 
			
		||||
    escapeSQLComment,
 | 
			
		||||
    formatTableComment,
 | 
			
		||||
    isFunction,
 | 
			
		||||
    isKeyword,
 | 
			
		||||
    strHasQuotes,
 | 
			
		||||
@@ -168,7 +170,13 @@ function mapMySQLType(typeName: string): string {
 | 
			
		||||
    return typeName;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportMySQL(diagram: Diagram): string {
 | 
			
		||||
export function exportMySQL({
 | 
			
		||||
    diagram,
 | 
			
		||||
    onlyRelationships = false,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    onlyRelationships?: boolean;
 | 
			
		||||
}): string {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
@@ -177,224 +185,245 @@ export function exportMySQL(diagram: Diagram): string {
 | 
			
		||||
    const relationships = diagram.relationships;
 | 
			
		||||
 | 
			
		||||
    // Start SQL script
 | 
			
		||||
    let sqlScript = '-- MySQL database export\n\n';
 | 
			
		||||
    let sqlScript = '-- MySQL database export\n';
 | 
			
		||||
 | 
			
		||||
    // MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
 | 
			
		||||
    sqlScript += 'START TRANSACTION;\n\n';
 | 
			
		||||
    if (!onlyRelationships) {
 | 
			
		||||
        // MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
 | 
			
		||||
        sqlScript += 'START TRANSACTION;\n';
 | 
			
		||||
 | 
			
		||||
    // Create databases (schemas) if they don't exist
 | 
			
		||||
    const schemas = new Set<string>();
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        if (table.schema) {
 | 
			
		||||
            schemas.add(table.schema);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (schemas.size > 0) {
 | 
			
		||||
        sqlScript += '\n';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate table creation SQL
 | 
			
		||||
    sqlScript += tables
 | 
			
		||||
        .map((table: DBTable) => {
 | 
			
		||||
            // Skip views
 | 
			
		||||
            if (table.isView) {
 | 
			
		||||
                return '';
 | 
			
		||||
        // Create databases (schemas) if they don't exist
 | 
			
		||||
        const schemas = new Set<string>();
 | 
			
		||||
        tables.forEach((table) => {
 | 
			
		||||
            if (table.schema) {
 | 
			
		||||
                schemas.add(table.schema);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            // Use schema prefix if available
 | 
			
		||||
            const tableName = table.schema
 | 
			
		||||
                ? `\`${table.schema}\`.\`${table.name}\``
 | 
			
		||||
                : `\`${table.name}\``;
 | 
			
		||||
        schemas.forEach((schema) => {
 | 
			
		||||
            sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            // Get primary key fields
 | 
			
		||||
            const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
        if (schemas.size > 0) {
 | 
			
		||||
            sqlScript += '\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            return `${
 | 
			
		||||
                table.comments ? `-- ${table.comments}\n` : ''
 | 
			
		||||
            }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
 | 
			
		||||
                .map((field: DBField) => {
 | 
			
		||||
                    const fieldName = `\`${field.name}\``;
 | 
			
		||||
        // Generate table creation SQL
 | 
			
		||||
        sqlScript += tables
 | 
			
		||||
            .map((table: DBTable) => {
 | 
			
		||||
                // Skip views
 | 
			
		||||
                if (table.isView) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    // Handle type name - map to MySQL compatible types
 | 
			
		||||
                    const typeName = mapMySQLType(field.type.name);
 | 
			
		||||
                // Use schema prefix if available
 | 
			
		||||
                const tableName = table.schema
 | 
			
		||||
                    ? `\`${table.schema}\`.\`${table.name}\``
 | 
			
		||||
                    : `\`${table.name}\``;
 | 
			
		||||
 | 
			
		||||
                    // Handle MySQL specific type formatting
 | 
			
		||||
                    let typeWithSize = typeName;
 | 
			
		||||
                    if (field.characterMaximumLength) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'varbinary'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                // Get primary key fields
 | 
			
		||||
                const primaryKeyFields = table.fields.filter(
 | 
			
		||||
                    (f) => f.primaryKey
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                return `${
 | 
			
		||||
                    table.comments ? formatTableComment(table.comments) : ''
 | 
			
		||||
                }\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
 | 
			
		||||
                    .map((field: DBField) => {
 | 
			
		||||
                        const fieldName = `\`${field.name}\``;
 | 
			
		||||
 | 
			
		||||
                        // Handle type name - map to MySQL compatible types
 | 
			
		||||
                        const typeName = mapMySQLType(field.type.name);
 | 
			
		||||
 | 
			
		||||
                        // Handle MySQL specific type formatting
 | 
			
		||||
                        let typeWithSize = typeName;
 | 
			
		||||
                        if (field.characterMaximumLength) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'varbinary'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision && field.scale) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                        if (field.precision && field.scale) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (field.precision) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision) {
 | 
			
		||||
 | 
			
		||||
                        // Set a default size for VARCHAR columns if not specified
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            typeName.toLowerCase() === 'varchar' &&
 | 
			
		||||
                            !field.characterMaximumLength
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                            typeWithSize = `${typeName}(255)`;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Set a default size for VARCHAR columns if not specified
 | 
			
		||||
                    if (
 | 
			
		||||
                        typeName.toLowerCase() === 'varchar' &&
 | 
			
		||||
                        !field.characterMaximumLength
 | 
			
		||||
                    ) {
 | 
			
		||||
                        typeWithSize = `${typeName}(255)`;
 | 
			
		||||
                    }
 | 
			
		||||
                        const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
 | 
			
		||||
                    const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
 | 
			
		||||
                    // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
 | 
			
		||||
                    let autoIncrement = '';
 | 
			
		||||
                    if (
 | 
			
		||||
                        field.primaryKey &&
 | 
			
		||||
                        (field.default?.toLowerCase().includes('identity') ||
 | 
			
		||||
                            field.default
 | 
			
		||||
                        // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
 | 
			
		||||
                        let autoIncrement = '';
 | 
			
		||||
                        if (
 | 
			
		||||
                            field.primaryKey &&
 | 
			
		||||
                            (field.default
 | 
			
		||||
                                ?.toLowerCase()
 | 
			
		||||
                                .includes('autoincrement') ||
 | 
			
		||||
                            field.default?.includes('nextval'))
 | 
			
		||||
                    ) {
 | 
			
		||||
                        autoIncrement = ' AUTO_INCREMENT';
 | 
			
		||||
                    }
 | 
			
		||||
                                .includes('identity') ||
 | 
			
		||||
                                field.default
 | 
			
		||||
                                    ?.toLowerCase()
 | 
			
		||||
                                    .includes('autoincrement') ||
 | 
			
		||||
                                field.default?.includes('nextval'))
 | 
			
		||||
                        ) {
 | 
			
		||||
                            autoIncrement = ' AUTO_INCREMENT';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                    const unique =
 | 
			
		||||
                        !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
                        // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                        const unique =
 | 
			
		||||
                            !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    // Handle default value
 | 
			
		||||
                    const defaultValue =
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        !field.default.toLowerCase().includes('identity') &&
 | 
			
		||||
                        !field.default
 | 
			
		||||
                            .toLowerCase()
 | 
			
		||||
                            .includes('autoincrement') &&
 | 
			
		||||
                        !field.default.includes('nextval')
 | 
			
		||||
                            ? ` DEFAULT ${parseMySQLDefault(field)}`
 | 
			
		||||
                        // Handle default value
 | 
			
		||||
                        const defaultValue =
 | 
			
		||||
                            field.default &&
 | 
			
		||||
                            !field.default.toLowerCase().includes('identity') &&
 | 
			
		||||
                            !field.default
 | 
			
		||||
                                .toLowerCase()
 | 
			
		||||
                                .includes('autoincrement') &&
 | 
			
		||||
                            !field.default.includes('nextval')
 | 
			
		||||
                                ? ` DEFAULT ${parseMySQLDefault(field)}`
 | 
			
		||||
                                : '';
 | 
			
		||||
 | 
			
		||||
                        // MySQL supports inline comments
 | 
			
		||||
                        const comment = field.comments
 | 
			
		||||
                            ? ` COMMENT '${escapeSQLComment(field.comments)}'`
 | 
			
		||||
                            : '';
 | 
			
		||||
 | 
			
		||||
                    // MySQL supports inline comments
 | 
			
		||||
                    const comment = field.comments
 | 
			
		||||
                        ? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
 | 
			
		||||
                        : '';
 | 
			
		||||
                        return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
 | 
			
		||||
                    })
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    // Add PRIMARY KEY as table constraint
 | 
			
		||||
                    primaryKeyFields.length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                              .map((f) => `\`${f.name}\``)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
                        : ''
 | 
			
		||||
                }\n)${
 | 
			
		||||
                    // MySQL supports table comments
 | 
			
		||||
                    table.comments
 | 
			
		||||
                        ? ` COMMENT='${escapeSQLComment(table.comments)}'`
 | 
			
		||||
                        : ''
 | 
			
		||||
                };\n${
 | 
			
		||||
                    // Add indexes - MySQL creates them separately from the table definition
 | 
			
		||||
                    (() => {
 | 
			
		||||
                        const validIndexes = table.indexes
 | 
			
		||||
                            .map((index) => {
 | 
			
		||||
                                // Get the list of fields for this index
 | 
			
		||||
                                const indexFields = index.fieldIds
 | 
			
		||||
                                    .map((fieldId) => {
 | 
			
		||||
                                        const field = table.fields.find(
 | 
			
		||||
                                            (f) => f.id === fieldId
 | 
			
		||||
                                        );
 | 
			
		||||
                                        return field ? field : null;
 | 
			
		||||
                                    })
 | 
			
		||||
                                    .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                    return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
 | 
			
		||||
                })
 | 
			
		||||
                .join(',\n')}${
 | 
			
		||||
                // Add PRIMARY KEY as table constraint
 | 
			
		||||
                primaryKeyFields.length > 0
 | 
			
		||||
                    ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                          .map((f) => `\`${f.name}\``)
 | 
			
		||||
                          .join(', ')})`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }\n)${
 | 
			
		||||
                // MySQL supports table comments
 | 
			
		||||
                table.comments
 | 
			
		||||
                    ? ` COMMENT='${table.comments.replace(/'/g, "''")}'`
 | 
			
		||||
                    : ''
 | 
			
		||||
            };\n\n${
 | 
			
		||||
                // Add indexes - MySQL creates them separately from the table definition
 | 
			
		||||
                table.indexes
 | 
			
		||||
                    .map((index) => {
 | 
			
		||||
                        // Get the list of fields for this index
 | 
			
		||||
                        const indexFields = index.fieldIds
 | 
			
		||||
                            .map((fieldId) => {
 | 
			
		||||
                                const field = table.fields.find(
 | 
			
		||||
                                    (f) => f.id === fieldId
 | 
			
		||||
                                // Skip if this index exactly matches the primary key fields
 | 
			
		||||
                                if (
 | 
			
		||||
                                    primaryKeyFields.length ===
 | 
			
		||||
                                        indexFields.length &&
 | 
			
		||||
                                    primaryKeyFields.every((pk) =>
 | 
			
		||||
                                        indexFields.some(
 | 
			
		||||
                                            (field) =>
 | 
			
		||||
                                                field && field.id === pk.id
 | 
			
		||||
                                        )
 | 
			
		||||
                                    )
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    return '';
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                // Create a unique index name by combining table name, field names, and a unique/non-unique indicator
 | 
			
		||||
                                const fieldNamesForIndex = indexFields
 | 
			
		||||
                                    .map((field) => field?.name || '')
 | 
			
		||||
                                    .join('_');
 | 
			
		||||
                                const uniqueIndicator = index.unique
 | 
			
		||||
                                    ? '_unique'
 | 
			
		||||
                                    : '';
 | 
			
		||||
                                const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
 | 
			
		||||
 | 
			
		||||
                                // Get the properly quoted field names
 | 
			
		||||
                                const indexFieldNames = indexFields
 | 
			
		||||
                                    .map((field) =>
 | 
			
		||||
                                        field ? `\`${field.name}\`` : ''
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                                // Check for text/blob fields that need special handling
 | 
			
		||||
                                const hasTextOrBlob = indexFields.some(
 | 
			
		||||
                                    (field) => {
 | 
			
		||||
                                        const typeName =
 | 
			
		||||
                                            field?.type.name.toLowerCase() ||
 | 
			
		||||
                                            '';
 | 
			
		||||
                                        return (
 | 
			
		||||
                                            typeName === 'text' ||
 | 
			
		||||
                                            typeName === 'mediumtext' ||
 | 
			
		||||
                                            typeName === 'longtext' ||
 | 
			
		||||
                                            typeName === 'blob'
 | 
			
		||||
                                        );
 | 
			
		||||
                                    }
 | 
			
		||||
                                );
 | 
			
		||||
                                return field ? field : null;
 | 
			
		||||
 | 
			
		||||
                                // If there are TEXT/BLOB fields, need to add prefix length
 | 
			
		||||
                                const indexFieldsWithPrefix = hasTextOrBlob
 | 
			
		||||
                                    ? indexFieldNames.map((name) => {
 | 
			
		||||
                                          const field = indexFields.find(
 | 
			
		||||
                                              (f) => `\`${f?.name}\`` === name
 | 
			
		||||
                                          );
 | 
			
		||||
                                          if (!field) return name;
 | 
			
		||||
 | 
			
		||||
                                          const typeName =
 | 
			
		||||
                                              field.type.name.toLowerCase();
 | 
			
		||||
                                          if (
 | 
			
		||||
                                              typeName === 'text' ||
 | 
			
		||||
                                              typeName === 'mediumtext' ||
 | 
			
		||||
                                              typeName === 'longtext' ||
 | 
			
		||||
                                              typeName === 'blob'
 | 
			
		||||
                                          ) {
 | 
			
		||||
                                              // Add a prefix length for TEXT/BLOB fields (required in MySQL)
 | 
			
		||||
                                              return `${name}(255)`;
 | 
			
		||||
                                          }
 | 
			
		||||
                                          return name;
 | 
			
		||||
                                      })
 | 
			
		||||
                                    : indexFieldNames;
 | 
			
		||||
 | 
			
		||||
                                return indexFieldNames.length > 0
 | 
			
		||||
                                    ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
 | 
			
		||||
                                    : '';
 | 
			
		||||
                            })
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        // Skip if this index exactly matches the primary key fields
 | 
			
		||||
                        if (
 | 
			
		||||
                            primaryKeyFields.length === indexFields.length &&
 | 
			
		||||
                            primaryKeyFields.every((pk) =>
 | 
			
		||||
                                indexFields.some(
 | 
			
		||||
                                    (field) => field && field.id === pk.id
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        ) {
 | 
			
		||||
                            return '';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Create a unique index name by combining table name, field names, and a unique/non-unique indicator
 | 
			
		||||
                        const fieldNamesForIndex = indexFields
 | 
			
		||||
                            .map((field) => field?.name || '')
 | 
			
		||||
                            .join('_');
 | 
			
		||||
                        const uniqueIndicator = index.unique ? '_unique' : '';
 | 
			
		||||
                        const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
 | 
			
		||||
 | 
			
		||||
                        // Get the properly quoted field names
 | 
			
		||||
                        const indexFieldNames = indexFields
 | 
			
		||||
                            .map((field) => (field ? `\`${field.name}\`` : ''))
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        // Check for text/blob fields that need special handling
 | 
			
		||||
                        const hasTextOrBlob = indexFields.some((field) => {
 | 
			
		||||
                            const typeName =
 | 
			
		||||
                                field?.type.name.toLowerCase() || '';
 | 
			
		||||
                            return (
 | 
			
		||||
                                typeName === 'text' ||
 | 
			
		||||
                                typeName === 'mediumtext' ||
 | 
			
		||||
                                typeName === 'longtext' ||
 | 
			
		||||
                                typeName === 'blob'
 | 
			
		||||
                            );
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        // If there are TEXT/BLOB fields, need to add prefix length
 | 
			
		||||
                        const indexFieldsWithPrefix = hasTextOrBlob
 | 
			
		||||
                            ? indexFieldNames.map((name) => {
 | 
			
		||||
                                  const field = indexFields.find(
 | 
			
		||||
                                      (f) => `\`${f?.name}\`` === name
 | 
			
		||||
                                  );
 | 
			
		||||
                                  if (!field) return name;
 | 
			
		||||
 | 
			
		||||
                                  const typeName =
 | 
			
		||||
                                      field.type.name.toLowerCase();
 | 
			
		||||
                                  if (
 | 
			
		||||
                                      typeName === 'text' ||
 | 
			
		||||
                                      typeName === 'mediumtext' ||
 | 
			
		||||
                                      typeName === 'longtext' ||
 | 
			
		||||
                                      typeName === 'blob'
 | 
			
		||||
                                  ) {
 | 
			
		||||
                                      // Add a prefix length for TEXT/BLOB fields (required in MySQL)
 | 
			
		||||
                                      return `${name}(255)`;
 | 
			
		||||
                                  }
 | 
			
		||||
                                  return name;
 | 
			
		||||
                              })
 | 
			
		||||
                            : indexFieldNames;
 | 
			
		||||
 | 
			
		||||
                        return indexFieldNames.length > 0
 | 
			
		||||
                            ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
 | 
			
		||||
                        return validIndexes.length > 0
 | 
			
		||||
                            ? `\n-- Indexes\n${validIndexes.join('\n')}`
 | 
			
		||||
                            : '';
 | 
			
		||||
                    })
 | 
			
		||||
                    .filter(Boolean)
 | 
			
		||||
                    .join('\n')
 | 
			
		||||
            }`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
        .join('\n');
 | 
			
		||||
                    })()
 | 
			
		||||
                }\n`;
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
            .join('\n');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate foreign keys
 | 
			
		||||
    if (relationships.length > 0) {
 | 
			
		||||
        sqlScript += '\n-- Foreign key constraints\n\n';
 | 
			
		||||
        sqlScript += '\n-- Foreign key constraints\n';
 | 
			
		||||
 | 
			
		||||
        sqlScript += relationships
 | 
			
		||||
        const foreignKeys = relationships
 | 
			
		||||
            .map((r: DBRelationship) => {
 | 
			
		||||
                const sourceTable = tables.find(
 | 
			
		||||
                    (t) => t.id === r.sourceTableId
 | 
			
		||||
@@ -423,25 +452,62 @@ export function exportMySQL(diagram: Diagram): string {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const sourceTableName = sourceTable.schema
 | 
			
		||||
                    ? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
 | 
			
		||||
                    : `\`${sourceTable.name}\``;
 | 
			
		||||
                const targetTableName = targetTable.schema
 | 
			
		||||
                    ? `\`${targetTable.schema}\`.\`${targetTable.name}\``
 | 
			
		||||
                    : `\`${targetTable.name}\``;
 | 
			
		||||
                // Determine which table should have the foreign key based on cardinality
 | 
			
		||||
                let fkTable, fkField, refTable, refField;
 | 
			
		||||
 | 
			
		||||
                if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'many'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on target table
 | 
			
		||||
                    fkTable = targetTable;
 | 
			
		||||
                    fkField = targetField;
 | 
			
		||||
                    refTable = sourceTable;
 | 
			
		||||
                    refField = sourceField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'many' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on source table
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | 
			
		||||
                    // We'll keep the current behavior for 1:1
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Many-to-many relationships need a junction table, skip for now
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const fkTableName = fkTable.schema
 | 
			
		||||
                    ? `\`${fkTable.schema}\`.\`${fkTable.name}\``
 | 
			
		||||
                    : `\`${fkTable.name}\``;
 | 
			
		||||
                const refTableName = refTable.schema
 | 
			
		||||
                    ? `\`${refTable.schema}\`.\`${refTable.name}\``
 | 
			
		||||
                    : `\`${refTable.name}\``;
 | 
			
		||||
 | 
			
		||||
                // Create a descriptive constraint name
 | 
			
		||||
                const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
 | 
			
		||||
                const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
 | 
			
		||||
 | 
			
		||||
                // MySQL supports ON DELETE and ON UPDATE actions
 | 
			
		||||
                return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
 | 
			
		||||
                return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean) // Remove empty strings
 | 
			
		||||
            .join('\n');
 | 
			
		||||
            .filter(Boolean); // Remove empty strings
 | 
			
		||||
 | 
			
		||||
        sqlScript += foreignKeys.join('\n');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Commit transaction
 | 
			
		||||
    sqlScript += '\nCOMMIT;\n';
 | 
			
		||||
    sqlScript += '\n\nCOMMIT;\n';
 | 
			
		||||
 | 
			
		||||
    return sqlScript;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
    exportFieldComment,
 | 
			
		||||
    escapeSQLComment,
 | 
			
		||||
    formatTableComment,
 | 
			
		||||
    isFunction,
 | 
			
		||||
    isKeyword,
 | 
			
		||||
    strHasQuotes,
 | 
			
		||||
@@ -140,10 +142,16 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return typesSql + '\n';
 | 
			
		||||
    return typesSql ? typesSql + '\n' : '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportPostgreSQL(diagram: Diagram): string {
 | 
			
		||||
export function exportPostgreSQL({
 | 
			
		||||
    diagram,
 | 
			
		||||
    onlyRelationships = false,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    onlyRelationships?: boolean;
 | 
			
		||||
}): string {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
@@ -154,290 +162,391 @@ export function exportPostgreSQL(diagram: Diagram): string {
 | 
			
		||||
 | 
			
		||||
    // Create CREATE SCHEMA statements for all schemas
 | 
			
		||||
    let sqlScript = '';
 | 
			
		||||
    const schemas = new Set<string>();
 | 
			
		||||
    if (!onlyRelationships) {
 | 
			
		||||
        const schemas = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        if (table.schema) {
 | 
			
		||||
            schemas.add(table.schema);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Also collect schemas from custom types
 | 
			
		||||
    customTypes.forEach((customType) => {
 | 
			
		||||
        if (customType.schema) {
 | 
			
		||||
            schemas.add(customType.schema);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Add schema creation statements
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
 | 
			
		||||
    });
 | 
			
		||||
    sqlScript += '\n';
 | 
			
		||||
 | 
			
		||||
    // Add custom types (enums and composite types)
 | 
			
		||||
    sqlScript += exportCustomTypes(customTypes);
 | 
			
		||||
 | 
			
		||||
    // Add sequence creation statements
 | 
			
		||||
    const sequences = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        table.fields.forEach((field) => {
 | 
			
		||||
            if (field.default) {
 | 
			
		||||
                // Match nextval('schema.sequence_name') or nextval('sequence_name')
 | 
			
		||||
                const match = field.default.match(
 | 
			
		||||
                    /nextval\('([^']+)'(?:::[^)]+)?\)/
 | 
			
		||||
                );
 | 
			
		||||
                if (match) {
 | 
			
		||||
                    sequences.add(match[1]);
 | 
			
		||||
                }
 | 
			
		||||
        tables.forEach((table) => {
 | 
			
		||||
            if (table.schema) {
 | 
			
		||||
                schemas.add(table.schema);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    sequences.forEach((sequence) => {
 | 
			
		||||
        sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
 | 
			
		||||
    });
 | 
			
		||||
    sqlScript += '\n';
 | 
			
		||||
 | 
			
		||||
    // Generate table creation SQL
 | 
			
		||||
    sqlScript += tables
 | 
			
		||||
        .map((table: DBTable) => {
 | 
			
		||||
            // Skip views
 | 
			
		||||
            if (table.isView) {
 | 
			
		||||
                return '';
 | 
			
		||||
        // Also collect schemas from custom types
 | 
			
		||||
        customTypes.forEach((customType) => {
 | 
			
		||||
            if (customType.schema) {
 | 
			
		||||
                schemas.add(customType.schema);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            const tableName = table.schema
 | 
			
		||||
                ? `"${table.schema}"."${table.name}"`
 | 
			
		||||
                : `"${table.name}"`;
 | 
			
		||||
        // Add schema creation statements
 | 
			
		||||
        schemas.forEach((schema) => {
 | 
			
		||||
            sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
 | 
			
		||||
        });
 | 
			
		||||
        if (schemas.size > 0) {
 | 
			
		||||
            sqlScript += '\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // Get primary key fields
 | 
			
		||||
            const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
        // Add custom types (enums and composite types)
 | 
			
		||||
        sqlScript += exportCustomTypes(customTypes);
 | 
			
		||||
 | 
			
		||||
            return `${
 | 
			
		||||
                table.comments ? `-- ${table.comments}\n` : ''
 | 
			
		||||
            }CREATE TABLE ${tableName} (\n${table.fields
 | 
			
		||||
                .map((field: DBField) => {
 | 
			
		||||
                    const fieldName = `"${field.name}"`;
 | 
			
		||||
        // Add sequence creation statements
 | 
			
		||||
        const sequences = new Set<string>();
 | 
			
		||||
 | 
			
		||||
                    // Handle type name - map problematic types to PostgreSQL compatible types
 | 
			
		||||
                    const typeName = mapPostgresType(
 | 
			
		||||
                        field.type.name,
 | 
			
		||||
                        field.name
 | 
			
		||||
        tables.forEach((table) => {
 | 
			
		||||
            table.fields.forEach((field) => {
 | 
			
		||||
                if (field.default) {
 | 
			
		||||
                    // Match nextval('schema.sequence_name') or nextval('sequence_name')
 | 
			
		||||
                    const match = field.default.match(
 | 
			
		||||
                        /nextval\('([^']+)'(?:::[^)]+)?\)/
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    // Handle PostgreSQL specific type formatting
 | 
			
		||||
                    let typeWithSize = typeName;
 | 
			
		||||
                    let serialType = null;
 | 
			
		||||
 | 
			
		||||
                    if (field.increment && !field.nullable) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'integer' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'int'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            serialType = 'SERIAL';
 | 
			
		||||
                        } else if (typeName.toLowerCase() === 'bigint') {
 | 
			
		||||
                            serialType = 'BIGSERIAL';
 | 
			
		||||
                        } else if (typeName.toLowerCase() === 'smallint') {
 | 
			
		||||
                            serialType = 'SMALLSERIAL';
 | 
			
		||||
                        }
 | 
			
		||||
                    if (match) {
 | 
			
		||||
                        sequences.add(match[1]);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
                    if (field.characterMaximumLength) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'character varying' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'character'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
        sequences.forEach((sequence) => {
 | 
			
		||||
            sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
 | 
			
		||||
        });
 | 
			
		||||
        if (sequences.size > 0) {
 | 
			
		||||
            sqlScript += '\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Generate table creation SQL
 | 
			
		||||
        sqlScript += tables
 | 
			
		||||
            .map((table: DBTable) => {
 | 
			
		||||
                // Skip views
 | 
			
		||||
                if (table.isView) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const tableName = table.schema
 | 
			
		||||
                    ? `"${table.schema}"."${table.name}"`
 | 
			
		||||
                    : `"${table.name}"`;
 | 
			
		||||
 | 
			
		||||
                // Get primary key fields
 | 
			
		||||
                const primaryKeyFields = table.fields.filter(
 | 
			
		||||
                    (f) => f.primaryKey
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                return `${
 | 
			
		||||
                    table.comments ? formatTableComment(table.comments) : ''
 | 
			
		||||
                }CREATE TABLE ${tableName} (\n${table.fields
 | 
			
		||||
                    .map((field: DBField) => {
 | 
			
		||||
                        const fieldName = `"${field.name}"`;
 | 
			
		||||
 | 
			
		||||
                        // Handle type name - map problematic types to PostgreSQL compatible types
 | 
			
		||||
                        const typeName = mapPostgresType(
 | 
			
		||||
                            field.type.name,
 | 
			
		||||
                            field.name
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        // Handle PostgreSQL specific type formatting
 | 
			
		||||
                        let typeWithSize = typeName;
 | 
			
		||||
                        let serialType = null;
 | 
			
		||||
 | 
			
		||||
                        if (field.increment && !field.nullable) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'integer' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'int'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                serialType = 'SERIAL';
 | 
			
		||||
                            } else if (typeName.toLowerCase() === 'bigint') {
 | 
			
		||||
                                serialType = 'BIGSERIAL';
 | 
			
		||||
                            } else if (typeName.toLowerCase() === 'smallint') {
 | 
			
		||||
                                serialType = 'SMALLSERIAL';
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision && field.scale) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
 | 
			
		||||
                        if (field.characterMaximumLength) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                                typeName.toLowerCase() ===
 | 
			
		||||
                                    'character varying' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'character'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                        if (field.precision && field.scale) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (field.precision) {
 | 
			
		||||
                            if (
 | 
			
		||||
                                typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                                typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                            ) {
 | 
			
		||||
                                typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Handle array types (check if the type name ends with '[]')
 | 
			
		||||
                    if (typeName.endsWith('[]')) {
 | 
			
		||||
                        typeWithSize = typeWithSize.replace('[]', '') + '[]';
 | 
			
		||||
                    }
 | 
			
		||||
                        // Handle array types (check if the type name ends with '[]')
 | 
			
		||||
                        if (typeName.endsWith('[]')) {
 | 
			
		||||
                            typeWithSize =
 | 
			
		||||
                                typeWithSize.replace('[]', '') + '[]';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
                        const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
 | 
			
		||||
                    // Handle identity generation
 | 
			
		||||
                    let identity = '';
 | 
			
		||||
                    if (field.default && field.default.includes('nextval')) {
 | 
			
		||||
                        // PostgreSQL already handles this with DEFAULT nextval()
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        field.default.toLowerCase().includes('identity')
 | 
			
		||||
                    ) {
 | 
			
		||||
                        identity = ' GENERATED BY DEFAULT AS IDENTITY';
 | 
			
		||||
                    }
 | 
			
		||||
                        // Handle identity generation
 | 
			
		||||
                        let identity = '';
 | 
			
		||||
                        if (
 | 
			
		||||
                            field.default &&
 | 
			
		||||
                            field.default.includes('nextval')
 | 
			
		||||
                        ) {
 | 
			
		||||
                            // PostgreSQL already handles this with DEFAULT nextval()
 | 
			
		||||
                        } else if (
 | 
			
		||||
                            field.default &&
 | 
			
		||||
                            field.default.toLowerCase().includes('identity')
 | 
			
		||||
                        ) {
 | 
			
		||||
                            identity = ' GENERATED BY DEFAULT AS IDENTITY';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                    // This avoids redundant uniqueness constraints
 | 
			
		||||
                    const unique =
 | 
			
		||||
                        !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
                        // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                        // This avoids redundant uniqueness constraints
 | 
			
		||||
                        const unique =
 | 
			
		||||
                            !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    // Handle default value using PostgreSQL specific parser
 | 
			
		||||
                    const defaultValue =
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        !field.default.toLowerCase().includes('identity')
 | 
			
		||||
                            ? ` DEFAULT ${parsePostgresDefault(field)}`
 | 
			
		||||
                            : '';
 | 
			
		||||
                        // Handle default value using PostgreSQL specific parser
 | 
			
		||||
                        const defaultValue =
 | 
			
		||||
                            field.default &&
 | 
			
		||||
                            !field.default.toLowerCase().includes('identity')
 | 
			
		||||
                                ? ` DEFAULT ${parsePostgresDefault(field)}`
 | 
			
		||||
                                : '';
 | 
			
		||||
 | 
			
		||||
                    // Do not add PRIMARY KEY as a column constraint - will add as table constraint
 | 
			
		||||
                    return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
 | 
			
		||||
                })
 | 
			
		||||
                .join(',\n')}${
 | 
			
		||||
                primaryKeyFields.length > 0
 | 
			
		||||
                    ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                          .map((f) => `"${f.name}"`)
 | 
			
		||||
                          .join(', ')})`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }\n);\n\n${
 | 
			
		||||
                // Add table comments
 | 
			
		||||
                table.comments
 | 
			
		||||
                    ? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }${
 | 
			
		||||
                // Add column comments
 | 
			
		||||
                table.fields
 | 
			
		||||
                    .filter((f) => f.comments)
 | 
			
		||||
                    .map(
 | 
			
		||||
                        (f) =>
 | 
			
		||||
                            `COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n`
 | 
			
		||||
                    )
 | 
			
		||||
                    .join('')
 | 
			
		||||
            }\n${
 | 
			
		||||
                // Add indexes only for non-primary key fields or composite indexes
 | 
			
		||||
                // This avoids duplicate indexes on primary key columns
 | 
			
		||||
                table.indexes
 | 
			
		||||
                    .map((index) => {
 | 
			
		||||
                        // Get the list of fields for this index
 | 
			
		||||
                        const indexFields = index.fieldIds
 | 
			
		||||
                            .map((fieldId) => {
 | 
			
		||||
                                const field = table.fields.find(
 | 
			
		||||
                                    (f) => f.id === fieldId
 | 
			
		||||
                        // Do not add PRIMARY KEY as a column constraint - will add as table constraint
 | 
			
		||||
                        return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
 | 
			
		||||
                    })
 | 
			
		||||
                    .join(',\n')}${
 | 
			
		||||
                    primaryKeyFields.length > 0
 | 
			
		||||
                        ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                              .map((f) => `"${f.name}"`)
 | 
			
		||||
                              .join(', ')})`
 | 
			
		||||
                        : ''
 | 
			
		||||
                }\n);${
 | 
			
		||||
                    // Add table comments
 | 
			
		||||
                    table.comments
 | 
			
		||||
                        ? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
 | 
			
		||||
                        : ''
 | 
			
		||||
                }${
 | 
			
		||||
                    // Add column comments
 | 
			
		||||
                    table.fields
 | 
			
		||||
                        .filter((f) => f.comments)
 | 
			
		||||
                        .map(
 | 
			
		||||
                            (f) =>
 | 
			
		||||
                                `\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
 | 
			
		||||
                        )
 | 
			
		||||
                        .join('')
 | 
			
		||||
                }${
 | 
			
		||||
                    // Add indexes only for non-primary key fields or composite indexes
 | 
			
		||||
                    // This avoids duplicate indexes on primary key columns
 | 
			
		||||
                    (() => {
 | 
			
		||||
                        const validIndexes = table.indexes
 | 
			
		||||
                            .map((index) => {
 | 
			
		||||
                                // Get the list of fields for this index
 | 
			
		||||
                                const indexFields = index.fieldIds
 | 
			
		||||
                                    .map((fieldId) => {
 | 
			
		||||
                                        const field = table.fields.find(
 | 
			
		||||
                                            (f) => f.id === fieldId
 | 
			
		||||
                                        );
 | 
			
		||||
                                        return field ? field : null;
 | 
			
		||||
                                    })
 | 
			
		||||
                                    .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                                // Skip if this index exactly matches the primary key fields
 | 
			
		||||
                                // This prevents creating redundant indexes
 | 
			
		||||
                                if (
 | 
			
		||||
                                    primaryKeyFields.length ===
 | 
			
		||||
                                        indexFields.length &&
 | 
			
		||||
                                    primaryKeyFields.every((pk) =>
 | 
			
		||||
                                        indexFields.some(
 | 
			
		||||
                                            (field) =>
 | 
			
		||||
                                                field && field.id === pk.id
 | 
			
		||||
                                        )
 | 
			
		||||
                                    )
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    return '';
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                // Create unique index name using table name and index name
 | 
			
		||||
                                // This ensures index names are unique across the database
 | 
			
		||||
                                const safeTableName = table.name.replace(
 | 
			
		||||
                                    /[^a-zA-Z0-9_]/g,
 | 
			
		||||
                                    '_'
 | 
			
		||||
                                );
 | 
			
		||||
                                return field ? field : null;
 | 
			
		||||
                                const safeIndexName = index.name.replace(
 | 
			
		||||
                                    /[^a-zA-Z0-9_]/g,
 | 
			
		||||
                                    '_'
 | 
			
		||||
                                );
 | 
			
		||||
 | 
			
		||||
                                // Limit index name length to avoid PostgreSQL's 63-character identifier limit
 | 
			
		||||
                                let combinedName = `${safeTableName}_${safeIndexName}`;
 | 
			
		||||
                                if (combinedName.length > 60) {
 | 
			
		||||
                                    // If too long, use just the index name or a truncated version
 | 
			
		||||
                                    combinedName =
 | 
			
		||||
                                        safeIndexName.length > 60
 | 
			
		||||
                                            ? safeIndexName.substring(0, 60)
 | 
			
		||||
                                            : safeIndexName;
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                const indexName = `"${combinedName}"`;
 | 
			
		||||
 | 
			
		||||
                                // Get the properly quoted field names
 | 
			
		||||
                                const indexFieldNames = indexFields
 | 
			
		||||
                                    .map((field) =>
 | 
			
		||||
                                        field ? `"${field.name}"` : ''
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                                return indexFieldNames.length > 0
 | 
			
		||||
                                    ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
 | 
			
		||||
                                    : '';
 | 
			
		||||
                            })
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        // Skip if this index exactly matches the primary key fields
 | 
			
		||||
                        // This prevents creating redundant indexes
 | 
			
		||||
                        if (
 | 
			
		||||
                            primaryKeyFields.length === indexFields.length &&
 | 
			
		||||
                            primaryKeyFields.every((pk) =>
 | 
			
		||||
                                indexFields.some(
 | 
			
		||||
                                    (field) => field && field.id === pk.id
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        ) {
 | 
			
		||||
                            return '';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Create unique index name using table name and index name
 | 
			
		||||
                        // This ensures index names are unique across the database
 | 
			
		||||
                        const safeTableName = table.name.replace(
 | 
			
		||||
                            /[^a-zA-Z0-9_]/g,
 | 
			
		||||
                            '_'
 | 
			
		||||
                        );
 | 
			
		||||
                        const safeIndexName = index.name.replace(
 | 
			
		||||
                            /[^a-zA-Z0-9_]/g,
 | 
			
		||||
                            '_'
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        // Limit index name length to avoid PostgreSQL's 63-character identifier limit
 | 
			
		||||
                        let combinedName = `${safeTableName}_${safeIndexName}`;
 | 
			
		||||
                        if (combinedName.length > 60) {
 | 
			
		||||
                            // If too long, use just the index name or a truncated version
 | 
			
		||||
                            combinedName =
 | 
			
		||||
                                safeIndexName.length > 60
 | 
			
		||||
                                    ? safeIndexName.substring(0, 60)
 | 
			
		||||
                                    : safeIndexName;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        const indexName = `"${combinedName}"`;
 | 
			
		||||
 | 
			
		||||
                        // Get the properly quoted field names
 | 
			
		||||
                        const indexFieldNames = indexFields
 | 
			
		||||
                            .map((field) => (field ? `"${field.name}"` : ''))
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        return indexFieldNames.length > 0
 | 
			
		||||
                            ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
 | 
			
		||||
                        return validIndexes.length > 0
 | 
			
		||||
                            ? `\n-- Indexes\n${validIndexes.join('\n')}`
 | 
			
		||||
                            : '';
 | 
			
		||||
                    })
 | 
			
		||||
                    .filter(Boolean)
 | 
			
		||||
                    .join('')
 | 
			
		||||
            }`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
        .join('\n');
 | 
			
		||||
                    })()
 | 
			
		||||
                }\n`;
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
            .join('\n');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate foreign keys
 | 
			
		||||
    sqlScript += `\n${relationships
 | 
			
		||||
        .map((r: DBRelationship) => {
 | 
			
		||||
            const sourceTable = tables.find((t) => t.id === r.sourceTableId);
 | 
			
		||||
            const targetTable = tables.find((t) => t.id === r.targetTableId);
 | 
			
		||||
    if (relationships.length > 0) {
 | 
			
		||||
        sqlScript += '\n-- Foreign key constraints\n';
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                !sourceTable ||
 | 
			
		||||
                !targetTable ||
 | 
			
		||||
                sourceTable.isView ||
 | 
			
		||||
                targetTable.isView
 | 
			
		||||
            ) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
        // Process all relationships and create FK objects with schema info
 | 
			
		||||
        const foreignKeys = relationships
 | 
			
		||||
            .map((r: DBRelationship) => {
 | 
			
		||||
                const sourceTable = tables.find(
 | 
			
		||||
                    (t) => t.id === r.sourceTableId
 | 
			
		||||
                );
 | 
			
		||||
                const targetTable = tables.find(
 | 
			
		||||
                    (t) => t.id === r.targetTableId
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            const sourceField = sourceTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.sourceFieldId
 | 
			
		||||
            );
 | 
			
		||||
            const targetField = targetTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.targetFieldId
 | 
			
		||||
            );
 | 
			
		||||
                if (
 | 
			
		||||
                    !sourceTable ||
 | 
			
		||||
                    !targetTable ||
 | 
			
		||||
                    sourceTable.isView ||
 | 
			
		||||
                    targetTable.isView
 | 
			
		||||
                ) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if (!sourceField || !targetField) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
                const sourceField = sourceTable.fields.find(
 | 
			
		||||
                    (f) => f.id === r.sourceFieldId
 | 
			
		||||
                );
 | 
			
		||||
                const targetField = targetTable.fields.find(
 | 
			
		||||
                    (f) => f.id === r.targetFieldId
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            const sourceTableName = sourceTable.schema
 | 
			
		||||
                ? `"${sourceTable.schema}"."${sourceTable.name}"`
 | 
			
		||||
                : `"${sourceTable.name}"`;
 | 
			
		||||
            const targetTableName = targetTable.schema
 | 
			
		||||
                ? `"${targetTable.schema}"."${targetTable.name}"`
 | 
			
		||||
                : `"${targetTable.name}"`;
 | 
			
		||||
                if (!sourceField || !targetField) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            // Create a unique constraint name by combining table and field names
 | 
			
		||||
            // Ensure it stays within PostgreSQL's 63-character limit for identifiers
 | 
			
		||||
            // and doesn't get truncated in a way that breaks SQL syntax
 | 
			
		||||
            const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`;
 | 
			
		||||
            // Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
 | 
			
		||||
            const safeConstraintName =
 | 
			
		||||
                baseName.length > 60
 | 
			
		||||
                    ? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
 | 
			
		||||
                    : baseName.replace(/[^a-zA-Z0-9_]/g, '_');
 | 
			
		||||
                // Determine which table should have the foreign key based on cardinality
 | 
			
		||||
                let fkTable, fkField, refTable, refField;
 | 
			
		||||
 | 
			
		||||
            const constraintName = `"${safeConstraintName}"`;
 | 
			
		||||
                if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'many'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on target table
 | 
			
		||||
                    fkTable = targetTable;
 | 
			
		||||
                    fkField = targetField;
 | 
			
		||||
                    refTable = sourceTable;
 | 
			
		||||
                    refField = sourceField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'many' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // FK goes on source table
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else if (
 | 
			
		||||
                    r.sourceCardinality === 'one' &&
 | 
			
		||||
                    r.targetCardinality === 'one'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | 
			
		||||
                    // We'll keep the current behavior for 1:1
 | 
			
		||||
                    fkTable = sourceTable;
 | 
			
		||||
                    fkField = sourceField;
 | 
			
		||||
                    refTable = targetTable;
 | 
			
		||||
                    refField = targetField;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Many-to-many relationships need a junction table, skip for now
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings
 | 
			
		||||
        .join('\n')}`;
 | 
			
		||||
                const fkTableName = fkTable.schema
 | 
			
		||||
                    ? `"${fkTable.schema}"."${fkTable.name}"`
 | 
			
		||||
                    : `"${fkTable.name}"`;
 | 
			
		||||
                const refTableName = refTable.schema
 | 
			
		||||
                    ? `"${refTable.schema}"."${refTable.name}"`
 | 
			
		||||
                    : `"${refTable.name}"`;
 | 
			
		||||
 | 
			
		||||
                // Create a unique constraint name by combining table and field names
 | 
			
		||||
                // Ensure it stays within PostgreSQL's 63-character limit for identifiers
 | 
			
		||||
                // and doesn't get truncated in a way that breaks SQL syntax
 | 
			
		||||
                const baseName = `fk_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.name}`;
 | 
			
		||||
                // Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
 | 
			
		||||
                const safeConstraintName =
 | 
			
		||||
                    baseName.length > 60
 | 
			
		||||
                        ? baseName
 | 
			
		||||
                              .substring(0, 60)
 | 
			
		||||
                              .replace(/[^a-zA-Z0-9_]/g, '_')
 | 
			
		||||
                        : baseName.replace(/[^a-zA-Z0-9_]/g, '_');
 | 
			
		||||
 | 
			
		||||
                const constraintName = `"${safeConstraintName}"`;
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    schema: fkTable.schema || 'public',
 | 
			
		||||
                    sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");`,
 | 
			
		||||
                };
 | 
			
		||||
            })
 | 
			
		||||
            .filter(Boolean); // Remove empty objects
 | 
			
		||||
 | 
			
		||||
        // Group foreign keys by schema
 | 
			
		||||
        const fksBySchema = foreignKeys.reduce(
 | 
			
		||||
            (acc, fk) => {
 | 
			
		||||
                if (!fk) return acc;
 | 
			
		||||
                const schema = fk.schema;
 | 
			
		||||
                if (!acc[schema]) {
 | 
			
		||||
                    acc[schema] = [];
 | 
			
		||||
                }
 | 
			
		||||
                acc[schema].push(fk.sql);
 | 
			
		||||
                return acc;
 | 
			
		||||
            },
 | 
			
		||||
            {} as Record<string, string[]>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Sort schemas and generate SQL with separators
 | 
			
		||||
        const sortedSchemas = Object.keys(fksBySchema).sort();
 | 
			
		||||
        const fkSql = sortedSchemas
 | 
			
		||||
            .map((schema, index) => {
 | 
			
		||||
                const schemaFks = fksBySchema[schema].join('\n');
 | 
			
		||||
                if (index === 0) {
 | 
			
		||||
                    return `-- Schema: ${schema}\n${schemaFks}`;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return `\n-- Schema: ${schema}\n${schemaFks}`;
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .join('\n');
 | 
			
		||||
 | 
			
		||||
        sqlScript += fkSql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return sqlScript;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
    exportFieldComment,
 | 
			
		||||
    formatTableComment,
 | 
			
		||||
    isFunction,
 | 
			
		||||
    isKeyword,
 | 
			
		||||
    strHasQuotes,
 | 
			
		||||
@@ -139,7 +140,13 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
 | 
			
		||||
    return typeName;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportSQLite(diagram: Diagram): string {
 | 
			
		||||
export function exportSQLite({
 | 
			
		||||
    diagram,
 | 
			
		||||
    onlyRelationships = false,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    onlyRelationships?: boolean;
 | 
			
		||||
}): string {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
@@ -148,10 +155,10 @@ export function exportSQLite(diagram: Diagram): string {
 | 
			
		||||
    const relationships = diagram.relationships;
 | 
			
		||||
 | 
			
		||||
    // Start SQL script - SQLite doesn't use schemas, so we skip schema creation
 | 
			
		||||
    let sqlScript = '-- SQLite database export\n\n';
 | 
			
		||||
    let sqlScript = '-- SQLite database export\n';
 | 
			
		||||
 | 
			
		||||
    // Begin transaction for faster import
 | 
			
		||||
    sqlScript += 'BEGIN TRANSACTION;\n\n';
 | 
			
		||||
    sqlScript += 'BEGIN TRANSACTION;\n';
 | 
			
		||||
 | 
			
		||||
    // SQLite doesn't have sequences, so we skip sequence creation
 | 
			
		||||
 | 
			
		||||
@@ -165,151 +172,167 @@ export function exportSQLite(diagram: Diagram): string {
 | 
			
		||||
        'sqlite_master',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Generate table creation SQL
 | 
			
		||||
    sqlScript += tables
 | 
			
		||||
        .map((table: DBTable) => {
 | 
			
		||||
            // Skip views
 | 
			
		||||
            if (table.isView) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
    if (!onlyRelationships) {
 | 
			
		||||
        // Generate table creation SQL
 | 
			
		||||
        sqlScript += tables
 | 
			
		||||
            .map((table: DBTable) => {
 | 
			
		||||
                // Skip views
 | 
			
		||||
                if (table.isView) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            // Skip SQLite system tables
 | 
			
		||||
            if (sqliteSystemTables.includes(table.name.toLowerCase())) {
 | 
			
		||||
                return `-- Skipping SQLite system table: "${table.name}"\n`;
 | 
			
		||||
            }
 | 
			
		||||
                // Skip SQLite system tables
 | 
			
		||||
                if (sqliteSystemTables.includes(table.name.toLowerCase())) {
 | 
			
		||||
                    return `-- Skipping SQLite system table: "${table.name}"\n`;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            // SQLite doesn't use schema prefixes, so we use just the table name
 | 
			
		||||
            // Include the schema in a comment if it exists
 | 
			
		||||
            const schemaComment = table.schema
 | 
			
		||||
                ? `-- Original schema: ${table.schema}\n`
 | 
			
		||||
                : '';
 | 
			
		||||
            const tableName = `"${table.name}"`;
 | 
			
		||||
                // SQLite doesn't use schema prefixes, so we use just the table name
 | 
			
		||||
                // Include the schema in a comment if it exists
 | 
			
		||||
                const schemaComment = table.schema
 | 
			
		||||
                    ? `-- Original schema: ${table.schema}\n`
 | 
			
		||||
                    : '';
 | 
			
		||||
                const tableName = `"${table.name}"`;
 | 
			
		||||
 | 
			
		||||
            // Get primary key fields
 | 
			
		||||
            const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
                // Get primary key fields
 | 
			
		||||
                const primaryKeyFields = table.fields.filter(
 | 
			
		||||
                    (f) => f.primaryKey
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            // Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
 | 
			
		||||
            const singleIntegerPrimaryKey =
 | 
			
		||||
                primaryKeyFields.length === 1 &&
 | 
			
		||||
                (primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
 | 
			
		||||
                    primaryKeyFields[0].type.name.toLowerCase() === 'int');
 | 
			
		||||
                // Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
 | 
			
		||||
                const singleIntegerPrimaryKey =
 | 
			
		||||
                    primaryKeyFields.length === 1 &&
 | 
			
		||||
                    (primaryKeyFields[0].type.name.toLowerCase() ===
 | 
			
		||||
                        'integer' ||
 | 
			
		||||
                        primaryKeyFields[0].type.name.toLowerCase() === 'int');
 | 
			
		||||
 | 
			
		||||
            return `${schemaComment}${
 | 
			
		||||
                table.comments ? `-- ${table.comments}\n` : ''
 | 
			
		||||
            }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
 | 
			
		||||
                .map((field: DBField) => {
 | 
			
		||||
                    const fieldName = `"${field.name}"`;
 | 
			
		||||
                return `${schemaComment}${
 | 
			
		||||
                    table.comments ? formatTableComment(table.comments) : ''
 | 
			
		||||
                }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
 | 
			
		||||
                    .map((field: DBField) => {
 | 
			
		||||
                        const fieldName = `"${field.name}"`;
 | 
			
		||||
 | 
			
		||||
                    // Handle type name - map to SQLite compatible types
 | 
			
		||||
                    const 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?.toLowerCase().includes('identity') ||
 | 
			
		||||
                            field.default
 | 
			
		||||
                        // Handle autoincrement - only works with INTEGER PRIMARY KEY
 | 
			
		||||
                        let autoIncrement = '';
 | 
			
		||||
                        if (
 | 
			
		||||
                            field.primaryKey &&
 | 
			
		||||
                            singleIntegerPrimaryKey &&
 | 
			
		||||
                            (field.default
 | 
			
		||||
                                ?.toLowerCase()
 | 
			
		||||
                                .includes('autoincrement') ||
 | 
			
		||||
                            field.default?.includes('nextval'))
 | 
			
		||||
                    ) {
 | 
			
		||||
                        autoIncrement = ' AUTOINCREMENT';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                    const unique =
 | 
			
		||||
                        !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    // Handle default value - Special handling for datetime() function
 | 
			
		||||
                    let defaultValue = '';
 | 
			
		||||
                    if (
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        !field.default.toLowerCase().includes('identity') &&
 | 
			
		||||
                        !field.default
 | 
			
		||||
                            .toLowerCase()
 | 
			
		||||
                            .includes('autoincrement') &&
 | 
			
		||||
                        !field.default.includes('nextval')
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
 | 
			
		||||
                        if (field.default.includes("datetime(''now'')")) {
 | 
			
		||||
                            defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
 | 
			
		||||
                        } else {
 | 
			
		||||
                            defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
 | 
			
		||||
                                .includes('identity') ||
 | 
			
		||||
                                field.default
 | 
			
		||||
                                    ?.toLowerCase()
 | 
			
		||||
                                    .includes('autoincrement') ||
 | 
			
		||||
                                field.default?.includes('nextval'))
 | 
			
		||||
                        ) {
 | 
			
		||||
                            autoIncrement = ' AUTOINCREMENT';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Add PRIMARY KEY inline only for single INTEGER primary key
 | 
			
		||||
                    const primaryKey =
 | 
			
		||||
                        field.primaryKey && singleIntegerPrimaryKey
 | 
			
		||||
                            ? ' PRIMARY KEY' + autoIncrement
 | 
			
		||||
                            : '';
 | 
			
		||||
                        // Only add UNIQUE constraint if the field is not part of the primary key
 | 
			
		||||
                        const unique =
 | 
			
		||||
                            !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
 | 
			
		||||
                })
 | 
			
		||||
                .join(',\n')}${
 | 
			
		||||
                // Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
 | 
			
		||||
                primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
 | 
			
		||||
                    ? `,\n    PRIMARY KEY (${primaryKeyFields
 | 
			
		||||
                          .map((f) => `"${f.name}"`)
 | 
			
		||||
                          .join(', ')})`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }\n);\n\n${
 | 
			
		||||
                // Add indexes - SQLite doesn't support indexes in CREATE TABLE
 | 
			
		||||
                table.indexes
 | 
			
		||||
                    .map((index) => {
 | 
			
		||||
                        // Skip indexes that exactly match the primary key
 | 
			
		||||
                        const indexFields = index.fieldIds
 | 
			
		||||
                            .map((fieldId) => {
 | 
			
		||||
                                const field = table.fields.find(
 | 
			
		||||
                                    (f) => f.id === fieldId
 | 
			
		||||
                                );
 | 
			
		||||
                                return field ? field : null;
 | 
			
		||||
                        // Handle 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(', ')});`
 | 
			
		||||
                                    : '';
 | 
			
		||||
                            })
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        // Get the properly quoted field names
 | 
			
		||||
                        const indexFieldNames = indexFields
 | 
			
		||||
                            .map((field) => (field ? `"${field.name}"` : ''))
 | 
			
		||||
                            .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                        // Skip if this index exactly matches the primary key fields
 | 
			
		||||
                        if (
 | 
			
		||||
                            primaryKeyFields.length === indexFields.length &&
 | 
			
		||||
                            primaryKeyFields.every((pk) =>
 | 
			
		||||
                                indexFields.some(
 | 
			
		||||
                                    (field) => field && field.id === pk.id
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        ) {
 | 
			
		||||
                            return '';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Create safe index name
 | 
			
		||||
                        const safeIndexName = `${table.name}_${index.name}`
 | 
			
		||||
                            .replace(/[^a-zA-Z0-9_]/g, '_')
 | 
			
		||||
                            .substring(0, 60);
 | 
			
		||||
 | 
			
		||||
                        return indexFieldNames.length > 0
 | 
			
		||||
                            ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
 | 
			
		||||
                        return validIndexes.length > 0
 | 
			
		||||
                            ? `\n-- Indexes\n${validIndexes.join('\n')}`
 | 
			
		||||
                            : '';
 | 
			
		||||
                    })
 | 
			
		||||
                    .filter(Boolean)
 | 
			
		||||
                    .join('\n')
 | 
			
		||||
            }`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
        .join('\n');
 | 
			
		||||
 | 
			
		||||
                    })()
 | 
			
		||||
                }\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
 | 
			
		||||
@@ -318,7 +341,7 @@ export function exportSQLite(diagram: Diagram): string {
 | 
			
		||||
        sqlScript += '\n-- Foreign key constraints\n';
 | 
			
		||||
        sqlScript +=
 | 
			
		||||
            '-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
 | 
			
		||||
        sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
 | 
			
		||||
        sqlScript += '-- PRAGMA foreign_keys = ON;\n';
 | 
			
		||||
 | 
			
		||||
        relationships.forEach((r: DBRelationship) => {
 | 
			
		||||
            const sourceTable = tables.find((t) => t.id === r.sourceTableId);
 | 
			
		||||
@@ -346,8 +369,44 @@ export function exportSQLite(diagram: Diagram): string {
 | 
			
		||||
                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 "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
 | 
			
		||||
            sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,10 +36,12 @@ export const exportBaseSQL = ({
 | 
			
		||||
    diagram,
 | 
			
		||||
    targetDatabaseType,
 | 
			
		||||
    isDBMLFlow = false,
 | 
			
		||||
    onlyRelationships = false,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    targetDatabaseType: DatabaseType;
 | 
			
		||||
    isDBMLFlow?: boolean;
 | 
			
		||||
    onlyRelationships?: boolean;
 | 
			
		||||
}): string => {
 | 
			
		||||
    const { tables, relationships } = diagram;
 | 
			
		||||
 | 
			
		||||
@@ -50,16 +52,16 @@ export const exportBaseSQL = ({
 | 
			
		||||
    if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
 | 
			
		||||
        switch (diagram.databaseType) {
 | 
			
		||||
            case DatabaseType.SQL_SERVER:
 | 
			
		||||
                return exportMSSQL(diagram);
 | 
			
		||||
                return exportMSSQL({ diagram, onlyRelationships });
 | 
			
		||||
            case DatabaseType.POSTGRESQL:
 | 
			
		||||
                return exportPostgreSQL(diagram);
 | 
			
		||||
                return exportPostgreSQL({ diagram, onlyRelationships });
 | 
			
		||||
            case DatabaseType.SQLITE:
 | 
			
		||||
                return exportSQLite(diagram);
 | 
			
		||||
                return exportSQLite({ diagram, onlyRelationships });
 | 
			
		||||
            case DatabaseType.MYSQL:
 | 
			
		||||
            case DatabaseType.MARIADB:
 | 
			
		||||
                return exportMySQL(diagram);
 | 
			
		||||
                return exportMySQL({ diagram, onlyRelationships });
 | 
			
		||||
            default:
 | 
			
		||||
                return exportPostgreSQL(diagram);
 | 
			
		||||
                return exportPostgreSQL({ diagram, onlyRelationships });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +133,23 @@ export const exportBaseSQL = ({
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        sqlScript += '\n'; // Add a newline if custom types were processed
 | 
			
		||||
        if (
 | 
			
		||||
            diagram.customTypes.some(
 | 
			
		||||
                (ct) =>
 | 
			
		||||
                    (ct.kind === 'enum' &&
 | 
			
		||||
                        ct.values &&
 | 
			
		||||
                        ct.values.length > 0 &&
 | 
			
		||||
                        targetDatabaseType === DatabaseType.POSTGRESQL &&
 | 
			
		||||
                        !isDBMLFlow) ||
 | 
			
		||||
                    (ct.kind === 'composite' &&
 | 
			
		||||
                        ct.fields &&
 | 
			
		||||
                        ct.fields.length > 0 &&
 | 
			
		||||
                        (targetDatabaseType === DatabaseType.POSTGRESQL ||
 | 
			
		||||
                            isDBMLFlow))
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            sqlScript += '\n';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add CREATE SEQUENCE statements
 | 
			
		||||
@@ -154,7 +172,9 @@ export const exportBaseSQL = ({
 | 
			
		||||
    sequences.forEach((sequence) => {
 | 
			
		||||
        sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
 | 
			
		||||
    });
 | 
			
		||||
    sqlScript += '\n';
 | 
			
		||||
    if (sequences.size > 0) {
 | 
			
		||||
        sqlScript += '\n';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Loop through each non-view table to generate the SQL statements
 | 
			
		||||
    nonViewTables.forEach((table) => {
 | 
			
		||||
@@ -163,6 +183,12 @@ export const exportBaseSQL = ({
 | 
			
		||||
            : table.name;
 | 
			
		||||
        sqlScript += `CREATE TABLE ${tableName} (\n`;
 | 
			
		||||
 | 
			
		||||
        // Check for composite primary keys
 | 
			
		||||
        const primaryKeyFields = table.fields.filter(
 | 
			
		||||
            (field) => field.primaryKey
 | 
			
		||||
        );
 | 
			
		||||
        const hasCompositePrimaryKey = primaryKeyFields.length > 1;
 | 
			
		||||
 | 
			
		||||
        table.fields.forEach((field, index) => {
 | 
			
		||||
            let typeName = simplifyDataType(field.type.name);
 | 
			
		||||
 | 
			
		||||
@@ -214,14 +240,33 @@ export const exportBaseSQL = ({
 | 
			
		||||
                typeName = 'text[]';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle special types
 | 
			
		||||
            if (
 | 
			
		||||
                typeName.toLowerCase() === 'char' &&
 | 
			
		||||
                !field.characterMaximumLength
 | 
			
		||||
            ) {
 | 
			
		||||
                // Default char without length to char(1)
 | 
			
		||||
                typeName = 'char';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sqlScript += `  ${field.name} ${typeName}`;
 | 
			
		||||
 | 
			
		||||
            // Add size for character types
 | 
			
		||||
            if (field.characterMaximumLength) {
 | 
			
		||||
            if (
 | 
			
		||||
                field.characterMaximumLength &&
 | 
			
		||||
                parseInt(field.characterMaximumLength) > 0 &&
 | 
			
		||||
                field.type.name.toLowerCase() !== 'decimal'
 | 
			
		||||
            ) {
 | 
			
		||||
                sqlScript += `(${field.characterMaximumLength})`;
 | 
			
		||||
            } else if (field.type.name.toLowerCase().includes('varchar')) {
 | 
			
		||||
                // Keep varchar sizing, but don't apply to TEXT (previously enum)
 | 
			
		||||
                sqlScript += `(500)`;
 | 
			
		||||
            } else if (
 | 
			
		||||
                typeName.toLowerCase() === 'char' &&
 | 
			
		||||
                !field.characterMaximumLength
 | 
			
		||||
            ) {
 | 
			
		||||
                // Default char without explicit length to char(1) for compatibility
 | 
			
		||||
                sqlScript += `(1)`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add precision and scale for numeric types
 | 
			
		||||
@@ -246,49 +291,63 @@ export const exportBaseSQL = ({
 | 
			
		||||
                // Temp remove default user-define value when it have it
 | 
			
		||||
                let fieldDefault = field.default;
 | 
			
		||||
 | 
			
		||||
                // Remove the type cast part after :: if it exists
 | 
			
		||||
                if (fieldDefault.includes('::')) {
 | 
			
		||||
                    const endedWithParentheses = fieldDefault.endsWith(')');
 | 
			
		||||
                    fieldDefault = fieldDefault.split('::')[0];
 | 
			
		||||
                // Skip invalid default values for DBML export
 | 
			
		||||
                if (
 | 
			
		||||
                    fieldDefault === 'has default' ||
 | 
			
		||||
                    fieldDefault === 'DEFAULT has default'
 | 
			
		||||
                ) {
 | 
			
		||||
                    // Skip this default value as it's invalid SQL
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Remove the type cast part after :: if it exists
 | 
			
		||||
                    if (fieldDefault.includes('::')) {
 | 
			
		||||
                        const endedWithParentheses = fieldDefault.endsWith(')');
 | 
			
		||||
                        fieldDefault = fieldDefault.split('::')[0];
 | 
			
		||||
 | 
			
		||||
                    if (
 | 
			
		||||
                        (fieldDefault.startsWith('(') &&
 | 
			
		||||
                            !fieldDefault.endsWith(')')) ||
 | 
			
		||||
                        endedWithParentheses
 | 
			
		||||
                    ) {
 | 
			
		||||
                        fieldDefault += ')';
 | 
			
		||||
                        if (
 | 
			
		||||
                            (fieldDefault.startsWith('(') &&
 | 
			
		||||
                                !fieldDefault.endsWith(')')) ||
 | 
			
		||||
                            endedWithParentheses
 | 
			
		||||
                        ) {
 | 
			
		||||
                            fieldDefault += ')';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (fieldDefault === `('now')`) {
 | 
			
		||||
                    fieldDefault = `now()`;
 | 
			
		||||
                }
 | 
			
		||||
                    if (fieldDefault === `('now')`) {
 | 
			
		||||
                        fieldDefault = `now()`;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                sqlScript += ` DEFAULT ${fieldDefault}`;
 | 
			
		||||
                    sqlScript += ` DEFAULT ${fieldDefault}`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle PRIMARY KEY constraint
 | 
			
		||||
            if (field.primaryKey) {
 | 
			
		||||
            // Handle PRIMARY KEY constraint - only add inline if not composite
 | 
			
		||||
            if (field.primaryKey && !hasCompositePrimaryKey) {
 | 
			
		||||
                sqlScript += ' PRIMARY KEY';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add a comma after each field except the last one
 | 
			
		||||
            if (index < table.fields.length - 1) {
 | 
			
		||||
            // Add a comma after each field except the last one (or before composite primary key)
 | 
			
		||||
            if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
 | 
			
		||||
                sqlScript += ',\n';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        sqlScript += '\n);\n\n';
 | 
			
		||||
        // Add composite primary key constraint if needed
 | 
			
		||||
        if (hasCompositePrimaryKey) {
 | 
			
		||||
            const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
 | 
			
		||||
            sqlScript += `\n  PRIMARY KEY (${pkFieldNames})`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sqlScript += '\n);\n';
 | 
			
		||||
 | 
			
		||||
        // Add table comment
 | 
			
		||||
        if (table.comments) {
 | 
			
		||||
            sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments}';\n`;
 | 
			
		||||
            sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table.fields.forEach((field) => {
 | 
			
		||||
            // Add column comment
 | 
			
		||||
            if (field.comments) {
 | 
			
		||||
                sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments}';\n`;
 | 
			
		||||
                sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -303,16 +362,19 @@ export const exportBaseSQL = ({
 | 
			
		||||
                .join(', ');
 | 
			
		||||
 | 
			
		||||
            if (fieldNames) {
 | 
			
		||||
                const indexName = table.schema
 | 
			
		||||
                    ? `${table.schema}_${index.name}`
 | 
			
		||||
                    : index.name;
 | 
			
		||||
                const indexName =
 | 
			
		||||
                    table.schema && !isDBMLFlow
 | 
			
		||||
                        ? `${table.schema}_${index.name}`
 | 
			
		||||
                        : index.name;
 | 
			
		||||
                sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        sqlScript += '\n';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
 | 
			
		||||
        sqlScript += '\n';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle relationships (foreign keys)
 | 
			
		||||
    relationships?.forEach((relationship) => {
 | 
			
		||||
        const sourceTable = nonViewTables.find(
 | 
			
		||||
@@ -335,13 +397,52 @@ export const exportBaseSQL = ({
 | 
			
		||||
            sourceTableField &&
 | 
			
		||||
            targetTableField
 | 
			
		||||
        ) {
 | 
			
		||||
            const sourceTableName = sourceTable.schema
 | 
			
		||||
                ? `${sourceTable.schema}.${sourceTable.name}`
 | 
			
		||||
                : sourceTable.name;
 | 
			
		||||
            const targetTableName = targetTable.schema
 | 
			
		||||
                ? `${targetTable.schema}.${targetTable.name}`
 | 
			
		||||
                : targetTable.name;
 | 
			
		||||
            sqlScript += `ALTER TABLE ${sourceTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${sourceTableField.name}) REFERENCES ${targetTableName} (${targetTableField.name});\n`;
 | 
			
		||||
            // Determine which table should have the foreign key based on cardinality
 | 
			
		||||
            // In a 1:many relationship, the foreign key goes on the "many" side
 | 
			
		||||
            // If source is "one" and target is "many", FK goes on target table
 | 
			
		||||
            // If source is "many" and target is "one", FK goes on source table
 | 
			
		||||
            let fkTable, fkField, refTable, refField;
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                relationship.sourceCardinality === 'one' &&
 | 
			
		||||
                relationship.targetCardinality === 'many'
 | 
			
		||||
            ) {
 | 
			
		||||
                // FK goes on target table
 | 
			
		||||
                fkTable = targetTable;
 | 
			
		||||
                fkField = targetTableField;
 | 
			
		||||
                refTable = sourceTable;
 | 
			
		||||
                refField = sourceTableField;
 | 
			
		||||
            } else if (
 | 
			
		||||
                relationship.sourceCardinality === 'many' &&
 | 
			
		||||
                relationship.targetCardinality === 'one'
 | 
			
		||||
            ) {
 | 
			
		||||
                // FK goes on source table
 | 
			
		||||
                fkTable = sourceTable;
 | 
			
		||||
                fkField = sourceTableField;
 | 
			
		||||
                refTable = targetTable;
 | 
			
		||||
                refField = targetTableField;
 | 
			
		||||
            } else if (
 | 
			
		||||
                relationship.sourceCardinality === 'one' &&
 | 
			
		||||
                relationship.targetCardinality === 'one'
 | 
			
		||||
            ) {
 | 
			
		||||
                // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | 
			
		||||
                // We'll keep the current behavior for 1:1
 | 
			
		||||
                fkTable = sourceTable;
 | 
			
		||||
                fkField = sourceTableField;
 | 
			
		||||
                refTable = targetTable;
 | 
			
		||||
                refField = targetTableField;
 | 
			
		||||
            } else {
 | 
			
		||||
                // Many-to-many relationships need a junction table, skip for now
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const fkTableName = fkTable.schema
 | 
			
		||||
                ? `${fkTable.schema}.${fkTable.name}`
 | 
			
		||||
                : fkTable.name;
 | 
			
		||||
            const refTableName = refTable.schema
 | 
			
		||||
                ? `${refTable.schema}.${refTable.name}`
 | 
			
		||||
                : refTable.name;
 | 
			
		||||
            sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										126
									
								
								src/lib/data/import-metadata/filter-metadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/lib/data/import-metadata/filter-metadata.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
import type { DatabaseMetadata } from './metadata-types/database-metadata';
 | 
			
		||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
 | 
			
		||||
 | 
			
		||||
export interface SelectedTable {
 | 
			
		||||
    schema?: string | null;
 | 
			
		||||
    table: string;
 | 
			
		||||
    type: 'table' | 'view';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function filterMetadataByTables({
 | 
			
		||||
    metadata,
 | 
			
		||||
    selectedTables: inputSelectedTables,
 | 
			
		||||
}: {
 | 
			
		||||
    metadata: DatabaseMetadata;
 | 
			
		||||
    selectedTables: SelectedTable[];
 | 
			
		||||
}): DatabaseMetadata {
 | 
			
		||||
    const selectedTables = inputSelectedTables.map((st) => {
 | 
			
		||||
        // Normalize schema names to ensure consistent filtering
 | 
			
		||||
        const schema = schemaNameToDomainSchemaName(st.schema) ?? '';
 | 
			
		||||
        return {
 | 
			
		||||
            ...st,
 | 
			
		||||
            schema,
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create sets for faster lookup
 | 
			
		||||
    const selectedTableSet = new Set(
 | 
			
		||||
        selectedTables
 | 
			
		||||
            .filter((st) => st.type === 'table')
 | 
			
		||||
            .map((st) => `${st.schema}.${st.table}`)
 | 
			
		||||
    );
 | 
			
		||||
    const selectedViewSet = new Set(
 | 
			
		||||
        selectedTables
 | 
			
		||||
            .filter((st) => st.type === 'view')
 | 
			
		||||
            .map((st) => `${st.schema}.${st.table}`)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Filter tables
 | 
			
		||||
    const filteredTables = metadata.tables.filter((table) => {
 | 
			
		||||
        const schema = schemaNameToDomainSchemaName(table.schema) ?? '';
 | 
			
		||||
        const tableId = `${schema}.${table.table}`;
 | 
			
		||||
        return selectedTableSet.has(tableId);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Filter views - include views that were explicitly selected
 | 
			
		||||
    const filteredViews =
 | 
			
		||||
        metadata.views?.filter((view) => {
 | 
			
		||||
            const schema = schemaNameToDomainSchemaName(view.schema) ?? '';
 | 
			
		||||
            const viewName = view.view_name ?? '';
 | 
			
		||||
            const viewId = `${schema}.${viewName}`;
 | 
			
		||||
            return selectedViewSet.has(viewId);
 | 
			
		||||
        }) || [];
 | 
			
		||||
 | 
			
		||||
    // Filter columns - include columns from both tables and views
 | 
			
		||||
    const filteredColumns = metadata.columns.filter((col) => {
 | 
			
		||||
        const fromTable = filteredTables.some(
 | 
			
		||||
            (tb) => tb.schema === col.schema && tb.table === col.table
 | 
			
		||||
        );
 | 
			
		||||
        // For views, the column.table field might contain the view name
 | 
			
		||||
        const fromView = filteredViews.some(
 | 
			
		||||
            (view) => view.schema === col.schema && view.view_name === col.table
 | 
			
		||||
        );
 | 
			
		||||
        return fromTable || fromView;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Filter primary keys
 | 
			
		||||
    const filteredPrimaryKeys = metadata.pk_info.filter((pk) =>
 | 
			
		||||
        filteredTables.some(
 | 
			
		||||
            (tb) => tb.schema === pk.schema && tb.table === pk.table
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Filter indexes
 | 
			
		||||
    const filteredIndexes = metadata.indexes.filter((idx) =>
 | 
			
		||||
        filteredTables.some(
 | 
			
		||||
            (tb) => tb.schema === idx.schema && tb.table === idx.table
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Filter foreign keys - include if either source or target table is selected
 | 
			
		||||
    // This ensures all relationships related to selected tables are preserved
 | 
			
		||||
    const filteredForeignKeys = metadata.fk_info.filter((fk) => {
 | 
			
		||||
        // Handle reference_schema and reference_table fields from the JSON
 | 
			
		||||
        const targetSchema = fk.reference_schema;
 | 
			
		||||
        const targetTable = (fk.reference_table || '').replace(/^"+|"+$/g, ''); // Remove extra quotes
 | 
			
		||||
 | 
			
		||||
        const sourceIncluded = filteredTables.some(
 | 
			
		||||
            (tb) => tb.schema === fk.schema && tb.table === fk.table
 | 
			
		||||
        );
 | 
			
		||||
        const targetIncluded = filteredTables.some(
 | 
			
		||||
            (tb) => tb.schema === targetSchema && tb.table === targetTable
 | 
			
		||||
        );
 | 
			
		||||
        return sourceIncluded || targetIncluded;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const schemasWithTables = new Set(filteredTables.map((tb) => tb.schema));
 | 
			
		||||
    const schemasWithViews = new Set(filteredViews.map((view) => view.schema));
 | 
			
		||||
 | 
			
		||||
    // Filter custom types if they exist
 | 
			
		||||
    const filteredCustomTypes =
 | 
			
		||||
        metadata.custom_types?.filter((customType) => {
 | 
			
		||||
            // Also check if the type is used by any of the selected tables' columns
 | 
			
		||||
            const typeUsedInColumns = filteredColumns.some(
 | 
			
		||||
                (col) =>
 | 
			
		||||
                    col.type === customType.type ||
 | 
			
		||||
                    col.type.includes(customType.type) // Handle array types like "custom_type[]"
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                schemasWithTables.has(customType.schema) ||
 | 
			
		||||
                schemasWithViews.has(customType.schema) ||
 | 
			
		||||
                typeUsedInColumns
 | 
			
		||||
            );
 | 
			
		||||
        }) || [];
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        ...metadata,
 | 
			
		||||
        tables: filteredTables,
 | 
			
		||||
        columns: filteredColumns,
 | 
			
		||||
        pk_info: filteredPrimaryKeys,
 | 
			
		||||
        indexes: filteredIndexes,
 | 
			
		||||
        fk_info: filteredForeignKeys,
 | 
			
		||||
        views: filteredViews,
 | 
			
		||||
        custom_types: filteredCustomTypes,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
 | 
			
		||||
import type { TableInfo } from './table-info';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
@@ -33,20 +32,12 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createAggregatedIndexes = ({
 | 
			
		||||
    tableInfo,
 | 
			
		||||
    tableSchema,
 | 
			
		||||
    indexes,
 | 
			
		||||
    tableIndexes,
 | 
			
		||||
}: {
 | 
			
		||||
    tableInfo: TableInfo;
 | 
			
		||||
    indexes: IndexInfo[];
 | 
			
		||||
    tableIndexes: IndexInfo[];
 | 
			
		||||
    tableSchema?: string;
 | 
			
		||||
}): AggregatedIndexInfo[] => {
 | 
			
		||||
    const tableIndexes = indexes.filter((idx) => {
 | 
			
		||||
        const indexSchema = schemaNameToDomainSchemaName(idx.schema);
 | 
			
		||||
 | 
			
		||||
        return idx.table === tableInfo.table && indexSchema === tableSchema;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return Object.values(
 | 
			
		||||
        tableIndexes.reduce(
 | 
			
		||||
            (acc, idx) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,140 +2,151 @@ const withExtras = false;
 | 
			
		||||
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
 | 
			
		||||
const withoutDefault = `""`;
 | 
			
		||||
 | 
			
		||||
export const mariaDBQuery = `WITH fk_info as (
 | 
			
		||||
  (SELECT (@fk_info:=NULL),
 | 
			
		||||
              (SELECT (0)
 | 
			
		||||
               FROM (SELECT kcu.table_schema,
 | 
			
		||||
                kcu.table_name,
 | 
			
		||||
                kcu.column_name as fk_column,
 | 
			
		||||
                kcu.constraint_name as foreign_key_name,
 | 
			
		||||
                kcu.referenced_table_schema as reference_schema,
 | 
			
		||||
                kcu.referenced_table_name as reference_table,
 | 
			
		||||
                kcu.referenced_column_name as reference_column,
 | 
			
		||||
                CONCAT('FOREIGN KEY (', kcu.column_name, ') REFERENCES ',
 | 
			
		||||
                       kcu.referenced_table_name, '(', kcu.referenced_column_name, ') ',
 | 
			
		||||
                       'ON UPDATE ', rc.update_rule,
 | 
			
		||||
                       ' ON DELETE ', rc.delete_rule) AS fk_def
 | 
			
		||||
            FROM
 | 
			
		||||
                information_schema.key_column_usage kcu
 | 
			
		||||
            JOIN
 | 
			
		||||
                information_schema.referential_constraints rc
 | 
			
		||||
                ON kcu.constraint_name = rc.constraint_name
 | 
			
		||||
                    AND kcu.table_schema = rc.constraint_schema
 | 
			
		||||
                  AND kcu.table_name = rc.table_name
 | 
			
		||||
            WHERE
 | 
			
		||||
                kcu.referenced_table_name IS NOT NULL) as fk
 | 
			
		||||
               WHERE table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
                   AND table_schema = DATABASE()
 | 
			
		||||
                   AND (0x00) IN (@fk_info:=CONCAT_WS(',', @fk_info, CONCAT('{"schema":"',table_schema,
 | 
			
		||||
                                               '","table":"',table_name,
 | 
			
		||||
                                               '","column":"', IFNULL(fk_column, ''),
 | 
			
		||||
                                                          '","foreign_key_name":"', IFNULL(foreign_key_name, ''),
 | 
			
		||||
                                                          '","reference_schema":"', IFNULL(reference_schema, ''),
 | 
			
		||||
                                                          '","reference_table":"', IFNULL(reference_table, ''),
 | 
			
		||||
                                                          '","reference_column":"', IFNULL(reference_column, ''),
 | 
			
		||||
                                                          '","fk_def":"', IFNULL(fk_def, ''),
 | 
			
		||||
                                               '"}')))))
 | 
			
		||||
), pk_info AS (
 | 
			
		||||
    (SELECT (@pk_info:=NULL),
 | 
			
		||||
              (SELECT (0)
 | 
			
		||||
               FROM (SELECT TABLE_SCHEMA,
 | 
			
		||||
                            TABLE_NAME AS pk_table,
 | 
			
		||||
                            COLUMN_NAME AS pk_column,
 | 
			
		||||
                            (SELECT CONCAT('PRIMARY KEY (', GROUP_CONCAT(inc.COLUMN_NAME ORDER BY inc.ORDINAL_POSITION SEPARATOR ', '), ')')
 | 
			
		||||
export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
 | 
			
		||||
SELECT CAST(CONCAT(
 | 
			
		||||
    '{"fk_info": [',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(fk.table_schema as CHAR),
 | 
			
		||||
               '","table":"', fk.table_name,
 | 
			
		||||
               '","column":"', IFNULL(fk.fk_column, ''),
 | 
			
		||||
               '","foreign_key_name":"', IFNULL(fk.foreign_key_name, ''),
 | 
			
		||||
               '","reference_table":"', IFNULL(fk.reference_table, ''),
 | 
			
		||||
               '","reference_schema":"', IFNULL(fk.reference_schema, ''),
 | 
			
		||||
               '","reference_column":"', IFNULL(fk.reference_column, ''),
 | 
			
		||||
               '","fk_def":"', IFNULL(fk.fk_def, ''), '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT kcu.table_schema,
 | 
			
		||||
               kcu.table_name,
 | 
			
		||||
               kcu.column_name AS fk_column,
 | 
			
		||||
               kcu.constraint_name AS foreign_key_name,
 | 
			
		||||
               kcu.referenced_table_schema as reference_schema,
 | 
			
		||||
               kcu.referenced_table_name AS reference_table,
 | 
			
		||||
               kcu.referenced_column_name AS reference_column,
 | 
			
		||||
               CONCAT('FOREIGN KEY (', kcu.column_name, ') REFERENCES ',
 | 
			
		||||
                      kcu.referenced_table_name, '(', kcu.referenced_column_name, ') ',
 | 
			
		||||
                      'ON UPDATE ', rc.update_rule,
 | 
			
		||||
                      ' ON DELETE ', rc.delete_rule) AS fk_def
 | 
			
		||||
        FROM information_schema.key_column_usage kcu
 | 
			
		||||
        JOIN information_schema.referential_constraints rc
 | 
			
		||||
          ON kcu.constraint_name = rc.constraint_name
 | 
			
		||||
         AND kcu.table_schema = rc.constraint_schema
 | 
			
		||||
         AND kcu.table_name = rc.table_name
 | 
			
		||||
        WHERE kcu.referenced_table_name IS NOT NULL
 | 
			
		||||
          AND kcu.table_schema = DATABASE()
 | 
			
		||||
    ) AS fk), ''),
 | 
			
		||||
    '], "pk_info": [',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(pk.TABLE_SCHEMA as CHAR),
 | 
			
		||||
               '","table":"', pk.pk_table,
 | 
			
		||||
               '","column":"', pk.pk_column,
 | 
			
		||||
               '","pk_def":"', IFNULL(pk.pk_def, ''), '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT TABLE_SCHEMA,
 | 
			
		||||
               TABLE_NAME AS pk_table,
 | 
			
		||||
               COLUMN_NAME AS pk_column,
 | 
			
		||||
               (SELECT CONCAT('PRIMARY KEY (', GROUP_CONCAT(inc.COLUMN_NAME ORDER BY inc.ORDINAL_POSITION SEPARATOR ', '), ')')
 | 
			
		||||
                               FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE as inc
 | 
			
		||||
                               WHERE inc.CONSTRAINT_NAME = 'PRIMARY' and
 | 
			
		||||
                                     outc.TABLE_SCHEMA = inc.TABLE_SCHEMA and
 | 
			
		||||
                                     outc.TABLE_NAME = inc.TABLE_NAME) AS pk_def
 | 
			
		||||
                       FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE as outc
 | 
			
		||||
                       WHERE CONSTRAINT_NAME = 'PRIMARY'
 | 
			
		||||
                       GROUP BY TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
 | 
			
		||||
                       ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION) AS pk
 | 
			
		||||
               WHERE table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
               AND table_schema = DATABASE()
 | 
			
		||||
               AND (0x00) IN (@pk_info:=CONCAT_WS(',', @pk_info, CONCAT('{"schema":"', table_schema,
 | 
			
		||||
                                                                        '","table":"', pk_table,
 | 
			
		||||
                                                                        '","column":"', pk_column,
 | 
			
		||||
                                                                        '","pk_def":"', IFNULL(pk_def, ''),
 | 
			
		||||
                                                                        '"}')))))
 | 
			
		||||
), cols as
 | 
			
		||||
(
 | 
			
		||||
  (SELECT (@cols := NULL),
 | 
			
		||||
        (SELECT (0)
 | 
			
		||||
         FROM information_schema.columns cols
 | 
			
		||||
         WHERE cols.table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
           AND cols.table_schema = DATABASE()
 | 
			
		||||
           AND (0x00) IN (@cols := CONCAT_WS(',', @cols, CONCAT(
 | 
			
		||||
                '{"schema":"', cols.table_schema,
 | 
			
		||||
                '","table":"', cols.table_name,
 | 
			
		||||
                '","name":"', REPLACE(cols.column_name, '"', '\\"'),
 | 
			
		||||
                '","type":"', LOWER(cols.data_type),
 | 
			
		||||
                '","character_maximum_length":"', IFNULL(cols.character_maximum_length, 'null'),
 | 
			
		||||
                '","precision":',
 | 
			
		||||
                    CASE
 | 
			
		||||
                        WHEN cols.data_type IN ('decimal', 'numeric')
 | 
			
		||||
                        THEN CONCAT('{"precision":', IFNULL(cols.numeric_precision, 'null'),
 | 
			
		||||
                                    ',"scale":', IFNULL(cols.numeric_scale, 'null'), '}')
 | 
			
		||||
                        ELSE 'null'
 | 
			
		||||
                    END,
 | 
			
		||||
                ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
                ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
                ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
                '","collation":"', IFNULL(cols.collation_name, ''), '"}'
 | 
			
		||||
            )))))
 | 
			
		||||
), indexes as (
 | 
			
		||||
  (SELECT (@indexes:=NULL),
 | 
			
		||||
                (SELECT (0)
 | 
			
		||||
                 FROM information_schema.statistics indexes
 | 
			
		||||
                 WHERE table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
                     AND table_schema = DATABASE()
 | 
			
		||||
                     AND (0x00) IN  (@indexes:=CONCAT_WS(',', @indexes, CONCAT('{"schema":"',indexes.table_schema,
 | 
			
		||||
                                         '","table":"',indexes.table_name,
 | 
			
		||||
                                         '","name":"', indexes.index_name,
 | 
			
		||||
                                         '","size":',
 | 
			
		||||
                                                                      (SELECT IFNULL(SUM(stat_value * @@innodb_page_size), -1) AS size_in_bytes
 | 
			
		||||
                                                                       FROM mysql.innodb_index_stats
 | 
			
		||||
                                                                       WHERE stat_name = 'size'
 | 
			
		||||
                                                                           AND index_name != 'PRIMARY'
 | 
			
		||||
                                                                           AND index_name = indexes.index_name
 | 
			
		||||
                                                                           AND TABLE_NAME = indexes.table_name
 | 
			
		||||
                                                                           AND database_name = indexes.table_schema),
 | 
			
		||||
                                                                  ',"column":"', indexes.column_name,
 | 
			
		||||
                                                      '","index_type":"', LOWER(indexes.index_type),
 | 
			
		||||
                                                      '","cardinality":', indexes.cardinality,
 | 
			
		||||
                                                      ',"direction":"', (CASE WHEN indexes.collation = 'D' THEN 'desc' ELSE 'asc' END),
 | 
			
		||||
                                                      '","column_position":', indexes.seq_in_index,
 | 
			
		||||
                                                      ',"unique":', IF(indexes.non_unique = 1, 'false', 'true'), '}')))))
 | 
			
		||||
), tbls as
 | 
			
		||||
(
 | 
			
		||||
  (SELECT (@tbls:=NULL),
 | 
			
		||||
              (SELECT (0)
 | 
			
		||||
               FROM information_schema.tables tbls
 | 
			
		||||
               WHERE table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
                   AND table_schema = DATABASE()
 | 
			
		||||
                   AND (0x00) IN (@tbls:=CONCAT_WS(',', @tbls, CONCAT('{', '"schema":"', \`TABLE_SCHEMA\`, '",',
 | 
			
		||||
                                               '"table":"', \`TABLE_NAME\`, '",',
 | 
			
		||||
                                             '"rows":', IFNULL(\`TABLE_ROWS\`, 0),
 | 
			
		||||
                                             ', "type":"', IFNULL(\`TABLE_TYPE\`, ''), '",',
 | 
			
		||||
                                             '"engine":"', IFNULL(\`ENGINE\`, ''), '",',
 | 
			
		||||
                                             '"collation":"', IFNULL(\`TABLE_COLLATION\`, ''), '"}')))))
 | 
			
		||||
), views as (
 | 
			
		||||
(SELECT (@views:=NULL),
 | 
			
		||||
              (SELECT (0)
 | 
			
		||||
               FROM information_schema.views views
 | 
			
		||||
               WHERE table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
                   AND table_schema = DATABASE()
 | 
			
		||||
                   AND (0x00) IN (@views:=CONCAT_WS(',', @views, CONCAT('{', '"schema":"', \`TABLE_SCHEMA\`, '",',
 | 
			
		||||
                                                   '"view_name":"', \`TABLE_NAME\`, '",',
 | 
			
		||||
                                                   '"view_definition":""}'))) ) )
 | 
			
		||||
)
 | 
			
		||||
(SELECT CAST(CONCAT('{"fk_info": [',IFNULL(@fk_info,''),
 | 
			
		||||
                '], "pk_info": [', IFNULL(@pk_info, ''),
 | 
			
		||||
            '], "columns": [',IFNULL(@cols,''),
 | 
			
		||||
            '], "indexes": [',IFNULL(@indexes,''),
 | 
			
		||||
            '], "tables":[',IFNULL(@tbls,''),
 | 
			
		||||
            '], "views":[',IFNULL(@views,''),
 | 
			
		||||
            '], "database_name": "', DATABASE(),
 | 
			
		||||
            '", "version": "', VERSION(), '"}') AS CHAR) AS metadata_json_to_import
 | 
			
		||||
 FROM fk_info, pk_info, cols, indexes, tbls, views);
 | 
			
		||||
        FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE as outc
 | 
			
		||||
        WHERE CONSTRAINT_NAME = 'PRIMARY'
 | 
			
		||||
              and table_schema LIKE IFNULL(NULL, '%')
 | 
			
		||||
              AND table_schema = DATABASE()
 | 
			
		||||
        GROUP BY TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
 | 
			
		||||
    ) AS pk), ''),
 | 
			
		||||
    '], "columns": [',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(cols.table_schema as CHAR),
 | 
			
		||||
               '","table":"', cols.table_name,
 | 
			
		||||
               '","name":"', REPLACE(cols.column_name, '"', '\\"'),
 | 
			
		||||
               '","type":"', LOWER(cols.data_type),
 | 
			
		||||
               '","character_maximum_length":"', IFNULL(cols.character_maximum_length, 'null'),
 | 
			
		||||
               '","precision":',
 | 
			
		||||
               IF(cols.data_type IN ('decimal', 'numeric'),
 | 
			
		||||
                  CONCAT('{"precision":', IFNULL(cols.numeric_precision, 'null'),
 | 
			
		||||
                         ',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
 | 
			
		||||
               ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
               ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
               ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
               '","collation":"', IFNULL(cols.collation_name, ''), '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT cols.table_schema,
 | 
			
		||||
               cols.table_name,
 | 
			
		||||
               cols.column_name,
 | 
			
		||||
               LOWER(cols.data_type) AS data_type,
 | 
			
		||||
               cols.character_maximum_length,
 | 
			
		||||
               cols.numeric_precision,
 | 
			
		||||
               cols.numeric_scale,
 | 
			
		||||
               cols.ordinal_position,
 | 
			
		||||
               cols.is_nullable,
 | 
			
		||||
               cols.column_default,
 | 
			
		||||
               cols.collation_name
 | 
			
		||||
        FROM information_schema.columns cols
 | 
			
		||||
        WHERE cols.table_schema = DATABASE()
 | 
			
		||||
    ) AS cols), ''),
 | 
			
		||||
    '], "indexes": [',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(idx.table_schema as CHAR),
 | 
			
		||||
               '","table":"', idx.table_name,
 | 
			
		||||
               '","name":"', idx.index_name,
 | 
			
		||||
               '","size":', IFNULL(
 | 
			
		||||
                    (SELECT SUM(stat_value * @@innodb_page_size)
 | 
			
		||||
                     FROM mysql.innodb_index_stats
 | 
			
		||||
                     WHERE stat_name = 'size'
 | 
			
		||||
                       AND index_name != 'PRIMARY'
 | 
			
		||||
                       AND index_name = idx.index_name
 | 
			
		||||
                       AND TABLE_NAME = idx.table_name
 | 
			
		||||
                       AND database_name = idx.table_schema), -1),
 | 
			
		||||
               ',"column":"', idx.column_name,
 | 
			
		||||
               '","index_type":"', LOWER(idx.index_type),
 | 
			
		||||
               '","cardinality":', idx.cardinality,
 | 
			
		||||
               ',"direction":"', (CASE WHEN idx.collation = 'D' THEN 'desc' ELSE 'asc' END),
 | 
			
		||||
               '","column_position":', idx.seq_in_index,
 | 
			
		||||
               ',"unique":', IF(idx.non_unique = 1, 'false', 'true'), '}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT indexes.table_schema,
 | 
			
		||||
               indexes.table_name,
 | 
			
		||||
               indexes.index_name,
 | 
			
		||||
               indexes.column_name,
 | 
			
		||||
               LOWER(indexes.index_type) AS index_type,
 | 
			
		||||
               indexes.cardinality,
 | 
			
		||||
               indexes.collation,
 | 
			
		||||
               indexes.non_unique,
 | 
			
		||||
               indexes.seq_in_index
 | 
			
		||||
        FROM information_schema.statistics indexes
 | 
			
		||||
        WHERE indexes.table_schema = DATABASE()
 | 
			
		||||
    ) AS idx), ''),
 | 
			
		||||
    '], "tables":[',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(tbls.TABLE_SCHEMA as CHAR),
 | 
			
		||||
               '","table":"', tbls.TABLE_NAME,
 | 
			
		||||
               '","rows":', IFNULL(tbls.TABLE_ROWS, 0),
 | 
			
		||||
               ',"type":"', IFNULL(tbls.TABLE_TYPE, ''),
 | 
			
		||||
               '","engine":"', IFNULL(tbls.ENGINE, ''),
 | 
			
		||||
               '","collation":"', IFNULL(tbls.TABLE_COLLATION, ''), '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT \`TABLE_SCHEMA\`,
 | 
			
		||||
               \`TABLE_NAME\`,
 | 
			
		||||
               \`TABLE_ROWS\`,
 | 
			
		||||
               \`TABLE_TYPE\`,
 | 
			
		||||
               \`ENGINE\`,
 | 
			
		||||
               \`TABLE_COLLATION\`
 | 
			
		||||
        FROM information_schema.tables tbls
 | 
			
		||||
        WHERE tbls.table_schema = DATABASE()
 | 
			
		||||
    ) AS tbls), ''),
 | 
			
		||||
    '], "views":[',
 | 
			
		||||
    IFNULL((SELECT GROUP_CONCAT(
 | 
			
		||||
        CONCAT('{"schema":"', cast(vws.TABLE_SCHEMA as CHAR),
 | 
			
		||||
               '","view_name":"', vws.view_name,
 | 
			
		||||
               '","view_definition":"', view_definition, '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT \`TABLE_SCHEMA\`,
 | 
			
		||||
               \`TABLE_NAME\` AS view_name,
 | 
			
		||||
               null AS view_definition
 | 
			
		||||
        FROM information_schema.views vws
 | 
			
		||||
        WHERE vws.table_schema = DATABASE()
 | 
			
		||||
    ) AS vws), ''),
 | 
			
		||||
    '], "database_name": "', DATABASE(),
 | 
			
		||||
    '", "version": "', VERSION(), '"}') AS CHAR) AS metadata_json_to_import
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,10 @@ export const getPostgresQuery = (
 | 
			
		||||
                AND views.schemaname NOT IN ('auth', 'extensions', 'pgsodium', 'realtime', 'storage', 'vault')
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const supabaseCustomTypesFilter = `
 | 
			
		||||
                AND n.nspname NOT IN ('auth', 'extensions', 'pgsodium', 'realtime', 'storage', 'vault')
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const timescaleFilters = `
 | 
			
		||||
                AND connamespace::regnamespace::text !~ '^(timescaledb_|_timescaledb_)'
 | 
			
		||||
    `;
 | 
			
		||||
@@ -55,6 +59,10 @@ export const getPostgresQuery = (
 | 
			
		||||
                AND views.schemaname !~ '^(timescaledb_|_timescaledb_)'
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const timescaleCustomTypesFilter = `
 | 
			
		||||
                AND n.nspname !~ '^(timescaledb_|_timescaledb_)'
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    const withExtras = false;
 | 
			
		||||
 | 
			
		||||
    const withDefault = `COALESCE(replace(replace(cols.column_default, '"', '\\"'), '\\x', '\\\\x'), '')`;
 | 
			
		||||
@@ -232,7 +240,7 @@ cols AS (
 | 
			
		||||
                                                FROM pg_stat_user_tables s
 | 
			
		||||
                                                WHERE tbls.TABLE_SCHEMA = s.schemaname AND tbls.TABLE_NAME = s.relname),
 | 
			
		||||
                                                0), ', "type":"', tbls.TABLE_TYPE, '",', '"engine":"",', '"collation":"",',
 | 
			
		||||
                        '"comment":"', COALESCE(replace(replace(dsc.description, '"', '\\"'), '\\x', '\\\\x'), ''),
 | 
			
		||||
                        '"comment":"', ${withExtras ? withComments : withoutComments},
 | 
			
		||||
                        '"}'
 | 
			
		||||
                )),
 | 
			
		||||
                ',') AS tbls_metadata
 | 
			
		||||
@@ -282,9 +290,9 @@ cols AS (
 | 
			
		||||
        JOIN pg_namespace n ON n.oid = t.typnamespace
 | 
			
		||||
        WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') ${
 | 
			
		||||
            databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
 | 
			
		||||
                ? timescaleViewsFilter
 | 
			
		||||
                ? timescaleCustomTypesFilter
 | 
			
		||||
                : databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
 | 
			
		||||
                  ? supabaseViewsFilter
 | 
			
		||||
                  ? supabaseCustomTypesFilter
 | 
			
		||||
                  : ''
 | 
			
		||||
        }
 | 
			
		||||
        GROUP BY n.nspname, t.typname
 | 
			
		||||
@@ -315,9 +323,9 @@ cols AS (
 | 
			
		||||
              AND a.attnum > 0 AND NOT a.attisdropped
 | 
			
		||||
              AND n.nspname NOT IN ('pg_catalog', 'information_schema') ${
 | 
			
		||||
                  databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
 | 
			
		||||
                      ? timescaleViewsFilter
 | 
			
		||||
                      ? timescaleCustomTypesFilter
 | 
			
		||||
                      : databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
 | 
			
		||||
                        ? supabaseViewsFilter
 | 
			
		||||
                        ? supabaseCustomTypesFilter
 | 
			
		||||
                        : ''
 | 
			
		||||
              }
 | 
			
		||||
            GROUP BY n.nspname, t.typname
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,6 @@
 | 
			
		||||
import { waitFor } from '@/lib/utils';
 | 
			
		||||
import { isDatabaseMetadata } from './metadata-types/database-metadata';
 | 
			
		||||
 | 
			
		||||
export const fixMetadataJson = async (
 | 
			
		||||
    metadataJson: string
 | 
			
		||||
): Promise<string> => {
 | 
			
		||||
    await waitFor(1000);
 | 
			
		||||
 | 
			
		||||
export const fixMetadataJson = (metadataJson: string): string => {
 | 
			
		||||
    // Replace problematic array default values with null
 | 
			
		||||
    metadataJson = metadataJson.replace(
 | 
			
		||||
        /"default": "?'?\[[^\]]*\]'?"?(\\")?(,|\})/gs,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										132
									
								
								src/lib/data/sql-import/__tests__/sql-validator-autofix.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/lib/data/sql-import/__tests__/sql-validator-autofix.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { validateSQL } from '../sql-validator';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain';
 | 
			
		||||
 | 
			
		||||
describe('SQL Validator Auto-fix', () => {
 | 
			
		||||
    it('should provide auto-fix for cast operator errors', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE dragons (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    lair_location GEOGRAPHY(POINT, 4326)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Problematic queries with cast operator errors
 | 
			
		||||
SELECT id: :text FROM dragons;
 | 
			
		||||
SELECT ST_X(lair_location: :geometry) AS longitude FROM dragons;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        // Should detect errors
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(result.errors.length).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
        // Should provide fixed SQL
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        // Fixed SQL should have correct cast operators
 | 
			
		||||
        expect(result.fixedSQL).toContain('::text');
 | 
			
		||||
        expect(result.fixedSQL).toContain('::geometry');
 | 
			
		||||
        expect(result.fixedSQL).not.toContain(': :');
 | 
			
		||||
 | 
			
		||||
        // The CREATE TABLE should remain intact
 | 
			
		||||
        expect(result.fixedSQL).toContain('GEOGRAPHY(POINT, 4326)');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multi-line cast operator errors', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
SELECT AVG(power_level): :DECIMAL(3,
 | 
			
		||||
2) FROM enchantments;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
        expect(result.fixedSQL).toContain('::DECIMAL(3,');
 | 
			
		||||
        expect(result.fixedSQL).not.toContain(': :');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should auto-fix split DECIMAL declarations', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE potions (
 | 
			
		||||
    id INTEGER PRIMARY KEY,
 | 
			
		||||
    strength DECIMAL(10,
 | 
			
		||||
    2) NOT NULL,
 | 
			
		||||
    effectiveness NUMERIC(5,
 | 
			
		||||
    3) DEFAULT 0.000
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(result.errors.length).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
        // Should provide fixed SQL
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        // Fixed SQL should have DECIMAL on one line
 | 
			
		||||
        expect(result.fixedSQL).toContain('DECIMAL(10,2)');
 | 
			
		||||
        expect(result.fixedSQL).toContain('NUMERIC(5,3)');
 | 
			
		||||
        expect(result.fixedSQL).not.toMatch(
 | 
			
		||||
            /DECIMAL\s*\(\s*\d+\s*,\s*\n\s*\d+\s*\)/
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Should have warning about auto-fix
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) =>
 | 
			
		||||
                w.message.includes('Auto-fixed split DECIMAL/NUMERIC')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple auto-fixes together', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE enchantments (
 | 
			
		||||
    id INTEGER PRIMARY KEY,
 | 
			
		||||
    power_level DECIMAL(10,
 | 
			
		||||
    2) NOT NULL,
 | 
			
		||||
    magic_type VARCHAR(50)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
SELECT AVG(power_level): :DECIMAL(3,
 | 
			
		||||
2) FROM enchantments;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        // Should fix both issues
 | 
			
		||||
        expect(result.fixedSQL).toContain('DECIMAL(10,2)');
 | 
			
		||||
        expect(result.fixedSQL).toContain('::DECIMAL(3,');
 | 
			
		||||
        expect(result.fixedSQL).not.toContain(': :');
 | 
			
		||||
 | 
			
		||||
        // Should have warnings for both fixes
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) =>
 | 
			
		||||
                w.message.includes('Auto-fixed cast operator')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) =>
 | 
			
		||||
                w.message.includes('Auto-fixed split DECIMAL/NUMERIC')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should preserve original SQL when no errors', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE wizards (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(true);
 | 
			
		||||
        expect(result.errors).toHaveLength(0);
 | 
			
		||||
        expect(result.fixedSQL).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										145
									
								
								src/lib/data/sql-import/__tests__/sql-validator.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/lib/data/sql-import/__tests__/sql-validator.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { validateSQL } from '../sql-validator';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain';
 | 
			
		||||
 | 
			
		||||
describe('SQL Validator', () => {
 | 
			
		||||
    it('should detect cast operator errors (: :)', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE wizards (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    spellbook JSONB,
 | 
			
		||||
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
SELECT id: :text FROM wizards;
 | 
			
		||||
SELECT COUNT(*): :integer FROM wizards;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(result.errors).toHaveLength(2);
 | 
			
		||||
        expect(result.errors[0].message).toContain('Invalid cast operator');
 | 
			
		||||
        expect(result.errors[0].suggestion).toBe('Replace ": :" with "::"');
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
        expect(result.fixedSQL).toContain('::text');
 | 
			
		||||
        expect(result.fixedSQL).toContain('::integer');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should detect split DECIMAL declarations', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE potions (
 | 
			
		||||
    id INTEGER PRIMARY KEY,
 | 
			
		||||
    power_level DECIMAL(10,
 | 
			
		||||
    2) NOT NULL
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        expect(
 | 
			
		||||
            result.errors.some((e) =>
 | 
			
		||||
                e.message.includes('DECIMAL type declaration is split')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should warn about extensions', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
 | 
			
		||||
CREATE EXTENSION postgis;
 | 
			
		||||
CREATE TABLE dragons (id UUID PRIMARY KEY);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) => w.message.includes('CREATE EXTENSION'))
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should warn about functions and triggers', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE OR REPLACE FUNCTION update_timestamp()
 | 
			
		||||
RETURNS TRIGGER AS $$
 | 
			
		||||
BEGIN
 | 
			
		||||
    NEW.updated_at = CURRENT_TIMESTAMP;
 | 
			
		||||
    RETURN NEW;
 | 
			
		||||
END;
 | 
			
		||||
$$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
CREATE TRIGGER update_wizards_timestamp
 | 
			
		||||
BEFORE UPDATE ON wizards
 | 
			
		||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) =>
 | 
			
		||||
                w.message.includes('Function definitions')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings.some((w) =>
 | 
			
		||||
                w.message.includes('Trigger definitions')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should validate clean SQL as valid', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE wizards (
 | 
			
		||||
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
 | 
			
		||||
    magic_email VARCHAR(255) UNIQUE NOT NULL,
 | 
			
		||||
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE spells (
 | 
			
		||||
    id SERIAL PRIMARY KEY,
 | 
			
		||||
    wizard_id UUID REFERENCES wizards(id),
 | 
			
		||||
    name VARCHAR(200) NOT NULL,
 | 
			
		||||
    incantation TEXT
 | 
			
		||||
);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(true);
 | 
			
		||||
        expect(result.errors).toHaveLength(0);
 | 
			
		||||
        expect(result.fixedSQL).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle the fifth example file issues', () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- Sample from the problematic file
 | 
			
		||||
UPDATE magic_towers 
 | 
			
		||||
SET 
 | 
			
		||||
    power_average = (
 | 
			
		||||
        SELECT AVG(power): :DECIMAL(3,
 | 
			
		||||
        2) 
 | 
			
		||||
        FROM enchantments 
 | 
			
		||||
        WHERE tower_id = NEW.tower_id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
SELECT 
 | 
			
		||||
    ST_X(t.location: :geometry) AS longitude,
 | 
			
		||||
    ST_Y(t.location: :geometry) AS latitude
 | 
			
		||||
FROM towers t;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = validateSQL(sql, DatabaseType.POSTGRESQL);
 | 
			
		||||
 | 
			
		||||
        expect(result.isValid).toBe(false);
 | 
			
		||||
        // Should find multiple cast operator errors
 | 
			
		||||
        expect(
 | 
			
		||||
            result.errors.filter((e) =>
 | 
			
		||||
                e.message.includes('Invalid cast operator')
 | 
			
		||||
            ).length
 | 
			
		||||
        ).toBeGreaterThan(0);
 | 
			
		||||
        expect(result.fixedSQL).toBeDefined();
 | 
			
		||||
        expect(result.fixedSQL).not.toContain(': :');
 | 
			
		||||
        expect(result.fixedSQL).toContain('::DECIMAL');
 | 
			
		||||
        expect(result.fixedSQL).toContain('::geometry');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -3,10 +3,13 @@ import { generateDiagramId, generateId } from '@/lib/utils';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
 | 
			
		||||
import { randomColor } from '@/lib/colors';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
// Common interfaces for SQL entities
 | 
			
		||||
export interface SQLColumn {
 | 
			
		||||
@@ -15,11 +18,14 @@ export interface SQLColumn {
 | 
			
		||||
    nullable: boolean;
 | 
			
		||||
    primaryKey: boolean;
 | 
			
		||||
    unique: boolean;
 | 
			
		||||
    typeArgs?: {
 | 
			
		||||
        length?: number;
 | 
			
		||||
        precision?: number;
 | 
			
		||||
        scale?: number;
 | 
			
		||||
    };
 | 
			
		||||
    typeArgs?:
 | 
			
		||||
        | {
 | 
			
		||||
              length?: number;
 | 
			
		||||
              precision?: number;
 | 
			
		||||
              scale?: number;
 | 
			
		||||
          }
 | 
			
		||||
        | number[]
 | 
			
		||||
        | string;
 | 
			
		||||
    comment?: string;
 | 
			
		||||
    default?: string;
 | 
			
		||||
    increment?: boolean;
 | 
			
		||||
@@ -62,6 +68,7 @@ export interface SQLParserResult {
 | 
			
		||||
    relationships: SQLForeignKey[];
 | 
			
		||||
    types?: SQLCustomType[];
 | 
			
		||||
    enums?: SQLEnumType[];
 | 
			
		||||
    warnings?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Define more specific types for SQL AST nodes
 | 
			
		||||
@@ -543,6 +550,50 @@ export function convertToChartDBDiagram(
 | 
			
		||||
            ) {
 | 
			
		||||
                // Ensure integer types are preserved
 | 
			
		||||
                mappedType = { id: 'integer', name: 'integer' };
 | 
			
		||||
            } else if (
 | 
			
		||||
                sourceDatabaseType === DatabaseType.POSTGRESQL &&
 | 
			
		||||
                parserResult.enums &&
 | 
			
		||||
                parserResult.enums.some(
 | 
			
		||||
                    (e) => e.name.toLowerCase() === column.type.toLowerCase()
 | 
			
		||||
                )
 | 
			
		||||
            ) {
 | 
			
		||||
                // If the column type matches a custom enum type, preserve it
 | 
			
		||||
                mappedType = {
 | 
			
		||||
                    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(
 | 
			
		||||
@@ -565,22 +616,68 @@ export function convertToChartDBDiagram(
 | 
			
		||||
 | 
			
		||||
            // Add type arguments if present
 | 
			
		||||
            if (column.typeArgs) {
 | 
			
		||||
                // Transfer length for varchar/char types
 | 
			
		||||
                if (
 | 
			
		||||
                    column.typeArgs.length !== undefined &&
 | 
			
		||||
                    (field.type.id === 'varchar' || field.type.id === 'char')
 | 
			
		||||
                ) {
 | 
			
		||||
                    field.characterMaximumLength =
 | 
			
		||||
                        column.typeArgs.length.toString();
 | 
			
		||||
                // Handle string typeArgs (e.g., 'max' for varchar(max))
 | 
			
		||||
                if (typeof column.typeArgs === 'string') {
 | 
			
		||||
                    if (
 | 
			
		||||
                        (field.type.id === 'varchar' ||
 | 
			
		||||
                            field.type.id === 'nvarchar') &&
 | 
			
		||||
                        column.typeArgs === 'max'
 | 
			
		||||
                    ) {
 | 
			
		||||
                        field.characterMaximumLength = 'max';
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Transfer precision/scale for numeric types
 | 
			
		||||
                if (
 | 
			
		||||
                    column.typeArgs.precision !== undefined &&
 | 
			
		||||
                    (field.type.id === 'numeric' || field.type.id === 'decimal')
 | 
			
		||||
                // Handle array typeArgs (SQL Server format)
 | 
			
		||||
                else if (
 | 
			
		||||
                    Array.isArray(column.typeArgs) &&
 | 
			
		||||
                    column.typeArgs.length > 0
 | 
			
		||||
                ) {
 | 
			
		||||
                    field.precision = column.typeArgs.precision;
 | 
			
		||||
                    field.scale = column.typeArgs.scale;
 | 
			
		||||
                    if (
 | 
			
		||||
                        field.type.id === 'varchar' ||
 | 
			
		||||
                        field.type.id === 'nvarchar' ||
 | 
			
		||||
                        field.type.id === 'char' ||
 | 
			
		||||
                        field.type.id === 'nchar'
 | 
			
		||||
                    ) {
 | 
			
		||||
                        field.characterMaximumLength =
 | 
			
		||||
                            column.typeArgs[0].toString();
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        (field.type.id === 'numeric' ||
 | 
			
		||||
                            field.type.id === 'decimal') &&
 | 
			
		||||
                        column.typeArgs.length >= 2
 | 
			
		||||
                    ) {
 | 
			
		||||
                        field.precision = column.typeArgs[0];
 | 
			
		||||
                        field.scale = column.typeArgs[1];
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // Handle object typeArgs (standard format)
 | 
			
		||||
                else if (
 | 
			
		||||
                    typeof column.typeArgs === 'object' &&
 | 
			
		||||
                    !Array.isArray(column.typeArgs)
 | 
			
		||||
                ) {
 | 
			
		||||
                    const typeArgsObj = column.typeArgs as {
 | 
			
		||||
                        length?: number;
 | 
			
		||||
                        precision?: number;
 | 
			
		||||
                        scale?: number;
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    // Transfer length for varchar/char types
 | 
			
		||||
                    if (
 | 
			
		||||
                        typeArgsObj.length !== undefined &&
 | 
			
		||||
                        (field.type.id === 'varchar' ||
 | 
			
		||||
                            field.type.id === 'char')
 | 
			
		||||
                    ) {
 | 
			
		||||
                        field.characterMaximumLength =
 | 
			
		||||
                            typeArgsObj.length.toString();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Transfer precision/scale for numeric types
 | 
			
		||||
                    if (
 | 
			
		||||
                        typeArgsObj.precision !== undefined &&
 | 
			
		||||
                        (field.type.id === 'numeric' ||
 | 
			
		||||
                            field.type.id === 'decimal')
 | 
			
		||||
                    ) {
 | 
			
		||||
                        field.precision = typeArgsObj.precision;
 | 
			
		||||
                        field.scale = typeArgsObj.scale;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -588,25 +685,38 @@ export function convertToChartDBDiagram(
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create indexes
 | 
			
		||||
        const indexes = table.indexes.map((sqlIndex) => {
 | 
			
		||||
            const fieldIds = sqlIndex.columns.map((columnName) => {
 | 
			
		||||
                const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                if (!field) {
 | 
			
		||||
                    throw new Error(
 | 
			
		||||
                        `Index references non-existent column: ${columnName}`
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                return field.id;
 | 
			
		||||
            });
 | 
			
		||||
        const indexes = table.indexes
 | 
			
		||||
            .map((sqlIndex) => {
 | 
			
		||||
                const fieldIds = sqlIndex.columns
 | 
			
		||||
                    .map((columnName) => {
 | 
			
		||||
                        const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                        if (!field) {
 | 
			
		||||
                            console.warn(
 | 
			
		||||
                                `Index ${sqlIndex.name} references non-existent column: ${columnName} in table ${table.name}. Skipping this column.`
 | 
			
		||||
                            );
 | 
			
		||||
                            return null;
 | 
			
		||||
                        }
 | 
			
		||||
                        return field.id;
 | 
			
		||||
                    })
 | 
			
		||||
                    .filter((id): id is string => id !== null);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: sqlIndex.name,
 | 
			
		||||
                fieldIds,
 | 
			
		||||
                unique: sqlIndex.unique,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
                // Only create index if at least one column was found
 | 
			
		||||
                if (fieldIds.length === 0) {
 | 
			
		||||
                    console.warn(
 | 
			
		||||
                        `Index ${sqlIndex.name} has no valid columns. Skipping index.`
 | 
			
		||||
                    );
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: sqlIndex.name,
 | 
			
		||||
                    fieldIds,
 | 
			
		||||
                    unique: sqlIndex.unique,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                };
 | 
			
		||||
            })
 | 
			
		||||
            .filter((idx): idx is DBIndex => idx !== null);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            id: newId,
 | 
			
		||||
@@ -708,12 +818,29 @@ export function convertToChartDBDiagram(
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Convert SQL enum types to ChartDB custom types
 | 
			
		||||
    const customTypes: DBCustomType[] = [];
 | 
			
		||||
 | 
			
		||||
    if (parserResult.enums) {
 | 
			
		||||
        parserResult.enums.forEach((enumType, index) => {
 | 
			
		||||
            customTypes.push({
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: enumType.name,
 | 
			
		||||
                schema: 'public', // Default to public schema for now
 | 
			
		||||
                kind: DBCustomTypeKind.enum,
 | 
			
		||||
                values: enumType.values,
 | 
			
		||||
                order: index,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const diagram = {
 | 
			
		||||
        id: generateDiagramId(),
 | 
			
		||||
        name: `SQL Import (${sourceDatabaseType})`,
 | 
			
		||||
        databaseType: targetDatabaseType,
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        customTypes: customTypes.length > 0 ? customTypes : undefined,
 | 
			
		||||
        createdAt: new Date(),
 | 
			
		||||
        updatedAt: new Date(),
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,458 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('PostgreSQL Core Parser Tests', () => {
 | 
			
		||||
    it('should parse basic tables', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE wizards (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                name VARCHAR(255) NOT NULL
 | 
			
		||||
            );
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('wizards');
 | 
			
		||||
        expect(result.tables[0].columns).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should parse foreign key relationships', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE guilds (id INTEGER PRIMARY KEY);
 | 
			
		||||
            CREATE TABLE mages (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                guild_id INTEGER REFERENCES guilds(id)
 | 
			
		||||
            );
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
        expect(result.relationships[0].sourceTable).toBe('mages');
 | 
			
		||||
        expect(result.relationships[0].targetTable).toBe('guilds');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should skip functions with warnings', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE test_table (id INTEGER PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            CREATE FUNCTION test_func() RETURNS VOID AS $$
 | 
			
		||||
            BEGIN
 | 
			
		||||
                NULL;
 | 
			
		||||
            END;
 | 
			
		||||
            $$ LANGUAGE plpgsql;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.warnings).toBeDefined();
 | 
			
		||||
        expect(result.warnings!.some((w) => w.includes('Function'))).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle tables that fail to parse', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE valid_table (id INTEGER PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            -- This table has syntax that might fail parsing
 | 
			
		||||
            CREATE TABLE complex_table (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                value NUMERIC(10,
 | 
			
		||||
2) GENERATED ALWAYS AS (1 + 1) STORED
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            CREATE TABLE another_valid (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                complex_ref INTEGER REFERENCES complex_table(id)
 | 
			
		||||
            );
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should find all 3 tables even if complex_table fails to parse
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.tables.map((t) => t.name).sort()).toEqual([
 | 
			
		||||
            'another_valid',
 | 
			
		||||
            'complex_table',
 | 
			
		||||
            'valid_table',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Should still find the foreign key relationship
 | 
			
		||||
        expect(
 | 
			
		||||
            result.relationships.some(
 | 
			
		||||
                (r) =>
 | 
			
		||||
                    r.sourceTable === 'another_valid' &&
 | 
			
		||||
                    r.targetTable === 'complex_table'
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should parse the magical academy system fixture', async () => {
 | 
			
		||||
        const sql = `-- Magical Academy System Database Schema
 | 
			
		||||
-- This is a test fixture representing a typical magical academy system
 | 
			
		||||
 | 
			
		||||
CREATE TABLE magic_schools(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    name text NOT NULL,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    updated_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE towers(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    name text NOT NULL,
 | 
			
		||||
    location text,
 | 
			
		||||
    crystal_frequency varchar(20),
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE magical_ranks(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    name text NOT NULL,
 | 
			
		||||
    description text,
 | 
			
		||||
    is_system boolean NOT NULL DEFAULT false,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE spell_permissions(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    spell_school text NOT NULL,
 | 
			
		||||
    spell_action text NOT NULL,
 | 
			
		||||
    description text,
 | 
			
		||||
    UNIQUE (spell_school, spell_action)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE rank_permissions(
 | 
			
		||||
    rank_id uuid NOT NULL REFERENCES magical_ranks(id) ON DELETE CASCADE,
 | 
			
		||||
    permission_id uuid NOT NULL REFERENCES spell_permissions(id) ON DELETE CASCADE,
 | 
			
		||||
    granted_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    PRIMARY KEY (rank_id, permission_id)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE grimoire_types(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    name text NOT NULL,
 | 
			
		||||
    description text,
 | 
			
		||||
    is_active boolean NOT NULL DEFAULT true
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE wizards(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    username text NOT NULL,
 | 
			
		||||
    email text NOT NULL,
 | 
			
		||||
    password_hash text NOT NULL,
 | 
			
		||||
    first_name text NOT NULL,
 | 
			
		||||
    last_name text NOT NULL,
 | 
			
		||||
    is_active boolean NOT NULL DEFAULT true,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    UNIQUE (school_id, username),
 | 
			
		||||
    UNIQUE (email)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- This function should not prevent the next table from being parsed
 | 
			
		||||
CREATE FUNCTION enforce_wizard_tower_school()
 | 
			
		||||
RETURNS TRIGGER AS $$
 | 
			
		||||
BEGIN
 | 
			
		||||
    IF NOT EXISTS (
 | 
			
		||||
        SELECT 1 FROM towers 
 | 
			
		||||
        WHERE id = NEW.tower_id AND school_id = NEW.school_id
 | 
			
		||||
    ) THEN
 | 
			
		||||
        RAISE EXCEPTION 'Tower does not belong to magic school';
 | 
			
		||||
    END IF;
 | 
			
		||||
    RETURN NEW;
 | 
			
		||||
END;
 | 
			
		||||
$$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE wizard_ranks(
 | 
			
		||||
    wizard_id uuid NOT NULL REFERENCES wizards(id) ON DELETE CASCADE,
 | 
			
		||||
    rank_id uuid NOT NULL REFERENCES magical_ranks(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    assigned_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    assigned_by uuid REFERENCES wizards(id),
 | 
			
		||||
    PRIMARY KEY (wizard_id, rank_id, tower_id)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE apprentices(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    apprentice_id text NOT NULL, -- Magical Apprentice Identifier
 | 
			
		||||
    first_name text NOT NULL,
 | 
			
		||||
    last_name text NOT NULL,
 | 
			
		||||
    date_of_birth date NOT NULL,
 | 
			
		||||
    magical_affinity varchar(10),
 | 
			
		||||
    email text,
 | 
			
		||||
    crystal_phone varchar(20),
 | 
			
		||||
    dormitory text,
 | 
			
		||||
    emergency_contact jsonb,
 | 
			
		||||
    patron_info jsonb,
 | 
			
		||||
    primary_mentor uuid REFERENCES wizards(id),
 | 
			
		||||
    referring_wizard uuid REFERENCES wizards(id),
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    UNIQUE (school_id, apprentice_id)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE spell_lessons(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
    instructor_id uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
    lesson_date timestamptz NOT NULL,
 | 
			
		||||
    duration_minutes integer NOT NULL DEFAULT 30,
 | 
			
		||||
    status text NOT NULL DEFAULT 'scheduled',
 | 
			
		||||
    notes text,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    created_by uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
    CONSTRAINT valid_status CHECK (status IN ('scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE grimoires(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
    lesson_id uuid REFERENCES spell_lessons(id),
 | 
			
		||||
    grimoire_type_id uuid NOT NULL REFERENCES grimoire_types(id),
 | 
			
		||||
    instructor_id uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
    content jsonb NOT NULL,
 | 
			
		||||
    enchantments jsonb,
 | 
			
		||||
    is_sealed boolean NOT NULL DEFAULT false,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    updated_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE tuition_scrolls(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
    scroll_number text NOT NULL,
 | 
			
		||||
    scroll_date date NOT NULL DEFAULT CURRENT_DATE,
 | 
			
		||||
    due_date date NOT NULL,
 | 
			
		||||
    subtotal numeric(10,2) NOT NULL,
 | 
			
		||||
    magical_tax numeric(10,2) NOT NULL DEFAULT 0,
 | 
			
		||||
    scholarship_amount numeric(10,2) NOT NULL DEFAULT 0,
 | 
			
		||||
    total_gold numeric(10,2) NOT NULL,
 | 
			
		||||
    status text NOT NULL DEFAULT 'draft',
 | 
			
		||||
    notes text,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    created_by uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
    UNIQUE (school_id, scroll_number),
 | 
			
		||||
    CONSTRAINT valid_scroll_status CHECK (status IN ('draft', 'sent', 'paid', 'overdue', 'cancelled'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE scroll_line_items(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
    description text NOT NULL,
 | 
			
		||||
    quantity numeric(10,2) NOT NULL DEFAULT 1,
 | 
			
		||||
    gold_per_unit numeric(10,2) NOT NULL,
 | 
			
		||||
    total_gold numeric(10,2) NOT NULL,
 | 
			
		||||
    lesson_id uuid REFERENCES spell_lessons(id),
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE patron_sponsorships(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
    patron_house text NOT NULL,
 | 
			
		||||
    sponsorship_code text NOT NULL,
 | 
			
		||||
    claim_number text NOT NULL,
 | 
			
		||||
    claim_date date NOT NULL DEFAULT CURRENT_DATE,
 | 
			
		||||
    gold_requested numeric(10,2) NOT NULL,
 | 
			
		||||
    gold_approved numeric(10,2),
 | 
			
		||||
    status text NOT NULL DEFAULT 'submitted',
 | 
			
		||||
    denial_reason text,
 | 
			
		||||
    notes text,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    updated_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    UNIQUE (claim_number),
 | 
			
		||||
    CONSTRAINT valid_sponsorship_status CHECK (status IN ('draft', 'submitted', 'in_review', 'approved', 'partial', 'denied', 'appealed'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE gold_payments(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
    payment_date timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    gold_amount numeric(10,2) NOT NULL,
 | 
			
		||||
    payment_method text NOT NULL,
 | 
			
		||||
    reference_rune text,
 | 
			
		||||
    notes text,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    created_by uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
    CONSTRAINT valid_payment_method CHECK (payment_method IN ('gold_coins', 'crystal_transfer', 'mithril_card', 'dragon_scale', 'patron_sponsorship', 'other'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE arcane_logs(
 | 
			
		||||
    id bigserial PRIMARY KEY,
 | 
			
		||||
    school_id uuid,
 | 
			
		||||
    wizard_id uuid,
 | 
			
		||||
    tower_id uuid,
 | 
			
		||||
    table_name text NOT NULL,
 | 
			
		||||
    record_id uuid,
 | 
			
		||||
    spell_operation text NOT NULL,
 | 
			
		||||
    old_values jsonb,
 | 
			
		||||
    new_values jsonb,
 | 
			
		||||
    casting_source inet,
 | 
			
		||||
    magical_signature text,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    FOREIGN KEY (school_id) REFERENCES magic_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,
 | 
			
		||||
    CONSTRAINT valid_spell_operation CHECK (spell_operation IN ('INSERT', 'UPDATE', 'DELETE'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Enable Row Level Security
 | 
			
		||||
ALTER TABLE wizards ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
ALTER TABLE apprentices ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
ALTER TABLE grimoires ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
ALTER TABLE spell_lessons ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
ALTER TABLE tuition_scrolls ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
 | 
			
		||||
-- Create RLS Policies
 | 
			
		||||
CREATE POLICY school_isolation_wizards ON wizards
 | 
			
		||||
    FOR ALL TO authenticated
 | 
			
		||||
    USING (school_id = current_setting('app.current_school')::uuid);
 | 
			
		||||
 | 
			
		||||
CREATE POLICY school_isolation_apprentices ON apprentices
 | 
			
		||||
    FOR ALL TO authenticated
 | 
			
		||||
    USING (school_id = current_setting('app.current_school')::uuid);
 | 
			
		||||
 | 
			
		||||
-- Create arcane audit trigger function
 | 
			
		||||
CREATE FUNCTION arcane_audit_trigger()
 | 
			
		||||
RETURNS TRIGGER AS $$
 | 
			
		||||
BEGIN
 | 
			
		||||
    INSERT INTO arcane_logs (
 | 
			
		||||
        school_id,
 | 
			
		||||
        wizard_id,
 | 
			
		||||
        tower_id,
 | 
			
		||||
        table_name,
 | 
			
		||||
        record_id,
 | 
			
		||||
        spell_operation,
 | 
			
		||||
        old_values,
 | 
			
		||||
        new_values
 | 
			
		||||
    ) VALUES (
 | 
			
		||||
        current_setting('app.current_school', true)::uuid,
 | 
			
		||||
        current_setting('app.current_wizard', true)::uuid,
 | 
			
		||||
        current_setting('app.current_tower', true)::uuid,
 | 
			
		||||
        TG_TABLE_NAME,
 | 
			
		||||
        COALESCE(NEW.id, OLD.id),
 | 
			
		||||
        TG_OP,
 | 
			
		||||
        CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) ELSE NULL END,
 | 
			
		||||
        CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) ELSE NULL END
 | 
			
		||||
    );
 | 
			
		||||
    RETURN NEW;
 | 
			
		||||
END;
 | 
			
		||||
$$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
-- Create triggers
 | 
			
		||||
CREATE TRIGGER arcane_audit_wizards AFTER INSERT OR UPDATE OR DELETE ON wizards
 | 
			
		||||
    FOR EACH ROW EXECUTE FUNCTION arcane_audit_trigger();
 | 
			
		||||
 | 
			
		||||
CREATE TRIGGER arcane_audit_apprentices AFTER INSERT OR UPDATE OR DELETE ON apprentices
 | 
			
		||||
    FOR EACH ROW EXECUTE FUNCTION arcane_audit_trigger();`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should find all 16 tables
 | 
			
		||||
        expect(result.tables).toHaveLength(16);
 | 
			
		||||
 | 
			
		||||
        const tableNames = result.tables.map((t) => t.name).sort();
 | 
			
		||||
        const expectedTables = [
 | 
			
		||||
            'apprentices',
 | 
			
		||||
            'arcane_logs',
 | 
			
		||||
            'gold_payments',
 | 
			
		||||
            'grimoire_types',
 | 
			
		||||
            'grimoires',
 | 
			
		||||
            'magic_schools',
 | 
			
		||||
            'magical_ranks',
 | 
			
		||||
            'patron_sponsorships',
 | 
			
		||||
            'rank_permissions',
 | 
			
		||||
            'scroll_line_items',
 | 
			
		||||
            'spell_lessons',
 | 
			
		||||
            'spell_permissions',
 | 
			
		||||
            'towers',
 | 
			
		||||
            'tuition_scrolls',
 | 
			
		||||
            'wizard_ranks',
 | 
			
		||||
            'wizards',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        expect(tableNames).toEqual(expectedTables);
 | 
			
		||||
 | 
			
		||||
        // Should have many relationships
 | 
			
		||||
        expect(result.relationships.length).toBeGreaterThan(30);
 | 
			
		||||
 | 
			
		||||
        // Should have warnings about unsupported features
 | 
			
		||||
        expect(result.warnings).toBeDefined();
 | 
			
		||||
        expect(result.warnings!.length).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
        // Verify specific critical relationships exist
 | 
			
		||||
        const hasWizardSchoolFK = result.relationships.some(
 | 
			
		||||
            (r) =>
 | 
			
		||||
                r.sourceTable === 'wizards' &&
 | 
			
		||||
                r.targetTable === 'magic_schools' &&
 | 
			
		||||
                r.sourceColumn === 'school_id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(hasWizardSchoolFK).toBe(true);
 | 
			
		||||
 | 
			
		||||
        const hasApprenticeMentorFK = result.relationships.some(
 | 
			
		||||
            (r) =>
 | 
			
		||||
                r.sourceTable === 'apprentices' &&
 | 
			
		||||
                r.targetTable === 'wizards' &&
 | 
			
		||||
                r.sourceColumn === 'primary_mentor'
 | 
			
		||||
        );
 | 
			
		||||
        expect(hasApprenticeMentorFK).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle ALTER TABLE ENABLE ROW LEVEL SECURITY', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE secure_table (id INTEGER PRIMARY KEY);
 | 
			
		||||
            ALTER TABLE secure_table ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.warnings).toBeDefined();
 | 
			
		||||
        // The warning should mention row level security
 | 
			
		||||
        expect(
 | 
			
		||||
            result.warnings!.some((w) =>
 | 
			
		||||
                w.toLowerCase().includes('row level security')
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should extract foreign keys even from unparsed tables', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE base (id UUID PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            -- Intentionally malformed to fail parsing
 | 
			
		||||
            CREATE TABLE malformed (
 | 
			
		||||
                id UUID PRIMARY KEY,
 | 
			
		||||
                base_id UUID REFERENCES base(id),
 | 
			
		||||
                FOREIGN KEY (base_id) REFERENCES base(id) ON DELETE CASCADE,
 | 
			
		||||
                value NUMERIC(10, 
 | 
			
		||||
            2) -- Missing closing paren will cause parse failure
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should still create the table entry
 | 
			
		||||
        expect(result.tables.map((t) => t.name)).toContain('malformed');
 | 
			
		||||
 | 
			
		||||
        // Should extract the foreign key
 | 
			
		||||
        const fks = result.relationships.filter(
 | 
			
		||||
            (r) => r.sourceTable === 'malformed'
 | 
			
		||||
        );
 | 
			
		||||
        expect(fks.length).toBeGreaterThan(0);
 | 
			
		||||
        expect(fks[0].targetTable).toBe('base');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,330 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('PostgreSQL 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 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        name text NOT NULL,
 | 
			
		||||
                        created_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE towers(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        name text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE ranks(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        name text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE spell_permissions(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        spell_type text NOT NULL,
 | 
			
		||||
                        casting_level text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE rank_spell_permissions(
 | 
			
		||||
                        rank_id uuid NOT NULL REFERENCES ranks(id) ON DELETE CASCADE,
 | 
			
		||||
                        spell_permission_id uuid NOT NULL REFERENCES spell_permissions(id) ON DELETE CASCADE,
 | 
			
		||||
                        PRIMARY KEY (rank_id, spell_permission_id)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE grimoire_types(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        name text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE wizards(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        wizard_name text NOT NULL,
 | 
			
		||||
                        email text NOT NULL,
 | 
			
		||||
                        UNIQUE (school_id, wizard_name)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE FUNCTION enforce_wizard_tower_school()
 | 
			
		||||
                    RETURNS TRIGGER AS $$
 | 
			
		||||
                    BEGIN
 | 
			
		||||
                        -- Function body
 | 
			
		||||
                        RETURN NEW;
 | 
			
		||||
                    END;
 | 
			
		||||
                    $$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE wizard_ranks(
 | 
			
		||||
                        wizard_id uuid NOT NULL REFERENCES wizards(id) ON DELETE CASCADE,
 | 
			
		||||
                        rank_id uuid NOT NULL REFERENCES ranks(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        assigned_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
                        PRIMARY KEY (wizard_id, rank_id, tower_id)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE apprentices(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        first_name text NOT NULL,
 | 
			
		||||
                        last_name text NOT NULL,
 | 
			
		||||
                        enrollment_date date NOT NULL,
 | 
			
		||||
                        primary_mentor uuid REFERENCES wizards(id),
 | 
			
		||||
                        sponsoring_wizard uuid REFERENCES wizards(id)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE spell_lessons(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
                        instructor_id uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
                        lesson_date timestamptz NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE grimoires(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
                        grimoire_type_id uuid NOT NULL REFERENCES grimoire_types(id),
 | 
			
		||||
                        author_wizard_id uuid NOT NULL REFERENCES wizards(id),
 | 
			
		||||
                        content jsonb NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE tuition_scrolls(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        school_id uuid NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
 | 
			
		||||
                        tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
                        apprentice_id uuid NOT NULL REFERENCES apprentices(id) ON DELETE CASCADE,
 | 
			
		||||
                        total_amount numeric(10,2) NOT NULL,
 | 
			
		||||
                        status text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE tuition_items(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        tuition_scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
                        description text NOT NULL,
 | 
			
		||||
                        amount numeric(10,2) NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE patron_sponsorships(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        tuition_scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
                        patron_house text NOT NULL,
 | 
			
		||||
                        sponsorship_code text NOT NULL,
 | 
			
		||||
                        status text NOT NULL
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE gold_payments(
 | 
			
		||||
                        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                        tuition_scroll_id uuid NOT NULL REFERENCES tuition_scrolls(id) ON DELETE CASCADE,
 | 
			
		||||
                        amount numeric(10,2) NOT NULL,
 | 
			
		||||
                        payment_date timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE arcane_logs(
 | 
			
		||||
                        id bigserial PRIMARY KEY,
 | 
			
		||||
                        school_id uuid,
 | 
			
		||||
                        wizard_id uuid,
 | 
			
		||||
                        tower_id uuid,
 | 
			
		||||
                        table_name text NOT NULL,
 | 
			
		||||
                        operation text NOT NULL,
 | 
			
		||||
                        record_id uuid,
 | 
			
		||||
                        changes jsonb,
 | 
			
		||||
                        created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
                        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
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    -- Enable RLS
 | 
			
		||||
                    ALTER TABLE wizards ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
                    ALTER TABLE apprentices ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
 | 
			
		||||
                    -- Create policies
 | 
			
		||||
                    CREATE POLICY school_isolation ON wizards
 | 
			
		||||
                        FOR ALL TO public
 | 
			
		||||
                        USING (school_id = current_setting('app.current_school')::uuid);
 | 
			
		||||
                `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(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);
 | 
			
		||||
 | 
			
		||||
            // Should have warnings about functions, policies, and RLS
 | 
			
		||||
            expect(result.warnings).toBeDefined();
 | 
			
		||||
            expect(result.warnings!.length).toBeGreaterThan(0);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Enchanted Bazaar Example', () => {
 | 
			
		||||
        it('should parse the enchanted bazaar example with functions and policies', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                    -- Enchanted Bazaar tables with complex features
 | 
			
		||||
                    CREATE TABLE merchants(
 | 
			
		||||
                        id SERIAL PRIMARY KEY,
 | 
			
		||||
                        name VARCHAR(255) NOT NULL,
 | 
			
		||||
                        email VARCHAR(255) UNIQUE NOT NULL,
 | 
			
		||||
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE artifacts(
 | 
			
		||||
                        id SERIAL PRIMARY KEY,
 | 
			
		||||
                        merchant_id INTEGER REFERENCES merchants(id) ON DELETE CASCADE,
 | 
			
		||||
                        name VARCHAR(255) NOT NULL,
 | 
			
		||||
                        price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
 | 
			
		||||
                        enchantment_charges INTEGER DEFAULT 0 CHECK (enchantment_charges >= 0)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    -- Function that should be skipped
 | 
			
		||||
                    CREATE FUNCTION consume_charges(artifact_id INTEGER, charges_used INTEGER)
 | 
			
		||||
                    RETURNS VOID AS $$
 | 
			
		||||
                    BEGIN
 | 
			
		||||
                        UPDATE artifacts SET enchantment_charges = enchantment_charges - charges_used WHERE id = artifact_id;
 | 
			
		||||
                    END;
 | 
			
		||||
                    $$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE trades(
 | 
			
		||||
                        id SERIAL PRIMARY KEY,
 | 
			
		||||
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
                        status VARCHAR(50) DEFAULT 'negotiating'
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    CREATE TABLE trade_items(
 | 
			
		||||
                        trade_id INTEGER REFERENCES trades(id) ON DELETE CASCADE,
 | 
			
		||||
                        artifact_id INTEGER REFERENCES artifacts(id),
 | 
			
		||||
                        quantity INTEGER NOT NULL CHECK (quantity > 0),
 | 
			
		||||
                        agreed_price DECIMAL(10, 2) NOT NULL,
 | 
			
		||||
                        PRIMARY KEY (trade_id, artifact_id)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    -- Enable RLS
 | 
			
		||||
                    ALTER TABLE artifacts ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
 | 
			
		||||
                    -- Create policy
 | 
			
		||||
                    CREATE POLICY merchant_artifacts ON artifacts
 | 
			
		||||
                        FOR ALL TO merchants
 | 
			
		||||
                        USING (merchant_id = current_user_id());
 | 
			
		||||
 | 
			
		||||
                    -- Create trigger
 | 
			
		||||
                    CREATE TRIGGER charge_consumption_trigger
 | 
			
		||||
                        AFTER INSERT ON trade_items
 | 
			
		||||
                        FOR EACH ROW
 | 
			
		||||
                        EXECUTE FUNCTION consume_charges();
 | 
			
		||||
                `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            // Should parse all tables despite functions, policies, 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
 | 
			
		||||
            if (tableNames.includes('marketplace_tokens')) {
 | 
			
		||||
                // Real file relationships
 | 
			
		||||
                expect(
 | 
			
		||||
                    result.relationships.some(
 | 
			
		||||
                        (r) =>
 | 
			
		||||
                            r.sourceTable === 'marketplace_listings' &&
 | 
			
		||||
                            r.targetTable === 'inventory_items'
 | 
			
		||||
                    )
 | 
			
		||||
                ).toBe(true);
 | 
			
		||||
            } else {
 | 
			
		||||
                // Mock data 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('Function') ||
 | 
			
		||||
                            w.includes('Policy') ||
 | 
			
		||||
                            w.includes('Trigger') ||
 | 
			
		||||
                            w.includes('ROW LEVEL SECURITY')
 | 
			
		||||
                    )
 | 
			
		||||
                ).toBe(true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,116 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('PostgreSQL Parser Integration', () => {
 | 
			
		||||
    it('should parse simple SQL', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE wizards (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                name VARCHAR(255)
 | 
			
		||||
            );
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('wizards');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle functions correctly', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE wizards (id INTEGER PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            CREATE FUNCTION get_wizard() RETURNS INTEGER AS $$
 | 
			
		||||
            BEGIN
 | 
			
		||||
                RETURN 1;
 | 
			
		||||
            END;
 | 
			
		||||
            $$ LANGUAGE plpgsql;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('wizards');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle policies correctly', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE ancient_scrolls (id INTEGER PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            CREATE POLICY wizard_policy ON ancient_scrolls
 | 
			
		||||
                FOR SELECT
 | 
			
		||||
                USING (true);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle RLS correctly', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE enchanted_vault (id INTEGER PRIMARY KEY);
 | 
			
		||||
            ALTER TABLE enchanted_vault ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle triggers correctly', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE spell_log (id INTEGER PRIMARY KEY);
 | 
			
		||||
            
 | 
			
		||||
            CREATE TRIGGER spell_trigger
 | 
			
		||||
                AFTER INSERT ON spell_log
 | 
			
		||||
                FOR EACH ROW
 | 
			
		||||
                EXECUTE FUNCTION spell_func();
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should preserve all relationships', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
            CREATE TABLE guilds (id INTEGER PRIMARY KEY);
 | 
			
		||||
            CREATE TABLE wizards (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                guild_id INTEGER REFERENCES guilds(id)
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            -- This function should trigger improved parser
 | 
			
		||||
            CREATE FUNCTION dummy() RETURNS VOID AS $$ BEGIN END; $$ LANGUAGE plpgsql;
 | 
			
		||||
            
 | 
			
		||||
            CREATE TABLE quests (
 | 
			
		||||
                id INTEGER PRIMARY KEY,
 | 
			
		||||
                wizard_id INTEGER REFERENCES wizards(id),
 | 
			
		||||
                guild_id INTEGER REFERENCES guilds(id)
 | 
			
		||||
            );
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.relationships).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
        // Verify all relationships are preserved
 | 
			
		||||
        expect(
 | 
			
		||||
            result.relationships.some(
 | 
			
		||||
                (r) => r.sourceTable === 'wizards' && r.targetTable === 'guilds'
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
        expect(
 | 
			
		||||
            result.relationships.some(
 | 
			
		||||
                (r) => r.sourceTable === 'quests' && r.targetTable === 'wizards'
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
        expect(
 | 
			
		||||
            result.relationships.some(
 | 
			
		||||
                (r) => r.sourceTable === 'quests' && r.targetTable === 'guilds'
 | 
			
		||||
            )
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,491 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('PostgreSQL Parser', () => {
 | 
			
		||||
    describe('Basic Table Parsing', () => {
 | 
			
		||||
        it('should parse simple tables with basic data types', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE wizards (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    name VARCHAR(255) NOT NULL,
 | 
			
		||||
                    magic_email TEXT UNIQUE,
 | 
			
		||||
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].name).toBe('wizards');
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(4);
 | 
			
		||||
            expect(result.tables[0].columns[0].name).toBe('id');
 | 
			
		||||
            expect(result.tables[0].columns[0].type).toBe('INTEGER');
 | 
			
		||||
            expect(result.tables[0].columns[0].primaryKey).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should parse multiple tables', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE guilds (
 | 
			
		||||
                    id SERIAL PRIMARY KEY,
 | 
			
		||||
                    name VARCHAR(100) NOT NULL
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                CREATE TABLE mages (
 | 
			
		||||
                    id SERIAL PRIMARY KEY,
 | 
			
		||||
                    name VARCHAR(100) NOT NULL,
 | 
			
		||||
                    guild_id INTEGER REFERENCES guilds(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(2);
 | 
			
		||||
            expect(result.tables.map((t) => t.name).sort()).toEqual([
 | 
			
		||||
                'guilds',
 | 
			
		||||
                'mages',
 | 
			
		||||
            ]);
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            expect(result.relationships[0].sourceTable).toBe('mages');
 | 
			
		||||
            expect(result.relationships[0].targetTable).toBe('guilds');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle IF NOT EXISTS clause', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS potions (
 | 
			
		||||
                    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                    name TEXT NOT NULL
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].name).toBe('potions');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Complex Data Types', () => {
 | 
			
		||||
        it('should handle UUID and special PostgreSQL types', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE special_types (
 | 
			
		||||
                    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
                    data JSONB,
 | 
			
		||||
                    tags TEXT[],
 | 
			
		||||
                    location POINT,
 | 
			
		||||
                    mana_cost MONEY,
 | 
			
		||||
                    binary_data BYTEA
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            const columns = result.tables[0].columns;
 | 
			
		||||
            expect(columns.find((c) => c.name === 'id')?.type).toBe('UUID');
 | 
			
		||||
            expect(columns.find((c) => c.name === 'data')?.type).toBe('JSONB');
 | 
			
		||||
            expect(columns.find((c) => c.name === 'tags')?.type).toBe('TEXT[]');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle numeric with precision', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE treasury (
 | 
			
		||||
                    id SERIAL PRIMARY KEY,
 | 
			
		||||
                    amount NUMERIC(10, 2),
 | 
			
		||||
                    percentage DECIMAL(5, 2),
 | 
			
		||||
                    big_number BIGINT
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            const columns = result.tables[0].columns;
 | 
			
		||||
            // Parser limitation: scale on separate line is not captured
 | 
			
		||||
            const amountType = columns.find((c) => c.name === 'amount')?.type;
 | 
			
		||||
            expect(amountType).toMatch(/^NUMERIC/);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle multi-line numeric definitions', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE multi_line (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    value NUMERIC(10,
 | 
			
		||||
2),
 | 
			
		||||
                    another_col TEXT
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(3);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Foreign Key Relationships', () => {
 | 
			
		||||
        it('should parse inline foreign keys', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE realms (id INTEGER PRIMARY KEY);
 | 
			
		||||
                CREATE TABLE sanctuaries (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    realm_id INTEGER REFERENCES realms(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            expect(result.relationships[0].sourceTable).toBe('sanctuaries');
 | 
			
		||||
            expect(result.relationships[0].targetTable).toBe('realms');
 | 
			
		||||
            expect(result.relationships[0].sourceColumn).toBe('realm_id');
 | 
			
		||||
            expect(result.relationships[0].targetColumn).toBe('id');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should parse table-level foreign key constraints', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE enchantment_orders (id INTEGER PRIMARY KEY);
 | 
			
		||||
                CREATE TABLE enchantment_items (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    order_id INTEGER,
 | 
			
		||||
                    CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES enchantment_orders(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            expect(result.relationships[0].sourceTable).toBe(
 | 
			
		||||
                'enchantment_items'
 | 
			
		||||
            );
 | 
			
		||||
            expect(result.relationships[0].targetTable).toBe(
 | 
			
		||||
                'enchantment_orders'
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should parse composite foreign keys', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE magic_schools (id UUID PRIMARY KEY);
 | 
			
		||||
                CREATE TABLE quests (
 | 
			
		||||
                    school_id UUID,
 | 
			
		||||
                    quest_id UUID,
 | 
			
		||||
                    name TEXT,
 | 
			
		||||
                    PRIMARY KEY (school_id, quest_id),
 | 
			
		||||
                    FOREIGN KEY (school_id) REFERENCES magic_schools(id)
 | 
			
		||||
                );
 | 
			
		||||
                CREATE TABLE rituals (
 | 
			
		||||
                    id UUID PRIMARY KEY,
 | 
			
		||||
                    school_id UUID,
 | 
			
		||||
                    quest_id UUID,
 | 
			
		||||
                    FOREIGN KEY (school_id, quest_id) REFERENCES quests(school_id, quest_id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(3);
 | 
			
		||||
            // Composite foreign keys are not fully supported
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            expect(result.relationships[0].sourceTable).toBe('quests');
 | 
			
		||||
            expect(result.relationships[0].targetTable).toBe('magic_schools');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle ON DELETE and ON UPDATE clauses', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE wizards (id INTEGER PRIMARY KEY);
 | 
			
		||||
                CREATE TABLE scrolls (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    wizard_id INTEGER REFERENCES wizards(id) ON DELETE CASCADE ON UPDATE CASCADE
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            // ON DELETE/UPDATE clauses are not preserved in output
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Constraints', () => {
 | 
			
		||||
        it('should parse unique constraints', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE wizards (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    magic_email TEXT UNIQUE,
 | 
			
		||||
                    wizard_name TEXT,
 | 
			
		||||
                    UNIQUE (wizard_name)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            const columns = result.tables[0].columns;
 | 
			
		||||
            expect(columns.find((c) => c.name === 'magic_email')?.unique).toBe(
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should parse check constraints', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE potions (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    mana_cost DECIMAL CHECK (mana_cost > 0),
 | 
			
		||||
                    quantity INTEGER,
 | 
			
		||||
                    CONSTRAINT positive_quantity CHECK (quantity >= 0)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(3);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should parse composite primary keys', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE enchantment_items (
 | 
			
		||||
                    order_id INTEGER,
 | 
			
		||||
                    potion_id INTEGER,
 | 
			
		||||
                    quantity INTEGER,
 | 
			
		||||
                    PRIMARY KEY (order_id, potion_id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            const columns = result.tables[0].columns;
 | 
			
		||||
            expect(columns.filter((c) => c.primaryKey)).toHaveLength(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Generated Columns', () => {
 | 
			
		||||
        it('should handle GENERATED ALWAYS AS IDENTITY', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE items (
 | 
			
		||||
                    id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
 | 
			
		||||
                    name TEXT
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns[0].increment).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle GENERATED BY DEFAULT AS IDENTITY', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE items (
 | 
			
		||||
                    id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
 | 
			
		||||
                    name TEXT
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns[0].increment).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle computed columns', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE calculations (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    value1 NUMERIC,
 | 
			
		||||
                    value2 NUMERIC,
 | 
			
		||||
                    total NUMERIC GENERATED ALWAYS AS (value1 + value2) STORED
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(4);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Unsupported Statements', () => {
 | 
			
		||||
        it('should skip and warn about functions', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE wizards (id INTEGER PRIMARY KEY);
 | 
			
		||||
                
 | 
			
		||||
                CREATE FUNCTION get_wizard_name(wizard_id INTEGER)
 | 
			
		||||
                RETURNS TEXT AS $$
 | 
			
		||||
                BEGIN
 | 
			
		||||
                    RETURN 'test';
 | 
			
		||||
                END;
 | 
			
		||||
                $$ LANGUAGE plpgsql;
 | 
			
		||||
                
 | 
			
		||||
                CREATE TABLE scrolls (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    wizard_id INTEGER REFERENCES wizards(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(2);
 | 
			
		||||
            expect(result.warnings).toBeDefined();
 | 
			
		||||
            expect(result.warnings!.some((w) => w.includes('Function'))).toBe(
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should skip and warn about triggers', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE spell_audit_log (id SERIAL PRIMARY KEY);
 | 
			
		||||
                
 | 
			
		||||
                CREATE TRIGGER spell_audit_trigger
 | 
			
		||||
                AFTER INSERT ON spell_audit_log
 | 
			
		||||
                FOR EACH ROW
 | 
			
		||||
                EXECUTE FUNCTION spell_audit_function();
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.warnings).toBeDefined();
 | 
			
		||||
            expect(result.warnings!.some((w) => w.includes('Trigger'))).toBe(
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should skip and warn about policies', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE arcane_secrets (id INTEGER PRIMARY KEY);
 | 
			
		||||
                
 | 
			
		||||
                CREATE POLICY wizard_policy ON arcane_secrets
 | 
			
		||||
                FOR SELECT
 | 
			
		||||
                TO public
 | 
			
		||||
                USING (true);
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.warnings).toBeDefined();
 | 
			
		||||
            expect(result.warnings!.some((w) => w.includes('Policy'))).toBe(
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should skip and warn about RLS', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE enchanted_vault (id INTEGER PRIMARY KEY);
 | 
			
		||||
                ALTER TABLE enchanted_vault ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.warnings).toBeDefined();
 | 
			
		||||
            expect(
 | 
			
		||||
                result.warnings!.some((w) =>
 | 
			
		||||
                    w.toLowerCase().includes('row level security')
 | 
			
		||||
                )
 | 
			
		||||
            ).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Edge Cases', () => {
 | 
			
		||||
        it('should handle tables after failed function parsing', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE before_enchantment (id INTEGER PRIMARY KEY);
 | 
			
		||||
                
 | 
			
		||||
                CREATE FUNCTION complex_spell()
 | 
			
		||||
                RETURNS TABLE(id INTEGER, name TEXT) AS $$
 | 
			
		||||
                BEGIN
 | 
			
		||||
                    RETURN QUERY SELECT 1, 'test';
 | 
			
		||||
                END;
 | 
			
		||||
                $$ LANGUAGE plpgsql;
 | 
			
		||||
                
 | 
			
		||||
                CREATE TABLE after_enchantment (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    ref_id INTEGER REFERENCES before_enchantment(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(2);
 | 
			
		||||
            expect(result.tables.map((t) => t.name).sort()).toEqual([
 | 
			
		||||
                'after_enchantment',
 | 
			
		||||
                'before_enchantment',
 | 
			
		||||
            ]);
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle empty or null input', async () => {
 | 
			
		||||
            const result1 = await fromPostgres('');
 | 
			
		||||
            expect(result1.tables).toHaveLength(0);
 | 
			
		||||
            expect(result1.relationships).toHaveLength(0);
 | 
			
		||||
 | 
			
		||||
            const result2 = await fromPostgres('   \n   ');
 | 
			
		||||
            expect(result2.tables).toHaveLength(0);
 | 
			
		||||
            expect(result2.relationships).toHaveLength(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle comments in various positions', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                -- This is a comment
 | 
			
		||||
                CREATE TABLE /* inline comment */ wizards (
 | 
			
		||||
                    id INTEGER PRIMARY KEY, -- end of line comment
 | 
			
		||||
                    /* multi-line
 | 
			
		||||
                       comment */
 | 
			
		||||
                    name TEXT
 | 
			
		||||
                );
 | 
			
		||||
                -- Another comment
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].name).toBe('wizards');
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(2);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle dollar-quoted strings', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE spell_messages (
 | 
			
		||||
                    id INTEGER PRIMARY KEY,
 | 
			
		||||
                    template TEXT DEFAULT $tag$Hello, 'world'!$tag$,
 | 
			
		||||
                    content TEXT
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables[0].columns).toHaveLength(3);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Foreign Key Extraction from Unparsed Tables', () => {
 | 
			
		||||
        it('should extract foreign keys from tables that fail to parse', async () => {
 | 
			
		||||
            const sql = `
 | 
			
		||||
                CREATE TABLE ancient_artifact (id UUID PRIMARY KEY);
 | 
			
		||||
                
 | 
			
		||||
                -- This table has syntax that might fail parsing
 | 
			
		||||
                CREATE TABLE mystical_formula (
 | 
			
		||||
                    id UUID PRIMARY KEY,
 | 
			
		||||
                    artifact_ref UUID REFERENCES ancient_artifact(id),
 | 
			
		||||
                    value NUMERIC(10,
 | 
			
		||||
2) GENERATED ALWAYS AS (1 + 1) STORED,
 | 
			
		||||
                    FOREIGN KEY (artifact_ref) REFERENCES ancient_artifact(id) ON DELETE CASCADE
 | 
			
		||||
                );
 | 
			
		||||
                
 | 
			
		||||
                CREATE TABLE enchanted_relic (
 | 
			
		||||
                    id UUID PRIMARY KEY,
 | 
			
		||||
                    formula_ref UUID REFERENCES mystical_formula(id)
 | 
			
		||||
                );
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(3);
 | 
			
		||||
            // Should find foreign keys even if mystical_formula fails to parse
 | 
			
		||||
            expect(result.relationships.length).toBeGreaterThanOrEqual(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,199 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('PostgreSQL Parser Regression Tests', () => {
 | 
			
		||||
    it('should parse all 16 tables from the magical academy example', async () => {
 | 
			
		||||
        // This is a regression test for the issue where 3 tables were missing
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- Core tables
 | 
			
		||||
CREATE TABLE magic_schools(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    name text NOT NULL,
 | 
			
		||||
    created_at timestamptz NOT NULL DEFAULT now(),
 | 
			
		||||
    updated_at timestamptz NOT NULL DEFAULT now()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE towers(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    name text NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE wizards(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    wizard_name text NOT NULL,
 | 
			
		||||
    magic_email text NOT NULL,
 | 
			
		||||
    UNIQUE (school_id, wizard_name)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- This function should not prevent the wizards table from being parsed
 | 
			
		||||
CREATE FUNCTION enforce_wizard_tower_school()
 | 
			
		||||
    RETURNS TRIGGER AS $$
 | 
			
		||||
BEGIN
 | 
			
		||||
    RETURN NEW;
 | 
			
		||||
END;
 | 
			
		||||
$$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE wizard_ranks(
 | 
			
		||||
    wizard_id uuid NOT NULL REFERENCES wizards(id) ON DELETE CASCADE,
 | 
			
		||||
    rank_id uuid NOT NULL REFERENCES magical_ranks(id) ON DELETE CASCADE,
 | 
			
		||||
    tower_id uuid NOT NULL REFERENCES towers(id) ON DELETE CASCADE,
 | 
			
		||||
    PRIMARY KEY (wizard_id, rank_id, tower_id)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Another function that should be skipped
 | 
			
		||||
CREATE FUNCTION another_function() RETURNS void AS $$
 | 
			
		||||
BEGIN
 | 
			
		||||
    -- Do nothing
 | 
			
		||||
END;
 | 
			
		||||
$$ LANGUAGE plpgsql;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE magical_ranks(
 | 
			
		||||
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 | 
			
		||||
    school_id uuid NOT NULL REFERENCES magic_schools(id) ON DELETE CASCADE,
 | 
			
		||||
    name text NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Row level security should not break parsing
 | 
			
		||||
ALTER TABLE wizards ENABLE ROW LEVEL SECURITY;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE spell_logs(
 | 
			
		||||
    id bigserial PRIMARY KEY,
 | 
			
		||||
    school_id uuid,
 | 
			
		||||
    wizard_id uuid,
 | 
			
		||||
    action text NOT NULL
 | 
			
		||||
);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should find all 6 tables
 | 
			
		||||
        expect(result.tables).toHaveLength(6);
 | 
			
		||||
 | 
			
		||||
        const tableNames = result.tables.map((t) => t.name).sort();
 | 
			
		||||
        expect(tableNames).toEqual([
 | 
			
		||||
            'magic_schools',
 | 
			
		||||
            'magical_ranks',
 | 
			
		||||
            'spell_logs',
 | 
			
		||||
            'towers',
 | 
			
		||||
            'wizard_ranks',
 | 
			
		||||
            'wizards',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        if (result.warnings) {
 | 
			
		||||
            expect(result.warnings.length).toBeGreaterThan(0);
 | 
			
		||||
            expect(
 | 
			
		||||
                result.warnings.some(
 | 
			
		||||
                    (w) => w.includes('Function') || w.includes('security')
 | 
			
		||||
                )
 | 
			
		||||
            ).toBe(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            expect(result.tables).toHaveLength(6);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle tables with complex syntax that fail parsing', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE simple_table (
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    name text NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- This table has complex syntax that might fail parsing
 | 
			
		||||
CREATE TABLE complex_table (
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    value numeric(10,
 | 
			
		||||
2), -- Multi-line numeric
 | 
			
		||||
    computed numeric(5,2) GENERATED ALWAYS AS (value * 2) STORED,
 | 
			
		||||
    UNIQUE (id, value)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE another_table (
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    complex_id uuid REFERENCES complex_table(id),
 | 
			
		||||
    simple_id uuid REFERENCES simple_table(id)
 | 
			
		||||
);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should find all 3 tables even if complex_table fails to parse
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.tables.map((t) => t.name).sort()).toEqual([
 | 
			
		||||
            'another_table',
 | 
			
		||||
            'complex_table',
 | 
			
		||||
            'simple_table',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Should extract foreign keys even from unparsed tables
 | 
			
		||||
        const fksFromAnother = result.relationships.filter(
 | 
			
		||||
            (r) => r.sourceTable === 'another_table'
 | 
			
		||||
        );
 | 
			
		||||
        expect(fksFromAnother).toHaveLength(2);
 | 
			
		||||
        expect(
 | 
			
		||||
            fksFromAnother.some((fk) => fk.targetTable === 'complex_table')
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
        expect(
 | 
			
		||||
            fksFromAnother.some((fk) => fk.targetTable === 'simple_table')
 | 
			
		||||
        ).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should count relationships correctly for multi-tenant system', async () => {
 | 
			
		||||
        // Simplified version focusing on relationship counting
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE tenants(id uuid PRIMARY KEY);
 | 
			
		||||
CREATE TABLE branches(
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    tenant_id uuid NOT NULL REFERENCES tenants(id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE roles(
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    tenant_id uuid NOT NULL REFERENCES tenants(id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE permissions(id uuid PRIMARY KEY);
 | 
			
		||||
CREATE TABLE role_permissions(
 | 
			
		||||
    role_id uuid NOT NULL REFERENCES roles(id),
 | 
			
		||||
    permission_id uuid NOT NULL REFERENCES permissions(id),
 | 
			
		||||
    PRIMARY KEY (role_id, permission_id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE record_types(
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    tenant_id uuid NOT NULL REFERENCES tenants(id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE users(
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    tenant_id uuid NOT NULL REFERENCES tenants(id),
 | 
			
		||||
    branch_id uuid NOT NULL REFERENCES branches(id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE user_roles(
 | 
			
		||||
    user_id uuid NOT NULL REFERENCES users(id),
 | 
			
		||||
    role_id uuid NOT NULL REFERENCES roles(id),
 | 
			
		||||
    branch_id uuid NOT NULL REFERENCES branches(id),
 | 
			
		||||
    PRIMARY KEY (user_id, role_id, branch_id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE patients(
 | 
			
		||||
    id uuid PRIMARY KEY,
 | 
			
		||||
    tenant_id uuid NOT NULL REFERENCES tenants(id),
 | 
			
		||||
    branch_id uuid NOT NULL REFERENCES branches(id),
 | 
			
		||||
    primary_physician uuid REFERENCES users(id),
 | 
			
		||||
    referring_physician uuid REFERENCES users(id)
 | 
			
		||||
);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Count expected relationships:
 | 
			
		||||
        // branches: 1 (tenant_id -> tenants)
 | 
			
		||||
        // roles: 1 (tenant_id -> tenants)
 | 
			
		||||
        // role_permissions: 2 (role_id -> roles, permission_id -> permissions)
 | 
			
		||||
        // record_types: 1 (tenant_id -> tenants)
 | 
			
		||||
        // users: 2 (tenant_id -> tenants, branch_id -> branches)
 | 
			
		||||
        // user_roles: 3 (user_id -> users, role_id -> roles, branch_id -> branches)
 | 
			
		||||
        // patients: 4 (tenant_id -> tenants, branch_id -> branches, primary_physician -> users, referring_physician -> users)
 | 
			
		||||
        // Total: 14
 | 
			
		||||
 | 
			
		||||
        expect(result.relationships).toHaveLength(14);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,149 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('Activities table import - PostgreSQL specific types', () => {
 | 
			
		||||
    it('should correctly parse the activities table with PostgreSQL-specific types', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE public.activities (
 | 
			
		||||
  id serial4 NOT NULL,
 | 
			
		||||
  user_id int4 NOT NULL,
 | 
			
		||||
  workflow_id int4 NULL,
 | 
			
		||||
  task_id int4 NULL,
 | 
			
		||||
  "action" character varying(50) NOT NULL,
 | 
			
		||||
  description text NOT NULL,
 | 
			
		||||
  created_at timestamp DEFAULT now() NOT NULL,
 | 
			
		||||
  is_read bool DEFAULT false NOT NULL,
 | 
			
		||||
  CONSTRAINT activities_pkey PRIMARY KEY (id)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const table = result.tables[0];
 | 
			
		||||
        expect(table.name).toBe('activities');
 | 
			
		||||
        expect(table.columns).toHaveLength(8);
 | 
			
		||||
 | 
			
		||||
        // Check each column
 | 
			
		||||
        const columns = table.columns;
 | 
			
		||||
 | 
			
		||||
        // id column - serial4 should become INTEGER with auto-increment
 | 
			
		||||
        const idCol = columns.find((c) => c.name === 'id');
 | 
			
		||||
        expect(idCol).toBeDefined();
 | 
			
		||||
        expect(idCol?.type).toBe('INTEGER');
 | 
			
		||||
        expect(idCol?.primaryKey).toBe(true);
 | 
			
		||||
        expect(idCol?.increment).toBe(true);
 | 
			
		||||
        expect(idCol?.nullable).toBe(false);
 | 
			
		||||
 | 
			
		||||
        // user_id column - int4 should become INTEGER
 | 
			
		||||
        const userIdCol = columns.find((c) => c.name === 'user_id');
 | 
			
		||||
        expect(userIdCol).toBeDefined();
 | 
			
		||||
        expect(userIdCol?.type).toBe('INTEGER');
 | 
			
		||||
        expect(userIdCol?.nullable).toBe(false);
 | 
			
		||||
 | 
			
		||||
        // workflow_id column - int4 NULL
 | 
			
		||||
        const workflowIdCol = columns.find((c) => c.name === 'workflow_id');
 | 
			
		||||
        expect(workflowIdCol).toBeDefined();
 | 
			
		||||
        expect(workflowIdCol?.type).toBe('INTEGER');
 | 
			
		||||
        expect(workflowIdCol?.nullable).toBe(true);
 | 
			
		||||
 | 
			
		||||
        // task_id column - int4 NULL
 | 
			
		||||
        const taskIdCol = columns.find((c) => c.name === 'task_id');
 | 
			
		||||
        expect(taskIdCol).toBeDefined();
 | 
			
		||||
        expect(taskIdCol?.type).toBe('INTEGER');
 | 
			
		||||
        expect(taskIdCol?.nullable).toBe(true);
 | 
			
		||||
 | 
			
		||||
        // action column - character varying(50)
 | 
			
		||||
        const actionCol = columns.find((c) => c.name === 'action');
 | 
			
		||||
        expect(actionCol).toBeDefined();
 | 
			
		||||
        expect(actionCol?.type).toBe('VARCHAR(50)');
 | 
			
		||||
        expect(actionCol?.nullable).toBe(false);
 | 
			
		||||
 | 
			
		||||
        // description column - text
 | 
			
		||||
        const descriptionCol = columns.find((c) => c.name === 'description');
 | 
			
		||||
        expect(descriptionCol).toBeDefined();
 | 
			
		||||
        expect(descriptionCol?.type).toBe('TEXT');
 | 
			
		||||
        expect(descriptionCol?.nullable).toBe(false);
 | 
			
		||||
 | 
			
		||||
        // created_at column - timestamp with default
 | 
			
		||||
        const createdAtCol = columns.find((c) => c.name === 'created_at');
 | 
			
		||||
        expect(createdAtCol).toBeDefined();
 | 
			
		||||
        expect(createdAtCol?.type).toBe('TIMESTAMP');
 | 
			
		||||
        expect(createdAtCol?.nullable).toBe(false);
 | 
			
		||||
        expect(createdAtCol?.default).toContain('NOW');
 | 
			
		||||
 | 
			
		||||
        // is_read column - bool with default
 | 
			
		||||
        const isReadCol = columns.find((c) => c.name === 'is_read');
 | 
			
		||||
        expect(isReadCol).toBeDefined();
 | 
			
		||||
        expect(isReadCol?.type).toBe('BOOLEAN');
 | 
			
		||||
        expect(isReadCol?.nullable).toBe(false);
 | 
			
		||||
        expect(isReadCol?.default).toBe('FALSE');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle PostgreSQL type aliases correctly', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE type_test (
 | 
			
		||||
    id serial4,
 | 
			
		||||
    small_id serial2,
 | 
			
		||||
    big_id serial8,
 | 
			
		||||
    int_col int4,
 | 
			
		||||
    small_int smallint,
 | 
			
		||||
    big_int int8,
 | 
			
		||||
    bool_col bool,
 | 
			
		||||
    boolean_col boolean,
 | 
			
		||||
    varchar_col character varying(100),
 | 
			
		||||
    char_col character(10),
 | 
			
		||||
    text_col text,
 | 
			
		||||
    timestamp_col timestamp,
 | 
			
		||||
    timestamptz_col timestamptz,
 | 
			
		||||
    date_col date,
 | 
			
		||||
    time_col time,
 | 
			
		||||
    json_col json,
 | 
			
		||||
    jsonb_col jsonb
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
        const table = result.tables[0];
 | 
			
		||||
        const cols = table.columns;
 | 
			
		||||
 | 
			
		||||
        // Check serial types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'id')?.type).toBe('INTEGER');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'id')?.increment).toBe(true);
 | 
			
		||||
        expect(cols.find((c) => c.name === 'small_id')?.type).toBe('SMALLINT');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'small_id')?.increment).toBe(true);
 | 
			
		||||
        expect(cols.find((c) => c.name === 'big_id')?.type).toBe('BIGINT');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'big_id')?.increment).toBe(true);
 | 
			
		||||
 | 
			
		||||
        // Check integer types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'int_col')?.type).toBe('INTEGER');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'small_int')?.type).toBe('SMALLINT');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'big_int')?.type).toBe('BIGINT');
 | 
			
		||||
 | 
			
		||||
        // Check boolean types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'bool_col')?.type).toBe('BOOLEAN');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'boolean_col')?.type).toBe(
 | 
			
		||||
            'BOOLEAN'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check string types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'varchar_col')?.type).toBe(
 | 
			
		||||
            'VARCHAR(100)'
 | 
			
		||||
        );
 | 
			
		||||
        expect(cols.find((c) => c.name === 'char_col')?.type).toBe('CHAR(10)');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'text_col')?.type).toBe('TEXT');
 | 
			
		||||
 | 
			
		||||
        // Check timestamp types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'timestamp_col')?.type).toBe(
 | 
			
		||||
            'TIMESTAMP'
 | 
			
		||||
        );
 | 
			
		||||
        expect(cols.find((c) => c.name === 'timestamptz_col')?.type).toBe(
 | 
			
		||||
            'TIMESTAMPTZ'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check other types
 | 
			
		||||
        expect(cols.find((c) => c.name === 'date_col')?.type).toBe('DATE');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'time_col')?.type).toBe('TIME');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'json_col')?.type).toBe('JSON');
 | 
			
		||||
        expect(cols.find((c) => c.name === 'jsonb_col')?.type).toBe('JSONB');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,307 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('ALTER TABLE FOREIGN KEY parsing with fallback', () => {
 | 
			
		||||
    it('should parse foreign keys from ALTER TABLE ONLY statements with DEFERRABLE', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE "public"."wizard" (
 | 
			
		||||
    "id" bigint NOT NULL,
 | 
			
		||||
    "name" character varying(255) NOT NULL,
 | 
			
		||||
    CONSTRAINT "wizard_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE "public"."spellbook" (
 | 
			
		||||
    "id" integer NOT NULL,
 | 
			
		||||
    "wizard_id" bigint NOT NULL,
 | 
			
		||||
    "title" character varying(254) NOT NULL,
 | 
			
		||||
    CONSTRAINT "spellbook_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
ALTER TABLE ONLY "public"."spellbook" ADD CONSTRAINT "spellbook_wizard_id_fk" FOREIGN KEY (wizard_id) REFERENCES wizard(id) DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.sourceTable).toBe('spellbook');
 | 
			
		||||
        expect(fk.targetTable).toBe('wizard');
 | 
			
		||||
        expect(fk.sourceColumn).toBe('wizard_id');
 | 
			
		||||
        expect(fk.targetColumn).toBe('id');
 | 
			
		||||
        expect(fk.name).toBe('spellbook_wizard_id_fk');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should parse foreign keys without schema qualification', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE dragon (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100) NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE dragon_rider (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    rider_name VARCHAR(100) NOT NULL,
 | 
			
		||||
    dragon_id UUID NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Without ONLY keyword and without schema
 | 
			
		||||
ALTER TABLE dragon_rider ADD CONSTRAINT dragon_rider_dragon_fk FOREIGN KEY (dragon_id) REFERENCES dragon(id);
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.sourceTable).toBe('dragon_rider');
 | 
			
		||||
        expect(fk.targetTable).toBe('dragon');
 | 
			
		||||
        expect(fk.sourceColumn).toBe('dragon_id');
 | 
			
		||||
        expect(fk.targetColumn).toBe('id');
 | 
			
		||||
        expect(fk.sourceSchema).toBe('public');
 | 
			
		||||
        expect(fk.targetSchema).toBe('public');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should parse foreign keys with mixed schema specifications', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE "magic_school"."instructor" (
 | 
			
		||||
    "id" bigint NOT NULL,
 | 
			
		||||
    "name" text NOT NULL,
 | 
			
		||||
    CONSTRAINT "instructor_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE "public"."apprentice" (
 | 
			
		||||
    "id" integer NOT NULL,
 | 
			
		||||
    "name" varchar(255) NOT NULL,
 | 
			
		||||
    "instructor_id" bigint NOT NULL,
 | 
			
		||||
    CONSTRAINT "apprentice_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Source table with public schema, target table with magic_school schema
 | 
			
		||||
ALTER TABLE ONLY "public"."apprentice" ADD CONSTRAINT "apprentice_instructor_fk" FOREIGN KEY (instructor_id) REFERENCES "magic_school"."instructor"(id) ON DELETE CASCADE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.sourceTable).toBe('apprentice');
 | 
			
		||||
        expect(fk.targetTable).toBe('instructor');
 | 
			
		||||
        expect(fk.sourceSchema).toBe('public');
 | 
			
		||||
        expect(fk.targetSchema).toBe('magic_school');
 | 
			
		||||
        expect(fk.sourceColumn).toBe('instructor_id');
 | 
			
		||||
        expect(fk.targetColumn).toBe('id');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should parse foreign keys with various constraint options', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE potion (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE ingredient (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE potion_ingredient (
 | 
			
		||||
    id SERIAL PRIMARY KEY,
 | 
			
		||||
    potion_id UUID NOT NULL,
 | 
			
		||||
    ingredient_id UUID NOT NULL,
 | 
			
		||||
    quantity INTEGER DEFAULT 1
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Different variations of ALTER TABLE foreign key syntax
 | 
			
		||||
ALTER TABLE potion_ingredient ADD CONSTRAINT potion_ingredient_potion_fk FOREIGN KEY (potion_id) REFERENCES potion(id) ON DELETE CASCADE ON UPDATE CASCADE;
 | 
			
		||||
ALTER TABLE ONLY potion_ingredient ADD CONSTRAINT potion_ingredient_ingredient_fk FOREIGN KEY (ingredient_id) REFERENCES ingredient(id) DEFERRABLE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.relationships).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
        // Check first FK (with ON DELETE CASCADE ON UPDATE CASCADE)
 | 
			
		||||
        const potionFK = result.relationships.find(
 | 
			
		||||
            (r) => r.sourceColumn === 'potion_id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(potionFK).toBeDefined();
 | 
			
		||||
        expect(potionFK?.targetTable).toBe('potion');
 | 
			
		||||
 | 
			
		||||
        // Check second FK (with DEFERRABLE)
 | 
			
		||||
        const ingredientFK = result.relationships.find(
 | 
			
		||||
            (r) => r.sourceColumn === 'ingredient_id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(ingredientFK).toBeDefined();
 | 
			
		||||
        expect(ingredientFK?.targetTable).toBe('ingredient');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle quoted and unquoted identifiers', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE "wizard_tower" (
 | 
			
		||||
    id BIGINT PRIMARY KEY,
 | 
			
		||||
    "tower_name" VARCHAR(255)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE wizard_resident (
 | 
			
		||||
    id SERIAL PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100),
 | 
			
		||||
    tower_id BIGINT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- First ALTER TABLE statement
 | 
			
		||||
ALTER TABLE wizard_resident ADD CONSTRAINT wizard_tower_fk FOREIGN KEY (tower_id) REFERENCES "wizard_tower"(id) DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
 | 
			
		||||
-- Second ALTER TABLE statement  
 | 
			
		||||
ALTER TABLE ONLY "wizard_resident" ADD CONSTRAINT "wizard_tower_fk2" FOREIGN KEY ("tower_id") REFERENCES "wizard_tower"("id") ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        console.log('Relationships found:', result.relationships.length);
 | 
			
		||||
        result.relationships.forEach((rel, i) => {
 | 
			
		||||
            console.log(
 | 
			
		||||
                `FK ${i + 1}: ${rel.sourceTable}.${rel.sourceColumn} -> ${rel.targetTable}.${rel.targetColumn}`
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
        console.log('Warnings:', result.warnings);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
        // At least one relationship should be found (the regex fallback should catch at least one)
 | 
			
		||||
        expect(result.relationships.length).toBeGreaterThanOrEqual(1);
 | 
			
		||||
 | 
			
		||||
        // Check the first relationship
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.sourceTable).toBe('wizard_resident');
 | 
			
		||||
        expect(fk.targetTable).toBe('wizard_tower');
 | 
			
		||||
        expect(fk.sourceColumn).toBe('tower_id');
 | 
			
		||||
        expect(fk.targetColumn).toBe('id');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle the exact problematic syntax from postgres_seven', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE "public"."users_user" (
 | 
			
		||||
    "id" bigint NOT NULL,
 | 
			
		||||
    "email" character varying(254) NOT NULL,
 | 
			
		||||
    CONSTRAINT "users_user_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE "public"."account_emailaddress" (
 | 
			
		||||
    "id" integer DEFAULT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
 | 
			
		||||
    "email" character varying(254) NOT NULL,
 | 
			
		||||
    "user_id" bigint NOT NULL,
 | 
			
		||||
    CONSTRAINT "account_emailaddress_pkey" PRIMARY KEY ("id")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Exact syntax from the problematic file with double DEFERRABLE
 | 
			
		||||
ALTER TABLE ONLY "public"."account_emailaddress" ADD CONSTRAINT "account_emailaddress_user_id_2c513194_fk_users_user_id" FOREIGN KEY (user_id) REFERENCES users_user(id) DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        console.log('Warnings:', result.warnings);
 | 
			
		||||
        console.log('Relationships:', result.relationships);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(2);
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.name).toBe(
 | 
			
		||||
            'account_emailaddress_user_id_2c513194_fk_users_user_id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(fk.sourceTable).toBe('account_emailaddress');
 | 
			
		||||
        expect(fk.targetTable).toBe('users_user');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple foreign keys in different formats', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE realm (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE region (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100),
 | 
			
		||||
    realm_id UUID
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE city (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100),
 | 
			
		||||
    region_id UUID,
 | 
			
		||||
    realm_id UUID
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Mix of syntaxes that might fail parsing
 | 
			
		||||
ALTER TABLE ONLY region ADD CONSTRAINT region_realm_fk FOREIGN KEY (realm_id) REFERENCES realm(id) DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
ALTER TABLE city ADD CONSTRAINT city_region_fk FOREIGN KEY (region_id) REFERENCES region(id) ON DELETE CASCADE;
 | 
			
		||||
ALTER TABLE ONLY "public"."city" ADD CONSTRAINT "city_realm_fk" FOREIGN KEY ("realm_id") REFERENCES "public"."realm"("id");
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.relationships).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
        // Verify all three relationships were captured
 | 
			
		||||
        const regionRealmFK = result.relationships.find(
 | 
			
		||||
            (r) => r.sourceTable === 'region' && r.targetTable === 'realm'
 | 
			
		||||
        );
 | 
			
		||||
        const cityRegionFK = result.relationships.find(
 | 
			
		||||
            (r) => r.sourceTable === 'city' && r.targetTable === 'region'
 | 
			
		||||
        );
 | 
			
		||||
        const cityRealmFK = result.relationships.find(
 | 
			
		||||
            (r) => r.sourceTable === 'city' && r.targetTable === 'realm'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(regionRealmFK).toBeDefined();
 | 
			
		||||
        expect(cityRegionFK).toBeDefined();
 | 
			
		||||
        expect(cityRealmFK).toBeDefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should use regex fallback for unparseable ALTER TABLE statements', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE magical_item (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(255)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE enchantment (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(255),
 | 
			
		||||
    item_id UUID NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- This should fail to parse due to syntax variations and trigger regex fallback
 | 
			
		||||
ALTER TABLE ONLY enchantment ADD CONSTRAINT enchantment_item_fk FOREIGN KEY (item_id) REFERENCES magical_item(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED DEFERRABLE;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        // Should find the foreign key even if parser fails
 | 
			
		||||
        expect(result.relationships).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
        const fk = result.relationships[0];
 | 
			
		||||
        expect(fk.name).toBe('enchantment_item_fk');
 | 
			
		||||
        expect(fk.sourceTable).toBe('enchantment');
 | 
			
		||||
        expect(fk.targetTable).toBe('magical_item');
 | 
			
		||||
        expect(fk.sourceColumn).toBe('item_id');
 | 
			
		||||
        expect(fk.targetColumn).toBe('id');
 | 
			
		||||
 | 
			
		||||
        // Should have a warning about the failed parse
 | 
			
		||||
        expect(result.warnings).toBeDefined();
 | 
			
		||||
        const hasAlterWarning = result.warnings!.some(
 | 
			
		||||
            (w) =>
 | 
			
		||||
                w.includes('Failed to parse statement') &&
 | 
			
		||||
                w.includes('ALTER TABLE')
 | 
			
		||||
        );
 | 
			
		||||
        expect(hasAlterWarning).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,84 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('Table with Comment Before CREATE TABLE', () => {
 | 
			
		||||
    it('should parse table with single-line comment before CREATE TABLE', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- Junction table for tracking which crystals power which enchantments.
 | 
			
		||||
CREATE TABLE crystal_enchantments (
 | 
			
		||||
    crystal_id UUID NOT NULL REFERENCES crystals(id) ON DELETE CASCADE,
 | 
			
		||||
    enchantment_id UUID NOT NULL REFERENCES enchantments(id) ON DELETE CASCADE,
 | 
			
		||||
    PRIMARY KEY (crystal_id, enchantment_id)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        console.log('\nDebug info:');
 | 
			
		||||
        console.log('Tables found:', result.tables.length);
 | 
			
		||||
        console.log(
 | 
			
		||||
            'Table names:',
 | 
			
		||||
            result.tables.map((t) => t.name)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('crystal_enchantments');
 | 
			
		||||
        expect(result.tables[0].columns).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle various comment formats before CREATE TABLE', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- This is a wizards table
 | 
			
		||||
CREATE TABLE wizards (
 | 
			
		||||
    id UUID PRIMARY KEY
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- This table stores
 | 
			
		||||
-- multiple artifacts
 | 
			
		||||
CREATE TABLE artifacts (
 | 
			
		||||
    id SERIAL PRIMARY KEY,
 | 
			
		||||
    name VARCHAR(100)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
/* This is a multi-line
 | 
			
		||||
   comment before table */
 | 
			
		||||
CREATE TABLE quests (
 | 
			
		||||
    id BIGSERIAL PRIMARY KEY
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Comment 1
 | 
			
		||||
-- Comment 2
 | 
			
		||||
-- Comment 3
 | 
			
		||||
CREATE TABLE spell_schools (
 | 
			
		||||
    id INTEGER PRIMARY KEY
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(4);
 | 
			
		||||
        const tableNames = result.tables.map((t) => t.name).sort();
 | 
			
		||||
        expect(tableNames).toEqual([
 | 
			
		||||
            'artifacts',
 | 
			
		||||
            'quests',
 | 
			
		||||
            'spell_schools',
 | 
			
		||||
            'wizards',
 | 
			
		||||
        ]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not confuse comment-only statements with tables', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- This is just a comment, not a table
 | 
			
		||||
-- Even though it mentions CREATE TABLE in the comment
 | 
			
		||||
-- It should not be parsed as a table
 | 
			
		||||
 | 
			
		||||
CREATE TABLE ancient_tome (
 | 
			
		||||
    id INTEGER PRIMARY KEY
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Another standalone comment`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('ancient_tome');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,113 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromPostgres } from '../postgresql';
 | 
			
		||||
 | 
			
		||||
describe('Comment removal before formatting', () => {
 | 
			
		||||
    it('should remove single-line comments', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- This is a comment that will be removed
 | 
			
		||||
CREATE TABLE magic_items (
 | 
			
		||||
    item_id INTEGER PRIMARY KEY, -- unique identifier
 | 
			
		||||
    spell_power VARCHAR(100) -- mystical energy level
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('magic_items');
 | 
			
		||||
        expect(result.tables[0].columns).toHaveLength(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should remove multi-line comments', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
/* This is a multi-line comment
 | 
			
		||||
   that spans multiple lines
 | 
			
		||||
   and will be removed */
 | 
			
		||||
CREATE TABLE wizard_inventory (
 | 
			
		||||
    wizard_id INTEGER PRIMARY KEY,
 | 
			
		||||
    /* Stores the magical
 | 
			
		||||
       artifacts collected */
 | 
			
		||||
    artifact_name VARCHAR(100)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].name).toBe('wizard_inventory');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should preserve strings that contain comment-like patterns', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE potion_recipes (
 | 
			
		||||
    recipe_id INTEGER PRIMARY KEY,
 | 
			
		||||
    brewing_note VARCHAR(100) DEFAULT '--shake before use',
 | 
			
		||||
    ingredient_source VARCHAR(200) DEFAULT 'https://alchemy.store',
 | 
			
		||||
    instructions TEXT DEFAULT '/* mix carefully */'
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        expect(result.tables[0].columns).toHaveLength(4);
 | 
			
		||||
 | 
			
		||||
        // Check that defaults are preserved
 | 
			
		||||
        const brewingNoteCol = result.tables[0].columns.find(
 | 
			
		||||
            (c) => c.name === 'brewing_note'
 | 
			
		||||
        );
 | 
			
		||||
        expect(brewingNoteCol?.default).toBeDefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle complex scenarios with comments before tables', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
-- Dragon types catalog
 | 
			
		||||
CREATE TABLE dragons (dragon_id INTEGER PRIMARY KEY);
 | 
			
		||||
 | 
			
		||||
/* Knights registry
 | 
			
		||||
   for the kingdom */
 | 
			
		||||
CREATE TABLE knights (knight_id INTEGER PRIMARY KEY);
 | 
			
		||||
 | 
			
		||||
-- Battle records junction
 | 
			
		||||
-- Tracks dragon-knight encounters
 | 
			
		||||
CREATE TABLE dragon_battles (
 | 
			
		||||
    dragon_id INTEGER REFERENCES dragons(dragon_id),
 | 
			
		||||
    knight_id INTEGER REFERENCES knights(knight_id),
 | 
			
		||||
    PRIMARY KEY (dragon_id, knight_id)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        const tableNames = result.tables.map((t) => t.name).sort();
 | 
			
		||||
        expect(tableNames).toEqual(['dragon_battles', 'dragons', 'knights']);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle the exact forth example scenario', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE spell_books (
 | 
			
		||||
    book_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
 | 
			
		||||
    title VARCHAR(100) NOT NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE spells (
 | 
			
		||||
    spell_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
 | 
			
		||||
    incantation VARCHAR(255) NOT NULL,
 | 
			
		||||
    effect TEXT, -- Magical effect description
 | 
			
		||||
    element VARCHAR(50) NOT NULL -- fire, water, earth, air
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Junction table linking spells to their books.
 | 
			
		||||
CREATE TABLE book_spells (
 | 
			
		||||
    book_id UUID NOT NULL REFERENCES spell_books(book_id) ON DELETE CASCADE,
 | 
			
		||||
    spell_id UUID NOT NULL REFERENCES spells(spell_id) ON DELETE CASCADE,
 | 
			
		||||
    PRIMARY KEY (book_id, spell_id)
 | 
			
		||||
);`;
 | 
			
		||||
 | 
			
		||||
        const result = await fromPostgres(sql);
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(3);
 | 
			
		||||
        expect(result.tables.map((t) => t.name).sort()).toEqual([
 | 
			
		||||
            'book_spells',
 | 
			
		||||
            'spell_books',
 | 
			
		||||
            'spells',
 | 
			
		||||
        ]);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user