mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 05:53:15 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			feat/table
			...
			v1.14.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8dfa7cc62e | ||
| 
						 | 
					23e93bfd01 | ||
| 
						 | 
					16f9f4671e | ||
| 
						 | 
					0c300e5e72 | ||
| 
						 | 
					b9a1e78b53 | ||
| 
						 | 
					337f7cdab4 | ||
| 
						 | 
					1b0390f0b7 | ||
| 
						 | 
					bc52933b58 | ||
| 
						 | 
					2fdad2344c | ||
| 
						 | 
					0c7eaa2df2 | ||
| 
						 | 
					a5f8e56b3c | 
							
								
								
									
										63
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,68 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
 | 
			
		||||
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
 | 
			
		||||
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
 | 
			
		||||
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
 | 
			
		||||
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
 | 
			
		||||
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
 | 
			
		||||
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
 | 
			
		||||
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
 | 
			
		||||
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
 | 
			
		||||
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
 | 
			
		||||
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
 | 
			
		||||
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
 | 
			
		||||
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
 | 
			
		||||
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
 | 
			
		||||
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
 | 
			
		||||
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
 | 
			
		||||
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
 | 
			
		||||
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
 | 
			
		||||
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
 | 
			
		||||
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
 | 
			
		||||
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
 | 
			
		||||
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
 | 
			
		||||
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
 | 
			
		||||
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
 | 
			
		||||
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
 | 
			
		||||
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
 | 
			
		||||
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
 | 
			
		||||
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
 | 
			
		||||
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
 | 
			
		||||
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
 | 
			
		||||
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
 | 
			
		||||
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
 | 
			
		||||
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
 | 
			
		||||
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
 | 
			
		||||
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
 | 
			
		||||
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
 | 
			
		||||
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
 | 
			
		||||
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
 | 
			
		||||
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
 | 
			
		||||
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
 | 
			
		||||
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
 | 
			
		||||
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
 | 
			
		||||
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
 | 
			
		||||
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
 | 
			
		||||
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
 | 
			
		||||
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
 | 
			
		||||
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
 | 
			
		||||
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
 | 
			
		||||
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
 | 
			
		||||
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
 | 
			
		||||
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
 | 
			
		||||
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
 | 
			
		||||
 | 
			
		||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "version": "1.13.2",
 | 
			
		||||
    "version": "1.14.0",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "chartdb",
 | 
			
		||||
            "version": "1.13.2",
 | 
			
		||||
            "version": "1.14.0",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
                "@dbml/core": "^3.9.5",
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
                "@radix-ui/react-toggle-group": "^1.1.0",
 | 
			
		||||
                "@radix-ui/react-tooltip": "^1.1.8",
 | 
			
		||||
                "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
                "@xyflow/react": "^12.3.1",
 | 
			
		||||
                "@xyflow/react": "^12.8.2",
 | 
			
		||||
                "ahooks": "^3.8.1",
 | 
			
		||||
                "ai": "^3.3.14",
 | 
			
		||||
                "class-variance-authority": "^0.7.1",
 | 
			
		||||
@@ -4603,12 +4603,12 @@
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@xyflow/react": {
 | 
			
		||||
            "version": "12.4.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
 | 
			
		||||
            "integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
 | 
			
		||||
            "version": "12.8.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
 | 
			
		||||
            "integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@xyflow/system": "0.0.50",
 | 
			
		||||
                "@xyflow/system": "0.0.66",
 | 
			
		||||
                "classcat": "^5.0.3",
 | 
			
		||||
                "zustand": "^4.4.0"
 | 
			
		||||
            },
 | 
			
		||||
@@ -4618,16 +4618,18 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@xyflow/system": {
 | 
			
		||||
            "version": "0.0.50",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
 | 
			
		||||
            "integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
 | 
			
		||||
            "version": "0.0.66",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
 | 
			
		||||
            "integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@types/d3-drag": "^3.0.7",
 | 
			
		||||
                "@types/d3-interpolate": "^3.0.4",
 | 
			
		||||
                "@types/d3-selection": "^3.0.10",
 | 
			
		||||
                "@types/d3-transition": "^3.0.8",
 | 
			
		||||
                "@types/d3-zoom": "^3.0.8",
 | 
			
		||||
                "d3-drag": "^3.0.0",
 | 
			
		||||
                "d3-interpolate": "^3.0.1",
 | 
			
		||||
                "d3-selection": "^3.0.0",
 | 
			
		||||
                "d3-zoom": "^3.0.0"
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.13.2",
 | 
			
		||||
    "version": "1.14.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
@@ -43,7 +43,7 @@
 | 
			
		||||
        "@radix-ui/react-toggle-group": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-tooltip": "^1.1.8",
 | 
			
		||||
        "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
        "@xyflow/react": "^12.3.1",
 | 
			
		||||
        "@xyflow/react": "^12.8.2",
 | 
			
		||||
        "ahooks": "^3.8.1",
 | 
			
		||||
        "ai": "^3.3.14",
 | 
			
		||||
        "class-variance-authority": "^0.7.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
 | 
			
		||||
    label: string;
 | 
			
		||||
    icon: LucideIcon;
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CodeSnippetProps {
 | 
			
		||||
@@ -172,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                                            <TooltipTrigger asChild>
 | 
			
		||||
                                                <span>
 | 
			
		||||
                                                    <Button
 | 
			
		||||
                                                        className="h-fit p-1.5"
 | 
			
		||||
                                                        className={cn(
 | 
			
		||||
                                                            'h-fit p-1.5',
 | 
			
		||||
                                                            action.className
 | 
			
		||||
                                                        )}
 | 
			
		||||
                                                        variant="outline"
 | 
			
		||||
                                                        onClick={action.onClick}
 | 
			
		||||
                                                    >
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								src/components/code-snippet/dbml/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/components/code-snippet/dbml/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
 | 
			
		||||
import * as monaco from 'monaco-editor';
 | 
			
		||||
 | 
			
		||||
export const highlightErrorLine = ({
 | 
			
		||||
    error,
 | 
			
		||||
    model,
 | 
			
		||||
    editorDecorationsCollection,
 | 
			
		||||
}: {
 | 
			
		||||
    error: DBMLError;
 | 
			
		||||
    model?: monaco.editor.ITextModel | null;
 | 
			
		||||
    editorDecorationsCollection:
 | 
			
		||||
        | monaco.editor.IEditorDecorationsCollection
 | 
			
		||||
        | undefined;
 | 
			
		||||
}) => {
 | 
			
		||||
    if (!model) return;
 | 
			
		||||
    if (!editorDecorationsCollection) return;
 | 
			
		||||
 | 
			
		||||
    const decorations = [
 | 
			
		||||
        {
 | 
			
		||||
            range: new monaco.Range(
 | 
			
		||||
                error.line,
 | 
			
		||||
                1,
 | 
			
		||||
                error.line,
 | 
			
		||||
                model.getLineMaxColumn(error.line)
 | 
			
		||||
            ),
 | 
			
		||||
            options: {
 | 
			
		||||
                isWholeLine: true,
 | 
			
		||||
                className: 'dbml-error-line',
 | 
			
		||||
                glyphMarginClassName: 'dbml-error-glyph',
 | 
			
		||||
                hoverMessage: { value: error.message },
 | 
			
		||||
                overviewRuler: {
 | 
			
		||||
                    color: '#ff0000',
 | 
			
		||||
                    position: monaco.editor.OverviewRulerLane.Right,
 | 
			
		||||
                    darkColor: '#ff0000',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    editorDecorationsCollection?.set(decorations);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const clearErrorHighlight = (
 | 
			
		||||
    editorDecorationsCollection:
 | 
			
		||||
        | monaco.editor.IEditorDecorationsCollection
 | 
			
		||||
        | undefined
 | 
			
		||||
) => {
 | 
			
		||||
    if (editorDecorationsCollection) {
 | 
			
		||||
        editorDecorationsCollection.clear();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -37,11 +37,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
    const datatypePattern = dataTypesNames.join('|');
 | 
			
		||||
 | 
			
		||||
    monaco.languages.setMonarchTokensProvider('dbml', {
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes'],
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
 | 
			
		||||
        datatypes: dataTypesNames,
 | 
			
		||||
        tokenizer: {
 | 
			
		||||
            root: [
 | 
			
		||||
                [/\b(Table|Ref|Indexes)\b/, 'keyword'],
 | 
			
		||||
                [
 | 
			
		||||
                    /\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
 | 
			
		||||
                    'keyword',
 | 
			
		||||
                ],
 | 
			
		||||
                [/\[.*?\]/, 'annotation'],
 | 
			
		||||
                [/'''/, 'string', '@tripleQuoteString'],
 | 
			
		||||
                [/".*?"/, 'string'],
 | 
			
		||||
 
 | 
			
		||||
@@ -95,6 +95,10 @@ export interface ChartDBContext {
 | 
			
		||||
    updateDiagramUpdatedAt: () => Promise<void>;
 | 
			
		||||
    clearDiagramData: () => Promise<void>;
 | 
			
		||||
    deleteDiagram: () => Promise<void>;
 | 
			
		||||
    updateDiagramData: (
 | 
			
		||||
        diagram: Diagram,
 | 
			
		||||
        options?: { forceUpdateStorage?: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
    updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
 | 
			
		||||
@@ -317,6 +321,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    loadDiagramFromData: emptyFn,
 | 
			
		||||
    clearDiagramData: emptyFn,
 | 
			
		||||
    deleteDiagram: emptyFn,
 | 
			
		||||
    updateDiagramData: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
    updateDatabaseType: emptyFn,
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    React.PropsWithChildren<ChartDBProviderProps>
 | 
			
		||||
> = ({ children, diagram, readonly: readonlyProp }) => {
 | 
			
		||||
    const { hasDiff } = useDiff();
 | 
			
		||||
    let db = useStorage();
 | 
			
		||||
    const dbStorage = useStorage();
 | 
			
		||||
    let db = dbStorage;
 | 
			
		||||
    const events = useEventEmitter<ChartDBEvent>();
 | 
			
		||||
    const { setSchemasFilter, schemasFilter } = useLocalConfig();
 | 
			
		||||
    const { addUndoAction, resetRedoStack, resetUndoStack } =
 | 
			
		||||
@@ -1585,6 +1586,16 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
 | 
			
		||||
        async (diagram, options) => {
 | 
			
		||||
            const st = options?.forceUpdateStorage ? dbStorage : db;
 | 
			
		||||
            await st.deleteDiagram(diagram.id);
 | 
			
		||||
            await st.addDiagram({ diagram });
 | 
			
		||||
            loadDiagramFromData(diagram);
 | 
			
		||||
        },
 | 
			
		||||
        [db, dbStorage, loadDiagramFromData]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
 | 
			
		||||
        async (diagramId: string) => {
 | 
			
		||||
            const diagram = await db.getDiagram(diagramId, {
 | 
			
		||||
@@ -1787,6 +1798,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                events,
 | 
			
		||||
                readonly,
 | 
			
		||||
                filterSchemas,
 | 
			
		||||
                updateDiagramData,
 | 
			
		||||
                updateDiagramId,
 | 
			
		||||
                updateDiagramName,
 | 
			
		||||
                loadDiagram,
 | 
			
		||||
 
 | 
			
		||||
@@ -32,14 +32,20 @@ export interface DiffContext {
 | 
			
		||||
    originalDiagram: Diagram | null;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    hasDiff: boolean;
 | 
			
		||||
    isSummaryOnly: boolean;
 | 
			
		||||
 | 
			
		||||
    calculateDiff: ({
 | 
			
		||||
        diagram,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
        options,
 | 
			
		||||
    }: {
 | 
			
		||||
        diagram: Diagram;
 | 
			
		||||
        newDiagram: Diagram;
 | 
			
		||||
        options?: {
 | 
			
		||||
            summaryOnly?: boolean;
 | 
			
		||||
        };
 | 
			
		||||
    }) => void;
 | 
			
		||||
    resetDiff: () => void;
 | 
			
		||||
 | 
			
		||||
    // table diff
 | 
			
		||||
    checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
@@ -60,6 +66,15 @@ export interface DiffContext {
 | 
			
		||||
    checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
 | 
			
		||||
    getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
 | 
			
		||||
    getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
 | 
			
		||||
    getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
 | 
			
		||||
    getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
 | 
			
		||||
    getFieldNewCharacterMaximumLength: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => string | null;
 | 
			
		||||
    getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
    getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
 | 
			
		||||
    // relationship diff
 | 
			
		||||
    checkIfNewRelationship: ({
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [fieldsChanged, setFieldsChanged] = React.useState<
 | 
			
		||||
        Map<string, boolean>
 | 
			
		||||
    >(new Map<string, boolean>());
 | 
			
		||||
    const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    const events = useEventEmitter<DiffEvent>();
 | 
			
		||||
 | 
			
		||||
@@ -127,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const calculateDiff: DiffContext['calculateDiff'] = useCallback(
 | 
			
		||||
        ({ diagram, newDiagram: newDiagramArg }) => {
 | 
			
		||||
        ({ diagram, newDiagram: newDiagramArg, options }) => {
 | 
			
		||||
            const {
 | 
			
		||||
                diffMap: newDiffs,
 | 
			
		||||
                changedTables: newChangedTables,
 | 
			
		||||
@@ -139,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            setFieldsChanged(newChangedFields);
 | 
			
		||||
            setNewDiagram(newDiagramArg);
 | 
			
		||||
            setOriginalDiagram(diagram);
 | 
			
		||||
            setIsSummaryOnly(options?.summaryOnly ?? false);
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'diff_calculated',
 | 
			
		||||
@@ -305,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewPrimaryKey = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewPrimaryKey']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'primaryKey',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'nullable',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewCharacterMaximumLength = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewCharacterMaximumLength']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'characterMaximumLength',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'scale',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewPrecision = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewPrecision']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'precision',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfNewRelationship = useCallback<
 | 
			
		||||
        DiffContext['checkIfNewRelationship']
 | 
			
		||||
    >(
 | 
			
		||||
@@ -339,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
 | 
			
		||||
        setDiffMap(new Map<string, ChartDBDiff>());
 | 
			
		||||
        setTablesChanged(new Map<string, boolean>());
 | 
			
		||||
        setFieldsChanged(new Map<string, boolean>());
 | 
			
		||||
        setNewDiagram(null);
 | 
			
		||||
        setOriginalDiagram(null);
 | 
			
		||||
        setIsSummaryOnly(false);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <diffContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -346,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                originalDiagram,
 | 
			
		||||
                diffMap,
 | 
			
		||||
                hasDiff: diffMap.size > 0,
 | 
			
		||||
                isSummaryOnly,
 | 
			
		||||
 | 
			
		||||
                calculateDiff,
 | 
			
		||||
                resetDiff,
 | 
			
		||||
 | 
			
		||||
                // table diff
 | 
			
		||||
                getTableNewName,
 | 
			
		||||
@@ -362,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                checkIfNewField,
 | 
			
		||||
                getFieldNewName,
 | 
			
		||||
                getFieldNewType,
 | 
			
		||||
                getFieldNewPrimaryKey,
 | 
			
		||||
                getFieldNewNullable,
 | 
			
		||||
                getFieldNewCharacterMaximumLength,
 | 
			
		||||
                getFieldNewScale,
 | 
			
		||||
                getFieldNewPrecision,
 | 
			
		||||
 | 
			
		||||
                // relationship diff
 | 
			
		||||
                checkIfNewRelationship,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import React, {
 | 
			
		||||
    Suspense,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import * as monaco from 'monaco-editor';
 | 
			
		||||
import type * as monaco from 'monaco-editor';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
@@ -36,45 +36,11 @@ import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { debounce } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
interface DBMLError {
 | 
			
		||||
    message: string;
 | 
			
		||||
    line: number;
 | 
			
		||||
    column: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseDBMLError(error: unknown): DBMLError | null {
 | 
			
		||||
    try {
 | 
			
		||||
        if (typeof error === 'string') {
 | 
			
		||||
            const parsed = JSON.parse(error);
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                const diag = parsed.diags[0];
 | 
			
		||||
                return {
 | 
			
		||||
                    message: diag.message,
 | 
			
		||||
                    line: diag.location.start.line,
 | 
			
		||||
                    column: diag.location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        } else if (error && typeof error === 'object' && 'diags' in error) {
 | 
			
		||||
            const parsed = error as {
 | 
			
		||||
                diags: Array<{
 | 
			
		||||
                    message: string;
 | 
			
		||||
                    location: { start: { line: number; column: number } };
 | 
			
		||||
                }>;
 | 
			
		||||
            };
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                return {
 | 
			
		||||
                    message: parsed.diags[0].message,
 | 
			
		||||
                    line: parsed.diags[0].location.start.line,
 | 
			
		||||
                    column: parsed.diags[0].location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error('Error parsing DBML error:', e);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
 | 
			
		||||
import {
 | 
			
		||||
    clearErrorHighlight,
 | 
			
		||||
    highlightErrorLine,
 | 
			
		||||
} from '@/components/code-snippet/dbml/utils';
 | 
			
		||||
 | 
			
		||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
 | 
			
		||||
    withCreateEmptyDiagram?: boolean;
 | 
			
		||||
@@ -150,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
        }
 | 
			
		||||
    }, [reorder, reorderTables]);
 | 
			
		||||
 | 
			
		||||
    const highlightErrorLine = useCallback((error: DBMLError) => {
 | 
			
		||||
        if (!editorRef.current) return;
 | 
			
		||||
 | 
			
		||||
        const model = editorRef.current.getModel();
 | 
			
		||||
        if (!model) return;
 | 
			
		||||
 | 
			
		||||
        const decorations = [
 | 
			
		||||
            {
 | 
			
		||||
                range: new monaco.Range(
 | 
			
		||||
                    error.line,
 | 
			
		||||
                    1,
 | 
			
		||||
                    error.line,
 | 
			
		||||
                    model.getLineMaxColumn(error.line)
 | 
			
		||||
                ),
 | 
			
		||||
                options: {
 | 
			
		||||
                    isWholeLine: true,
 | 
			
		||||
                    className: 'dbml-error-line',
 | 
			
		||||
                    glyphMarginClassName: 'dbml-error-glyph',
 | 
			
		||||
                    hoverMessage: { value: error.message },
 | 
			
		||||
                    overviewRuler: {
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        position: monaco.editor.OverviewRulerLane.Right,
 | 
			
		||||
                        darkColor: '#ff0000',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        decorationsCollection.current?.set(decorations);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const clearDecorations = useCallback(() => {
 | 
			
		||||
        decorationsCollection.current?.clear();
 | 
			
		||||
        clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const validateDBML = useCallback(
 | 
			
		||||
@@ -205,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
                        t('import_dbml_dialog.error.description') +
 | 
			
		||||
                            ` (1 error found - in line ${parsedError.line})`
 | 
			
		||||
                    );
 | 
			
		||||
                    highlightErrorLine(parsedError);
 | 
			
		||||
                    highlightErrorLine({
 | 
			
		||||
                        error: parsedError,
 | 
			
		||||
                        model: editorRef.current?.getModel(),
 | 
			
		||||
                        editorDecorationsCollection:
 | 
			
		||||
                            decorationsCollection.current,
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    setErrorMessage(
 | 
			
		||||
                        e instanceof Error ? e.message : JSON.stringify(e)
 | 
			
		||||
@@ -213,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [clearDecorations, highlightErrorLine, t]
 | 
			
		||||
        [clearDecorations, t]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
 | 
			
		||||
 
 | 
			
		||||
@@ -155,3 +155,29 @@
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Edit button emphasis animation */
 | 
			
		||||
@keyframes dbml_edit-button-emphasis {
 | 
			
		||||
    0% {
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
        transform: scale(1.1);
 | 
			
		||||
        box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0.1);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dbml-edit-button-emphasis {
 | 
			
		||||
    animation: dbml_edit-button-emphasis 0.6s ease-in-out;
 | 
			
		||||
    animation-iteration-count: 1;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -127,10 +127,6 @@ 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',
 | 
			
		||||
@@ -266,15 +262,6 @@ 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',
 | 
			
		||||
 
 | 
			
		||||
@@ -227,7 +227,7 @@ describe('DBML Export - SQL Generation Tests', () => {
 | 
			
		||||
            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
 | 
			
		||||
            expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle valid default values correctly', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,23 +11,7 @@ import { exportMySQL } from './export-per-type/mysql';
 | 
			
		||||
 | 
			
		||||
// Function to simplify verbose data type names
 | 
			
		||||
const simplifyDataType = (typeName: string): string => {
 | 
			
		||||
    const typeMap: Record<string, string> = {
 | 
			
		||||
        'character varying': 'varchar',
 | 
			
		||||
        'char varying': 'varchar',
 | 
			
		||||
        integer: 'int',
 | 
			
		||||
        int4: 'int',
 | 
			
		||||
        int8: 'bigint',
 | 
			
		||||
        serial4: 'serial',
 | 
			
		||||
        serial8: 'bigserial',
 | 
			
		||||
        float8: 'double precision',
 | 
			
		||||
        float4: 'real',
 | 
			
		||||
        bool: 'boolean',
 | 
			
		||||
        character: 'char',
 | 
			
		||||
        'timestamp without time zone': 'timestamp',
 | 
			
		||||
        'timestamp with time zone': 'timestamptz',
 | 
			
		||||
        'time without time zone': 'time',
 | 
			
		||||
        'time with time zone': 'timetz',
 | 
			
		||||
    };
 | 
			
		||||
    const typeMap: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
    return typeMap[typeName.toLowerCase()] || typeName;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1251
									
								
								src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1251
									
								
								src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										624
									
								
								src/lib/dbml/apply-dbml/apply-dbml.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										624
									
								
								src/lib/dbml/apply-dbml/apply-dbml.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,624 @@
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import type { Area } from '../../domain/area';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '../../domain/db-custom-type';
 | 
			
		||||
import type { DBDependency } from '../../domain/db-dependency';
 | 
			
		||||
import type { DBField } from '../../domain/db-field';
 | 
			
		||||
import type { DBIndex } from '../../domain/db-index';
 | 
			
		||||
import type { DBRelationship } from '../../domain/db-relationship';
 | 
			
		||||
import type { DBTable } from '../../domain/db-table';
 | 
			
		||||
import type { Diagram } from '../../domain/diagram';
 | 
			
		||||
 | 
			
		||||
type SourceIdToDataMap = Record<
 | 
			
		||||
    string,
 | 
			
		||||
    { schema?: string | null; name: string; color?: string }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type IdMappings = {
 | 
			
		||||
    tables: Record<string, string>;
 | 
			
		||||
    fields: Record<string, string>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Key generation functions remain the same for consistency
 | 
			
		||||
const createObjectKey = ({
 | 
			
		||||
    type,
 | 
			
		||||
    schema,
 | 
			
		||||
    otherSchema,
 | 
			
		||||
    parentName,
 | 
			
		||||
    otherParentName,
 | 
			
		||||
    name,
 | 
			
		||||
    otherName,
 | 
			
		||||
}: {
 | 
			
		||||
    type:
 | 
			
		||||
        | 'table'
 | 
			
		||||
        | 'field'
 | 
			
		||||
        | 'index'
 | 
			
		||||
        | 'relationship'
 | 
			
		||||
        | 'customType'
 | 
			
		||||
        | 'dependency'
 | 
			
		||||
        | 'area';
 | 
			
		||||
    schema?: string | null;
 | 
			
		||||
    otherSchema?: string | null;
 | 
			
		||||
    parentName?: string | null;
 | 
			
		||||
    otherParentName?: string | null;
 | 
			
		||||
    name: string;
 | 
			
		||||
    otherName?: string | null;
 | 
			
		||||
}) =>
 | 
			
		||||
    `${type}-${schema ? `${schema}.` : ''}${otherSchema ? `${otherSchema}.` : ''}${parentName ? `${parentName}.` : ''}${otherParentName ? `${otherParentName}.` : ''}${name}${otherName ? `.${otherName}` : ''}`;
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromTable = (table: DBTable) =>
 | 
			
		||||
    createObjectKey({
 | 
			
		||||
        type: 'table',
 | 
			
		||||
        schema: table.schema,
 | 
			
		||||
        name: table.name,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromField = (table: DBTable, field: DBField) =>
 | 
			
		||||
    createObjectKey({
 | 
			
		||||
        type: 'field',
 | 
			
		||||
        schema: table.schema,
 | 
			
		||||
        parentName: table.name,
 | 
			
		||||
        name: field.name,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromIndex = (table: DBTable, index: DBIndex) =>
 | 
			
		||||
    createObjectKey({
 | 
			
		||||
        type: 'index',
 | 
			
		||||
        schema: table.schema,
 | 
			
		||||
        parentName: table.name,
 | 
			
		||||
        name: index.name,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromRelationship = (
 | 
			
		||||
    relationship: DBRelationship,
 | 
			
		||||
    sourceIdToNameMap: SourceIdToDataMap
 | 
			
		||||
) => {
 | 
			
		||||
    const sourceTable = sourceIdToNameMap[relationship.sourceTableId];
 | 
			
		||||
    const targetTable = sourceIdToNameMap[relationship.targetTableId];
 | 
			
		||||
    const sourceField = sourceIdToNameMap[relationship.sourceFieldId];
 | 
			
		||||
    const targetField = sourceIdToNameMap[relationship.targetFieldId];
 | 
			
		||||
 | 
			
		||||
    if (!sourceTable || !targetTable || !sourceField || !targetField) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return createObjectKey({
 | 
			
		||||
        type: 'relationship',
 | 
			
		||||
        schema: sourceTable.schema,
 | 
			
		||||
        otherSchema: targetTable.schema,
 | 
			
		||||
        parentName: sourceTable.name,
 | 
			
		||||
        otherParentName: targetTable.name,
 | 
			
		||||
        name: sourceField.name,
 | 
			
		||||
        otherName: targetField.name,
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromCustomType = (customType: DBCustomType) =>
 | 
			
		||||
    createObjectKey({
 | 
			
		||||
        type: 'customType',
 | 
			
		||||
        schema: customType.schema,
 | 
			
		||||
        name: customType.name,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromDependency = (
 | 
			
		||||
    dependency: DBDependency,
 | 
			
		||||
    sourceIdToNameMap: SourceIdToDataMap
 | 
			
		||||
) => {
 | 
			
		||||
    const dependentTable = sourceIdToNameMap[dependency.dependentTableId];
 | 
			
		||||
    const table = sourceIdToNameMap[dependency.tableId];
 | 
			
		||||
 | 
			
		||||
    if (!dependentTable || !table) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return createObjectKey({
 | 
			
		||||
        type: 'dependency',
 | 
			
		||||
        schema: dependentTable.schema,
 | 
			
		||||
        otherSchema: table.schema,
 | 
			
		||||
        name: dependentTable.name,
 | 
			
		||||
        otherName: table.name,
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createObjectKeyFromArea = (area: Area) =>
 | 
			
		||||
    createObjectKey({
 | 
			
		||||
        type: 'area',
 | 
			
		||||
        name: area.name,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
// Helper function to build source mappings
 | 
			
		||||
const buildSourceMappings = (sourceDiagram: Diagram) => {
 | 
			
		||||
    const objectKeysToIdsMap: Record<string, string> = {};
 | 
			
		||||
    const sourceIdToDataMap: SourceIdToDataMap = {};
 | 
			
		||||
 | 
			
		||||
    // Map tables and their fields/indexes
 | 
			
		||||
    sourceDiagram.tables?.forEach((table) => {
 | 
			
		||||
        const tableKey = createObjectKeyFromTable(table);
 | 
			
		||||
        objectKeysToIdsMap[tableKey] = table.id;
 | 
			
		||||
        sourceIdToDataMap[table.id] = {
 | 
			
		||||
            schema: table.schema,
 | 
			
		||||
            name: table.name,
 | 
			
		||||
            color: table.color,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        table.fields?.forEach((field) => {
 | 
			
		||||
            const fieldKey = createObjectKeyFromField(table, field);
 | 
			
		||||
            objectKeysToIdsMap[fieldKey] = field.id;
 | 
			
		||||
            sourceIdToDataMap[field.id] = {
 | 
			
		||||
                schema: table.schema,
 | 
			
		||||
                name: field.name,
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        table.indexes?.forEach((index) => {
 | 
			
		||||
            const indexKey = createObjectKeyFromIndex(table, index);
 | 
			
		||||
            objectKeysToIdsMap[indexKey] = index.id;
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Map relationships
 | 
			
		||||
    sourceDiagram.relationships?.forEach((relationship) => {
 | 
			
		||||
        const key = createObjectKeyFromRelationship(
 | 
			
		||||
            relationship,
 | 
			
		||||
            sourceIdToDataMap
 | 
			
		||||
        );
 | 
			
		||||
        if (key) {
 | 
			
		||||
            objectKeysToIdsMap[key] = relationship.id;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Map custom types
 | 
			
		||||
    sourceDiagram.customTypes?.forEach((customType) => {
 | 
			
		||||
        const key = createObjectKeyFromCustomType(customType);
 | 
			
		||||
        objectKeysToIdsMap[key] = customType.id;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Map dependencies
 | 
			
		||||
    sourceDiagram.dependencies?.forEach((dependency) => {
 | 
			
		||||
        const key = createObjectKeyFromDependency(
 | 
			
		||||
            dependency,
 | 
			
		||||
            sourceIdToDataMap
 | 
			
		||||
        );
 | 
			
		||||
        if (key) {
 | 
			
		||||
            objectKeysToIdsMap[key] = dependency.id;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Map areas
 | 
			
		||||
    sourceDiagram.areas?.forEach((area) => {
 | 
			
		||||
        const key = createObjectKeyFromArea(area);
 | 
			
		||||
        objectKeysToIdsMap[key] = area.id;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return { objectKeysToIdsMap, sourceIdToDataMap };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Functional helper to update tables and collect ID mappings
 | 
			
		||||
const updateTables = ({
 | 
			
		||||
    targetTables,
 | 
			
		||||
    sourceTables,
 | 
			
		||||
    defaultDatabaseSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    targetTables: DBTable[] | undefined;
 | 
			
		||||
    sourceTables: DBTable[] | undefined;
 | 
			
		||||
    objectKeysToIdsMap: Record<string, string>;
 | 
			
		||||
    sourceIdToDataMap: SourceIdToDataMap;
 | 
			
		||||
    defaultDatabaseSchema?: string;
 | 
			
		||||
}): { tables: DBTable[]; idMappings: IdMappings } => {
 | 
			
		||||
    if (!targetTables)
 | 
			
		||||
        return { tables: [], idMappings: { tables: {}, fields: {} } };
 | 
			
		||||
    if (!sourceTables)
 | 
			
		||||
        return { tables: targetTables, idMappings: { tables: {}, fields: {} } };
 | 
			
		||||
 | 
			
		||||
    const idMappings: IdMappings = { tables: {}, fields: {} };
 | 
			
		||||
 | 
			
		||||
    // Create a map of source tables by schema + name
 | 
			
		||||
    const sourceTablesByKey = new Map<string, DBTable>();
 | 
			
		||||
    sourceTables.forEach((table) => {
 | 
			
		||||
        const key = createObjectKeyFromTable(table);
 | 
			
		||||
        sourceTablesByKey.set(key, table);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const updatedTables = targetTables.map((targetTable) => {
 | 
			
		||||
        // Try to find matching source table by schema + name
 | 
			
		||||
        const targetKey = createObjectKeyFromTable(targetTable);
 | 
			
		||||
        let sourceTable = sourceTablesByKey.get(targetKey);
 | 
			
		||||
 | 
			
		||||
        if (!sourceTable && defaultDatabaseSchema) {
 | 
			
		||||
            if (!targetTable.schema) {
 | 
			
		||||
                // If target table has no schema, try matching with default schema
 | 
			
		||||
                const defaultKey = createObjectKeyFromTable({
 | 
			
		||||
                    ...targetTable,
 | 
			
		||||
                    schema: defaultDatabaseSchema,
 | 
			
		||||
                });
 | 
			
		||||
                sourceTable = sourceTablesByKey.get(defaultKey);
 | 
			
		||||
            } else if (targetTable.schema === defaultDatabaseSchema) {
 | 
			
		||||
                // If target table's schema matches default, try matching without schema
 | 
			
		||||
                const noSchemaKey = createObjectKeyFromTable({
 | 
			
		||||
                    ...targetTable,
 | 
			
		||||
                    schema: undefined,
 | 
			
		||||
                });
 | 
			
		||||
                sourceTable = sourceTablesByKey.get(noSchemaKey);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!sourceTable) {
 | 
			
		||||
            // No matching source table found - keep target as-is
 | 
			
		||||
            return targetTable;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const sourceId = sourceTable.id;
 | 
			
		||||
        idMappings.tables[targetTable.id] = sourceId;
 | 
			
		||||
 | 
			
		||||
        // Update fields by matching on name within the table
 | 
			
		||||
        const sourceFieldsByName = new Map<string, DBField>();
 | 
			
		||||
        sourceTable.fields?.forEach((field) => {
 | 
			
		||||
            sourceFieldsByName.set(field.name, field);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const updatedFields = targetTable.fields?.map((targetField) => {
 | 
			
		||||
            const sourceField = sourceFieldsByName.get(targetField.name);
 | 
			
		||||
            if (sourceField) {
 | 
			
		||||
                idMappings.fields[targetField.id] = sourceField.id;
 | 
			
		||||
 | 
			
		||||
                // Use source field properties when there's a match
 | 
			
		||||
                return {
 | 
			
		||||
                    ...targetField,
 | 
			
		||||
                    id: sourceField.id,
 | 
			
		||||
                    createdAt: sourceField.createdAt,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            // For new fields not in source, keep target field as-is
 | 
			
		||||
            return targetField;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Update indexes by matching on name within the table
 | 
			
		||||
        const sourceIndexesByName = new Map<string, DBIndex>();
 | 
			
		||||
        sourceTable.indexes?.forEach((index) => {
 | 
			
		||||
            sourceIndexesByName.set(index.name, index);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
 | 
			
		||||
            const sourceIndex = sourceIndexesByName.get(targetIndex.name);
 | 
			
		||||
            if (sourceIndex) {
 | 
			
		||||
                return {
 | 
			
		||||
                    ...targetIndex,
 | 
			
		||||
                    id: sourceIndex.id,
 | 
			
		||||
                    createdAt: sourceIndex.createdAt,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            return targetIndex;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Build the result table, preserving source structure
 | 
			
		||||
        const resultTable: DBTable = {
 | 
			
		||||
            ...sourceTable,
 | 
			
		||||
            fields: updatedFields,
 | 
			
		||||
            indexes: updatedIndexes,
 | 
			
		||||
            comments: targetTable.comments,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Update nullable, unique, primaryKey from target fields
 | 
			
		||||
        if (targetTable.fields) {
 | 
			
		||||
            resultTable.fields = resultTable.fields?.map((field) => {
 | 
			
		||||
                const targetField = targetTable.fields?.find(
 | 
			
		||||
                    (f) => f.name === field.name
 | 
			
		||||
                );
 | 
			
		||||
                if (targetField) {
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...field,
 | 
			
		||||
                        nullable: targetField.nullable,
 | 
			
		||||
                        unique: targetField.unique,
 | 
			
		||||
                        primaryKey: targetField.primaryKey,
 | 
			
		||||
                        type: targetField.type,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                return field;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return resultTable;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return { tables: updatedTables, idMappings };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Functional helper to update custom types
 | 
			
		||||
const updateCustomTypes = (
 | 
			
		||||
    customTypes: DBCustomType[] | undefined,
 | 
			
		||||
    objectKeysToIdsMap: Record<string, string>
 | 
			
		||||
): DBCustomType[] => {
 | 
			
		||||
    if (!customTypes) return [];
 | 
			
		||||
 | 
			
		||||
    return customTypes.map((customType) => {
 | 
			
		||||
        const key = createObjectKeyFromCustomType(customType);
 | 
			
		||||
        const sourceId = objectKeysToIdsMap[key];
 | 
			
		||||
 | 
			
		||||
        if (sourceId) {
 | 
			
		||||
            return { ...customType, id: sourceId };
 | 
			
		||||
        }
 | 
			
		||||
        return customType;
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Functional helper to update relationships
 | 
			
		||||
const updateRelationships = (
 | 
			
		||||
    targetRelationships: DBRelationship[] | undefined,
 | 
			
		||||
    sourceRelationships: DBRelationship[] | undefined,
 | 
			
		||||
    idMappings: IdMappings
 | 
			
		||||
): DBRelationship[] => {
 | 
			
		||||
    // If target has no relationships, return empty array (relationships were removed)
 | 
			
		||||
    if (!targetRelationships || targetRelationships.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
    // If source has no relationships, we need to add the target relationships with updated IDs
 | 
			
		||||
    if (!sourceRelationships || sourceRelationships.length === 0) {
 | 
			
		||||
        return targetRelationships.map((targetRel) => {
 | 
			
		||||
            // Find the source IDs by reversing the mapping lookup
 | 
			
		||||
            let sourceTableId = targetRel.sourceTableId;
 | 
			
		||||
            let targetTableId = targetRel.targetTableId;
 | 
			
		||||
            let sourceFieldId = targetRel.sourceFieldId;
 | 
			
		||||
            let targetFieldId = targetRel.targetFieldId;
 | 
			
		||||
 | 
			
		||||
            // Find source table/field IDs from the mappings
 | 
			
		||||
            for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
 | 
			
		||||
                if (targetId === targetRel.sourceTableId) {
 | 
			
		||||
                    sourceTableId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
                if (targetId === targetRel.targetTableId) {
 | 
			
		||||
                    targetTableId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
 | 
			
		||||
                if (targetId === targetRel.sourceFieldId) {
 | 
			
		||||
                    sourceFieldId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
                if (targetId === targetRel.targetFieldId) {
 | 
			
		||||
                    targetFieldId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...targetRel,
 | 
			
		||||
                sourceTableId,
 | 
			
		||||
                targetTableId,
 | 
			
		||||
                sourceFieldId,
 | 
			
		||||
                targetFieldId,
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Map source relationships that have matches in target
 | 
			
		||||
    const resultRelationships: DBRelationship[] = [];
 | 
			
		||||
    const matchedTargetRelIds = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    sourceRelationships.forEach((sourceRel) => {
 | 
			
		||||
        // Find matching target relationship by checking if the target has a relationship
 | 
			
		||||
        // between the same tables and fields (using the ID mappings)
 | 
			
		||||
        const targetRel = targetRelationships.find((tgtRel) => {
 | 
			
		||||
            const mappedSourceTableId = idMappings.tables[tgtRel.sourceTableId];
 | 
			
		||||
            const mappedTargetTableId = idMappings.tables[tgtRel.targetTableId];
 | 
			
		||||
            const mappedSourceFieldId = idMappings.fields[tgtRel.sourceFieldId];
 | 
			
		||||
            const mappedTargetFieldId = idMappings.fields[tgtRel.targetFieldId];
 | 
			
		||||
 | 
			
		||||
            // Check both directions since relationships can be defined in either direction
 | 
			
		||||
            const directMatch =
 | 
			
		||||
                sourceRel.sourceTableId === mappedSourceTableId &&
 | 
			
		||||
                sourceRel.targetTableId === mappedTargetTableId &&
 | 
			
		||||
                sourceRel.sourceFieldId === mappedSourceFieldId &&
 | 
			
		||||
                sourceRel.targetFieldId === mappedTargetFieldId;
 | 
			
		||||
 | 
			
		||||
            const reverseMatch =
 | 
			
		||||
                sourceRel.sourceTableId === mappedTargetTableId &&
 | 
			
		||||
                sourceRel.targetTableId === mappedSourceTableId &&
 | 
			
		||||
                sourceRel.sourceFieldId === mappedTargetFieldId &&
 | 
			
		||||
                sourceRel.targetFieldId === mappedSourceFieldId;
 | 
			
		||||
 | 
			
		||||
            return directMatch || reverseMatch;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (targetRel) {
 | 
			
		||||
            matchedTargetRelIds.add(targetRel.id);
 | 
			
		||||
            // Preserve source relationship but update cardinalities from target
 | 
			
		||||
            const result: DBRelationship = {
 | 
			
		||||
                ...sourceRel,
 | 
			
		||||
                sourceCardinality: targetRel.sourceCardinality,
 | 
			
		||||
                targetCardinality: targetRel.targetCardinality,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Only include schema fields if they exist in the source relationship
 | 
			
		||||
            if (!sourceRel.sourceSchema) {
 | 
			
		||||
                delete result.sourceSchema;
 | 
			
		||||
            }
 | 
			
		||||
            if (!sourceRel.targetSchema) {
 | 
			
		||||
                delete result.targetSchema;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            resultRelationships.push(result);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Add any target relationships that weren't matched (new relationships)
 | 
			
		||||
    targetRelationships.forEach((targetRel) => {
 | 
			
		||||
        if (!matchedTargetRelIds.has(targetRel.id)) {
 | 
			
		||||
            // Find the source IDs by reversing the mapping lookup
 | 
			
		||||
            let sourceTableId = targetRel.sourceTableId;
 | 
			
		||||
            let targetTableId = targetRel.targetTableId;
 | 
			
		||||
            let sourceFieldId = targetRel.sourceFieldId;
 | 
			
		||||
            let targetFieldId = targetRel.targetFieldId;
 | 
			
		||||
 | 
			
		||||
            // Find source table/field IDs from the mappings
 | 
			
		||||
            for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
 | 
			
		||||
                if (targetId === targetRel.sourceTableId) {
 | 
			
		||||
                    sourceTableId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
                if (targetId === targetRel.targetTableId) {
 | 
			
		||||
                    targetTableId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
 | 
			
		||||
                if (targetId === targetRel.sourceFieldId) {
 | 
			
		||||
                    sourceFieldId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
                if (targetId === targetRel.targetFieldId) {
 | 
			
		||||
                    targetFieldId = srcId;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            resultRelationships.push({
 | 
			
		||||
                ...targetRel,
 | 
			
		||||
                sourceTableId,
 | 
			
		||||
                targetTableId,
 | 
			
		||||
                sourceFieldId,
 | 
			
		||||
                targetFieldId,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return resultRelationships;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Functional helper to update dependencies
 | 
			
		||||
const updateDependencies = (
 | 
			
		||||
    targetDependencies: DBDependency[] | undefined,
 | 
			
		||||
    sourceDependencies: DBDependency[] | undefined,
 | 
			
		||||
    idMappings: IdMappings
 | 
			
		||||
): DBDependency[] => {
 | 
			
		||||
    if (!targetDependencies) return [];
 | 
			
		||||
    if (!sourceDependencies) return targetDependencies;
 | 
			
		||||
 | 
			
		||||
    return targetDependencies.map((targetDep) => {
 | 
			
		||||
        // Find matching source dependency
 | 
			
		||||
        const sourceDep = sourceDependencies.find((srcDep) => {
 | 
			
		||||
            const srcTableId = idMappings.tables[targetDep.tableId];
 | 
			
		||||
            const srcDependentTableId =
 | 
			
		||||
                idMappings.tables[targetDep.dependentTableId];
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                srcDep.tableId === srcTableId &&
 | 
			
		||||
                srcDep.dependentTableId === srcDependentTableId
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (sourceDep) {
 | 
			
		||||
            return {
 | 
			
		||||
                ...targetDep,
 | 
			
		||||
                id: sourceDep.id,
 | 
			
		||||
                tableId:
 | 
			
		||||
                    idMappings.tables[targetDep.tableId] || targetDep.tableId,
 | 
			
		||||
                dependentTableId:
 | 
			
		||||
                    idMappings.tables[targetDep.dependentTableId] ||
 | 
			
		||||
                    targetDep.dependentTableId,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If no match found, just update the table references
 | 
			
		||||
        return {
 | 
			
		||||
            ...targetDep,
 | 
			
		||||
            tableId: idMappings.tables[targetDep.tableId] || targetDep.tableId,
 | 
			
		||||
            dependentTableId:
 | 
			
		||||
                idMappings.tables[targetDep.dependentTableId] ||
 | 
			
		||||
                targetDep.dependentTableId,
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Functional helper to update index field references
 | 
			
		||||
const updateIndexFieldReferences = (
 | 
			
		||||
    tables: DBTable[] | undefined,
 | 
			
		||||
    idMappings: IdMappings
 | 
			
		||||
): DBTable[] => {
 | 
			
		||||
    if (!tables) return [];
 | 
			
		||||
 | 
			
		||||
    return tables.map((table) => ({
 | 
			
		||||
        ...table,
 | 
			
		||||
        indexes: table.indexes?.map((index) => ({
 | 
			
		||||
            ...index,
 | 
			
		||||
            fieldIds: index.fieldIds.map(
 | 
			
		||||
                (fieldId) => idMappings.fields[fieldId] || fieldId
 | 
			
		||||
            ),
 | 
			
		||||
        })),
 | 
			
		||||
    }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const applyDBMLChanges = ({
 | 
			
		||||
    sourceDiagram,
 | 
			
		||||
    targetDiagram,
 | 
			
		||||
}: {
 | 
			
		||||
    sourceDiagram: Diagram;
 | 
			
		||||
    targetDiagram: Diagram;
 | 
			
		||||
}): Diagram => {
 | 
			
		||||
    // Step 1: Build mappings from source diagram
 | 
			
		||||
    const { objectKeysToIdsMap, sourceIdToDataMap } =
 | 
			
		||||
        buildSourceMappings(sourceDiagram);
 | 
			
		||||
 | 
			
		||||
    // Step 2: Update tables and collect ID mappings
 | 
			
		||||
    const { tables: updatedTables, idMappings } = updateTables({
 | 
			
		||||
        targetTables: targetDiagram.tables,
 | 
			
		||||
        sourceTables: sourceDiagram.tables,
 | 
			
		||||
        objectKeysToIdsMap,
 | 
			
		||||
        sourceIdToDataMap,
 | 
			
		||||
        defaultDatabaseSchema: defaultSchemas[sourceDiagram.databaseType],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Step 3: Update all other entities functionally
 | 
			
		||||
    const newCustomTypes = updateCustomTypes(
 | 
			
		||||
        targetDiagram.customTypes,
 | 
			
		||||
        objectKeysToIdsMap
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const updatedCustomTypes = [
 | 
			
		||||
        ...(sourceDiagram.customTypes?.filter(
 | 
			
		||||
            (ct) => ct.kind === DBCustomTypeKind.composite
 | 
			
		||||
        ) ?? []),
 | 
			
		||||
        ...newCustomTypes,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const updatedRelationships = updateRelationships(
 | 
			
		||||
        targetDiagram.relationships,
 | 
			
		||||
        sourceDiagram.relationships,
 | 
			
		||||
        idMappings
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const updatedDependencies = updateDependencies(
 | 
			
		||||
        targetDiagram.dependencies,
 | 
			
		||||
        sourceDiagram.dependencies,
 | 
			
		||||
        idMappings
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Step 4: Update index field references
 | 
			
		||||
    const finalTables = updateIndexFieldReferences(updatedTables, idMappings);
 | 
			
		||||
 | 
			
		||||
    // Sort relationships to match source order
 | 
			
		||||
    const sortedRelationships = [...updatedRelationships].sort((a, b) => {
 | 
			
		||||
        // Find source relationships to get their order
 | 
			
		||||
        const sourceRelA = sourceDiagram.relationships?.find(
 | 
			
		||||
            (r) => r.id === a.id
 | 
			
		||||
        );
 | 
			
		||||
        const sourceRelB = sourceDiagram.relationships?.find(
 | 
			
		||||
            (r) => r.id === b.id
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!sourceRelA || !sourceRelB) return 0;
 | 
			
		||||
 | 
			
		||||
        const indexA = sourceDiagram.relationships?.indexOf(sourceRelA) ?? 0;
 | 
			
		||||
        const indexB = sourceDiagram.relationships?.indexOf(sourceRelB) ?? 0;
 | 
			
		||||
 | 
			
		||||
        return indexA - indexB;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Return a new diagram object with tables sorted by order
 | 
			
		||||
    const result: Diagram = {
 | 
			
		||||
        ...sourceDiagram,
 | 
			
		||||
        tables: finalTables.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
 | 
			
		||||
        areas: targetDiagram.areas,
 | 
			
		||||
        relationships: sortedRelationships,
 | 
			
		||||
        dependencies: updatedDependencies,
 | 
			
		||||
        customTypes: updatedCustomTypes,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
};
 | 
			
		||||
@@ -957,4 +957,462 @@ describe('DBML Export - Issue Fixes', () => {
 | 
			
		||||
            '(email, created_at) [name: "idx_email_created"]'
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should export in the right format', () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
            id: 'mqqwkkodrxxd',
 | 
			
		||||
            name: 'Diagram 9',
 | 
			
		||||
            createdAt: new Date('2025-07-30T15:44:53.967Z'),
 | 
			
		||||
            updatedAt: new Date('2025-07-30T16:11:22.554Z'),
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
            tables: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: '8ftpn9qn0o2ddrvhzgdjro3zv',
 | 
			
		||||
                    name: 'table_1',
 | 
			
		||||
                    x: 260,
 | 
			
		||||
                    y: 80,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'w9wlmimvjaci2krhfb4v9bhy0',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            createdAt: 1753890297335,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: '#4dee8a',
 | 
			
		||||
                    createdAt: 1753890297335,
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    order: 0,
 | 
			
		||||
                    parentAreaId: null,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'wofcygo4u9623oueif9k3v734',
 | 
			
		||||
                    name: 'table_2',
 | 
			
		||||
                    x: -178.62499999999994,
 | 
			
		||||
                    y: -244.375,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: '6ca6p6lnss4d2top8pjcfsli7',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            createdAt: 1753891879081,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: '#4dee8a',
 | 
			
		||||
                    createdAt: 1753891879081,
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    order: 1,
 | 
			
		||||
                    parentAreaId: null,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            relationships: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'o5ynn1x9nxm5ipuugo690doau',
 | 
			
		||||
                    name: 'table_2_id_fk',
 | 
			
		||||
                    sourceTableId: 'wofcygo4u9623oueif9k3v734',
 | 
			
		||||
                    targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
 | 
			
		||||
                    sourceFieldId: '6ca6p6lnss4d2top8pjcfsli7',
 | 
			
		||||
                    targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
 | 
			
		||||
                    sourceCardinality: 'one',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: 1753891882554,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            dependencies: [],
 | 
			
		||||
            areas: [],
 | 
			
		||||
            customTypes: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        const expectedInlineDBML = `Table "table_1" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table "table_2" {
 | 
			
		||||
  "id" bigint [pk, not null, ref: < "table_1"."id"]
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const expectedStandardDBML = `Table "table_1" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table "table_2" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        expect(result.inlineDbml).toBe(expectedInlineDBML);
 | 
			
		||||
        expect(result.standardDbml).toBe(expectedStandardDBML);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle tables with multiple relationships correctly', () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
            id: 'test-diagram',
 | 
			
		||||
            name: 'Test',
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
            createdAt: new Date(),
 | 
			
		||||
            updatedAt: new Date(),
 | 
			
		||||
            tables: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'users',
 | 
			
		||||
                    name: 'users',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'users_id',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'posts',
 | 
			
		||||
                    name: 'posts',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'posts_id',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'posts_user_id',
 | 
			
		||||
                            name: 'user_id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'reviews',
 | 
			
		||||
                    name: 'reviews',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'reviews_id',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'reviews_user_id',
 | 
			
		||||
                            name: 'user_id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'user_activities',
 | 
			
		||||
                    name: 'user_activities',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'activities_id',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'activities_entity_id',
 | 
			
		||||
                            name: 'entity_id',
 | 
			
		||||
                            type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'activities_type',
 | 
			
		||||
                            name: 'activity_type',
 | 
			
		||||
                            type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            nullable: true,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: '50',
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            relationships: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'rel1',
 | 
			
		||||
                    name: 'fk_posts_user',
 | 
			
		||||
                    sourceTableId: 'posts',
 | 
			
		||||
                    sourceFieldId: 'posts_user_id',
 | 
			
		||||
                    targetTableId: 'users',
 | 
			
		||||
                    targetFieldId: 'users_id',
 | 
			
		||||
                    sourceCardinality: 'many',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'rel2',
 | 
			
		||||
                    name: 'fk_reviews_user',
 | 
			
		||||
                    sourceTableId: 'reviews',
 | 
			
		||||
                    sourceFieldId: 'reviews_user_id',
 | 
			
		||||
                    targetTableId: 'users',
 | 
			
		||||
                    targetFieldId: 'users_id',
 | 
			
		||||
                    sourceCardinality: 'many',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'rel3',
 | 
			
		||||
                    name: 'fk_activities_posts',
 | 
			
		||||
                    sourceTableId: 'user_activities',
 | 
			
		||||
                    sourceFieldId: 'activities_entity_id',
 | 
			
		||||
                    targetTableId: 'posts',
 | 
			
		||||
                    targetFieldId: 'posts_id',
 | 
			
		||||
                    sourceCardinality: 'many',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'rel4',
 | 
			
		||||
                    name: 'fk_activities_reviews',
 | 
			
		||||
                    sourceTableId: 'user_activities',
 | 
			
		||||
                    sourceFieldId: 'activities_entity_id',
 | 
			
		||||
                    targetTableId: 'reviews',
 | 
			
		||||
                    targetFieldId: 'reviews_id',
 | 
			
		||||
                    sourceCardinality: 'many',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        // Debug output removed
 | 
			
		||||
        // console.log('Inline DBML:', result.inlineDbml);
 | 
			
		||||
 | 
			
		||||
        // Check standard DBML output
 | 
			
		||||
        expect(result.standardDbml).toContain('Table "users" {');
 | 
			
		||||
        expect(result.standardDbml).toContain('Table "posts" {');
 | 
			
		||||
        expect(result.standardDbml).toContain('Table "reviews" {');
 | 
			
		||||
        expect(result.standardDbml).toContain('Table "user_activities" {');
 | 
			
		||||
 | 
			
		||||
        // Check that the entity_id field in user_activities has multiple relationships in inline DBML
 | 
			
		||||
        // The field should have both references in a single bracket
 | 
			
		||||
        expect(result.inlineDbml).toContain(
 | 
			
		||||
            '"entity_id" integer [not null, ref: < "posts"."id", ref: < "reviews"."id"]'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check that standard DBML has separate Ref entries for each relationship
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            'Ref "fk_0_fk_posts_user":"users"."id" < "posts"."user_id"'
 | 
			
		||||
        );
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            'Ref "fk_1_fk_reviews_user":"users"."id" < "reviews"."user_id"'
 | 
			
		||||
        );
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            'Ref "fk_2_fk_activities_posts":"posts"."id" < "user_activities"."entity_id"'
 | 
			
		||||
        );
 | 
			
		||||
        expect(result.standardDbml).toContain(
 | 
			
		||||
            'Ref "fk_3_fk_activities_reviews":"reviews"."id" < "user_activities"."entity_id"'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // No automatic comment is added for fields with multiple relationships
 | 
			
		||||
 | 
			
		||||
        // Check proper formatting - closing brace should be on a new line
 | 
			
		||||
        expect(result.inlineDbml).toMatch(
 | 
			
		||||
            /Table "user_activities" \{\s*\n\s*"id".*\n\s*"entity_id".*\]\s*\n\s*"activity_type".*\n\s*\}/
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Ensure no closing brace appears on the same line as a field with inline refs
 | 
			
		||||
        expect(result.inlineDbml).not.toMatch(/\[.*ref:.*\]\}/);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should properly format closing brace when table has both indexes and inline refs', () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
            id: 'test-diagram',
 | 
			
		||||
            name: 'Test',
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
            createdAt: new Date(),
 | 
			
		||||
            updatedAt: new Date(),
 | 
			
		||||
            tables: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'table1',
 | 
			
		||||
                    name: 'table_1',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'field1',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'index1',
 | 
			
		||||
                            name: 'index_1',
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            fieldIds: ['field1'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'table2',
 | 
			
		||||
                    name: 'table_2',
 | 
			
		||||
                    x: 0,
 | 
			
		||||
                    y: 0,
 | 
			
		||||
                    fields: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'field2',
 | 
			
		||||
                            name: 'id',
 | 
			
		||||
                            type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            collation: null,
 | 
			
		||||
                            default: null,
 | 
			
		||||
                            characterMaximumLength: null,
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    indexes: [],
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    isView: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            relationships: [
 | 
			
		||||
                {
 | 
			
		||||
                    id: 'rel1',
 | 
			
		||||
                    name: 'table2_id_fkey',
 | 
			
		||||
                    sourceTableId: 'table2',
 | 
			
		||||
                    sourceFieldId: 'field2',
 | 
			
		||||
                    targetTableId: 'table1',
 | 
			
		||||
                    targetFieldId: 'field1',
 | 
			
		||||
                    sourceCardinality: 'many',
 | 
			
		||||
                    targetCardinality: 'one',
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        // Check that the inline DBML has proper indentation
 | 
			
		||||
        expect(result.inlineDbml).toContain(`Table "table_1" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
 | 
			
		||||
  Indexes {
 | 
			
		||||
    id [name: "index_1"]
 | 
			
		||||
  }
 | 
			
		||||
}`);
 | 
			
		||||
 | 
			
		||||
        expect(result.inlineDbml).toContain(`Table "table_2" {
 | 
			
		||||
  "id" bigint [pk, not null, ref: < "table_1"."id"]
 | 
			
		||||
}`);
 | 
			
		||||
 | 
			
		||||
        // The issue was that it would generate:
 | 
			
		||||
        // Table "table_1" {
 | 
			
		||||
        //   "id" bigint [pk, not null]
 | 
			
		||||
        //
 | 
			
		||||
        //   Indexes {
 | 
			
		||||
        //     id [name: "index_1"]
 | 
			
		||||
        //
 | 
			
		||||
        // }
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        // Make sure there's no malformed closing brace
 | 
			
		||||
        expect(result.inlineDbml).not.toMatch(/\n\s*\n\s*}\s*\n}/);
 | 
			
		||||
        expect(result.inlineDbml).not.toMatch(/\s+\n}/);
 | 
			
		||||
 | 
			
		||||
        // Ensure there's no extra closing brace
 | 
			
		||||
        const braceBalance =
 | 
			
		||||
            (result.inlineDbml.match(/{/g) || []).length -
 | 
			
		||||
            (result.inlineDbml.match(/}/g) || []).length;
 | 
			
		||||
        expect(braceBalance).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { type DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
// Use DBCustomType for generating Enum DBML
 | 
			
		||||
const generateEnumsDBML = (customTypes: DBCustomType[] | undefined): string => {
 | 
			
		||||
@@ -249,34 +248,67 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
            fullMatch: string;
 | 
			
		||||
        };
 | 
			
		||||
    } = {};
 | 
			
		||||
    // Updated pattern to handle various table name formats including schema.table
 | 
			
		||||
    const tablePattern =
 | 
			
		||||
        /Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{([^}]*)}/g;
 | 
			
		||||
 | 
			
		||||
    let tableMatch;
 | 
			
		||||
    while ((tableMatch = tablePattern.exec(dbml)) !== null) {
 | 
			
		||||
        // Extract table name - handle schema.table format
 | 
			
		||||
    // Use a more sophisticated approach to handle nested braces
 | 
			
		||||
    let currentPos = 0;
 | 
			
		||||
    while (currentPos < dbml.length) {
 | 
			
		||||
        // Find the next table definition
 | 
			
		||||
        const tableStartPattern =
 | 
			
		||||
            /Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{/g;
 | 
			
		||||
        tableStartPattern.lastIndex = currentPos;
 | 
			
		||||
        const tableStartMatch = tableStartPattern.exec(dbml);
 | 
			
		||||
 | 
			
		||||
        if (!tableStartMatch) break;
 | 
			
		||||
 | 
			
		||||
        // Extract table name
 | 
			
		||||
        let tableName;
 | 
			
		||||
        if (tableMatch[1] && tableMatch[2]) {
 | 
			
		||||
            // Format: "schema"."table"
 | 
			
		||||
            tableName = `${tableMatch[1]}.${tableMatch[2]}`;
 | 
			
		||||
        } else if (tableMatch[1]) {
 | 
			
		||||
            // Format: "table" (no schema)
 | 
			
		||||
            tableName = tableMatch[1];
 | 
			
		||||
        if (tableStartMatch[1] && tableStartMatch[2]) {
 | 
			
		||||
            tableName = `${tableStartMatch[1]}.${tableStartMatch[2]}`;
 | 
			
		||||
        } else if (tableStartMatch[1]) {
 | 
			
		||||
            tableName = tableStartMatch[1];
 | 
			
		||||
        } else {
 | 
			
		||||
            // Other formats
 | 
			
		||||
            tableName = tableMatch[3] || tableMatch[4];
 | 
			
		||||
            tableName = tableStartMatch[3] || tableStartMatch[4];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Clean up any bracket syntax from table names
 | 
			
		||||
        const cleanTableName = tableName.replace(/\[([^\]]+)\]/g, '$1');
 | 
			
		||||
 | 
			
		||||
        tables[cleanTableName] = {
 | 
			
		||||
            start: tableMatch.index,
 | 
			
		||||
            end: tableMatch.index + tableMatch[0].length,
 | 
			
		||||
            content: tableMatch[5],
 | 
			
		||||
            fullMatch: tableMatch[0],
 | 
			
		||||
        };
 | 
			
		||||
        // Find the matching closing brace by counting nested braces
 | 
			
		||||
        const openBracePos =
 | 
			
		||||
            tableStartMatch.index + tableStartMatch[0].length - 1;
 | 
			
		||||
        let braceCount = 1;
 | 
			
		||||
        const contentStart = openBracePos + 1;
 | 
			
		||||
        let contentEnd = contentStart;
 | 
			
		||||
 | 
			
		||||
        for (let i = contentStart; i < dbml.length && braceCount > 0; i++) {
 | 
			
		||||
            if (dbml[i] === '{') braceCount++;
 | 
			
		||||
            else if (dbml[i] === '}') {
 | 
			
		||||
                braceCount--;
 | 
			
		||||
                if (braceCount === 0) {
 | 
			
		||||
                    contentEnd = i;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (braceCount === 0) {
 | 
			
		||||
            const content = dbml.substring(contentStart, contentEnd);
 | 
			
		||||
            const fullMatch = dbml.substring(
 | 
			
		||||
                tableStartMatch.index,
 | 
			
		||||
                contentEnd + 1
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            tables[cleanTableName] = {
 | 
			
		||||
                start: tableStartMatch.index,
 | 
			
		||||
                end: contentEnd + 1,
 | 
			
		||||
                content: content,
 | 
			
		||||
                fullMatch: fullMatch,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            currentPos = contentEnd + 1;
 | 
			
		||||
        } else {
 | 
			
		||||
            // Malformed DBML, skip this table
 | 
			
		||||
            currentPos = tableStartMatch.index + tableStartMatch[0].length;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (refs.length === 0 || Object.keys(tables).length === 0) {
 | 
			
		||||
@@ -286,9 +318,14 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
    // Create a map for faster table lookup
 | 
			
		||||
    const tableMap = new Map(Object.entries(tables));
 | 
			
		||||
 | 
			
		||||
    // 1. Add inline refs to table contents
 | 
			
		||||
    // 1. First, collect all refs per field
 | 
			
		||||
    const fieldRefs = new Map<
 | 
			
		||||
        string,
 | 
			
		||||
        { table: string; refs: string[]; relatedTables: string[] }
 | 
			
		||||
    >();
 | 
			
		||||
 | 
			
		||||
    refs.forEach((ref) => {
 | 
			
		||||
        let targetTableName, fieldNameToModify, inlineRefSyntax;
 | 
			
		||||
        let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
 | 
			
		||||
 | 
			
		||||
        if (ref.direction === '<') {
 | 
			
		||||
            targetTableName = ref.targetSchema
 | 
			
		||||
@@ -299,6 +336,7 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
                ? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
 | 
			
		||||
                : `"${ref.sourceTable}"."${ref.sourceField}"`;
 | 
			
		||||
            inlineRefSyntax = `ref: < ${sourceRef}`;
 | 
			
		||||
            relatedTable = ref.sourceTable;
 | 
			
		||||
        } else {
 | 
			
		||||
            targetTableName = ref.sourceSchema
 | 
			
		||||
                ? `${ref.sourceSchema}.${ref.sourceTable}`
 | 
			
		||||
@@ -308,13 +346,32 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
                ? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
 | 
			
		||||
                : `"${ref.targetTable}"."${ref.targetField}"`;
 | 
			
		||||
            inlineRefSyntax = `ref: > ${targetRef}`;
 | 
			
		||||
            relatedTable = ref.targetTable;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tableData = tableMap.get(targetTableName);
 | 
			
		||||
        const fieldKey = `${targetTableName}.${fieldNameToModify}`;
 | 
			
		||||
        const existing = fieldRefs.get(fieldKey) || {
 | 
			
		||||
            table: targetTableName,
 | 
			
		||||
            refs: [],
 | 
			
		||||
            relatedTables: [],
 | 
			
		||||
        };
 | 
			
		||||
        existing.refs.push(inlineRefSyntax);
 | 
			
		||||
        existing.relatedTables.push(relatedTable);
 | 
			
		||||
        fieldRefs.set(fieldKey, existing);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 2. Apply all refs to fields
 | 
			
		||||
    fieldRefs.forEach((fieldData, fieldKey) => {
 | 
			
		||||
        // fieldKey might be "schema.table.field" or just "table.field"
 | 
			
		||||
        const lastDotIndex = fieldKey.lastIndexOf('.');
 | 
			
		||||
        const tableName = fieldKey.substring(0, lastDotIndex);
 | 
			
		||||
        const fieldName = fieldKey.substring(lastDotIndex + 1);
 | 
			
		||||
        const tableData = tableMap.get(tableName);
 | 
			
		||||
 | 
			
		||||
        if (tableData) {
 | 
			
		||||
            // Updated pattern to capture field definition and all existing attributes in brackets
 | 
			
		||||
            const fieldPattern = new RegExp(
 | 
			
		||||
                `^([ \t]*"${fieldNameToModify}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
 | 
			
		||||
                `^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
 | 
			
		||||
                'gm'
 | 
			
		||||
            );
 | 
			
		||||
            let newContent = tableData.content;
 | 
			
		||||
@@ -322,11 +379,6 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
            newContent = newContent.replace(
 | 
			
		||||
                fieldPattern,
 | 
			
		||||
                (lineMatch, fieldPart, existingBrackets, commentPart) => {
 | 
			
		||||
                    // Avoid adding duplicate refs
 | 
			
		||||
                    if (lineMatch.includes('ref:')) {
 | 
			
		||||
                        return lineMatch;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Collect all attributes from existing brackets
 | 
			
		||||
                    const allAttributes: string[] = [];
 | 
			
		||||
                    if (existingBrackets) {
 | 
			
		||||
@@ -344,8 +396,8 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Add the new ref
 | 
			
		||||
                    allAttributes.push(inlineRefSyntax);
 | 
			
		||||
                    // Add all refs for this field
 | 
			
		||||
                    allAttributes.push(...fieldData.refs);
 | 
			
		||||
 | 
			
		||||
                    // Combine all attributes into a single bracket
 | 
			
		||||
                    const combinedAttributes = allAttributes.join(', ');
 | 
			
		||||
@@ -353,6 +405,7 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
                    // Preserve original spacing from fieldPart
 | 
			
		||||
                    const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
 | 
			
		||||
                    const fieldDefWithoutSpaces = fieldPart.trim();
 | 
			
		||||
 | 
			
		||||
                    return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
@@ -360,7 +413,7 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
            // Update the table content if modified
 | 
			
		||||
            if (newContent !== tableData.content) {
 | 
			
		||||
                tableData.content = newContent;
 | 
			
		||||
                tableMap.set(targetTableName, tableData);
 | 
			
		||||
                tableMap.set(tableName, tableData);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
@@ -376,10 +429,48 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
        reconstructedDbml += dbml.substring(lastIndex, tableData.start);
 | 
			
		||||
        // Preserve the original table definition format but with updated content
 | 
			
		||||
        const originalTableDef = tableData.fullMatch;
 | 
			
		||||
        const updatedTableDef = originalTableDef.replace(
 | 
			
		||||
            /{[^}]*}/,
 | 
			
		||||
            `{${tableData.content}}`
 | 
			
		||||
        let formattedContent = tableData.content;
 | 
			
		||||
 | 
			
		||||
        // Clean up content formatting:
 | 
			
		||||
        // 1. Split into lines to handle each line individually
 | 
			
		||||
        const lines = formattedContent.split('\n');
 | 
			
		||||
 | 
			
		||||
        // 2. Process lines to ensure proper formatting
 | 
			
		||||
        const processedLines = [];
 | 
			
		||||
        for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
            const line = lines[i];
 | 
			
		||||
            const trimmedLine = line.trimEnd();
 | 
			
		||||
 | 
			
		||||
            // Skip empty lines at the end if followed by a closing brace
 | 
			
		||||
            if (trimmedLine === '' && i === lines.length - 1) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Skip empty lines before a closing brace
 | 
			
		||||
            if (
 | 
			
		||||
                trimmedLine === '' &&
 | 
			
		||||
                i < lines.length - 1 &&
 | 
			
		||||
                lines[i + 1].trim().startsWith('}')
 | 
			
		||||
            ) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            processedLines.push(line);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        formattedContent = processedLines.join('\n');
 | 
			
		||||
 | 
			
		||||
        // Ensure content ends with a newline before the table's closing brace
 | 
			
		||||
        if (!formattedContent.endsWith('\n')) {
 | 
			
		||||
            formattedContent = formattedContent + '\n';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Since we properly extracted content with nested braces, we need to rebuild the table definition
 | 
			
		||||
        const tableHeader = originalTableDef.substring(
 | 
			
		||||
            0,
 | 
			
		||||
            originalTableDef.indexOf('{') + 1
 | 
			
		||||
        );
 | 
			
		||||
        const updatedTableDef = `${tableHeader}${formattedContent}}`;
 | 
			
		||||
        reconstructedDbml += updatedTableDef;
 | 
			
		||||
        lastIndex = tableData.end;
 | 
			
		||||
    }
 | 
			
		||||
@@ -392,7 +483,10 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
    const finalDbml = finalLines.join('\n').trim();
 | 
			
		||||
 | 
			
		||||
    // Clean up excessive empty lines - replace multiple consecutive empty lines with just one
 | 
			
		||||
    const cleanedDbml = finalDbml.replace(/\n\s*\n\s*\n/g, '\n\n');
 | 
			
		||||
    // But ensure there's at least one blank line between tables
 | 
			
		||||
    const cleanedDbml = finalDbml
 | 
			
		||||
        .replace(/\n\s*\n\s*\n/g, '\n\n')
 | 
			
		||||
        .replace(/}\n(?=Table)/g, '}\n\n');
 | 
			
		||||
 | 
			
		||||
    return cleanedDbml;
 | 
			
		||||
};
 | 
			
		||||
@@ -489,15 +583,15 @@ const fixTableBracketSyntax = (dbml: string): string => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Restore schema information that may have been stripped by the DBML importer
 | 
			
		||||
const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
 | 
			
		||||
    if (!diagram.tables) return dbml;
 | 
			
		||||
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
 | 
			
		||||
    if (!tables || tables.length === 0) return dbml;
 | 
			
		||||
 | 
			
		||||
    // Group tables by name to handle duplicates
 | 
			
		||||
    const tablesByName = new Map<
 | 
			
		||||
        string,
 | 
			
		||||
        Array<{ table: (typeof diagram.tables)[0]; index: number }>
 | 
			
		||||
        Array<{ table: DBTable; index: number }>
 | 
			
		||||
    >();
 | 
			
		||||
    diagram.tables.forEach((table, index) => {
 | 
			
		||||
    tables.forEach((table, index) => {
 | 
			
		||||
        const existing = tablesByName.get(table.name) || [];
 | 
			
		||||
        existing.push({ table, index });
 | 
			
		||||
        tablesByName.set(table.name, existing);
 | 
			
		||||
@@ -547,30 +641,20 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Multiple tables with the same name - need to be more careful
 | 
			
		||||
            const defaultSchema = defaultSchemas[diagram.databaseType];
 | 
			
		||||
 | 
			
		||||
            // Separate tables by whether they have the default schema or not
 | 
			
		||||
            const defaultSchemaTable = tablesGroup.find(
 | 
			
		||||
                ({ table }) => table.schema === defaultSchema
 | 
			
		||||
            );
 | 
			
		||||
            const nonDefaultSchemaTables = tablesGroup.filter(
 | 
			
		||||
                ({ table }) => table.schema && table.schema !== defaultSchema
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Find all table definitions for this name
 | 
			
		||||
            const escapedTableName = tableName.replace(
 | 
			
		||||
                /[.*+?^${}()|[\]\\]/g,
 | 
			
		||||
                '\\$&'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // First, handle tables that already have schema in DBML
 | 
			
		||||
            const schemaTablePattern = new RegExp(
 | 
			
		||||
                `Table\\s+"[^"]+"\\.\\s*"${escapedTableName}"\\s*{`,
 | 
			
		||||
                'g'
 | 
			
		||||
            );
 | 
			
		||||
            result = result.replace(schemaTablePattern, (match) => {
 | 
			
		||||
                // This table already has a schema, keep it as is
 | 
			
		||||
                return match;
 | 
			
		||||
            // Get tables that need schema restoration (those without schema in DBML)
 | 
			
		||||
            const tablesNeedingSchema = tablesGroup.filter(({ table }) => {
 | 
			
		||||
                // Check if this table's schema is already in the DBML
 | 
			
		||||
                const schemaPattern = new RegExp(
 | 
			
		||||
                    `Table\\s+"${table.schema}"\\.\\s*"${escapedTableName}"\\s*{`,
 | 
			
		||||
                    'g'
 | 
			
		||||
                );
 | 
			
		||||
                return !result.match(schemaPattern);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Then handle tables without schema in DBML
 | 
			
		||||
@@ -581,21 +665,25 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
 | 
			
		||||
 | 
			
		||||
            let noSchemaMatchIndex = 0;
 | 
			
		||||
            result = result.replace(noSchemaTablePattern, (match) => {
 | 
			
		||||
                // If we have a table with the default schema and this is the first match without schema,
 | 
			
		||||
                // it should be the default schema table
 | 
			
		||||
                if (noSchemaMatchIndex === 0 && defaultSchemaTable) {
 | 
			
		||||
                    noSchemaMatchIndex++;
 | 
			
		||||
                    return `Table "${defaultSchema}"."${tableName}" {`;
 | 
			
		||||
                // We need to match based on the order in the DBML output
 | 
			
		||||
                // For PostgreSQL DBML, the @dbml/core sorts tables by:
 | 
			
		||||
                // 1. Tables with schemas (alphabetically)
 | 
			
		||||
                // 2. Tables without schemas
 | 
			
		||||
                // Since both our tables have schemas, they should appear in order
 | 
			
		||||
 | 
			
		||||
                // Only process tables that need schema restoration
 | 
			
		||||
                if (noSchemaMatchIndex >= tablesNeedingSchema.length) {
 | 
			
		||||
                    return match;
 | 
			
		||||
                }
 | 
			
		||||
                // Otherwise, try to match with non-default schema tables
 | 
			
		||||
                const remainingNonDefault =
 | 
			
		||||
                    nonDefaultSchemaTables[
 | 
			
		||||
                        noSchemaMatchIndex - (defaultSchemaTable ? 1 : 0)
 | 
			
		||||
                    ];
 | 
			
		||||
                if (remainingNonDefault) {
 | 
			
		||||
                    noSchemaMatchIndex++;
 | 
			
		||||
                    return `Table "${remainingNonDefault.table.schema}"."${tableName}" {`;
 | 
			
		||||
 | 
			
		||||
                const correspondingTable =
 | 
			
		||||
                    tablesNeedingSchema[noSchemaMatchIndex];
 | 
			
		||||
                noSchemaMatchIndex++;
 | 
			
		||||
 | 
			
		||||
                if (correspondingTable && correspondingTable.table.schema) {
 | 
			
		||||
                    return `Table "${correspondingTable.table.schema}"."${tableName}" {`;
 | 
			
		||||
                }
 | 
			
		||||
                // If the table doesn't have a schema, keep it as is
 | 
			
		||||
                return match;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
@@ -807,7 +895,7 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Restore schema information that may have been stripped by DBML importer
 | 
			
		||||
        standard = restoreTableSchemas(standard, diagram);
 | 
			
		||||
        standard = restoreTableSchemas(standard, uniqueTables);
 | 
			
		||||
 | 
			
		||||
        // Prepend Enum DBML to the standard output
 | 
			
		||||
        if (enumsDBML) {
 | 
			
		||||
@@ -819,6 +907,14 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
        // Clean up excessive empty lines in both outputs
 | 
			
		||||
        standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');
 | 
			
		||||
        inline = inline.replace(/\n\s*\n\s*\n/g, '\n\n');
 | 
			
		||||
 | 
			
		||||
        // Ensure proper formatting with newline at end
 | 
			
		||||
        if (!standard.endsWith('\n')) {
 | 
			
		||||
            standard += '\n';
 | 
			
		||||
        }
 | 
			
		||||
        if (!inline.endsWith('\n')) {
 | 
			
		||||
            inline += '\n';
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error: unknown) {
 | 
			
		||||
        console.error(
 | 
			
		||||
            'Error during DBML generation process:',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { importDBMLToDiagram } from '../dbml-import';
 | 
			
		||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
describe('DBML Import - Fantasy Examples', () => {
 | 
			
		||||
    describe('Magical Academy System', () => {
 | 
			
		||||
@@ -613,6 +614,228 @@ Note quest_system_note {
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Enum Support', () => {
 | 
			
		||||
        it('should import enums as customTypes', async () => {
 | 
			
		||||
            const dbmlWithEnums = `
 | 
			
		||||
// Test DBML with various enum definitions
 | 
			
		||||
enum job_status {
 | 
			
		||||
  created [note: 'Waiting to be processed']
 | 
			
		||||
  running
 | 
			
		||||
  done
 | 
			
		||||
  failure
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Enum with schema
 | 
			
		||||
enum hr.employee_type {
 | 
			
		||||
  full_time
 | 
			
		||||
  part_time
 | 
			
		||||
  contractor
 | 
			
		||||
  intern
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Enum with special characters and spaces
 | 
			
		||||
enum grade {
 | 
			
		||||
  "A+"
 | 
			
		||||
  "A"
 | 
			
		||||
  "A-"
 | 
			
		||||
  "Not Yet Set"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table employees {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  name varchar(200) [not null]
 | 
			
		||||
  status job_status
 | 
			
		||||
  type hr.employee_type
 | 
			
		||||
  performance_grade grade
 | 
			
		||||
  created_at timestamp [default: 'now()']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table projects {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  name varchar(300) [not null]
 | 
			
		||||
  status job_status [not null]
 | 
			
		||||
  priority enum // inline enum without values - will be converted to varchar
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
            const diagram = await importDBMLToDiagram(dbmlWithEnums);
 | 
			
		||||
 | 
			
		||||
            // Verify customTypes are created for enums
 | 
			
		||||
            expect(diagram.customTypes).toBeDefined();
 | 
			
		||||
            expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade
 | 
			
		||||
 | 
			
		||||
            // Check job_status enum
 | 
			
		||||
            const jobStatusEnum = diagram.customTypes?.find(
 | 
			
		||||
                (ct) => ct.name === 'job_status' && !ct.schema
 | 
			
		||||
            );
 | 
			
		||||
            expect(jobStatusEnum).toBeDefined();
 | 
			
		||||
            expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
 | 
			
		||||
            expect(jobStatusEnum?.values).toEqual([
 | 
			
		||||
                'created',
 | 
			
		||||
                'running',
 | 
			
		||||
                'done',
 | 
			
		||||
                'failure',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Check hr.employee_type enum with schema
 | 
			
		||||
            const employeeTypeEnum = diagram.customTypes?.find(
 | 
			
		||||
                (ct) => ct.name === 'employee_type' && ct.schema === 'hr'
 | 
			
		||||
            );
 | 
			
		||||
            expect(employeeTypeEnum).toBeDefined();
 | 
			
		||||
            expect(employeeTypeEnum?.kind).toBe(DBCustomTypeKind.enum);
 | 
			
		||||
            expect(employeeTypeEnum?.values).toEqual([
 | 
			
		||||
                'full_time',
 | 
			
		||||
                'part_time',
 | 
			
		||||
                'contractor',
 | 
			
		||||
                'intern',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Check grade enum with quoted values
 | 
			
		||||
            const gradeEnum = diagram.customTypes?.find(
 | 
			
		||||
                (ct) => ct.name === 'grade' && !ct.schema
 | 
			
		||||
            );
 | 
			
		||||
            expect(gradeEnum).toBeDefined();
 | 
			
		||||
            expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum);
 | 
			
		||||
            expect(gradeEnum?.values).toEqual(['A+', 'A', 'A-', 'Not Yet Set']);
 | 
			
		||||
 | 
			
		||||
            // Verify tables are created
 | 
			
		||||
            expect(diagram.tables).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
            // Check that enum fields in tables reference the custom types
 | 
			
		||||
            const employeesTable = diagram.tables?.find(
 | 
			
		||||
                (t) => t.name === 'employees'
 | 
			
		||||
            );
 | 
			
		||||
            const statusField = employeesTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'status'
 | 
			
		||||
            );
 | 
			
		||||
            const typeField = employeesTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'type'
 | 
			
		||||
            );
 | 
			
		||||
            const gradeField = employeesTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'performance_grade'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Verify fields have correct types
 | 
			
		||||
            expect(statusField?.type.id).toBe('job_status');
 | 
			
		||||
            expect(typeField?.type.id).toBe('employee_type');
 | 
			
		||||
            expect(gradeField?.type.id).toBe('grade');
 | 
			
		||||
 | 
			
		||||
            // Check inline enum was converted to varchar
 | 
			
		||||
            const projectsTable = diagram.tables?.find(
 | 
			
		||||
                (t) => t.name === 'projects'
 | 
			
		||||
            );
 | 
			
		||||
            const priorityField = projectsTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'priority'
 | 
			
		||||
            );
 | 
			
		||||
            expect(priorityField?.type.id).toBe('varchar');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle enum values with notes', async () => {
 | 
			
		||||
            const dbmlWithEnumNotes = `
 | 
			
		||||
enum order_status {
 | 
			
		||||
  pending [note: 'Order has been placed but not confirmed']
 | 
			
		||||
  confirmed [note: 'Payment received and order confirmed']
 | 
			
		||||
  shipped [note: 'Order has been dispatched']
 | 
			
		||||
  delivered [note: 'Order delivered to customer']
 | 
			
		||||
  cancelled [note: 'Order cancelled by customer or system']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table orders {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  status order_status [not null]
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
            const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
 | 
			
		||||
 | 
			
		||||
            // Verify enum is created
 | 
			
		||||
            expect(diagram.customTypes).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
            const orderStatusEnum = diagram.customTypes?.[0];
 | 
			
		||||
            expect(orderStatusEnum?.name).toBe('order_status');
 | 
			
		||||
            expect(orderStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
 | 
			
		||||
            expect(orderStatusEnum?.values).toEqual([
 | 
			
		||||
                'pending',
 | 
			
		||||
                'confirmed',
 | 
			
		||||
                'shipped',
 | 
			
		||||
                'delivered',
 | 
			
		||||
                'cancelled',
 | 
			
		||||
            ]);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle multiple schemas with same enum names', async () => {
 | 
			
		||||
            const dbmlWithSameEnumNames = `
 | 
			
		||||
// Public schema status enum
 | 
			
		||||
enum status {
 | 
			
		||||
  active
 | 
			
		||||
  inactive
 | 
			
		||||
  deleted
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Admin schema status enum with different values
 | 
			
		||||
enum admin.status {
 | 
			
		||||
  pending_approval
 | 
			
		||||
  approved
 | 
			
		||||
  rejected
 | 
			
		||||
  suspended
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table public.users {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  status status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table admin.users {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  status admin.status
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
            const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
 | 
			
		||||
 | 
			
		||||
            // Verify both enums are created
 | 
			
		||||
            expect(diagram.customTypes).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
            // Check public.status enum
 | 
			
		||||
            const publicStatusEnum = diagram.customTypes?.find(
 | 
			
		||||
                (ct) => ct.name === 'status' && !ct.schema
 | 
			
		||||
            );
 | 
			
		||||
            expect(publicStatusEnum).toBeDefined();
 | 
			
		||||
            expect(publicStatusEnum?.values).toEqual([
 | 
			
		||||
                'active',
 | 
			
		||||
                'inactive',
 | 
			
		||||
                'deleted',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Check admin.status enum
 | 
			
		||||
            const adminStatusEnum = diagram.customTypes?.find(
 | 
			
		||||
                (ct) => ct.name === 'status' && ct.schema === 'admin'
 | 
			
		||||
            );
 | 
			
		||||
            expect(adminStatusEnum).toBeDefined();
 | 
			
		||||
            expect(adminStatusEnum?.values).toEqual([
 | 
			
		||||
                'pending_approval',
 | 
			
		||||
                'approved',
 | 
			
		||||
                'rejected',
 | 
			
		||||
                'suspended',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Verify fields reference correct enums
 | 
			
		||||
            const publicUsersTable = diagram.tables?.find(
 | 
			
		||||
                (t) => t.name === 'users' && t.schema === 'public'
 | 
			
		||||
            );
 | 
			
		||||
            const adminUsersTable = diagram.tables?.find(
 | 
			
		||||
                (t) => t.name === 'users' && t.schema === 'admin'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const publicStatusField = publicUsersTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'status'
 | 
			
		||||
            );
 | 
			
		||||
            const adminStatusField = adminUsersTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'status'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            expect(publicStatusField?.type.id).toBe('status');
 | 
			
		||||
            expect(adminStatusField?.type.id).toBe('status');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Edge Cases and Special Features', () => {
 | 
			
		||||
        it('should handle tables with all DBML features', async () => {
 | 
			
		||||
            const edgeCaseDBML = `
 | 
			
		||||
@@ -793,11 +1016,11 @@ Table "bb"."users" {
 | 
			
		||||
            expect(bbUsersTable?.fields).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
            expect(aaUsersTable?.fields[0].name).toBe('id');
 | 
			
		||||
            expect(aaUsersTable?.fields[0].type.id).toBe('int');
 | 
			
		||||
            expect(aaUsersTable?.fields[0].type.id).toBe('integer');
 | 
			
		||||
            expect(aaUsersTable?.fields[0].primaryKey).toBe(true);
 | 
			
		||||
 | 
			
		||||
            expect(bbUsersTable?.fields[0].name).toBe('id');
 | 
			
		||||
            expect(bbUsersTable?.fields[0].type.id).toBe('int');
 | 
			
		||||
            expect(bbUsersTable?.fields[0].type.id).toBe('integer');
 | 
			
		||||
            expect(bbUsersTable?.fields[0].primaryKey).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -1021,4 +1244,47 @@ Table "public_3"."comments" {
 | 
			
		||||
            expect(relationshipsHaveSchemas).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Notes Support', () => {
 | 
			
		||||
        it('should import table with note', async () => {
 | 
			
		||||
            const dbmlWithTableNote = `
 | 
			
		||||
Table products {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  name varchar(100)
 | 
			
		||||
  Note: 'This table stores product information'
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
            const diagram = await importDBMLToDiagram(dbmlWithTableNote);
 | 
			
		||||
 | 
			
		||||
            expect(diagram.tables).toHaveLength(1);
 | 
			
		||||
            const productsTable = diagram.tables?.[0];
 | 
			
		||||
            expect(productsTable?.name).toBe('products');
 | 
			
		||||
            expect(productsTable?.comments).toBe(
 | 
			
		||||
                'This table stores product information'
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should import field with note', async () => {
 | 
			
		||||
            const dbmlWithFieldNote = `
 | 
			
		||||
Table orders {
 | 
			
		||||
  id integer [pk]
 | 
			
		||||
  total numeric(10,2) [note: 'Order total including tax']
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
            const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
 | 
			
		||||
 | 
			
		||||
            expect(diagram.tables).toHaveLength(1);
 | 
			
		||||
            const ordersTable = diagram.tables?.[0];
 | 
			
		||||
            expect(ordersTable?.fields).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
            const totalField = ordersTable?.fields.find(
 | 
			
		||||
                (f) => f.name === 'total'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Field notes should be imported
 | 
			
		||||
            expect(totalField).toBeDefined();
 | 
			
		||||
            expect(totalField?.name).toBe('total');
 | 
			
		||||
            expect(totalField?.comments).toBe('Order total including tax');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								src/lib/dbml/dbml-import/dbml-import-error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/lib/dbml/dbml-import/dbml-import-error.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
export interface DBMLError {
 | 
			
		||||
    message: string;
 | 
			
		||||
    line: number;
 | 
			
		||||
    column: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseDBMLError(error: unknown): DBMLError | null {
 | 
			
		||||
    try {
 | 
			
		||||
        if (typeof error === 'string') {
 | 
			
		||||
            const parsed = JSON.parse(error);
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                const diag = parsed.diags[0];
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    message: diag.message,
 | 
			
		||||
                    line: diag.location.start.line,
 | 
			
		||||
                    column: diag.location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        } else if (error && typeof error === 'object' && 'diags' in error) {
 | 
			
		||||
            const parsed = error as {
 | 
			
		||||
                diags: Array<{
 | 
			
		||||
                    message: string;
 | 
			
		||||
                    location: { start: { line: number; column: number } };
 | 
			
		||||
                }>;
 | 
			
		||||
            };
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                return {
 | 
			
		||||
                    message: parsed.diags[0].message,
 | 
			
		||||
                    line: parsed.diags[0].location.start.line,
 | 
			
		||||
                    column: parsed.diags[0].location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error('Error parsing DBML error:', e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
@@ -4,10 +4,16 @@ 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 { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
 | 
			
		||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { randomColor } from '@/lib/colors';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type Field from '@dbml/core/types/model_structure/field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
// Preprocess DBML to handle unsupported features
 | 
			
		||||
export const preprocessDBML = (content: string): string => {
 | 
			
		||||
@@ -19,8 +25,8 @@ export const preprocessDBML = (content: string): string => {
 | 
			
		||||
    // Remove Note blocks
 | 
			
		||||
    processed = processed.replace(/Note\s+\w+\s*\{[^}]*\}/gs, '');
 | 
			
		||||
 | 
			
		||||
    // Remove enum definitions (blocks)
 | 
			
		||||
    processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
 | 
			
		||||
    // Don't remove enum definitions - we'll parse them
 | 
			
		||||
    // processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
 | 
			
		||||
 | 
			
		||||
    // Handle array types by converting them to text
 | 
			
		||||
    processed = processed.replace(/(\w+)\[\]/g, 'text');
 | 
			
		||||
@@ -77,6 +83,10 @@ interface DBMLField {
 | 
			
		||||
    pk?: boolean;
 | 
			
		||||
    not_null?: boolean;
 | 
			
		||||
    increment?: boolean;
 | 
			
		||||
    characterMaximumLength?: string | null;
 | 
			
		||||
    precision?: number | null;
 | 
			
		||||
    scale?: number | null;
 | 
			
		||||
    note?: string | { value: string } | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLIndexColumn {
 | 
			
		||||
@@ -110,39 +120,51 @@ interface DBMLRef {
 | 
			
		||||
    endpoints: [DBMLEndpoint, DBMLEndpoint];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
 | 
			
		||||
interface DBMLEnum {
 | 
			
		||||
    name: string;
 | 
			
		||||
    schema?: string | { name: string };
 | 
			
		||||
    values: Array<{ name: string; note?: string }>;
 | 
			
		||||
    note?: string | { value: string } | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mapDBMLTypeToDataType = (
 | 
			
		||||
    dbmlType: string,
 | 
			
		||||
    options?: { databaseType?: DatabaseType; enums?: DBMLEnum[] }
 | 
			
		||||
): DataTypeData => {
 | 
			
		||||
    const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
 | 
			
		||||
    const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
 | 
			
		||||
    if (matchedType) return matchedType;
 | 
			
		||||
    const typeMap: Record<string, string> = {
 | 
			
		||||
        int: 'int',
 | 
			
		||||
        integer: 'int',
 | 
			
		||||
        varchar: 'varchar',
 | 
			
		||||
        bool: 'boolean',
 | 
			
		||||
        boolean: 'boolean',
 | 
			
		||||
        number: 'numeric',
 | 
			
		||||
        string: 'varchar',
 | 
			
		||||
        text: 'text',
 | 
			
		||||
        timestamp: 'timestamp',
 | 
			
		||||
        datetime: 'timestamp',
 | 
			
		||||
        float: 'float',
 | 
			
		||||
        double: 'double',
 | 
			
		||||
        decimal: 'decimal',
 | 
			
		||||
        bigint: 'bigint',
 | 
			
		||||
        smallint: 'smallint',
 | 
			
		||||
        char: 'char',
 | 
			
		||||
    };
 | 
			
		||||
    const mappedType = typeMap[normalizedType];
 | 
			
		||||
    if (mappedType) {
 | 
			
		||||
        const foundType = genericDataTypes.find((t) => t.id === mappedType);
 | 
			
		||||
        if (foundType) return foundType;
 | 
			
		||||
 | 
			
		||||
    // Check if it's an enum type
 | 
			
		||||
    if (options?.enums) {
 | 
			
		||||
        const enumDef = options.enums.find((e) => {
 | 
			
		||||
            // Check both with and without schema prefix
 | 
			
		||||
            const enumName = e.name.toLowerCase();
 | 
			
		||||
            const enumFullName = e.schema
 | 
			
		||||
                ? `${e.schema}.${enumName}`
 | 
			
		||||
                : enumName;
 | 
			
		||||
            return (
 | 
			
		||||
                normalizedType === enumName || normalizedType === enumFullName
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (enumDef) {
 | 
			
		||||
            // Return enum as custom type reference
 | 
			
		||||
            return {
 | 
			
		||||
                id: enumDef.name,
 | 
			
		||||
                name: enumDef.name,
 | 
			
		||||
            } satisfies DataTypeData;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    const type = genericDataTypes.find((t) => t.id === 'varchar')!;
 | 
			
		||||
 | 
			
		||||
    const matchedType = findDataTypeDataById(
 | 
			
		||||
        normalizedType,
 | 
			
		||||
        options?.databaseType
 | 
			
		||||
    );
 | 
			
		||||
    if (matchedType) return matchedType;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        id: type.id,
 | 
			
		||||
        name: type.name,
 | 
			
		||||
    };
 | 
			
		||||
        id: normalizedType.split(' ').join('_').toLowerCase(),
 | 
			
		||||
        name: normalizedType,
 | 
			
		||||
    } satisfies DataTypeData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const determineCardinality = (
 | 
			
		||||
@@ -163,7 +185,10 @@ const determineCardinality = (
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const importDBMLToDiagram = async (
 | 
			
		||||
    dbmlContent: string
 | 
			
		||||
    dbmlContent: string,
 | 
			
		||||
    options?: {
 | 
			
		||||
        databaseType?: DatabaseType;
 | 
			
		||||
    }
 | 
			
		||||
): Promise<Diagram> => {
 | 
			
		||||
    try {
 | 
			
		||||
        // Handle empty content
 | 
			
		||||
@@ -171,7 +196,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                databaseType: DatabaseType.GENERIC,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
                createdAt: new Date(),
 | 
			
		||||
@@ -189,7 +214,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                databaseType: DatabaseType.GENERIC,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
                createdAt: new Date(),
 | 
			
		||||
@@ -204,7 +229,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                databaseType: DatabaseType.GENERIC,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
                createdAt: new Date(),
 | 
			
		||||
@@ -215,6 +240,55 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
        // Process all schemas, not just the first one
 | 
			
		||||
        const allTables: DBMLTable[] = [];
 | 
			
		||||
        const allRefs: DBMLRef[] = [];
 | 
			
		||||
        const allEnums: DBMLEnum[] = [];
 | 
			
		||||
 | 
			
		||||
        const getFieldExtraAttributes = (
 | 
			
		||||
            field: Field,
 | 
			
		||||
            enums: DBMLEnum[]
 | 
			
		||||
        ): Partial<DBMLField> => {
 | 
			
		||||
            if (!field.type || !field.type.args) {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const args = field.type.args.split(',') as string[];
 | 
			
		||||
 | 
			
		||||
            const dataType = mapDBMLTypeToDataType(field.type.type_name, {
 | 
			
		||||
                ...options,
 | 
			
		||||
                enums,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (dataType.fieldAttributes?.hasCharMaxLength) {
 | 
			
		||||
                const charMaxLength = args?.[0];
 | 
			
		||||
                return {
 | 
			
		||||
                    characterMaximumLength: charMaxLength,
 | 
			
		||||
                };
 | 
			
		||||
            } else if (
 | 
			
		||||
                dataType.fieldAttributes?.precision &&
 | 
			
		||||
                dataType.fieldAttributes?.scale
 | 
			
		||||
            ) {
 | 
			
		||||
                const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
 | 
			
		||||
                const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;
 | 
			
		||||
 | 
			
		||||
                const precision = precisionNum
 | 
			
		||||
                    ? isNaN(precisionNum)
 | 
			
		||||
                        ? undefined
 | 
			
		||||
                        : precisionNum
 | 
			
		||||
                    : undefined;
 | 
			
		||||
 | 
			
		||||
                const scale = scaleNum
 | 
			
		||||
                    ? isNaN(scaleNum)
 | 
			
		||||
                        ? undefined
 | 
			
		||||
                        : scaleNum
 | 
			
		||||
                    : undefined;
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    precision,
 | 
			
		||||
                    scale,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return {};
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        parsedData.schemas.forEach((schema) => {
 | 
			
		||||
            if (schema.tables) {
 | 
			
		||||
@@ -230,17 +304,18 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                        name: table.name,
 | 
			
		||||
                        schema: schemaName,
 | 
			
		||||
                        note: table.note,
 | 
			
		||||
                        fields: table.fields.map(
 | 
			
		||||
                            (field) =>
 | 
			
		||||
                                ({
 | 
			
		||||
                                    name: field.name,
 | 
			
		||||
                                    type: field.type,
 | 
			
		||||
                                    unique: field.unique,
 | 
			
		||||
                                    pk: field.pk,
 | 
			
		||||
                                    not_null: field.not_null,
 | 
			
		||||
                                    increment: field.increment,
 | 
			
		||||
                                }) satisfies DBMLField
 | 
			
		||||
                        ),
 | 
			
		||||
                        fields: table.fields.map((field): DBMLField => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                name: field.name,
 | 
			
		||||
                                type: field.type,
 | 
			
		||||
                                unique: field.unique,
 | 
			
		||||
                                pk: field.pk,
 | 
			
		||||
                                not_null: field.not_null,
 | 
			
		||||
                                increment: field.increment,
 | 
			
		||||
                                note: field.note,
 | 
			
		||||
                                ...getFieldExtraAttributes(field, allEnums),
 | 
			
		||||
                            } satisfies DBMLField;
 | 
			
		||||
                        }),
 | 
			
		||||
                        indexes:
 | 
			
		||||
                            table.indexes?.map((dbmlIndex) => {
 | 
			
		||||
                                let indexColumns: string[];
 | 
			
		||||
@@ -314,15 +389,34 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (schema.enums) {
 | 
			
		||||
                schema.enums.forEach((enumDef) => {
 | 
			
		||||
                    // Get schema name from enum or use schema's name
 | 
			
		||||
                    const enumSchema =
 | 
			
		||||
                        typeof enumDef.schema === 'string'
 | 
			
		||||
                            ? enumDef.schema
 | 
			
		||||
                            : enumDef.schema?.name || schema.name;
 | 
			
		||||
 | 
			
		||||
                    allEnums.push({
 | 
			
		||||
                        name: enumDef.name,
 | 
			
		||||
                        schema: enumSchema === 'public' ? '' : enumSchema,
 | 
			
		||||
                        values: enumDef.values || [],
 | 
			
		||||
                        note: enumDef.note,
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Extract only the necessary data from the parsed DBML
 | 
			
		||||
        const extractedData: {
 | 
			
		||||
            tables: DBMLTable[];
 | 
			
		||||
            refs: DBMLRef[];
 | 
			
		||||
            enums: DBMLEnum[];
 | 
			
		||||
        } = {
 | 
			
		||||
            tables: allTables,
 | 
			
		||||
            refs: allRefs,
 | 
			
		||||
            enums: allEnums,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Convert DBML tables to ChartDB table objects
 | 
			
		||||
@@ -332,18 +426,40 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
            const tableSpacing = 300;
 | 
			
		||||
 | 
			
		||||
            // Create fields first so we have their IDs
 | 
			
		||||
            const fields = table.fields.map((field) => ({
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: field.name.replace(/['"]/g, ''),
 | 
			
		||||
                type: mapDBMLTypeToGenericType(field.type.type_name),
 | 
			
		||||
                nullable: !field.not_null,
 | 
			
		||||
                primaryKey: field.pk || false,
 | 
			
		||||
                unique: field.unique || false,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
            }));
 | 
			
		||||
            const fields: DBField[] = table.fields.map((field) => {
 | 
			
		||||
                // Extract field note/comment
 | 
			
		||||
                let fieldComment: string | undefined;
 | 
			
		||||
                if (field.note) {
 | 
			
		||||
                    if (typeof field.note === 'string') {
 | 
			
		||||
                        fieldComment = field.note;
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        typeof field.note === 'object' &&
 | 
			
		||||
                        'value' in field.note
 | 
			
		||||
                    ) {
 | 
			
		||||
                        fieldComment = field.note.value;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: field.name.replace(/['"]/g, ''),
 | 
			
		||||
                    type: mapDBMLTypeToDataType(field.type.type_name, {
 | 
			
		||||
                        ...options,
 | 
			
		||||
                        enums: extractedData.enums,
 | 
			
		||||
                    }),
 | 
			
		||||
                    nullable: !field.not_null,
 | 
			
		||||
                    primaryKey: field.pk || false,
 | 
			
		||||
                    unique: field.unique || false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                    characterMaximumLength: field.characterMaximumLength,
 | 
			
		||||
                    precision: field.precision,
 | 
			
		||||
                    scale: field.scale,
 | 
			
		||||
                    ...(fieldComment ? { comments: fieldComment } : {}),
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Convert DBML indexes to ChartDB indexes
 | 
			
		||||
            const indexes =
 | 
			
		||||
            const indexes: DBIndex[] =
 | 
			
		||||
                table.indexes?.map((dbmlIndex) => {
 | 
			
		||||
                    const fieldIds = dbmlIndex.columns.map((columnName) => {
 | 
			
		||||
                        const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
@@ -395,7 +511,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                isView: false,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
                comments: tableComment,
 | 
			
		||||
            };
 | 
			
		||||
            } as DBTable;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create relationships using the refs
 | 
			
		||||
@@ -449,12 +565,43 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Convert DBML enums to custom types
 | 
			
		||||
        const customTypes: DBCustomType[] = extractedData.enums.map(
 | 
			
		||||
            (enumDef) => {
 | 
			
		||||
                // Extract values from enum
 | 
			
		||||
                const values = enumDef.values
 | 
			
		||||
                    .map((v) => {
 | 
			
		||||
                        // Handle both string values and objects with name property
 | 
			
		||||
                        if (typeof v === 'string') {
 | 
			
		||||
                            return v;
 | 
			
		||||
                        } else if (v && typeof v === 'object' && 'name' in v) {
 | 
			
		||||
                            return v.name.replace(/["']/g, ''); // Remove quotes from values
 | 
			
		||||
                        }
 | 
			
		||||
                        return '';
 | 
			
		||||
                    })
 | 
			
		||||
                    .filter((v) => v !== '');
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    schema:
 | 
			
		||||
                        typeof enumDef.schema === 'string'
 | 
			
		||||
                            ? enumDef.schema
 | 
			
		||||
                            : undefined,
 | 
			
		||||
                    name: enumDef.name,
 | 
			
		||||
                    kind: DBCustomTypeKind.enum,
 | 
			
		||||
                    values,
 | 
			
		||||
                    order: 0,
 | 
			
		||||
                } satisfies DBCustomType;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            id: generateDiagramId(),
 | 
			
		||||
            name: 'DBML Import',
 | 
			
		||||
            databaseType: DatabaseType.GENERIC,
 | 
			
		||||
            databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
            tables,
 | 
			
		||||
            relationships,
 | 
			
		||||
            customTypes,
 | 
			
		||||
            createdAt: new Date(),
 | 
			
		||||
            updatedAt: new Date(),
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -329,6 +329,27 @@ function compareFieldProperties({
 | 
			
		||||
        changedAttributes.push('comments');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        (newField.characterMaximumLength || oldField.characterMaximumLength) &&
 | 
			
		||||
        oldField.characterMaximumLength !== newField.characterMaximumLength
 | 
			
		||||
    ) {
 | 
			
		||||
        changedAttributes.push('characterMaximumLength');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        (newField.scale || oldField.scale) &&
 | 
			
		||||
        oldField.scale !== newField.scale
 | 
			
		||||
    ) {
 | 
			
		||||
        changedAttributes.push('scale');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        (newField.precision || oldField.precision) &&
 | 
			
		||||
        oldField.precision !== newField.precision
 | 
			
		||||
    ) {
 | 
			
		||||
        changedAttributes.push('precision');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changedAttributes.length > 0) {
 | 
			
		||||
        for (const attribute of changedAttributes) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,10 @@ export type FieldDiffAttribute =
 | 
			
		||||
    | 'primaryKey'
 | 
			
		||||
    | 'unique'
 | 
			
		||||
    | 'nullable'
 | 
			
		||||
    | 'comments';
 | 
			
		||||
    | 'comments'
 | 
			
		||||
    | 'characterMaximumLength'
 | 
			
		||||
    | 'precision'
 | 
			
		||||
    | 'scale';
 | 
			
		||||
 | 
			
		||||
export const fieldDiffAttributeSchema: z.ZodType<FieldDiffAttribute> = z.union([
 | 
			
		||||
    z.literal('name'),
 | 
			
		||||
@@ -61,8 +64,8 @@ export interface FieldDiffChanged {
 | 
			
		||||
    fieldId: string;
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attribute: FieldDiffAttribute;
 | 
			
		||||
    oldValue: string | boolean | DataType;
 | 
			
		||||
    newValue: string | boolean | DataType;
 | 
			
		||||
    oldValue: string | boolean | DataType | number;
 | 
			
		||||
    newValue: string | boolean | DataType | number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const fieldDiffChangedSchema: z.ZodType<FieldDiffChanged> = z.object({
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
 | 
			
		||||
                <NodeResizer
 | 
			
		||||
                    isVisible={focused}
 | 
			
		||||
                    lineClassName="!border-4 !border-transparent"
 | 
			
		||||
                    handleClassName="!h-[18px] !w-[18px] !rounded-full !bg-pink-600"
 | 
			
		||||
                    handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
 | 
			
		||||
                    minHeight={100}
 | 
			
		||||
                    minWidth={100}
 | 
			
		||||
                />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import {
 | 
			
		||||
    calcTableHeight,
 | 
			
		||||
    shouldShowTablesBySchemaFilter,
 | 
			
		||||
} from '@/lib/domain/db-table';
 | 
			
		||||
import { calcTableHeight } from '@/lib/domain/db-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if a table is inside an area based on their positions and dimensions
 | 
			
		||||
@@ -56,31 +53,9 @@ const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
 | 
			
		||||
 */
 | 
			
		||||
export const updateTablesParentAreas = (
 | 
			
		||||
    tables: DBTable[],
 | 
			
		||||
    areas: Area[],
 | 
			
		||||
    hiddenTableIds?: string[],
 | 
			
		||||
    filteredSchemas?: string[]
 | 
			
		||||
    areas: Area[]
 | 
			
		||||
): DBTable[] => {
 | 
			
		||||
    return tables.map((table) => {
 | 
			
		||||
        // Check if table is hidden by direct hiding or schema filter
 | 
			
		||||
        const isHiddenDirectly = hiddenTableIds?.includes(table.id) ?? false;
 | 
			
		||||
        const isHiddenBySchema = !shouldShowTablesBySchemaFilter(
 | 
			
		||||
            table,
 | 
			
		||||
            filteredSchemas
 | 
			
		||||
        );
 | 
			
		||||
        const isHidden = isHiddenDirectly || isHiddenBySchema;
 | 
			
		||||
 | 
			
		||||
        // If table is hidden, remove it from any area
 | 
			
		||||
        if (isHidden) {
 | 
			
		||||
            if (table.parentAreaId !== null) {
 | 
			
		||||
                return {
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    parentAreaId: null,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            return table;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // For visible tables, find containing area as before
 | 
			
		||||
        const containingArea = findContainingArea(table, areas);
 | 
			
		||||
        const newParentAreaId = containingArea?.id || null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,40 +5,26 @@ import React, {
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    X,
 | 
			
		||||
    Search,
 | 
			
		||||
    Eye,
 | 
			
		||||
    EyeOff,
 | 
			
		||||
    Database,
 | 
			
		||||
    Table,
 | 
			
		||||
    Funnel,
 | 
			
		||||
    Layers,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { shouldShowTableSchemaBySchemaFilter } from '@/lib/domain/db-table';
 | 
			
		||||
import {
 | 
			
		||||
    schemaNameToSchemaId,
 | 
			
		||||
    databasesWithSchemas,
 | 
			
		||||
} from '@/lib/domain/db-schema';
 | 
			
		||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { TreeView } from '@/components/tree-view/tree-view';
 | 
			
		||||
import type { TreeNode } from '@/components/tree-view/tree';
 | 
			
		||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
			
		||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
 | 
			
		||||
 | 
			
		||||
export interface CanvasFilterProps {
 | 
			
		||||
    onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NodeType = 'schema' | 'table' | 'area';
 | 
			
		||||
type NodeType = 'schema' | 'table';
 | 
			
		||||
 | 
			
		||||
type SchemaContext = { name: string };
 | 
			
		||||
type AreaContext = { id: string; name: string };
 | 
			
		||||
type TableContext = {
 | 
			
		||||
    tableSchema?: string | null;
 | 
			
		||||
    hidden: boolean;
 | 
			
		||||
@@ -46,7 +32,6 @@ type TableContext = {
 | 
			
		||||
 | 
			
		||||
type NodeContext = {
 | 
			
		||||
    schema: SchemaContext;
 | 
			
		||||
    area: AreaContext;
 | 
			
		||||
    table: TableContext;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -66,19 +51,12 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
        removeHiddenTableId,
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
        filterSchemas,
 | 
			
		||||
        areas,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { fitView, setNodes } = useReactFlow();
 | 
			
		||||
    const [searchQuery, setSearchQuery] = useState('');
 | 
			
		||||
    const [expanded, setExpanded] = useState<Record<string, boolean>>({});
 | 
			
		||||
    const [isFilterVisible, setIsFilterVisible] = useState(false);
 | 
			
		||||
    const [groupBy, setGroupBy] = useState<'schema' | 'area'>('schema');
 | 
			
		||||
    const searchInputRef = useRef<HTMLInputElement>(null);
 | 
			
		||||
    const supportsSchemas = useMemo(
 | 
			
		||||
        () => databasesWithSchemas.includes(databaseType),
 | 
			
		||||
        [databaseType]
 | 
			
		||||
    );
 | 
			
		||||
    const hasAreas = useMemo(() => areas.length > 0, [areas]);
 | 
			
		||||
 | 
			
		||||
    // Extract only the properties needed for tree data
 | 
			
		||||
    const relevantTableData = useMemo<RelevantTableData[]>(
 | 
			
		||||
@@ -93,137 +71,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
 | 
			
		||||
    // Convert tables to tree nodes
 | 
			
		||||
    const treeData = useMemo(() => {
 | 
			
		||||
        if (groupBy === 'area' && hasAreas) {
 | 
			
		||||
            // Group tables by area
 | 
			
		||||
            const tablesByArea = new Map<string, RelevantTableData[]>();
 | 
			
		||||
            const tablesWithoutArea: RelevantTableData[] = [];
 | 
			
		||||
 | 
			
		||||
            // Create a map of area id to area
 | 
			
		||||
            const areaMap = areas.reduce(
 | 
			
		||||
                (acc, area) => {
 | 
			
		||||
                    acc[area.id] = area;
 | 
			
		||||
                    return acc;
 | 
			
		||||
                },
 | 
			
		||||
                {} as Record<string, (typeof areas)[0]>
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            relevantTableData.forEach((table) => {
 | 
			
		||||
                const tableData = tables.find((t) => t.id === table.id);
 | 
			
		||||
                if (
 | 
			
		||||
                    tableData?.parentAreaId &&
 | 
			
		||||
                    areaMap[tableData.parentAreaId]
 | 
			
		||||
                ) {
 | 
			
		||||
                    const areaId = tableData.parentAreaId;
 | 
			
		||||
                    if (!tablesByArea.has(areaId)) {
 | 
			
		||||
                        tablesByArea.set(areaId, []);
 | 
			
		||||
                    }
 | 
			
		||||
                    tablesByArea.get(areaId)!.push(table);
 | 
			
		||||
                } else {
 | 
			
		||||
                    tablesWithoutArea.push(table);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Sort tables within each area
 | 
			
		||||
            tablesByArea.forEach((tables) => {
 | 
			
		||||
                tables.sort((a, b) => a.name.localeCompare(b.name));
 | 
			
		||||
            });
 | 
			
		||||
            tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
 | 
			
		||||
 | 
			
		||||
            // Convert to tree nodes
 | 
			
		||||
            const nodes: TreeNode<NodeType, NodeContext>[] = [];
 | 
			
		||||
 | 
			
		||||
            // Sort all areas by order or name (including empty ones)
 | 
			
		||||
            const sortedAreas = areas.sort((a, b) => {
 | 
			
		||||
                if (a.order !== undefined && b.order !== undefined) {
 | 
			
		||||
                    return a.order - b.order;
 | 
			
		||||
                }
 | 
			
		||||
                return a.name.localeCompare(b.name);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            sortedAreas.forEach((area) => {
 | 
			
		||||
                const areaTables = tablesByArea.get(area.id) || [];
 | 
			
		||||
                const areaNode: TreeNode<NodeType, NodeContext> = {
 | 
			
		||||
                    id: `area-${area.id}`,
 | 
			
		||||
                    name: `${area.name} (${areaTables.length})`,
 | 
			
		||||
                    type: 'area',
 | 
			
		||||
                    isFolder: true,
 | 
			
		||||
                    icon: Layers,
 | 
			
		||||
                    context: { id: area.id, name: area.name },
 | 
			
		||||
                    children: areaTables.map(
 | 
			
		||||
                        (table): TreeNode<NodeType, NodeContext> => {
 | 
			
		||||
                            const tableHidden =
 | 
			
		||||
                                hiddenTableIds?.includes(table.id) ?? false;
 | 
			
		||||
                            const visibleBySchema =
 | 
			
		||||
                                shouldShowTableSchemaBySchemaFilter({
 | 
			
		||||
                                    tableSchema: table.schema,
 | 
			
		||||
                                    filteredSchemas,
 | 
			
		||||
                                });
 | 
			
		||||
                            const hidden = tableHidden || !visibleBySchema;
 | 
			
		||||
 | 
			
		||||
                            return {
 | 
			
		||||
                                id: table.id,
 | 
			
		||||
                                name: table.name,
 | 
			
		||||
                                type: 'table',
 | 
			
		||||
                                isFolder: false,
 | 
			
		||||
                                icon: Table,
 | 
			
		||||
                                context: {
 | 
			
		||||
                                    tableSchema: table.schema,
 | 
			
		||||
                                    hidden: tableHidden,
 | 
			
		||||
                                },
 | 
			
		||||
                                className: hidden ? 'opacity-50' : '',
 | 
			
		||||
                            };
 | 
			
		||||
                        }
 | 
			
		||||
                    ),
 | 
			
		||||
                };
 | 
			
		||||
                nodes.push(areaNode);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Add "No Area" group if there are tables without areas
 | 
			
		||||
            if (tablesWithoutArea.length > 0) {
 | 
			
		||||
                const noAreaNode: TreeNode<NodeType, NodeContext> = {
 | 
			
		||||
                    id: 'area-no-area',
 | 
			
		||||
                    name: `${t('canvas_filter.no_area')} (${tablesWithoutArea.length})`,
 | 
			
		||||
                    type: 'area',
 | 
			
		||||
                    isFolder: true,
 | 
			
		||||
                    icon: Layers,
 | 
			
		||||
                    context: {
 | 
			
		||||
                        id: 'no-area',
 | 
			
		||||
                        name: t('canvas_filter.no_area'),
 | 
			
		||||
                    },
 | 
			
		||||
                    className: 'opacity-75',
 | 
			
		||||
                    children: tablesWithoutArea.map(
 | 
			
		||||
                        (table): TreeNode<NodeType, NodeContext> => {
 | 
			
		||||
                            const tableHidden =
 | 
			
		||||
                                hiddenTableIds?.includes(table.id) ?? false;
 | 
			
		||||
                            const visibleBySchema =
 | 
			
		||||
                                shouldShowTableSchemaBySchemaFilter({
 | 
			
		||||
                                    tableSchema: table.schema,
 | 
			
		||||
                                    filteredSchemas,
 | 
			
		||||
                                });
 | 
			
		||||
                            const hidden = tableHidden || !visibleBySchema;
 | 
			
		||||
 | 
			
		||||
                            return {
 | 
			
		||||
                                id: table.id,
 | 
			
		||||
                                name: table.name,
 | 
			
		||||
                                type: 'table',
 | 
			
		||||
                                isFolder: false,
 | 
			
		||||
                                icon: Table,
 | 
			
		||||
                                context: {
 | 
			
		||||
                                    tableSchema: table.schema,
 | 
			
		||||
                                    hidden: tableHidden,
 | 
			
		||||
                                },
 | 
			
		||||
                                className: hidden ? 'opacity-50' : '',
 | 
			
		||||
                            };
 | 
			
		||||
                        }
 | 
			
		||||
                    ),
 | 
			
		||||
                };
 | 
			
		||||
                nodes.push(noAreaNode);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return nodes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Default schema grouping
 | 
			
		||||
        // Group tables by schema
 | 
			
		||||
        const tablesBySchema = new Map<string, RelevantTableData[]>();
 | 
			
		||||
 | 
			
		||||
@@ -287,36 +134,16 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return nodes;
 | 
			
		||||
    }, [
 | 
			
		||||
        relevantTableData,
 | 
			
		||||
        databaseType,
 | 
			
		||||
        hiddenTableIds,
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
        groupBy,
 | 
			
		||||
        hasAreas,
 | 
			
		||||
        areas,
 | 
			
		||||
        tables,
 | 
			
		||||
        t,
 | 
			
		||||
    ]);
 | 
			
		||||
    }, [relevantTableData, databaseType, hiddenTableIds, filteredSchemas]);
 | 
			
		||||
 | 
			
		||||
    // Initialize expanded state with all schemas expanded only when grouping changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setExpanded((prevExpanded) => {
 | 
			
		||||
            const newExpanded: Record<string, boolean> = {};
 | 
			
		||||
 | 
			
		||||
            // Preserve existing expanded states for nodes that still exist
 | 
			
		||||
            treeData.forEach((node) => {
 | 
			
		||||
                if (node.id in prevExpanded) {
 | 
			
		||||
                    newExpanded[node.id] = prevExpanded[node.id];
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Default new nodes to expanded
 | 
			
		||||
                    newExpanded[node.id] = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return newExpanded;
 | 
			
		||||
    // Initialize expanded state with all schemas expanded
 | 
			
		||||
    useMemo(() => {
 | 
			
		||||
        const initialExpanded: Record<string, boolean> = {};
 | 
			
		||||
        treeData.forEach((node) => {
 | 
			
		||||
            initialExpanded[node.id] = true;
 | 
			
		||||
        });
 | 
			
		||||
    }, [groupBy, treeData]);
 | 
			
		||||
        setExpanded(initialExpanded);
 | 
			
		||||
    }, [treeData]);
 | 
			
		||||
 | 
			
		||||
    // Filter tree data based on search query
 | 
			
		||||
    const filteredTreeData: TreeNode<NodeType, NodeContext>[] = useMemo(() => {
 | 
			
		||||
@@ -392,45 +219,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
    // Render component that's always visible (eye indicator)
 | 
			
		||||
    const renderActions = useCallback(
 | 
			
		||||
        (node: TreeNode<NodeType, NodeContext>) => {
 | 
			
		||||
            if (node.type === 'area') {
 | 
			
		||||
                return (
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="ghost"
 | 
			
		||||
                        size="sm"
 | 
			
		||||
                        className="size-7 h-fit p-0"
 | 
			
		||||
                        disabled={!node.children || node.children.length === 0}
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                            e.stopPropagation();
 | 
			
		||||
                            // Toggle all tables in this area
 | 
			
		||||
                            const allHidden =
 | 
			
		||||
                                (node.children?.length > 0 &&
 | 
			
		||||
                                    node.children?.every((child) =>
 | 
			
		||||
                                        hiddenTableIds?.includes(child.id)
 | 
			
		||||
                                    )) ||
 | 
			
		||||
                                false;
 | 
			
		||||
 | 
			
		||||
                            node.children?.forEach((child) => {
 | 
			
		||||
                                if (child.type === 'table') {
 | 
			
		||||
                                    if (allHidden) {
 | 
			
		||||
                                        removeHiddenTableId(child.id);
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        addHiddenTableId(child.id);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {node.children?.every((child) =>
 | 
			
		||||
                            hiddenTableIds?.includes(child.id)
 | 
			
		||||
                        ) ? (
 | 
			
		||||
                            <EyeOff className="size-3.5 text-muted-foreground" />
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <Eye className="size-3.5" />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (node.type === 'schema') {
 | 
			
		||||
                const schemaContext = node.context as SchemaContext;
 | 
			
		||||
                const schemaId = schemaNameToSchemaId(schemaContext.name);
 | 
			
		||||
@@ -495,12 +283,35 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
                        className="size-7 h-fit p-0"
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                            e.stopPropagation();
 | 
			
		||||
                            // Simply toggle the table visibility
 | 
			
		||||
                            toggleTableVisibility(tableId, !hidden);
 | 
			
		||||
                            if (!visibleBySchema && tableSchema) {
 | 
			
		||||
                                // Unhide schema and hide all other tables
 | 
			
		||||
                                const schemaId =
 | 
			
		||||
                                    schemaNameToSchemaId(tableSchema);
 | 
			
		||||
                                filterSchemas([
 | 
			
		||||
                                    ...(filteredSchemas ?? []),
 | 
			
		||||
                                    schemaId,
 | 
			
		||||
                                ]);
 | 
			
		||||
                                const schemaNode = treeData.find(
 | 
			
		||||
                                    (s) =>
 | 
			
		||||
                                        (s.context as SchemaContext).name ===
 | 
			
		||||
                                        tableSchema
 | 
			
		||||
                                );
 | 
			
		||||
                                if (schemaNode) {
 | 
			
		||||
                                    schemaNode.children?.forEach((child) => {
 | 
			
		||||
                                        if (
 | 
			
		||||
                                            child.id !== tableId &&
 | 
			
		||||
                                            !hiddenTableIds?.includes(child.id)
 | 
			
		||||
                                        ) {
 | 
			
		||||
                                            addHiddenTableId(child.id);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                toggleTableVisibility(tableId, !hidden);
 | 
			
		||||
                            }
 | 
			
		||||
                        }}
 | 
			
		||||
                        disabled={!visibleBySchema}
 | 
			
		||||
                    >
 | 
			
		||||
                        {hidden ? (
 | 
			
		||||
                        {hidden || !visibleBySchema ? (
 | 
			
		||||
                            <EyeOff className="size-3.5 text-muted-foreground" />
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <Eye className="size-3.5" />
 | 
			
		||||
@@ -515,6 +326,7 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
            toggleTableVisibility,
 | 
			
		||||
            filteredSchemas,
 | 
			
		||||
            filterSchemas,
 | 
			
		||||
            treeData,
 | 
			
		||||
            hiddenTableIds,
 | 
			
		||||
            addHiddenTableId,
 | 
			
		||||
            removeHiddenTableId,
 | 
			
		||||
@@ -591,42 +403,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
 | 
			
		||||
                        className="h-full pl-9"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                {hasAreas && (
 | 
			
		||||
                    <div className="mt-2">
 | 
			
		||||
                        <ToggleGroup
 | 
			
		||||
                            type="single"
 | 
			
		||||
                            value={groupBy}
 | 
			
		||||
                            onValueChange={(value) => {
 | 
			
		||||
                                if (value)
 | 
			
		||||
                                    setGroupBy(value as 'schema' | 'area');
 | 
			
		||||
                            }}
 | 
			
		||||
                            className="w-full justify-start"
 | 
			
		||||
                        >
 | 
			
		||||
                            <ToggleGroupItem
 | 
			
		||||
                                value="schema"
 | 
			
		||||
                                aria-label={
 | 
			
		||||
                                    supportsSchemas
 | 
			
		||||
                                        ? 'Group by schema'
 | 
			
		||||
                                        : 'Default'
 | 
			
		||||
                                }
 | 
			
		||||
                                className="h-8 flex-1 gap-1.5 text-xs"
 | 
			
		||||
                            >
 | 
			
		||||
                                <Database className="size-3.5" />
 | 
			
		||||
                                {supportsSchemas
 | 
			
		||||
                                    ? t('canvas_filter.group_by_schema')
 | 
			
		||||
                                    : t('canvas_filter.default_grouping')}
 | 
			
		||||
                            </ToggleGroupItem>
 | 
			
		||||
                            <ToggleGroupItem
 | 
			
		||||
                                value="area"
 | 
			
		||||
                                aria-label="Group by area"
 | 
			
		||||
                                className="h-8 flex-1 gap-1.5 text-xs"
 | 
			
		||||
                            >
 | 
			
		||||
                                <Layers className="size-3.5" />
 | 
			
		||||
                                {t('canvas_filter.group_by_area')}
 | 
			
		||||
                            </ToggleGroupItem>
 | 
			
		||||
                        </ToggleGroup>
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {/* Table Tree */}
 | 
			
		||||
 
 | 
			
		||||
@@ -144,48 +144,15 @@ const tableToTableNode = (
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const areaToAreaNode = (
 | 
			
		||||
    area: Area,
 | 
			
		||||
    tables: DBTable[],
 | 
			
		||||
    hiddenTableIds?: string[],
 | 
			
		||||
    filteredSchemas?: string[]
 | 
			
		||||
): AreaNodeType => {
 | 
			
		||||
    // Check if all tables in this area are hidden
 | 
			
		||||
    const tablesInArea = tables.filter(
 | 
			
		||||
        (table) => table.parentAreaId === area.id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Don't hide area if it has no tables (empty area)
 | 
			
		||||
    if (tablesInArea.length === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
            id: area.id,
 | 
			
		||||
            type: 'area',
 | 
			
		||||
            position: { x: area.x, y: area.y },
 | 
			
		||||
            data: { area },
 | 
			
		||||
            width: area.width,
 | 
			
		||||
            height: area.height,
 | 
			
		||||
            zIndex: -10,
 | 
			
		||||
            hidden: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const allTablesHidden = tablesInArea.every(
 | 
			
		||||
        (table) =>
 | 
			
		||||
            hiddenTableIds?.includes(table.id) ||
 | 
			
		||||
            !shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        id: area.id,
 | 
			
		||||
        type: 'area',
 | 
			
		||||
        position: { x: area.x, y: area.y },
 | 
			
		||||
        data: { area },
 | 
			
		||||
        width: area.width,
 | 
			
		||||
        height: area.height,
 | 
			
		||||
        zIndex: -10,
 | 
			
		||||
        hidden: allTablesHidden,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
const areaToAreaNode = (area: Area): AreaNodeType => ({
 | 
			
		||||
    id: area.id,
 | 
			
		||||
    type: 'area',
 | 
			
		||||
    position: { x: area.x, y: area.y },
 | 
			
		||||
    data: { area },
 | 
			
		||||
    width: area.width,
 | 
			
		||||
    height: area.height,
 | 
			
		||||
    zIndex: -10,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface CanvasProps {
 | 
			
		||||
    initialTables: DBTable[];
 | 
			
		||||
@@ -448,14 +415,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                        },
 | 
			
		||||
                    };
 | 
			
		||||
                }),
 | 
			
		||||
                ...areas.map((area) =>
 | 
			
		||||
                    areaToAreaNode(
 | 
			
		||||
                        area,
 | 
			
		||||
                        tables,
 | 
			
		||||
                        hiddenTableIds,
 | 
			
		||||
                        filteredSchemas
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
                ...areas.map(areaToAreaNode),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            // Check if nodes actually changed
 | 
			
		||||
@@ -505,12 +465,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const checkParentAreas = debounce(() => {
 | 
			
		||||
            const updatedTables = updateTablesParentAreas(
 | 
			
		||||
                tables,
 | 
			
		||||
                areas,
 | 
			
		||||
                hiddenTableIds,
 | 
			
		||||
                filteredSchemas
 | 
			
		||||
            );
 | 
			
		||||
            const updatedTables = updateTablesParentAreas(tables, areas);
 | 
			
		||||
            const needsUpdate: Array<{
 | 
			
		||||
                id: string;
 | 
			
		||||
                parentAreaId: string | null;
 | 
			
		||||
@@ -520,6 +475,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                const oldTable = tables[index];
 | 
			
		||||
                if (
 | 
			
		||||
                    oldTable &&
 | 
			
		||||
                    (!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
 | 
			
		||||
                    newTable.parentAreaId !== oldTable.parentAreaId
 | 
			
		||||
                ) {
 | 
			
		||||
                    needsUpdate.push({
 | 
			
		||||
@@ -550,14 +506,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
        }, 300);
 | 
			
		||||
 | 
			
		||||
        checkParentAreas();
 | 
			
		||||
    }, [
 | 
			
		||||
        tablePositions,
 | 
			
		||||
        areas,
 | 
			
		||||
        updateTablesState,
 | 
			
		||||
        tables,
 | 
			
		||||
        hiddenTableIds,
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
    ]);
 | 
			
		||||
    }, [tablePositions, areas, updateTablesState, tables]);
 | 
			
		||||
 | 
			
		||||
    const onConnectHandler = useCallback(
 | 
			
		||||
        async (params: AddEdgeParams) => {
 | 
			
		||||
@@ -1064,6 +1013,21 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                    overlapGraph
 | 
			
		||||
                );
 | 
			
		||||
                setOverlapGraph(newOverlappingGraph);
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    setNodes((prevNodes) =>
 | 
			
		||||
                        prevNodes.map((n) => {
 | 
			
		||||
                            if (n.id === event.data.id) {
 | 
			
		||||
                                return {
 | 
			
		||||
                                    ...n,
 | 
			
		||||
                                    measured,
 | 
			
		||||
                                };
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return n;
 | 
			
		||||
                        })
 | 
			
		||||
                    );
 | 
			
		||||
                }, 0);
 | 
			
		||||
            } else if (
 | 
			
		||||
                event.action === 'add_field' ||
 | 
			
		||||
                event.action === 'remove_field'
 | 
			
		||||
@@ -1103,7 +1067,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                setOverlapGraph(overlappingTablesInDiagram);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [overlapGraph, setOverlapGraph, getNode, nodes, filteredSchemas]
 | 
			
		||||
        [
 | 
			
		||||
            overlapGraph,
 | 
			
		||||
            setOverlapGraph,
 | 
			
		||||
            getNode,
 | 
			
		||||
            nodes,
 | 
			
		||||
            filteredSchemas,
 | 
			
		||||
            setNodes,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    events.useSubscription(eventConsumer);
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,13 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            checkIfNewField,
 | 
			
		||||
            getFieldNewName,
 | 
			
		||||
            getFieldNewType,
 | 
			
		||||
            getFieldNewNullable,
 | 
			
		||||
            getFieldNewPrimaryKey,
 | 
			
		||||
            getFieldNewCharacterMaximumLength,
 | 
			
		||||
            getFieldNewPrecision,
 | 
			
		||||
            getFieldNewScale,
 | 
			
		||||
            checkIfFieldHasChange,
 | 
			
		||||
            isSummaryOnly,
 | 
			
		||||
        } = useDiff();
 | 
			
		||||
 | 
			
		||||
        const [diffState, setDiffState] = useState<{
 | 
			
		||||
@@ -160,12 +166,22 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            isDiffNewField: boolean;
 | 
			
		||||
            fieldDiffChangedName: string | null;
 | 
			
		||||
            fieldDiffChangedType: DBField['type'] | null;
 | 
			
		||||
            fieldDiffChangedNullable: boolean | null;
 | 
			
		||||
            fieldDiffChangedCharacterMaximumLength: string | null;
 | 
			
		||||
            fieldDiffChangedScale: number | null;
 | 
			
		||||
            fieldDiffChangedPrecision: number | null;
 | 
			
		||||
            fieldDiffChangedPrimaryKey: boolean | null;
 | 
			
		||||
            isDiffFieldChanged: boolean;
 | 
			
		||||
        }>({
 | 
			
		||||
            isDiffFieldRemoved: false,
 | 
			
		||||
            isDiffNewField: false,
 | 
			
		||||
            fieldDiffChangedName: null,
 | 
			
		||||
            fieldDiffChangedType: null,
 | 
			
		||||
            fieldDiffChangedNullable: null,
 | 
			
		||||
            fieldDiffChangedCharacterMaximumLength: null,
 | 
			
		||||
            fieldDiffChangedScale: null,
 | 
			
		||||
            fieldDiffChangedPrecision: null,
 | 
			
		||||
            fieldDiffChangedPrimaryKey: null,
 | 
			
		||||
            isDiffFieldChanged: false,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -183,6 +199,22 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                    fieldDiffChangedType: getFieldNewType({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    fieldDiffChangedNullable: getFieldNewNullable({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    fieldDiffChangedPrimaryKey: getFieldNewPrimaryKey({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    fieldDiffChangedCharacterMaximumLength:
 | 
			
		||||
                        getFieldNewCharacterMaximumLength({
 | 
			
		||||
                            fieldId: field.id,
 | 
			
		||||
                        }),
 | 
			
		||||
                    fieldDiffChangedScale: getFieldNewScale({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    fieldDiffChangedPrecision: getFieldNewPrecision({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    isDiffFieldChanged: checkIfFieldHasChange({
 | 
			
		||||
                        fieldId: field.id,
 | 
			
		||||
                        tableId: tableNodeId,
 | 
			
		||||
@@ -195,7 +227,12 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            checkIfNewField,
 | 
			
		||||
            getFieldNewName,
 | 
			
		||||
            getFieldNewType,
 | 
			
		||||
            getFieldNewPrimaryKey,
 | 
			
		||||
            getFieldNewNullable,
 | 
			
		||||
            checkIfFieldHasChange,
 | 
			
		||||
            getFieldNewCharacterMaximumLength,
 | 
			
		||||
            getFieldNewPrecision,
 | 
			
		||||
            getFieldNewScale,
 | 
			
		||||
            field.id,
 | 
			
		||||
            tableNodeId,
 | 
			
		||||
        ]);
 | 
			
		||||
@@ -206,6 +243,11 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            fieldDiffChangedName,
 | 
			
		||||
            fieldDiffChangedType,
 | 
			
		||||
            isDiffFieldChanged,
 | 
			
		||||
            fieldDiffChangedNullable,
 | 
			
		||||
            fieldDiffChangedPrimaryKey,
 | 
			
		||||
            fieldDiffChangedCharacterMaximumLength,
 | 
			
		||||
            fieldDiffChangedScale,
 | 
			
		||||
            fieldDiffChangedPrecision,
 | 
			
		||||
        } = diffState;
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = useCallback((e: React.MouseEvent) => {
 | 
			
		||||
@@ -233,6 +275,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        'z-0 max-h-0 overflow-hidden opacity-0': !visible,
 | 
			
		||||
                        'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
 | 
			
		||||
                            isDiffFieldChanged &&
 | 
			
		||||
                            !isSummaryOnly &&
 | 
			
		||||
                            !isDiffFieldRemoved &&
 | 
			
		||||
                            !isDiffNewField,
 | 
			
		||||
                        'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
 | 
			
		||||
@@ -297,7 +340,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        <SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
 | 
			
		||||
                    ) : isDiffNewField ? (
 | 
			
		||||
                        <SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
 | 
			
		||||
                    ) : isDiffFieldChanged ? (
 | 
			
		||||
                    ) : isDiffFieldChanged && !isSummaryOnly ? (
 | 
			
		||||
                        <SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {editMode && !readonly ? (
 | 
			
		||||
@@ -330,6 +373,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                                    isDiffNewField,
 | 
			
		||||
                                'text-sky-800 font-normal dark:text-sky-200':
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                    !isSummaryOnly &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isDiffNewField,
 | 
			
		||||
                            })}
 | 
			
		||||
@@ -359,7 +403,9 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                </div>
 | 
			
		||||
                {editMode ? null : (
 | 
			
		||||
                    <div className="ml-2 flex shrink-0 items-center justify-end gap-1.5">
 | 
			
		||||
                        {field.primaryKey ? (
 | 
			
		||||
                        {(field.primaryKey &&
 | 
			
		||||
                            fieldDiffChangedPrimaryKey === null) ||
 | 
			
		||||
                        fieldDiffChangedPrimaryKey ? (
 | 
			
		||||
                            <div
 | 
			
		||||
                                className={cn(
 | 
			
		||||
                                    'text-muted-foreground',
 | 
			
		||||
@@ -371,6 +417,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                                        ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                        : '',
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                        !isSummaryOnly &&
 | 
			
		||||
                                        !isDiffFieldRemoved &&
 | 
			
		||||
                                        !isDiffNewField
 | 
			
		||||
                                        ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
@@ -394,6 +441,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                                    : '',
 | 
			
		||||
                                isDiffFieldChanged &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isSummaryOnly &&
 | 
			
		||||
                                    !isDiffNewField
 | 
			
		||||
                                    ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                    : ''
 | 
			
		||||
@@ -412,9 +460,36 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                                        }
 | 
			
		||||
                                    </>
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    `${field.type.name.split(' ')[0]}${showFieldAttributes ? generateDBFieldSuffix(field) : ''}`
 | 
			
		||||
                                    `${field.type.name.split(' ')[0]}${
 | 
			
		||||
                                        showFieldAttributes
 | 
			
		||||
                                            ? generateDBFieldSuffix({
 | 
			
		||||
                                                  ...field,
 | 
			
		||||
                                                  ...{
 | 
			
		||||
                                                      precision:
 | 
			
		||||
                                                          fieldDiffChangedPrecision ??
 | 
			
		||||
                                                          field.precision,
 | 
			
		||||
                                                      scale:
 | 
			
		||||
                                                          fieldDiffChangedScale ??
 | 
			
		||||
                                                          field.scale,
 | 
			
		||||
                                                      characterMaximumLength:
 | 
			
		||||
                                                          fieldDiffChangedCharacterMaximumLength ??
 | 
			
		||||
                                                          field.characterMaximumLength,
 | 
			
		||||
                                                  },
 | 
			
		||||
                                              })
 | 
			
		||||
                                            : ''
 | 
			
		||||
                                    }`
 | 
			
		||||
                                )}
 | 
			
		||||
                                {fieldDiffChangedNullable !== null ? (
 | 
			
		||||
                                    fieldDiffChangedNullable ? (
 | 
			
		||||
                                        <span className="font-semibold">?</span>
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        <span className="line-through">?</span>
 | 
			
		||||
                                    )
 | 
			
		||||
                                ) : field.nullable ? (
 | 
			
		||||
                                    '?'
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    ''
 | 
			
		||||
                                )}
 | 
			
		||||
                                {field.nullable ? '?' : ''}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {readonly ? null : (
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
            checkIfTableHasChange,
 | 
			
		||||
            checkIfNewTable,
 | 
			
		||||
            checkIfTableRemoved,
 | 
			
		||||
            isSummaryOnly,
 | 
			
		||||
        } = useDiff();
 | 
			
		||||
 | 
			
		||||
        const fields = useMemo(() => table.fields, [table.fields]);
 | 
			
		||||
@@ -312,7 +313,10 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                    hasHighlightedCustomType
 | 
			
		||||
                        ? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-yellow-500 ring-offset-2 animate-scale'
 | 
			
		||||
                        : '',
 | 
			
		||||
                    isDiffTableChanged && !isDiffNewTable && !isDiffTableRemoved
 | 
			
		||||
                    isDiffTableChanged &&
 | 
			
		||||
                        !isSummaryOnly &&
 | 
			
		||||
                        !isDiffNewTable &&
 | 
			
		||||
                        !isDiffTableRemoved
 | 
			
		||||
                        ? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
 | 
			
		||||
                        : '',
 | 
			
		||||
                    isDiffNewTable
 | 
			
		||||
@@ -327,7 +331,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                isOverlapping,
 | 
			
		||||
                highlightOverlappingTables,
 | 
			
		||||
                hasHighlightedCustomType,
 | 
			
		||||
 | 
			
		||||
                isSummaryOnly,
 | 
			
		||||
                isDiffTableChanged,
 | 
			
		||||
                isDiffNewTable,
 | 
			
		||||
                isDiffTableRemoved,
 | 
			
		||||
@@ -364,7 +368,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                                ? 'new'
 | 
			
		||||
                                : isDiffTableRemoved
 | 
			
		||||
                                  ? 'removed'
 | 
			
		||||
                                  : isDiffTableChanged
 | 
			
		||||
                                  : isDiffTableChanged && !isSummaryOnly
 | 
			
		||||
                                    ? 'changed'
 | 
			
		||||
                                    : 'none'
 | 
			
		||||
                        }
 | 
			
		||||
@@ -397,7 +401,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                                        Table Removed
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : isDiffTableChanged ? (
 | 
			
		||||
                            ) : isDiffTableChanged && !isSummaryOnly ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <SquareDot
 | 
			
		||||
@@ -433,7 +437,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                                <Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : isDiffTableChanged ? (
 | 
			
		||||
                            ) : isDiffTableChanged && !isSummaryOnly ? (
 | 
			
		||||
                                <Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,10 @@
 | 
			
		||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
 | 
			
		||||
import React, {
 | 
			
		||||
    useMemo,
 | 
			
		||||
    useState,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
@@ -7,8 +13,28 @@ import type { EffectiveTheme } from '@/context/theme-context/theme-context';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import { ArrowLeftRight } from 'lucide-react';
 | 
			
		||||
import {
 | 
			
		||||
    AlertCircle,
 | 
			
		||||
    ArrowLeftRight,
 | 
			
		||||
    Check,
 | 
			
		||||
    Pencil,
 | 
			
		||||
    PencilOff,
 | 
			
		||||
    Undo2,
 | 
			
		||||
    X,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { generateDBMLFromDiagram } from '@/lib/dbml/dbml-export/dbml-export';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import { applyDBMLChanges } from '@/lib/dbml/apply-dbml/apply-dbml';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce';
 | 
			
		||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
 | 
			
		||||
import {
 | 
			
		||||
    clearErrorHighlight,
 | 
			
		||||
    highlightErrorLine,
 | 
			
		||||
} from '@/components/code-snippet/dbml/utils';
 | 
			
		||||
import type * as monaco from 'monaco-editor';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
 | 
			
		||||
 | 
			
		||||
export interface TableDBMLProps {
 | 
			
		||||
    filteredTables: DBTable[];
 | 
			
		||||
@@ -18,62 +44,53 @@ const getEditorTheme = (theme: EffectiveTheme) => {
 | 
			
		||||
    return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
    const { currentDiagram, updateDiagramData, databaseType } = useChartDB();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
 | 
			
		||||
        'inline'
 | 
			
		||||
    );
 | 
			
		||||
    const [isLoading, setIsLoading] = useState(true);
 | 
			
		||||
    const [standardDbml, setStandardDbml] = useState('');
 | 
			
		||||
    const [inlineDbml, setInlineDbml] = useState('');
 | 
			
		||||
    const isMountedRef = useRef(true);
 | 
			
		||||
    const [isEditButtonEmphasized, setIsEditButtonEmphasized] = useState(false);
 | 
			
		||||
 | 
			
		||||
    // --- Effect for handling empty field name warnings ---
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        let foundInvalidFields = false;
 | 
			
		||||
        const invalidTableNames = new Set<string>();
 | 
			
		||||
    const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
    const decorationsCollection =
 | 
			
		||||
        useRef<monaco.editor.IEditorDecorationsCollection>();
 | 
			
		||||
 | 
			
		||||
        filteredTables.forEach((table) => {
 | 
			
		||||
            table.fields.forEach((field) => {
 | 
			
		||||
                if (field.name === '') {
 | 
			
		||||
                    foundInvalidFields = true;
 | 
			
		||||
                    invalidTableNames.add(table.name);
 | 
			
		||||
    const handleEditorDidMount = useCallback(
 | 
			
		||||
        (editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
			
		||||
            editorRef.current = editor;
 | 
			
		||||
            decorationsCollection.current =
 | 
			
		||||
                editor.createDecorationsCollection();
 | 
			
		||||
 | 
			
		||||
            if (readOnlyDisposableRef.current) {
 | 
			
		||||
                readOnlyDisposableRef.current.dispose();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const readOnlyDisposable = editor.onDidAttemptReadOnlyEdit(() => {
 | 
			
		||||
                if (emphasisTimeoutRef.current) {
 | 
			
		||||
                    clearTimeout(emphasisTimeoutRef.current);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setIsEditButtonEmphasized(false);
 | 
			
		||||
 | 
			
		||||
                requestAnimationFrame(() => {
 | 
			
		||||
                    setIsEditButtonEmphasized(true);
 | 
			
		||||
 | 
			
		||||
                    emphasisTimeoutRef.current = setTimeout(() => {
 | 
			
		||||
                        setIsEditButtonEmphasized(false);
 | 
			
		||||
                    }, 600);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (foundInvalidFields) {
 | 
			
		||||
            const tableNamesString = Array.from(invalidTableNames).join(', ');
 | 
			
		||||
            toast({
 | 
			
		||||
                title: 'Warning',
 | 
			
		||||
                description: `Some fields had empty names in tables: [${tableNamesString}] and were excluded from the DBML export.`,
 | 
			
		||||
                variant: 'default',
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [filteredTables, toast]); // Depend on filteredTables and toast
 | 
			
		||||
 | 
			
		||||
    // Generate both standard and inline DBML formats
 | 
			
		||||
    const { standardDbml, inlineDbml } = useMemo(() => {
 | 
			
		||||
        // Create a filtered diagram with only the selected tables
 | 
			
		||||
        const filteredDiagram: Diagram = {
 | 
			
		||||
            ...currentDiagram,
 | 
			
		||||
            tables: filteredTables,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = generateDBMLFromDiagram(filteredDiagram);
 | 
			
		||||
 | 
			
		||||
        // Handle errors
 | 
			
		||||
        if (result.error) {
 | 
			
		||||
            toast({
 | 
			
		||||
                title: 'DBML Export Error',
 | 
			
		||||
                description: `Could not generate DBML: ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`,
 | 
			
		||||
                variant: 'destructive',
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            standardDbml: result.standardDbml,
 | 
			
		||||
            inlineDbml: result.inlineDbml,
 | 
			
		||||
        };
 | 
			
		||||
    }, [currentDiagram, filteredTables, toast]);
 | 
			
		||||
            readOnlyDisposableRef.current = readOnlyDisposable;
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Determine which DBML string to display
 | 
			
		||||
    const dbmlToDisplay = useMemo(
 | 
			
		||||
@@ -86,30 +103,339 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
        setDbmlFormat((prev) => (prev === 'inline' ? 'standard' : 'inline'));
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const [isEditMode, setIsEditMode] = useState(false);
 | 
			
		||||
    const [editedDbml, setEditedDbml] = useState<string>('');
 | 
			
		||||
    const lastDBMLChange = useRef(editedDbml);
 | 
			
		||||
    const { calculateDiff, originalDiagram, resetDiff, hasDiff, newDiagram } =
 | 
			
		||||
        useDiff();
 | 
			
		||||
    const { loadDiagramFromData } = useChartDB();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState<string>();
 | 
			
		||||
    const [warningMessage, setWarningMessage] = useState<string>();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { hideLoader, showLoader } = useFullScreenLoader();
 | 
			
		||||
    const emphasisTimeoutRef = useRef<NodeJS.Timeout>();
 | 
			
		||||
    const readOnlyDisposableRef = useRef<monaco.IDisposable>();
 | 
			
		||||
 | 
			
		||||
    // --- Check for empty field name warnings only on mount ---
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Only check when not in edit mode
 | 
			
		||||
        if (isEditMode) return;
 | 
			
		||||
 | 
			
		||||
        let foundInvalidFields = false;
 | 
			
		||||
        const invalidTableNames = new Set<string>();
 | 
			
		||||
 | 
			
		||||
        currentDiagram.tables?.forEach((table) => {
 | 
			
		||||
            table.fields.forEach((field) => {
 | 
			
		||||
                if (field.name === '') {
 | 
			
		||||
                    foundInvalidFields = true;
 | 
			
		||||
                    invalidTableNames.add(table.name);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (foundInvalidFields) {
 | 
			
		||||
            const tableNamesString = Array.from(invalidTableNames).join(', ');
 | 
			
		||||
            setWarningMessage(
 | 
			
		||||
                `Some fields had empty names in tables: [${tableNamesString}] and were excluded from the DBML export.`
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentDiagram.tables, t, isEditMode]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (isEditMode) {
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setErrorMessage(undefined);
 | 
			
		||||
        clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
 | 
			
		||||
        const generateDBML = async () => {
 | 
			
		||||
            setIsLoading(true);
 | 
			
		||||
 | 
			
		||||
            const result = generateDBMLFromDiagram(currentDiagram);
 | 
			
		||||
 | 
			
		||||
            // Handle errors
 | 
			
		||||
            if (result.error) {
 | 
			
		||||
                toast({
 | 
			
		||||
                    title: 'DBML Export Error',
 | 
			
		||||
                    description: `Could not generate DBML: ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`,
 | 
			
		||||
                    variant: 'destructive',
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setStandardDbml(result.standardDbml);
 | 
			
		||||
            setInlineDbml(result.inlineDbml);
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => generateDBML(), 0);
 | 
			
		||||
    }, [currentDiagram, toast, isEditMode]);
 | 
			
		||||
 | 
			
		||||
    // Update editedDbml when dbmlToDisplay changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!isLoading && dbmlToDisplay && !isEditMode) {
 | 
			
		||||
            setEditedDbml(dbmlToDisplay);
 | 
			
		||||
            lastDBMLChange.current = dbmlToDisplay;
 | 
			
		||||
        }
 | 
			
		||||
    }, [dbmlToDisplay, isLoading, isEditMode]);
 | 
			
		||||
 | 
			
		||||
    // Create the showDiff function
 | 
			
		||||
    const showDiff = useCallback(
 | 
			
		||||
        async (dbmlContent: string) => {
 | 
			
		||||
            clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
            setErrorMessage(undefined);
 | 
			
		||||
            try {
 | 
			
		||||
                const diagramFromDBML: Diagram = await importDBMLToDiagram(
 | 
			
		||||
                    dbmlContent,
 | 
			
		||||
                    { databaseType }
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const sourceDiagram: Diagram =
 | 
			
		||||
                    originalDiagram ?? currentDiagram;
 | 
			
		||||
 | 
			
		||||
                const targetDiagram: Diagram = {
 | 
			
		||||
                    ...sourceDiagram,
 | 
			
		||||
                    tables: diagramFromDBML.tables,
 | 
			
		||||
                    relationships: diagramFromDBML.relationships,
 | 
			
		||||
                    customTypes: diagramFromDBML.customTypes,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                const newDiagram = applyDBMLChanges({
 | 
			
		||||
                    sourceDiagram,
 | 
			
		||||
                    targetDiagram,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (originalDiagram) {
 | 
			
		||||
                    resetDiff();
 | 
			
		||||
                    loadDiagramFromData(originalDiagram);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                calculateDiff({
 | 
			
		||||
                    diagram: sourceDiagram,
 | 
			
		||||
                    newDiagram,
 | 
			
		||||
                    options: { summaryOnly: true },
 | 
			
		||||
                });
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                const dbmlError = parseDBMLError(error);
 | 
			
		||||
 | 
			
		||||
                if (dbmlError) {
 | 
			
		||||
                    highlightErrorLine({
 | 
			
		||||
                        error: dbmlError,
 | 
			
		||||
                        model: editorRef.current?.getModel(),
 | 
			
		||||
                        editorDecorationsCollection:
 | 
			
		||||
                            decorationsCollection.current,
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    setErrorMessage(
 | 
			
		||||
                        t('import_dbml_dialog.error.description') +
 | 
			
		||||
                            ` (1 error found - in line ${dbmlError.line})`
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            t,
 | 
			
		||||
            originalDiagram,
 | 
			
		||||
            currentDiagram,
 | 
			
		||||
            resetDiff,
 | 
			
		||||
            loadDiagramFromData,
 | 
			
		||||
            calculateDiff,
 | 
			
		||||
            databaseType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const debouncedShowDiff = useDebounce(showDiff, 1000);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!isEditMode || !editedDbml) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Only calculate diff if the DBML has changed
 | 
			
		||||
        if (editedDbml === lastDBMLChange.current) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        lastDBMLChange.current = editedDbml;
 | 
			
		||||
 | 
			
		||||
        debouncedShowDiff(editedDbml);
 | 
			
		||||
    }, [editedDbml, isEditMode, debouncedShowDiff]);
 | 
			
		||||
 | 
			
		||||
    const acceptChanges = useCallback(async () => {
 | 
			
		||||
        if (!editedDbml) return;
 | 
			
		||||
        if (!newDiagram) return;
 | 
			
		||||
 | 
			
		||||
        showLoader();
 | 
			
		||||
 | 
			
		||||
        await updateDiagramData(newDiagram, { forceUpdateStorage: true });
 | 
			
		||||
 | 
			
		||||
        resetDiff();
 | 
			
		||||
        setEditedDbml(editedDbml);
 | 
			
		||||
        setIsEditMode(false);
 | 
			
		||||
        lastDBMLChange.current = editedDbml;
 | 
			
		||||
        hideLoader();
 | 
			
		||||
    }, [
 | 
			
		||||
        editedDbml,
 | 
			
		||||
        updateDiagramData,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
        resetDiff,
 | 
			
		||||
        showLoader,
 | 
			
		||||
        hideLoader,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const undoChanges = useCallback(() => {
 | 
			
		||||
        if (!editedDbml) return;
 | 
			
		||||
        if (!originalDiagram) return;
 | 
			
		||||
 | 
			
		||||
        loadDiagramFromData(originalDiagram);
 | 
			
		||||
        setIsEditMode(false);
 | 
			
		||||
        resetDiff();
 | 
			
		||||
        setEditedDbml(dbmlToDisplay);
 | 
			
		||||
        lastDBMLChange.current = dbmlToDisplay;
 | 
			
		||||
    }, [
 | 
			
		||||
        editedDbml,
 | 
			
		||||
        loadDiagramFromData,
 | 
			
		||||
        originalDiagram,
 | 
			
		||||
        resetDiff,
 | 
			
		||||
        dbmlToDisplay,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        isMountedRef.current = true;
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            isMountedRef.current = false;
 | 
			
		||||
 | 
			
		||||
            if (emphasisTimeoutRef.current) {
 | 
			
		||||
                clearTimeout(emphasisTimeoutRef.current);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (readOnlyDisposableRef.current) {
 | 
			
		||||
                readOnlyDisposableRef.current.dispose();
 | 
			
		||||
                readOnlyDisposableRef.current = undefined;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const currentUndoChanges = undoChanges;
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                if (!isMountedRef.current) {
 | 
			
		||||
                    currentUndoChanges();
 | 
			
		||||
                }
 | 
			
		||||
            }, 0);
 | 
			
		||||
        };
 | 
			
		||||
    }, [undoChanges]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <CodeSnippet
 | 
			
		||||
            code={dbmlToDisplay}
 | 
			
		||||
            actionsTooltipSide="right"
 | 
			
		||||
            className="my-0.5"
 | 
			
		||||
            actions={[
 | 
			
		||||
                {
 | 
			
		||||
                    label: `Show ${dbmlFormat === 'inline' ? 'Standard' : 'Inline'} Refs`,
 | 
			
		||||
                    icon: ArrowLeftRight,
 | 
			
		||||
                    onClick: toggleFormat,
 | 
			
		||||
                },
 | 
			
		||||
            ]}
 | 
			
		||||
            editorProps={{
 | 
			
		||||
                height: '100%',
 | 
			
		||||
                defaultLanguage: 'dbml',
 | 
			
		||||
                beforeMount: setupDBMLLanguage,
 | 
			
		||||
                loading: false,
 | 
			
		||||
                theme: getEditorTheme(effectiveTheme),
 | 
			
		||||
                options: {
 | 
			
		||||
                    wordWrap: 'off',
 | 
			
		||||
                    mouseWheelZoom: false,
 | 
			
		||||
                    domReadOnly: true,
 | 
			
		||||
                },
 | 
			
		||||
            }}
 | 
			
		||||
        />
 | 
			
		||||
        <>
 | 
			
		||||
            <CodeSnippet
 | 
			
		||||
                code={editedDbml}
 | 
			
		||||
                loading={isLoading}
 | 
			
		||||
                actionsTooltipSide="right"
 | 
			
		||||
                className="my-0.5"
 | 
			
		||||
                allowCopy={!isEditMode}
 | 
			
		||||
                actions={
 | 
			
		||||
                    isEditMode && hasDiff
 | 
			
		||||
                        ? [
 | 
			
		||||
                              {
 | 
			
		||||
                                  label: 'Accept Changes',
 | 
			
		||||
                                  icon: Check,
 | 
			
		||||
                                  onClick: acceptChanges,
 | 
			
		||||
                                  className:
 | 
			
		||||
                                      'h-7 items-center gap-1.5 rounded-md border border-green-200 bg-green-50 px-2.5 py-1.5 text-xs font-medium text-green-600 shadow-sm hover:bg-green-100 dark:border-green-800 dark:bg-green-800 dark:text-green-200 dark:hover:bg-green-700',
 | 
			
		||||
                              },
 | 
			
		||||
                              {
 | 
			
		||||
                                  label: 'Undo Changes',
 | 
			
		||||
                                  icon: Undo2,
 | 
			
		||||
                                  onClick: undoChanges,
 | 
			
		||||
                                  className:
 | 
			
		||||
                                      'h-7 items-center gap-1.5 rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-xs font-medium text-red-600 shadow-sm hover:bg-red-100 dark:border-red-800 dark:bg-red-800 dark:text-red-200 dark:hover:bg-red-700',
 | 
			
		||||
                              },
 | 
			
		||||
                          ]
 | 
			
		||||
                        : isEditMode && !hasDiff
 | 
			
		||||
                          ? [
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: 'View',
 | 
			
		||||
                                    icon: PencilOff,
 | 
			
		||||
                                    onClick: () =>
 | 
			
		||||
                                        setIsEditMode((prev) => !prev),
 | 
			
		||||
                                },
 | 
			
		||||
                            ]
 | 
			
		||||
                          : [
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: `Show ${dbmlFormat === 'inline' ? 'Standard' : 'Inline'} Refs`,
 | 
			
		||||
                                    icon: ArrowLeftRight,
 | 
			
		||||
                                    onClick: toggleFormat,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: 'Edit',
 | 
			
		||||
                                    icon: Pencil,
 | 
			
		||||
                                    onClick: () =>
 | 
			
		||||
                                        setIsEditMode((prev) => !prev),
 | 
			
		||||
                                    className: isEditButtonEmphasized
 | 
			
		||||
                                        ? 'dbml-edit-button-emphasis'
 | 
			
		||||
                                        : undefined,
 | 
			
		||||
                                },
 | 
			
		||||
                            ]
 | 
			
		||||
                }
 | 
			
		||||
                editorProps={{
 | 
			
		||||
                    height: '100%',
 | 
			
		||||
                    defaultLanguage: 'dbml',
 | 
			
		||||
                    beforeMount: setupDBMLLanguage,
 | 
			
		||||
                    theme: getEditorTheme(effectiveTheme),
 | 
			
		||||
                    onMount: handleEditorDidMount,
 | 
			
		||||
                    options: {
 | 
			
		||||
                        wordWrap: 'off',
 | 
			
		||||
                        mouseWheelZoom: false,
 | 
			
		||||
                        readOnly: !isEditMode,
 | 
			
		||||
                    },
 | 
			
		||||
                    onChange: (value) => {
 | 
			
		||||
                        setEditedDbml(value ?? '');
 | 
			
		||||
                    },
 | 
			
		||||
                }}
 | 
			
		||||
            />
 | 
			
		||||
            {warningMessage ? (
 | 
			
		||||
                <div className="my-2 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-900/50 dark:bg-blue-950/20">
 | 
			
		||||
                    <div className="flex items-start gap-2">
 | 
			
		||||
                        <AlertCircle className="mt-0.5 size-4 shrink-0 text-blue-600 dark:text-blue-400" />
 | 
			
		||||
                        <div className="flex-1">
 | 
			
		||||
                            <p className="text-sm font-medium text-blue-800 dark:text-blue-200">
 | 
			
		||||
                                Warning
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p className="mt-0.5 text-xs text-blue-700 dark:text-blue-300">
 | 
			
		||||
                                {warningMessage}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <button
 | 
			
		||||
                            onClick={() => setWarningMessage(undefined)}
 | 
			
		||||
                            className="rounded p-0.5 text-blue-600 hover:bg-blue-100 dark:text-blue-400 dark:hover:bg-blue-900/50"
 | 
			
		||||
                            aria-label="Close warning"
 | 
			
		||||
                        >
 | 
			
		||||
                            <X className="size-3.5" />
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
            {errorMessage ? (
 | 
			
		||||
                <div className="my-2 rounded-md border border-orange-200 bg-orange-50 p-3 dark:border-orange-900/50 dark:bg-orange-950/20">
 | 
			
		||||
                    <div className="flex gap-2">
 | 
			
		||||
                        <AlertCircle className="mt-0.5 size-4 shrink-0 text-orange-600 dark:text-orange-400" />
 | 
			
		||||
                        <div className="flex-1">
 | 
			
		||||
                            <p className="text-sm font-medium text-orange-800 dark:text-orange-200">
 | 
			
		||||
                                Syntax Error
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p className="mt-0.5 text-xs text-orange-700 dark:text-orange-300">
 | 
			
		||||
                                {errorMessage ||
 | 
			
		||||
                                    t('import_dbml_dialog.error.description')}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -17,22 +17,13 @@ import {
 | 
			
		||||
    verticalListSortingStrategy,
 | 
			
		||||
} from '@dnd-kit/sortable';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb.ts';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
export interface TableListProps {
 | 
			
		||||
    tables: DBTable[];
 | 
			
		||||
    groupBy?: 'schema' | 'area';
 | 
			
		||||
    areas?: Area[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableList: React.FC<TableListProps> = ({
 | 
			
		||||
    tables,
 | 
			
		||||
    groupBy = 'schema',
 | 
			
		||||
    areas = [],
 | 
			
		||||
}) => {
 | 
			
		||||
export const TableList: React.FC<TableListProps> = ({ tables }) => {
 | 
			
		||||
    const { updateTablesState } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    const { openTableFromSidebar, openedTableInSidebar } = useLayout();
 | 
			
		||||
    const lastOpenedTable = React.useRef<string | null>(null);
 | 
			
		||||
@@ -96,134 +87,62 @@ export const TableList: React.FC<TableListProps> = ({
 | 
			
		||||
        }
 | 
			
		||||
    }, [scrollToTable, openedTableInSidebar]);
 | 
			
		||||
 | 
			
		||||
    const sortTables = useCallback((tablesToSort: DBTable[]) => {
 | 
			
		||||
        return tablesToSort.sort((table1: DBTable, table2: DBTable) => {
 | 
			
		||||
            // if one table has order and the other doesn't, the one with order should come first
 | 
			
		||||
            if (table1.order && table2.order === undefined) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (table1.order === undefined && table2.order) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if both tables have order, sort by order
 | 
			
		||||
            if (table1.order !== undefined && table2.order !== undefined) {
 | 
			
		||||
                return (table1.order ?? 0) - (table2.order ?? 0);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if both tables don't have order, sort by name
 | 
			
		||||
            if (table1.isView === table2.isView) {
 | 
			
		||||
                // Both are either tables or views, so sort alphabetically by name
 | 
			
		||||
                return table1.name.localeCompare(table2.name);
 | 
			
		||||
            }
 | 
			
		||||
            // If one is a view and the other is not, put tables first
 | 
			
		||||
            return table1.isView ? 1 : -1;
 | 
			
		||||
        });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const groupedTables = useMemo(() => {
 | 
			
		||||
        if (groupBy === 'area') {
 | 
			
		||||
            // Group tables by area
 | 
			
		||||
            const tablesWithArea: Record<string, DBTable[]> = {};
 | 
			
		||||
            const tablesWithoutArea: DBTable[] = [];
 | 
			
		||||
 | 
			
		||||
            // Create a map of area id to area name
 | 
			
		||||
            const areaMap = areas.reduce(
 | 
			
		||||
                (acc, area) => {
 | 
			
		||||
                    acc[area.id] = area.name;
 | 
			
		||||
                    return acc;
 | 
			
		||||
                },
 | 
			
		||||
                {} as Record<string, string>
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            tables.forEach((table) => {
 | 
			
		||||
                if (table.parentAreaId && areaMap[table.parentAreaId]) {
 | 
			
		||||
                    if (!tablesWithArea[table.parentAreaId]) {
 | 
			
		||||
                        tablesWithArea[table.parentAreaId] = [];
 | 
			
		||||
                    }
 | 
			
		||||
                    tablesWithArea[table.parentAreaId].push(table);
 | 
			
		||||
                } else {
 | 
			
		||||
                    tablesWithoutArea.push(table);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Sort areas by their order or name
 | 
			
		||||
            const sortedAreas = areas
 | 
			
		||||
                .filter((area) => tablesWithArea[area.id])
 | 
			
		||||
                .sort((a, b) => {
 | 
			
		||||
                    if (a.order !== undefined && b.order !== undefined) {
 | 
			
		||||
                        return a.order - b.order;
 | 
			
		||||
                    }
 | 
			
		||||
                    return a.name.localeCompare(b.name);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                ...sortedAreas.map((area) => ({
 | 
			
		||||
                    id: area.id,
 | 
			
		||||
                    name: area.name,
 | 
			
		||||
                    tables: sortTables(tablesWithArea[area.id]),
 | 
			
		||||
                })),
 | 
			
		||||
                ...(tablesWithoutArea.length > 0
 | 
			
		||||
                    ? [
 | 
			
		||||
                          {
 | 
			
		||||
                              id: 'no-area',
 | 
			
		||||
                              name: t('side_panel.tables_section.no_area'),
 | 
			
		||||
                              tables: sortTables(tablesWithoutArea),
 | 
			
		||||
                          },
 | 
			
		||||
                      ]
 | 
			
		||||
                    : []),
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Default - no grouping, just return all tables as one group
 | 
			
		||||
        return [
 | 
			
		||||
            {
 | 
			
		||||
                id: 'all',
 | 
			
		||||
                name: '',
 | 
			
		||||
                tables: sortTables(tables),
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
    }, [tables, groupBy, areas, sortTables, t]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex flex-col gap-3">
 | 
			
		||||
            {groupedTables.map((group) => (
 | 
			
		||||
                <div key={group.id}>
 | 
			
		||||
                    {group.name && (
 | 
			
		||||
                        <div className="mb-2 px-2 text-xs font-medium text-muted-foreground">
 | 
			
		||||
                            {group.name}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                    <Accordion
 | 
			
		||||
                        type="single"
 | 
			
		||||
                        collapsible
 | 
			
		||||
                        className="flex w-full flex-col gap-1"
 | 
			
		||||
                        value={openedTableInSidebar}
 | 
			
		||||
                        onValueChange={openTableFromSidebar}
 | 
			
		||||
                        onAnimationEnd={handleScrollToTable}
 | 
			
		||||
                    >
 | 
			
		||||
                        <DndContext
 | 
			
		||||
                            sensors={sensors}
 | 
			
		||||
                            collisionDetection={closestCenter}
 | 
			
		||||
                            onDragEnd={handleDragEnd}
 | 
			
		||||
                        >
 | 
			
		||||
                            <SortableContext
 | 
			
		||||
                                items={group.tables}
 | 
			
		||||
                                strategy={verticalListSortingStrategy}
 | 
			
		||||
                            >
 | 
			
		||||
                                {group.tables.map((table) => (
 | 
			
		||||
                                    <TableListItem
 | 
			
		||||
                                        key={table.id}
 | 
			
		||||
                                        table={table}
 | 
			
		||||
                                        ref={refs[table.id]}
 | 
			
		||||
                                    />
 | 
			
		||||
                                ))}
 | 
			
		||||
                            </SortableContext>
 | 
			
		||||
                        </DndContext>
 | 
			
		||||
                    </Accordion>
 | 
			
		||||
                </div>
 | 
			
		||||
            ))}
 | 
			
		||||
        </div>
 | 
			
		||||
        <Accordion
 | 
			
		||||
            type="single"
 | 
			
		||||
            collapsible
 | 
			
		||||
            className="flex w-full flex-col gap-1"
 | 
			
		||||
            value={openedTableInSidebar}
 | 
			
		||||
            onValueChange={openTableFromSidebar}
 | 
			
		||||
            onAnimationEnd={handleScrollToTable}
 | 
			
		||||
        >
 | 
			
		||||
            <DndContext
 | 
			
		||||
                sensors={sensors}
 | 
			
		||||
                collisionDetection={closestCenter}
 | 
			
		||||
                onDragEnd={handleDragEnd}
 | 
			
		||||
            >
 | 
			
		||||
                <SortableContext
 | 
			
		||||
                    items={tables}
 | 
			
		||||
                    strategy={verticalListSortingStrategy}
 | 
			
		||||
                >
 | 
			
		||||
                    {tables
 | 
			
		||||
                        .sort((table1: DBTable, table2: DBTable) => {
 | 
			
		||||
                            // if one table has order and the other doesn't, the one with order should come first
 | 
			
		||||
                            if (table1.order && table2.order === undefined) {
 | 
			
		||||
                                return -1;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (table1.order === undefined && table2.order) {
 | 
			
		||||
                                return 1;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // if both tables have order, sort by order
 | 
			
		||||
                            if (
 | 
			
		||||
                                table1.order !== undefined &&
 | 
			
		||||
                                table2.order !== undefined
 | 
			
		||||
                            ) {
 | 
			
		||||
                                return (
 | 
			
		||||
                                    (table1.order ?? 0) - (table2.order ?? 0)
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // if both tables don't have order, sort by name
 | 
			
		||||
                            if (table1.isView === table2.isView) {
 | 
			
		||||
                                // Both are either tables or views, so sort alphabetically by name
 | 
			
		||||
                                return table1.name.localeCompare(table2.name);
 | 
			
		||||
                            }
 | 
			
		||||
                            // If one is a view and the other is not, put tables first
 | 
			
		||||
                            return table1.isView ? 1 : -1;
 | 
			
		||||
                        })
 | 
			
		||||
                        .map((table) => (
 | 
			
		||||
                            <TableListItem
 | 
			
		||||
                                key={table.id}
 | 
			
		||||
                                table={table}
 | 
			
		||||
                                ref={refs[table.id]}
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                </SortableContext>
 | 
			
		||||
            </DndContext>
 | 
			
		||||
        </Accordion>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React, { useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import { TableList } from './table-list/table-list';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Table, List, X, Code, Layers, Database } from 'lucide-react';
 | 
			
		||||
import { Table, List, X, Code } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
 | 
			
		||||
@@ -21,32 +21,18 @@ import { TableDBML } from './table-dbml/table-dbml';
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
			
		||||
import { getOperatingSystem } from '@/lib/utils';
 | 
			
		||||
import type { DBSchema } from '@/lib/domain';
 | 
			
		||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
 | 
			
		||||
import { databasesWithSchemas } from '@/lib/domain/db-schema';
 | 
			
		||||
 | 
			
		||||
export interface TablesSectionProps {}
 | 
			
		||||
 | 
			
		||||
export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
    const {
 | 
			
		||||
        createTable,
 | 
			
		||||
        tables,
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
        schemas,
 | 
			
		||||
        areas,
 | 
			
		||||
        databaseType,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { createTable, tables, filteredSchemas, schemas } = useChartDB();
 | 
			
		||||
    const { openTableSchemaDialog } = useDialog();
 | 
			
		||||
    const viewport = useViewport();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { openTableFromSidebar } = useLayout();
 | 
			
		||||
    const [filterText, setFilterText] = React.useState('');
 | 
			
		||||
    const [showDBML, setShowDBML] = useState(false);
 | 
			
		||||
    const [groupBy, setGroupBy] = useState<'schema' | 'area'>('schema');
 | 
			
		||||
    const filterInputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
    const supportsSchemas = useMemo(
 | 
			
		||||
        () => databasesWithSchemas.includes(databaseType),
 | 
			
		||||
        [databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const filteredTables = useMemo(() => {
 | 
			
		||||
        const filterTableName: (table: DBTable) => boolean = (table) =>
 | 
			
		||||
@@ -176,37 +162,6 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
                    {t('side_panel.tables_section.add_table')}
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="mb-2">
 | 
			
		||||
                <ToggleGroup
 | 
			
		||||
                    type="single"
 | 
			
		||||
                    value={groupBy}
 | 
			
		||||
                    onValueChange={(value) => {
 | 
			
		||||
                        if (value) setGroupBy(value as 'schema' | 'area');
 | 
			
		||||
                    }}
 | 
			
		||||
                    className="w-full justify-start"
 | 
			
		||||
                >
 | 
			
		||||
                    <ToggleGroupItem
 | 
			
		||||
                        value="schema"
 | 
			
		||||
                        aria-label={
 | 
			
		||||
                            supportsSchemas ? 'Group by schema' : 'Default'
 | 
			
		||||
                        }
 | 
			
		||||
                        className="h-8 flex-1 gap-1.5 text-xs"
 | 
			
		||||
                    >
 | 
			
		||||
                        <Database className="size-3.5" />
 | 
			
		||||
                        {supportsSchemas
 | 
			
		||||
                            ? t('side_panel.tables_section.group_by_schema')
 | 
			
		||||
                            : t('side_panel.tables_section.default_grouping')}
 | 
			
		||||
                    </ToggleGroupItem>
 | 
			
		||||
                    <ToggleGroupItem
 | 
			
		||||
                        value="area"
 | 
			
		||||
                        aria-label="Group by area"
 | 
			
		||||
                        className="h-8 flex-1 gap-1.5 text-xs"
 | 
			
		||||
                    >
 | 
			
		||||
                        <Layers className="size-3.5" />
 | 
			
		||||
                        {t('side_panel.tables_section.group_by_area')}
 | 
			
		||||
                    </ToggleGroupItem>
 | 
			
		||||
                </ToggleGroup>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex flex-1 flex-col overflow-hidden">
 | 
			
		||||
                {showDBML ? (
 | 
			
		||||
                    <TableDBML filteredTables={filteredTables} />
 | 
			
		||||
@@ -238,11 +193,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <TableList
 | 
			
		||||
                                tables={filteredTables}
 | 
			
		||||
                                groupBy={groupBy}
 | 
			
		||||
                                areas={areas}
 | 
			
		||||
                            />
 | 
			
		||||
                            <TableList tables={filteredTables} />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                )}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user