mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +00:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			jf/add_edi
			...
			459698b5d0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					459698b5d0 | ||
| 
						 | 
					7ad0e7712d | ||
| 
						 | 
					34475add32 | ||
| 
						 | 
					38fedcec0c | ||
| 
						 | 
					498655e7b7 | ||
| 
						 | 
					bcd8aa9378 | ||
| 
						 | 
					b15bc945ac | ||
| 
						 | 
					c3c646bf7c | ||
| 
						 | 
					57b3b8777f | ||
| 
						 | 
					bb033091b1 | ||
| 
						 | 
					c9ac8929c5 | ||
| 
						 | 
					c567c0a5f3 | ||
| 
						 | 
					2dc1a6fc75 | ||
| 
						 | 
					98f6edd5c8 | ||
| 
						 | 
					47a7a73a13 | ||
| 
						 | 
					d71b46e8b5 | ||
| 
						 | 
					e4c4a3b354 | ||
| 
						 | 
					1b8d51b73c | ||
| 
						 | 
					93d72a896b | ||
| 
						 | 
					9991077978 | ||
| 
						 | 
					bc82f9d6a8 | ||
| 
						 | 
					26dc299cd2 | ||
| 
						 | 
					d6ba4a4074 | ||
| 
						 | 
					d09379e8be | ||
| 
						 | 
					bdc41c0b74 | ||
| 
						 | 
					d3dbf41894 | ||
| 
						 | 
					e6783a89cc | ||
| 
						 | 
					af3638da7a | ||
| 
						 | 
					8954d893bb | 
							
								
								
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,44 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.16.0](https://github.com/chartdb/chartdb/compare/v1.15.1...v1.16.0) (2025-09-24)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add area context menu and UI improvements ([#918](https://github.com/chartdb/chartdb/issues/918)) ([d09379e](https://github.com/chartdb/chartdb/commit/d09379e8be0fa3c83ca77ff62ae815fe4db9869b))
 | 
			
		||||
* add quick table mode on canvas ([#915](https://github.com/chartdb/chartdb/issues/915)) ([8954d89](https://github.com/chartdb/chartdb/commit/8954d893bbfee45bb311380115fb14ebbf3a3133))
 | 
			
		||||
* add zoom navigation buttons to canvas filter for tables and areas ([#903](https://github.com/chartdb/chartdb/issues/903)) ([a0fb1ed](https://github.com/chartdb/chartdb/commit/a0fb1ed08ba18b66354fa3498d610097a83d4afc))
 | 
			
		||||
* **import-db:** add DBML syntax to import database dialog ([#768](https://github.com/chartdb/chartdb/issues/768)) ([af3638d](https://github.com/chartdb/chartdb/commit/af3638da7a9b70f281ceaddbc2f712a713d90cda))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add areas width and height + table width to diff check ([#931](https://github.com/chartdb/chartdb/issues/931)) ([98f6edd](https://github.com/chartdb/chartdb/commit/98f6edd5c8a8e9130e892b2d841744e0cf63a7bf))
 | 
			
		||||
* add diff x,y ([#928](https://github.com/chartdb/chartdb/issues/928)) ([e4c4a3b](https://github.com/chartdb/chartdb/commit/e4c4a3b35484d9ece955a5aec577603dde73d634))
 | 
			
		||||
* add support for ALTER TABLE ADD COLUMN in PostgreSQL importer ([#892](https://github.com/chartdb/chartdb/issues/892)) ([ec6e46f](https://github.com/chartdb/chartdb/commit/ec6e46fe81ea1806c179c50a4c5779d8596008aa))
 | 
			
		||||
* add tests for diff ([#930](https://github.com/chartdb/chartdb/issues/930)) ([47a7a73](https://github.com/chartdb/chartdb/commit/47a7a73a137b87dfa6e67aff5f939cf64ccf4601))
 | 
			
		||||
* dbml edit mode glitch ([#925](https://github.com/chartdb/chartdb/issues/925)) ([93d72a8](https://github.com/chartdb/chartdb/commit/93d72a896bab9aa79d8ea2f876126887e432214c))
 | 
			
		||||
* dbml export default time bug ([#922](https://github.com/chartdb/chartdb/issues/922)) ([bc82f9d](https://github.com/chartdb/chartdb/commit/bc82f9d6a8fe4de2f7e0fc465e0a20c5dbf8f41d))
 | 
			
		||||
* dbml export renaming fields bug ([#921](https://github.com/chartdb/chartdb/issues/921)) ([26dc299](https://github.com/chartdb/chartdb/commit/26dc299cd28e9890d191c13f84a15ac38ae48b11))
 | 
			
		||||
* **dbml:** export array fields without quotes ([#911](https://github.com/chartdb/chartdb/issues/911)) ([5e81c18](https://github.com/chartdb/chartdb/commit/5e81c1848aaa911990e1e881d62525f5254d6d34))
 | 
			
		||||
* diff logic ([#927](https://github.com/chartdb/chartdb/issues/927)) ([1b8d51b](https://github.com/chartdb/chartdb/commit/1b8d51b73c4ed4b7c5929adcb17a44927c7defca))
 | 
			
		||||
* export dbml issues after upgrade version ([#883](https://github.com/chartdb/chartdb/issues/883)) ([07937a2](https://github.com/chartdb/chartdb/commit/07937a2f51708b1c10b45c2bd1f9a9acf5c3f708))
 | 
			
		||||
* export sql + import metadata lib ([#902](https://github.com/chartdb/chartdb/issues/902)) ([ffddcdc](https://github.com/chartdb/chartdb/commit/ffddcdcc987bacb0e0d7e8dea27d08d3a8c5a8c8))
 | 
			
		||||
* handle bidirectional relationships in DBML export ([#924](https://github.com/chartdb/chartdb/issues/924)) ([9991077](https://github.com/chartdb/chartdb/commit/99910779789a9c6ef113d06bc3de31e35b9b04d1))
 | 
			
		||||
* import dbml set pk field unique ([#920](https://github.com/chartdb/chartdb/issues/920)) ([d6ba4a4](https://github.com/chartdb/chartdb/commit/d6ba4a40749d85d2703f120600df4345dab3c561))
 | 
			
		||||
* improve SQL default value parsing for PostgreSQL, MySQL, and SQL Server with proper type handling and casting support ([#900](https://github.com/chartdb/chartdb/issues/900)) ([fe9ef27](https://github.com/chartdb/chartdb/commit/fe9ef275b8619dcfd7e57541a62a6237a16d29a8))
 | 
			
		||||
* move area utils ([#932](https://github.com/chartdb/chartdb/issues/932)) ([2dc1a6f](https://github.com/chartdb/chartdb/commit/2dc1a6fc7519e0a455b0e1306601195deb156c96))
 | 
			
		||||
* move auto arrange to toolbar ([#904](https://github.com/chartdb/chartdb/issues/904)) ([b016a70](https://github.com/chartdb/chartdb/commit/b016a70691bc22af5720b4de683e8c9353994fcc))
 | 
			
		||||
* remove general db creation ([#901](https://github.com/chartdb/chartdb/issues/901)) ([df89f0b](https://github.com/chartdb/chartdb/commit/df89f0b6b9ba3fcc8b05bae4f60c0dc4ad1d2215))
 | 
			
		||||
* remove many to many rel option ([#933](https://github.com/chartdb/chartdb/issues/933)) ([c567c0a](https://github.com/chartdb/chartdb/commit/c567c0a5f39157b2c430e92192b6750304d7a834))
 | 
			
		||||
* reset increment and default when change field ([#896](https://github.com/chartdb/chartdb/issues/896)) ([e5e1d59](https://github.com/chartdb/chartdb/commit/e5e1d5932762422ea63acfd6cf9fe4f03aa822f7))
 | 
			
		||||
* **sql-import:** handle SQL Server DDL with multiple tables, inline foreign keys, and case-insensitive field matching ([#897](https://github.com/chartdb/chartdb/issues/897)) ([2a64dee](https://github.com/chartdb/chartdb/commit/2a64deebb87a11ee3892024c3273d682bb86f7ef))
 | 
			
		||||
* **sql-import:** support ALTER TABLE ALTER COLUMN TYPE in PostgreSQL importer ([#895](https://github.com/chartdb/chartdb/issues/895)) ([aa29061](https://github.com/chartdb/chartdb/commit/aa290615caf806d7d0374c848d50b4636fde7e96))
 | 
			
		||||
* **sqlite:** improve parser to handle tables without column types and fix column detection ([#914](https://github.com/chartdb/chartdb/issues/914)) ([d3dbf41](https://github.com/chartdb/chartdb/commit/d3dbf41894d74f0ffce9afe3bd810f065aa53017))
 | 
			
		||||
* trigger edit table on canvas from context menu ([#919](https://github.com/chartdb/chartdb/issues/919)) ([bdc41c0](https://github.com/chartdb/chartdb/commit/bdc41c0b74d9d9918e7b6cd2152fa07c0c58ce60))
 | 
			
		||||
* update deps vulns ([#909](https://github.com/chartdb/chartdb/issues/909)) ([2bd9ca2](https://github.com/chartdb/chartdb/commit/2bd9ca25b2c7b1f053ff4fdc8c5cfc1b0e65901d))
 | 
			
		||||
* upgrade dbml lib ([#880](https://github.com/chartdb/chartdb/issues/880)) ([d8e0bc7](https://github.com/chartdb/chartdb/commit/d8e0bc7db8881971ddaea7177bcebee13cc865f6))
 | 
			
		||||
 | 
			
		||||
## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "version": "1.15.1",
 | 
			
		||||
    "version": "1.16.0",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "chartdb",
 | 
			
		||||
            "version": "1.15.1",
 | 
			
		||||
            "version": "1.16.0",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
                "@dbml/core": "^3.13.9",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.15.1",
 | 
			
		||||
    "version": "1.16.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ export interface CodeSnippetProps {
 | 
			
		||||
    className?: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
    codeToCopy?: string;
 | 
			
		||||
    language?: 'sql' | 'shell';
 | 
			
		||||
    language?: 'sql' | 'shell' | 'dbml';
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    autoScroll?: boolean;
 | 
			
		||||
    isComplete?: boolean;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
        base: 'vs-dark',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'comment', foreground: '6A9955' }, // Comments
 | 
			
		||||
            { token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
 | 
			
		||||
            { token: 'string', foreground: 'CE9178' }, // Strings
 | 
			
		||||
            { token: 'annotation', foreground: '9CDCFE' }, // [annotations]
 | 
			
		||||
            { token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
 | 
			
		||||
            { token: 'operator', foreground: 'D4D4D4' }, // Operators
 | 
			
		||||
            { token: 'datatype', foreground: '4EC9B0' }, // Data types
 | 
			
		||||
            { token: 'type', foreground: '4EC9B0' }, // Data types
 | 
			
		||||
            { token: 'identifier', foreground: '9CDCFE' }, // Field names
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
@@ -23,12 +25,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
        base: 'vs',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'comment', foreground: '008000' }, // Comments
 | 
			
		||||
            { token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
 | 
			
		||||
            { token: 'string', foreground: 'A31515' }, // Strings
 | 
			
		||||
            { token: 'annotation', foreground: '001080' }, // [annotations]
 | 
			
		||||
            { token: 'delimiter', foreground: '000000' }, // Braces {}
 | 
			
		||||
            { token: 'operator', foreground: '000000' }, // Operators
 | 
			
		||||
            { token: 'type', foreground: '267F99' }, // Data types
 | 
			
		||||
            { token: 'identifier', foreground: '001080' }, // Field names
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
@@ -37,23 +41,59 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
    const datatypePattern = dataTypesNames.join('|');
 | 
			
		||||
 | 
			
		||||
    monaco.languages.setMonarchTokensProvider('dbml', {
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum', 'enum'],
 | 
			
		||||
        datatypes: dataTypesNames,
 | 
			
		||||
        operators: ['>', '<', '-'],
 | 
			
		||||
 | 
			
		||||
        tokenizer: {
 | 
			
		||||
            root: [
 | 
			
		||||
                // Comments
 | 
			
		||||
                [/\/\/.*$/, 'comment'],
 | 
			
		||||
 | 
			
		||||
                // Keywords - case insensitive
 | 
			
		||||
                [
 | 
			
		||||
                    /\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',
 | 
			
		||||
                ],
 | 
			
		||||
 | 
			
		||||
                // Annotations in brackets
 | 
			
		||||
                [/\[.*?\]/, 'annotation'],
 | 
			
		||||
 | 
			
		||||
                // Strings
 | 
			
		||||
                [/'''/, 'string', '@tripleQuoteString'],
 | 
			
		||||
                [/".*?"/, 'string'],
 | 
			
		||||
                [/'.*?'/, 'string'],
 | 
			
		||||
                [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-terminated string
 | 
			
		||||
                [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-terminated string
 | 
			
		||||
                [/"/, 'string', '@string_double'],
 | 
			
		||||
                [/'/, 'string', '@string_single'],
 | 
			
		||||
                [/`.*?`/, 'string'],
 | 
			
		||||
                [/[{}]/, 'delimiter'],
 | 
			
		||||
                [/[<>]/, 'operator'],
 | 
			
		||||
                [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
 | 
			
		||||
 | 
			
		||||
                // Delimiters and operators
 | 
			
		||||
                [/[{}()]/, 'delimiter'],
 | 
			
		||||
                [/[<>-]/, 'operator'],
 | 
			
		||||
                [/:/, 'delimiter'],
 | 
			
		||||
 | 
			
		||||
                // Data types
 | 
			
		||||
                [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'],
 | 
			
		||||
 | 
			
		||||
                // Numbers
 | 
			
		||||
                [/\d+/, 'number'],
 | 
			
		||||
 | 
			
		||||
                // Identifiers
 | 
			
		||||
                [/[a-zA-Z_]\w*/, 'identifier'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            string_double: [
 | 
			
		||||
                [/[^\\"]+/, 'string'],
 | 
			
		||||
                [/\\./, 'string.escape'],
 | 
			
		||||
                [/"/, 'string', '@pop'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            string_single: [
 | 
			
		||||
                [/[^\\']+/, 'string'],
 | 
			
		||||
                [/\\./, 'string.escape'],
 | 
			
		||||
                [/'/, 'string', '@pop'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            tripleQuoteString: [
 | 
			
		||||
                [/[^']+/, 'string'],
 | 
			
		||||
                [/'''/, 'string', '@pop'],
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,14 @@ export interface ColorPickerProps {
 | 
			
		||||
    color: string;
 | 
			
		||||
    onChange: (color: string) => void;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    popoverOnMouseDown?: (e: React.MouseEvent) => void;
 | 
			
		||||
    popoverOnClick?: (e: React.MouseEvent) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ColorPicker = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof PopoverTrigger>,
 | 
			
		||||
    ColorPickerProps
 | 
			
		||||
>(({ color, onChange, disabled }, ref) => {
 | 
			
		||||
>(({ color, onChange, disabled, popoverOnMouseDown, popoverOnClick }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Popover>
 | 
			
		||||
            <PopoverTrigger
 | 
			
		||||
@@ -37,7 +39,11 @@ export const ColorPicker = React.forwardRef<
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
            </PopoverTrigger>
 | 
			
		||||
            <PopoverContent className="w-fit">
 | 
			
		||||
            <PopoverContent
 | 
			
		||||
                className="w-fit"
 | 
			
		||||
                onMouseDown={popoverOnMouseDown}
 | 
			
		||||
                onClick={popoverOnClick}
 | 
			
		||||
            >
 | 
			
		||||
                <div className="grid grid-cols-4 gap-2">
 | 
			
		||||
                    {colorOptions.map((option) => (
 | 
			
		||||
                        <div
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,9 @@ export interface SelectBoxProps {
 | 
			
		||||
    popoverClassName?: string;
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
    footerButtons?: React.ReactNode;
 | 
			
		||||
    commandOnMouseDown?: (e: React.MouseEvent) => void;
 | 
			
		||||
    commandOnClick?: (e: React.MouseEvent) => void;
 | 
			
		||||
    onSearchChange?: (search: string) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
@@ -83,6 +86,9 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            popoverClassName,
 | 
			
		||||
            readonly,
 | 
			
		||||
            footerButtons,
 | 
			
		||||
            commandOnMouseDown,
 | 
			
		||||
            commandOnClick,
 | 
			
		||||
            onSearchChange,
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => {
 | 
			
		||||
@@ -236,6 +242,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                    <CommandItem
 | 
			
		||||
                        className="flex items-center"
 | 
			
		||||
                        key={option.value}
 | 
			
		||||
                        value={option.label}
 | 
			
		||||
                        keywords={option.regex ? [option.regex] : undefined}
 | 
			
		||||
                        onSelect={() =>
 | 
			
		||||
                            handleSelect(
 | 
			
		||||
@@ -243,6 +250,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                                matches?.map((match) => match?.toString())
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        onMouseDown={commandOnMouseDown}
 | 
			
		||||
                        onClick={commandOnClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {multiple && (
 | 
			
		||||
                            <div
 | 
			
		||||
@@ -288,7 +297,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                    </CommandItem>
 | 
			
		||||
                );
 | 
			
		||||
            },
 | 
			
		||||
            [value, multiple, searchTerm, handleSelect, optionSuffix]
 | 
			
		||||
            [
 | 
			
		||||
                value,
 | 
			
		||||
                multiple,
 | 
			
		||||
                searchTerm,
 | 
			
		||||
                handleSelect,
 | 
			
		||||
                optionSuffix,
 | 
			
		||||
                commandOnClick,
 | 
			
		||||
                commandOnMouseDown,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
@@ -366,6 +383,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                        popoverClassName
 | 
			
		||||
                    )}
 | 
			
		||||
                    align="center"
 | 
			
		||||
                    onMouseDown={(e) => e.stopPropagation()}
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                >
 | 
			
		||||
                    <Command
 | 
			
		||||
                        filter={(value, search, keywords) => {
 | 
			
		||||
@@ -388,7 +407,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                        <div className="relative">
 | 
			
		||||
                            <CommandInput
 | 
			
		||||
                                value={searchTerm}
 | 
			
		||||
                                onValueChange={(e) => setSearchTerm(e)}
 | 
			
		||||
                                onValueChange={(e) => {
 | 
			
		||||
                                    setSearchTerm(e);
 | 
			
		||||
                                    onSearchChange?.(e);
 | 
			
		||||
                                }}
 | 
			
		||||
                                ref={ref}
 | 
			
		||||
                                placeholder={inputPlaceholder ?? 'Search...'}
 | 
			
		||||
                                className="h-9"
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,41 @@ export interface CanvasContext {
 | 
			
		||||
    overlapGraph: Graph<string>;
 | 
			
		||||
    setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
    showFilter: boolean;
 | 
			
		||||
    editTableModeTable: {
 | 
			
		||||
        tableId: string;
 | 
			
		||||
        fieldId?: string;
 | 
			
		||||
    } | null;
 | 
			
		||||
    setEditTableModeTable: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<{
 | 
			
		||||
            tableId: string;
 | 
			
		||||
            fieldId?: string;
 | 
			
		||||
        } | null>
 | 
			
		||||
    >;
 | 
			
		||||
    tempFloatingEdge: {
 | 
			
		||||
        sourceNodeId: string;
 | 
			
		||||
        targetNodeId?: string;
 | 
			
		||||
    } | null;
 | 
			
		||||
    setTempFloatingEdge: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<{
 | 
			
		||||
            sourceNodeId: string;
 | 
			
		||||
            targetNodeId?: string;
 | 
			
		||||
        } | null>
 | 
			
		||||
    >;
 | 
			
		||||
    startFloatingEdgeCreation: ({
 | 
			
		||||
        sourceNodeId,
 | 
			
		||||
    }: {
 | 
			
		||||
        sourceNodeId: string;
 | 
			
		||||
    }) => void;
 | 
			
		||||
    endFloatingEdgeCreation: () => void;
 | 
			
		||||
    hoveringTableId: string | null;
 | 
			
		||||
    setHoveringTableId: React.Dispatch<React.SetStateAction<string | null>>;
 | 
			
		||||
    showCreateRelationshipNode: (params: {
 | 
			
		||||
        sourceTableId: string;
 | 
			
		||||
        targetTableId: string;
 | 
			
		||||
        x: number;
 | 
			
		||||
        y: number;
 | 
			
		||||
    }) => void;
 | 
			
		||||
    hideCreateRelationshipNode: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
@@ -23,4 +58,14 @@ export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
    overlapGraph: createGraph(),
 | 
			
		||||
    setShowFilter: emptyFn,
 | 
			
		||||
    showFilter: false,
 | 
			
		||||
    editTableModeTable: null,
 | 
			
		||||
    setEditTableModeTable: emptyFn,
 | 
			
		||||
    tempFloatingEdge: null,
 | 
			
		||||
    setTempFloatingEdge: emptyFn,
 | 
			
		||||
    startFloatingEdgeCreation: emptyFn,
 | 
			
		||||
    endFloatingEdgeCreation: emptyFn,
 | 
			
		||||
    hoveringTableId: null,
 | 
			
		||||
    setHoveringTableId: emptyFn,
 | 
			
		||||
    showCreateRelationshipNode: emptyFn,
 | 
			
		||||
    hideCreateRelationshipNode: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import React, {
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import type { CanvasContext } from './canvas-context';
 | 
			
		||||
import { canvasContext } from './canvas-context';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { adjustTablePositions } from '@/lib/domain/db-table';
 | 
			
		||||
@@ -15,6 +16,10 @@ import { createGraph } from '@/lib/graph';
 | 
			
		||||
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
 | 
			
		||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_RELATIONSHIP_NODE_ID,
 | 
			
		||||
    type CreateRelationshipNodeType,
 | 
			
		||||
} from '@/pages/editor-page/canvas/create-relationship-node/create-relationship-node';
 | 
			
		||||
 | 
			
		||||
interface CanvasProviderProps {
 | 
			
		||||
    children: ReactNode;
 | 
			
		||||
@@ -30,11 +35,21 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
        diagramId,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { filter, loading: filterLoading } = useDiagramFilter();
 | 
			
		||||
    const { fitView } = useReactFlow();
 | 
			
		||||
    const { fitView, screenToFlowPosition, setNodes } = useReactFlow();
 | 
			
		||||
    const [overlapGraph, setOverlapGraph] =
 | 
			
		||||
        useState<Graph<string>>(createGraph());
 | 
			
		||||
    const [editTableModeTable, setEditTableModeTable] = useState<{
 | 
			
		||||
        tableId: string;
 | 
			
		||||
        fieldId?: string;
 | 
			
		||||
    } | null>(null);
 | 
			
		||||
 | 
			
		||||
    const [showFilter, setShowFilter] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const [tempFloatingEdge, setTempFloatingEdge] =
 | 
			
		||||
        useState<CanvasContext['tempFloatingEdge']>(null);
 | 
			
		||||
 | 
			
		||||
    const [hoveringTableId, setHoveringTableId] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
    const diagramIdActiveFilterRef = useRef<string>();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -118,6 +133,66 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const startFloatingEdgeCreation: CanvasContext['startFloatingEdgeCreation'] =
 | 
			
		||||
        useCallback(({ sourceNodeId }) => {
 | 
			
		||||
            setShowFilter(false);
 | 
			
		||||
            setTempFloatingEdge({
 | 
			
		||||
                sourceNodeId,
 | 
			
		||||
            });
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
    const endFloatingEdgeCreation: CanvasContext['endFloatingEdgeCreation'] =
 | 
			
		||||
        useCallback(() => {
 | 
			
		||||
            setTempFloatingEdge(null);
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
    const hideCreateRelationshipNode: CanvasContext['hideCreateRelationshipNode'] =
 | 
			
		||||
        useCallback(() => {
 | 
			
		||||
            setNodes((nds) =>
 | 
			
		||||
                nds.filter((n) => n.id !== CREATE_RELATIONSHIP_NODE_ID)
 | 
			
		||||
            );
 | 
			
		||||
            endFloatingEdgeCreation();
 | 
			
		||||
        }, [setNodes, endFloatingEdgeCreation]);
 | 
			
		||||
 | 
			
		||||
    const showCreateRelationshipNode: CanvasContext['showCreateRelationshipNode'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            ({ sourceTableId, targetTableId, x, y }) => {
 | 
			
		||||
                setTempFloatingEdge((edge) =>
 | 
			
		||||
                    edge
 | 
			
		||||
                        ? {
 | 
			
		||||
                              ...edge,
 | 
			
		||||
                              targetNodeId: targetTableId,
 | 
			
		||||
                          }
 | 
			
		||||
                        : null
 | 
			
		||||
                );
 | 
			
		||||
                const cursorPos = screenToFlowPosition({
 | 
			
		||||
                    x,
 | 
			
		||||
                    y,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                const newNode: CreateRelationshipNodeType = {
 | 
			
		||||
                    id: CREATE_RELATIONSHIP_NODE_ID,
 | 
			
		||||
                    type: 'create-relationship',
 | 
			
		||||
                    position: cursorPos,
 | 
			
		||||
                    data: {
 | 
			
		||||
                        sourceTableId,
 | 
			
		||||
                        targetTableId,
 | 
			
		||||
                    },
 | 
			
		||||
                    draggable: true,
 | 
			
		||||
                    selectable: false,
 | 
			
		||||
                    zIndex: 1000,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                setNodes((nds) => {
 | 
			
		||||
                    const nodesWithoutOldCreateRelationshipNode = nds.filter(
 | 
			
		||||
                        (n) => n.id !== CREATE_RELATIONSHIP_NODE_ID
 | 
			
		||||
                    );
 | 
			
		||||
                    return [...nodesWithoutOldCreateRelationshipNode, newNode];
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [screenToFlowPosition, setNodes]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <canvasContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -127,6 +202,16 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
                overlapGraph,
 | 
			
		||||
                setShowFilter,
 | 
			
		||||
                showFilter,
 | 
			
		||||
                editTableModeTable,
 | 
			
		||||
                setEditTableModeTable,
 | 
			
		||||
                tempFloatingEdge: tempFloatingEdge,
 | 
			
		||||
                setTempFloatingEdge: setTempFloatingEdge,
 | 
			
		||||
                startFloatingEdgeCreation: startFloatingEdgeCreation,
 | 
			
		||||
                endFloatingEdgeCreation: endFloatingEdgeCreation,
 | 
			
		||||
                hoveringTableId,
 | 
			
		||||
                setHoveringTableId,
 | 
			
		||||
                showCreateRelationshipNode,
 | 
			
		||||
                hideCreateRelationshipNode,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,10 +74,10 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        useState<string>();
 | 
			
		||||
 | 
			
		||||
    const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
 | 
			
		||||
        const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
 | 
			
		||||
        const { tablesToAdd, fieldsToAdd, relationshipsToAdd } = event.data;
 | 
			
		||||
        setTables((tables) =>
 | 
			
		||||
            [...tables, ...(tablesAdded ?? [])].map((table) => {
 | 
			
		||||
                const fields = fieldsAdded.get(table.id);
 | 
			
		||||
            [...tables, ...(tablesToAdd ?? [])].map((table) => {
 | 
			
		||||
                const fields = fieldsToAdd.get(table.id);
 | 
			
		||||
                return fields
 | 
			
		||||
                    ? { ...table, fields: [...table.fields, ...fields] }
 | 
			
		||||
                    : table;
 | 
			
		||||
@@ -85,7 +85,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        );
 | 
			
		||||
        setRelationships((relationships) => [
 | 
			
		||||
            ...relationships,
 | 
			
		||||
            ...(relationshipsAdded ?? []),
 | 
			
		||||
            ...(relationshipsToAdd ?? []),
 | 
			
		||||
        ]);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
@@ -350,6 +350,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                isView: false,
 | 
			
		||||
                order: tables.length,
 | 
			
		||||
                ...attributes,
 | 
			
		||||
                schema: attributes?.schema ?? defaultSchemas[databaseType],
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            table.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
 | 
			
		||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
 | 
			
		||||
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
 | 
			
		||||
 | 
			
		||||
@@ -67,12 +66,6 @@ export interface DialogContext {
 | 
			
		||||
        params: Omit<ImportDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeImportDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Import DBML dialog
 | 
			
		||||
    openImportDBMLDialog: (
 | 
			
		||||
        params?: Omit<ImportDBMLDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeImportDBMLDialog: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
@@ -96,6 +89,4 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeExportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDiagramDialog: emptyFn,
 | 
			
		||||
    closeImportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDBMLDialog: emptyFn,
 | 
			
		||||
    closeImportDBMLDialog: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
 | 
			
		||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -132,11 +130,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [openImportDiagramDialog, setOpenImportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    // Import DBML dialog
 | 
			
		||||
    const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
 | 
			
		||||
    const [importDBMLDialogParams, setImportDBMLDialogParams] =
 | 
			
		||||
        useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <dialogContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -165,11 +158,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
 | 
			
		||||
                closeImportDiagramDialog: () =>
 | 
			
		||||
                    setOpenImportDiagramDialog(false),
 | 
			
		||||
                openImportDBMLDialog: (params) => {
 | 
			
		||||
                    setImportDBMLDialogParams(params);
 | 
			
		||||
                    setOpenImportDBMLDialog(true);
 | 
			
		||||
                },
 | 
			
		||||
                closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
@@ -204,10 +192,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            />
 | 
			
		||||
            <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
 | 
			
		||||
            <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
 | 
			
		||||
            <ImportDBMLDialog
 | 
			
		||||
                dialog={{ open: openImportDBMLDialog }}
 | 
			
		||||
                {...importDBMLDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
        </dialogContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,9 @@ export type DiffEventBase<T extends DiffEventType, D> = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffCalculatedData = {
 | 
			
		||||
    tablesAdded: DBTable[];
 | 
			
		||||
    fieldsAdded: Map<string, DBField[]>;
 | 
			
		||||
    relationshipsAdded: DBRelationship[];
 | 
			
		||||
    tablesToAdd: DBTable[];
 | 
			
		||||
    fieldsToAdd: Map<string, DBField[]>;
 | 
			
		||||
    relationshipsToAdd: DBRelationship[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffCalculatedEvent = DiffEventBase<
 | 
			
		||||
@@ -44,15 +44,21 @@ export interface DiffContext {
 | 
			
		||||
        options?: {
 | 
			
		||||
            summaryOnly?: boolean;
 | 
			
		||||
        };
 | 
			
		||||
    }) => void;
 | 
			
		||||
    }) => { foundDiff: boolean };
 | 
			
		||||
    resetDiff: () => void;
 | 
			
		||||
 | 
			
		||||
    // table diff
 | 
			
		||||
    checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    getTableNewName: ({ tableId }: { tableId: string }) => string | null;
 | 
			
		||||
    getTableNewColor: ({ tableId }: { tableId: string }) => string | null;
 | 
			
		||||
    getTableNewName: ({ tableId }: { tableId: string }) => {
 | 
			
		||||
        old: string;
 | 
			
		||||
        new: string;
 | 
			
		||||
    } | null;
 | 
			
		||||
    getTableNewColor: ({ tableId }: { tableId: string }) => {
 | 
			
		||||
        old: string;
 | 
			
		||||
        new: string;
 | 
			
		||||
    } | null;
 | 
			
		||||
 | 
			
		||||
    // field diff
 | 
			
		||||
    checkIfFieldHasChange: ({
 | 
			
		||||
@@ -64,17 +70,41 @@ export interface DiffContext {
 | 
			
		||||
    }) => boolean;
 | 
			
		||||
    checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
 | 
			
		||||
    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;
 | 
			
		||||
    getFieldNewName: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: string; new: string } | null;
 | 
			
		||||
    getFieldNewType: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: DataType; new: DataType } | null;
 | 
			
		||||
    getFieldNewPrimaryKey: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: boolean; new: boolean } | null;
 | 
			
		||||
    getFieldNewNullable: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: boolean; new: boolean } | null;
 | 
			
		||||
    getFieldNewCharacterMaximumLength: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => string | null;
 | 
			
		||||
    getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
    getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
    }) => { old: string; new: string } | null;
 | 
			
		||||
    getFieldNewScale: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: number; new: number } | null;
 | 
			
		||||
    getFieldNewPrecision: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => { old: number; new: number } | null;
 | 
			
		||||
 | 
			
		||||
    // relationship diff
 | 
			
		||||
    checkIfNewRelationship: ({
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
 | 
			
		||||
    const events = useEventEmitter<DiffEvent>();
 | 
			
		||||
 | 
			
		||||
    const generateNewFieldsMap = useCallback(
 | 
			
		||||
    const generateFieldsToAddMap = useCallback(
 | 
			
		||||
        ({
 | 
			
		||||
            diffMap,
 | 
			
		||||
            newDiagram,
 | 
			
		||||
@@ -66,7 +66,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const findNewRelationships = useCallback(
 | 
			
		||||
    const findRelationshipsToAdd = useCallback(
 | 
			
		||||
        ({
 | 
			
		||||
            diffMap,
 | 
			
		||||
            newDiagram,
 | 
			
		||||
@@ -101,7 +101,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            diffMap: DiffMap;
 | 
			
		||||
        }): DiffCalculatedData => {
 | 
			
		||||
            return {
 | 
			
		||||
                tablesAdded:
 | 
			
		||||
                tablesToAdd:
 | 
			
		||||
                    newDiagram?.tables?.filter((table) => {
 | 
			
		||||
                        const tableKey = getDiffMapKey({
 | 
			
		||||
                            diffObject: 'table',
 | 
			
		||||
@@ -114,17 +114,17 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                        );
 | 
			
		||||
                    }) ?? [],
 | 
			
		||||
 | 
			
		||||
                fieldsAdded: generateNewFieldsMap({
 | 
			
		||||
                fieldsToAdd: generateFieldsToAddMap({
 | 
			
		||||
                    diffMap: diffMap,
 | 
			
		||||
                    newDiagram: newDiagram,
 | 
			
		||||
                }),
 | 
			
		||||
                relationshipsAdded: findNewRelationships({
 | 
			
		||||
                relationshipsToAdd: findRelationshipsToAdd({
 | 
			
		||||
                    diffMap: diffMap,
 | 
			
		||||
                    newDiagram: newDiagram,
 | 
			
		||||
                }),
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        [findNewRelationships, generateNewFieldsMap]
 | 
			
		||||
        [findRelationshipsToAdd, generateFieldsToAddMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const calculateDiff: DiffContext['calculateDiff'] = useCallback(
 | 
			
		||||
@@ -149,6 +149,8 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    newDiagram: newDiagramArg,
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return { foundDiff: !!newDiffs.size };
 | 
			
		||||
        },
 | 
			
		||||
        [setDiffMap, events, generateDiffCalculatedData]
 | 
			
		||||
    );
 | 
			
		||||
@@ -165,7 +167,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(tableNameKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                    return {
 | 
			
		||||
                        new: diff.newValue as string,
 | 
			
		||||
                        old: diff.oldValue as string,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -186,7 +191,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(tableColorKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                    return {
 | 
			
		||||
                        new: diff.newValue as string,
 | 
			
		||||
                        old: diff.oldValue as string,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
@@ -277,7 +285,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as string,
 | 
			
		||||
                        new: diff.newValue as string,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -298,7 +309,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as DataType;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as DataType,
 | 
			
		||||
                        new: diff.newValue as DataType,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -321,7 +335,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as boolean,
 | 
			
		||||
                        new: diff.newValue as boolean,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -342,7 +359,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as boolean,
 | 
			
		||||
                        new: diff.newValue as boolean,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -365,7 +385,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as string,
 | 
			
		||||
                        new: diff.newValue as string,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -386,7 +409,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as number,
 | 
			
		||||
                        new: diff.newValue as number,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -409,7 +435,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                    return {
 | 
			
		||||
                        old: diff.oldValue as number,
 | 
			
		||||
                        new: diff.newValue as number,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,14 @@ import {
 | 
			
		||||
    type ValidationResult,
 | 
			
		||||
} from '@/lib/data/sql-import/sql-validator';
 | 
			
		||||
import { SQLValidationStatus } from './sql-validation-status';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
import { detectImportMethod } from '@/lib/import-method/detect-import-method';
 | 
			
		||||
import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
 | 
			
		||||
import {
 | 
			
		||||
    clearErrorHighlight,
 | 
			
		||||
    highlightErrorLine,
 | 
			
		||||
} from '@/components/code-snippet/dbml/utils';
 | 
			
		||||
 | 
			
		||||
const calculateContentSizeMB = (content: string): number => {
 | 
			
		||||
    return content.length / (1024 * 1024); // Convert to MB
 | 
			
		||||
@@ -55,49 +63,6 @@ const calculateIsLargeFile = (content: string): boolean => {
 | 
			
		||||
const errorScriptOutputMessage =
 | 
			
		||||
    'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
 | 
			
		||||
 | 
			
		||||
// Helper to detect if content is likely SQL DDL or JSON
 | 
			
		||||
const detectContentType = (content: string): 'query' | 'ddl' | null => {
 | 
			
		||||
    if (!content || content.trim().length === 0) return null;
 | 
			
		||||
 | 
			
		||||
    // Common SQL DDL keywords
 | 
			
		||||
    const ddlKeywords = [
 | 
			
		||||
        'CREATE TABLE',
 | 
			
		||||
        'ALTER TABLE',
 | 
			
		||||
        'DROP TABLE',
 | 
			
		||||
        'CREATE INDEX',
 | 
			
		||||
        'CREATE VIEW',
 | 
			
		||||
        'CREATE PROCEDURE',
 | 
			
		||||
        'CREATE FUNCTION',
 | 
			
		||||
        'CREATE SCHEMA',
 | 
			
		||||
        'CREATE DATABASE',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const upperContent = content.toUpperCase();
 | 
			
		||||
 | 
			
		||||
    // Check for SQL DDL patterns
 | 
			
		||||
    const hasDDLKeywords = ddlKeywords.some((keyword) =>
 | 
			
		||||
        upperContent.includes(keyword)
 | 
			
		||||
    );
 | 
			
		||||
    if (hasDDLKeywords) return 'ddl';
 | 
			
		||||
 | 
			
		||||
    // Check if it looks like JSON
 | 
			
		||||
    try {
 | 
			
		||||
        // Just check structure, don't need full parse for detection
 | 
			
		||||
        if (
 | 
			
		||||
            (content.trim().startsWith('{') && content.trim().endsWith('}')) ||
 | 
			
		||||
            (content.trim().startsWith('[') && content.trim().endsWith(']'))
 | 
			
		||||
        ) {
 | 
			
		||||
            return 'query';
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        // Not valid JSON, might be partial
 | 
			
		||||
        console.error('Error detecting content type:', error);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If we can't confidently detect, return null
 | 
			
		||||
    return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface ImportDatabaseProps {
 | 
			
		||||
    goBack?: () => void;
 | 
			
		||||
    onImport: () => void;
 | 
			
		||||
@@ -111,8 +76,8 @@ export interface ImportDatabaseProps {
 | 
			
		||||
    >;
 | 
			
		||||
    keepDialogAfterImport?: boolean;
 | 
			
		||||
    title: string;
 | 
			
		||||
    importMethod: 'query' | 'ddl';
 | 
			
		||||
    setImportMethod: (method: 'query' | 'ddl') => void;
 | 
			
		||||
    importMethod: ImportMethod;
 | 
			
		||||
    setImportMethod: (method: ImportMethod) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
@@ -132,6 +97,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState('');
 | 
			
		||||
    const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
 | 
			
		||||
    const decorationsCollection = useRef<editor.IEditorDecorationsCollection>();
 | 
			
		||||
    const pasteDisposableRef = useRef<IDisposable | null>(null);
 | 
			
		||||
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
@@ -146,15 +112,20 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const [isAutoFixing, setIsAutoFixing] = useState(false);
 | 
			
		||||
    const [showAutoFixButton, setShowAutoFixButton] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const clearDecorations = useCallback(() => {
 | 
			
		||||
        clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setScriptResult('');
 | 
			
		||||
        setErrorMessage('');
 | 
			
		||||
        setShowCheckJsonButton(false);
 | 
			
		||||
    }, [importMethod, setScriptResult]);
 | 
			
		||||
 | 
			
		||||
    // Check if the ddl is valid
 | 
			
		||||
    // Check if the ddl or dbml is valid
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (importMethod !== 'ddl') {
 | 
			
		||||
        clearDecorations();
 | 
			
		||||
        if (importMethod === 'query') {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
@@ -163,9 +134,54 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        if (!scriptResult.trim()) {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (importMethod === 'dbml') {
 | 
			
		||||
            // Validate DBML by parsing it
 | 
			
		||||
            const validateResponse = verifyDBML(scriptResult);
 | 
			
		||||
            if (!validateResponse.hasError) {
 | 
			
		||||
                setErrorMessage('');
 | 
			
		||||
                setSqlValidation({
 | 
			
		||||
                    isValid: true,
 | 
			
		||||
                    errors: [],
 | 
			
		||||
                    warnings: [],
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                let errorMsg = 'Invalid DBML syntax';
 | 
			
		||||
                let line: number = 1;
 | 
			
		||||
 | 
			
		||||
                if (validateResponse.parsedError) {
 | 
			
		||||
                    errorMsg = validateResponse.parsedError.message;
 | 
			
		||||
                    line = validateResponse.parsedError.line;
 | 
			
		||||
                    highlightErrorLine({
 | 
			
		||||
                        error: validateResponse.parsedError,
 | 
			
		||||
                        model: editorRef.current?.getModel(),
 | 
			
		||||
                        editorDecorationsCollection:
 | 
			
		||||
                            decorationsCollection.current,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setSqlValidation({
 | 
			
		||||
                    isValid: false,
 | 
			
		||||
                    errors: [
 | 
			
		||||
                        {
 | 
			
		||||
                            message: errorMsg,
 | 
			
		||||
                            line: line,
 | 
			
		||||
                            type: 'syntax' as const,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    warnings: [],
 | 
			
		||||
                });
 | 
			
		||||
                setErrorMessage(errorMsg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // SQL validation
 | 
			
		||||
        // First run our validation based on database type
 | 
			
		||||
        const validation = validateSQL(scriptResult, databaseType);
 | 
			
		||||
        setSqlValidation(validation);
 | 
			
		||||
@@ -192,7 +208,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                setErrorMessage(result.error);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }, [importMethod, scriptResult, databaseType]);
 | 
			
		||||
    }, [importMethod, scriptResult, databaseType, clearDecorations]);
 | 
			
		||||
 | 
			
		||||
    // Check if the script result is a valid JSON
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -320,6 +336,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const handleEditorDidMount = useCallback(
 | 
			
		||||
        (editor: editor.IStandaloneCodeEditor) => {
 | 
			
		||||
            editorRef.current = editor;
 | 
			
		||||
            decorationsCollection.current =
 | 
			
		||||
                editor.createDecorationsCollection();
 | 
			
		||||
 | 
			
		||||
            // Cleanup previous disposable if it exists
 | 
			
		||||
            if (pasteDisposableRef.current) {
 | 
			
		||||
@@ -338,7 +356,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                const isLargeFile = calculateIsLargeFile(content);
 | 
			
		||||
 | 
			
		||||
                // First, detect content type to determine if we should switch modes
 | 
			
		||||
                const detectedType = detectContentType(content);
 | 
			
		||||
                const detectedType = detectImportMethod(content);
 | 
			
		||||
                if (detectedType && detectedType !== importMethod) {
 | 
			
		||||
                    // Switch to the detected mode immediately
 | 
			
		||||
                    setImportMethod(detectedType);
 | 
			
		||||
@@ -352,7 +370,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                ?.run();
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                    // For DDL mode, do NOT format as it can break the SQL
 | 
			
		||||
                    // For DDL and DBML modes, do NOT format as it can break the syntax
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Content type didn't change, apply formatting based on current mode
 | 
			
		||||
                    if (importMethod === 'query' && !isLargeFile) {
 | 
			
		||||
@@ -363,7 +381,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                ?.run();
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                    // For DDL mode or large files, do NOT format
 | 
			
		||||
                    // For DDL and DBML modes or large files, do NOT format
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@@ -410,16 +428,25 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                <div className="w-full text-center text-xs text-muted-foreground">
 | 
			
		||||
                    {importMethod === 'query'
 | 
			
		||||
                        ? 'Smart Query Output'
 | 
			
		||||
                        : 'SQL Script'}
 | 
			
		||||
                        : importMethod === 'dbml'
 | 
			
		||||
                          ? 'DBML Script'
 | 
			
		||||
                          : 'SQL Script'}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1 overflow-hidden">
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        <Editor
 | 
			
		||||
                            value={scriptResult}
 | 
			
		||||
                            onChange={debouncedHandleInputChange}
 | 
			
		||||
                            language={importMethod === 'query' ? 'json' : 'sql'}
 | 
			
		||||
                            language={
 | 
			
		||||
                                importMethod === 'query'
 | 
			
		||||
                                    ? 'json'
 | 
			
		||||
                                    : importMethod === 'dbml'
 | 
			
		||||
                                      ? 'dbml'
 | 
			
		||||
                                      : 'sql'
 | 
			
		||||
                            }
 | 
			
		||||
                            loading={<Spinner />}
 | 
			
		||||
                            onMount={handleEditorDidMount}
 | 
			
		||||
                            beforeMount={setupDBMLLanguage}
 | 
			
		||||
                            theme={
 | 
			
		||||
                                effectiveTheme === 'dark'
 | 
			
		||||
                                    ? 'dbml-dark'
 | 
			
		||||
@@ -430,7 +457,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                minimap: { enabled: false },
 | 
			
		||||
                                scrollBeyondLastLine: false,
 | 
			
		||||
                                automaticLayout: true,
 | 
			
		||||
                                glyphMargin: false,
 | 
			
		||||
                                lineNumbers: 'on',
 | 
			
		||||
                                guides: {
 | 
			
		||||
                                    indentation: false,
 | 
			
		||||
@@ -455,7 +481,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                    </Suspense>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
 | 
			
		||||
                {errorMessage ||
 | 
			
		||||
                ((importMethod === 'ddl' || importMethod === 'dbml') &&
 | 
			
		||||
                    sqlValidation) ? (
 | 
			
		||||
                    <SQLValidationStatus
 | 
			
		||||
                        validation={sqlValidation}
 | 
			
		||||
                        errorMessage={errorMessage}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,11 @@ import {
 | 
			
		||||
    AvatarImage,
 | 
			
		||||
} from '@/components/avatar/avatar';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Code } from 'lucide-react';
 | 
			
		||||
import { Code, FileCode } from 'lucide-react';
 | 
			
		||||
import { SmartQueryInstructions } from './instructions/smart-query-instructions';
 | 
			
		||||
import { DDLInstructions } from './instructions/ddl-instructions';
 | 
			
		||||
import { DBMLInstructions } from './instructions/dbml-instructions';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
@@ -30,8 +32,8 @@ export interface InstructionsSectionProps {
 | 
			
		||||
    setDatabaseEdition: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<DatabaseEdition | undefined>
 | 
			
		||||
    >;
 | 
			
		||||
    importMethod: 'query' | 'ddl';
 | 
			
		||||
    setImportMethod: (method: 'query' | 'ddl') => void;
 | 
			
		||||
    importMethod: ImportMethod;
 | 
			
		||||
    setImportMethod: (method: ImportMethod) => void;
 | 
			
		||||
    showSSMSInfoDialog: boolean;
 | 
			
		||||
    setShowSSMSInfoDialog: (show: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
@@ -125,9 +127,9 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
 | 
			
		||||
                        className="ml-1 flex-wrap justify-start gap-2"
 | 
			
		||||
                        value={importMethod}
 | 
			
		||||
                        onValueChange={(value) => {
 | 
			
		||||
                            let selectedImportMethod: 'query' | 'ddl' = 'query';
 | 
			
		||||
                            let selectedImportMethod: ImportMethod = 'query';
 | 
			
		||||
                            if (value) {
 | 
			
		||||
                                selectedImportMethod = value as 'query' | 'ddl';
 | 
			
		||||
                                selectedImportMethod = value as ImportMethod;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            setImportMethod(selectedImportMethod);
 | 
			
		||||
@@ -150,10 +152,20 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
 | 
			
		||||
                            className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
			
		||||
                        >
 | 
			
		||||
                            <Avatar className="size-4 rounded-none">
 | 
			
		||||
                                <Code size={16} />
 | 
			
		||||
                                <FileCode size={16} />
 | 
			
		||||
                            </Avatar>
 | 
			
		||||
                            SQL Script
 | 
			
		||||
                        </ToggleGroupItem>
 | 
			
		||||
                        <ToggleGroupItem
 | 
			
		||||
                            value="dbml"
 | 
			
		||||
                            variant="outline"
 | 
			
		||||
                            className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
			
		||||
                        >
 | 
			
		||||
                            <Avatar className="size-4 rounded-none">
 | 
			
		||||
                                <Code size={16} />
 | 
			
		||||
                            </Avatar>
 | 
			
		||||
                            DBML
 | 
			
		||||
                        </ToggleGroupItem>
 | 
			
		||||
                    </ToggleGroup>
 | 
			
		||||
                </div>
 | 
			
		||||
            )}
 | 
			
		||||
@@ -167,11 +179,16 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
 | 
			
		||||
                        showSSMSInfoDialog={showSSMSInfoDialog}
 | 
			
		||||
                        setShowSSMSInfoDialog={setShowSSMSInfoDialog}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                ) : importMethod === 'ddl' ? (
 | 
			
		||||
                    <DDLInstructions
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <DBMLInstructions
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
 | 
			
		||||
export interface DBMLInstructionsProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DBMLInstructions: React.FC<DBMLInstructionsProps> = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div className="flex flex-col gap-1 text-sm text-primary">
 | 
			
		||||
                <div>
 | 
			
		||||
                    Paste your DBML (Database Markup Language) schema definition
 | 
			
		||||
                    here →
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="flex h-64 flex-col gap-1 text-sm text-primary">
 | 
			
		||||
                <h4 className="text-xs font-medium">Example:</h4>
 | 
			
		||||
                <CodeSnippet
 | 
			
		||||
                    className="h-full"
 | 
			
		||||
                    allowCopy={false}
 | 
			
		||||
                    editorProps={{
 | 
			
		||||
                        beforeMount: setupDBMLLanguage,
 | 
			
		||||
                    }}
 | 
			
		||||
                    code={`Table users {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  username varchar
 | 
			
		||||
  email varchar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table posts {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  user_id int [ref: > users.id]
 | 
			
		||||
  title varchar
 | 
			
		||||
  content text
 | 
			
		||||
}`}
 | 
			
		||||
                    language={'dbml'}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -43,8 +43,8 @@ const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Execute the following command in your terminal:',
 | 
			
		||||
            code: `sqlite3 <database_file_path>\n.dump > <output_file_path>`,
 | 
			
		||||
            example: `sqlite3 my_db.db\n.dump > schema_export.sql`,
 | 
			
		||||
            code: `sqlite3 <database_file_path>\n".schema" > <output_file_path>`,
 | 
			
		||||
            example: `sqlite3 my_db.db\n".schema" > schema_export.sql`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
 | 
			
		||||
 | 
			
		||||
            {hasErrors ? (
 | 
			
		||||
                <div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
 | 
			
		||||
                    <ScrollArea className="h-24">
 | 
			
		||||
                    <ScrollArea className="h-fit max-h-24">
 | 
			
		||||
                        <div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
 | 
			
		||||
                            {validation?.errors
 | 
			
		||||
                                .slice(0, 3)
 | 
			
		||||
@@ -137,7 +137,7 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
 | 
			
		||||
 | 
			
		||||
            {hasWarnings && !hasErrors ? (
 | 
			
		||||
                <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
 | 
			
		||||
                    <ScrollArea className="h-24">
 | 
			
		||||
                    <ScrollArea className="h-fit max-h-24">
 | 
			
		||||
                        <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
 | 
			
		||||
                            <div className="flex items-start gap-2">
 | 
			
		||||
                                <AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,11 @@ import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
			
		||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
 | 
			
		||||
import {
 | 
			
		||||
    defaultDBMLDiagramName,
 | 
			
		||||
    importDBMLToDiagram,
 | 
			
		||||
} from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
@@ -30,11 +35,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { diagramId } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<ImportMethod>('query');
 | 
			
		||||
    const [databaseType, setDatabaseType] = useState<DatabaseType>(
 | 
			
		||||
        DatabaseType.GENERIC
 | 
			
		||||
    );
 | 
			
		||||
    const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
 | 
			
		||||
    const { closeCreateDiagramDialog } = useDialog();
 | 
			
		||||
    const { updateConfig } = useConfig();
 | 
			
		||||
    const [scriptResult, setScriptResult] = useState('');
 | 
			
		||||
    const [databaseEdition, setDatabaseEdition] = useState<
 | 
			
		||||
@@ -89,6 +94,14 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
                    sourceDatabaseType: databaseType,
 | 
			
		||||
                    targetDatabaseType: databaseType,
 | 
			
		||||
                });
 | 
			
		||||
            } else if (importMethod === 'dbml') {
 | 
			
		||||
                diagram = await importDBMLToDiagram(scriptResult, {
 | 
			
		||||
                    databaseType,
 | 
			
		||||
                });
 | 
			
		||||
                // Update the diagram name if it's the default
 | 
			
		||||
                if (diagram.name === defaultDBMLDiagramName) {
 | 
			
		||||
                    diagram.name = `Diagram ${diagramNumber}`;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                let metadata: DatabaseMetadata | undefined = databaseMetadata;
 | 
			
		||||
 | 
			
		||||
@@ -152,10 +165,6 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        await updateConfig({ config: { defaultDiagramId: diagram.id } });
 | 
			
		||||
        closeCreateDiagramDialog();
 | 
			
		||||
        navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
        setTimeout(
 | 
			
		||||
            () => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
 | 
			
		||||
            700
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseType,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
@@ -164,14 +173,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        navigate,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
        diagramNumber,
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const importNewDiagramOrFilterTables = useCallback(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            setIsParsingMetadata(true);
 | 
			
		||||
 | 
			
		||||
            if (importMethod === 'ddl') {
 | 
			
		||||
            if (importMethod === 'ddl' || importMethod === 'dbml') {
 | 
			
		||||
                await importNewDiagram();
 | 
			
		||||
            } else {
 | 
			
		||||
                // Parse metadata asynchronously to avoid blocking the UI
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useAlert } from '@/context/alert-context/alert-context';
 | 
			
		||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
			
		||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
@@ -24,7 +26,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    databaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<ImportMethod>('query');
 | 
			
		||||
    const { closeImportDatabaseDialog } = useDialog();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const {
 | 
			
		||||
@@ -65,6 +67,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
                sourceDatabaseType: databaseType,
 | 
			
		||||
                targetDatabaseType: databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        } else if (importMethod === 'dbml') {
 | 
			
		||||
            diagram = await importDBMLToDiagram(scriptResult, {
 | 
			
		||||
                databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            const databaseMetadata: DatabaseMetadata =
 | 
			
		||||
                loadDatabaseMetadata(scriptResult);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,359 +0,0 @@
 | 
			
		||||
import React, {
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useState,
 | 
			
		||||
    Suspense,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import type * as monaco from 'monaco-editor';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
    DialogClose,
 | 
			
		||||
    DialogContent,
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Editor } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { AlertCircle } from 'lucide-react';
 | 
			
		||||
import {
 | 
			
		||||
    importDBMLToDiagram,
 | 
			
		||||
    sanitizeDBML,
 | 
			
		||||
    preprocessDBML,
 | 
			
		||||
} from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { Parser } from '@dbml/core';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { debounce } from '@/lib/utils';
 | 
			
		||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
 | 
			
		||||
import {
 | 
			
		||||
    clearErrorHighlight,
 | 
			
		||||
    highlightErrorLine,
 | 
			
		||||
} from '@/components/code-snippet/dbml/utils';
 | 
			
		||||
 | 
			
		||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
 | 
			
		||||
    withCreateEmptyDiagram?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    withCreateEmptyDiagram,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const initialDBML = `// Use DBML to define your database structure
 | 
			
		||||
// Simple Blog System with Comments Example
 | 
			
		||||
 | 
			
		||||
Table users {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  name varchar
 | 
			
		||||
  email varchar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table posts {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  title varchar
 | 
			
		||||
  content text
 | 
			
		||||
  user_id integer
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table comments {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  content text
 | 
			
		||||
  post_id integer
 | 
			
		||||
  user_id integer
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Relationships
 | 
			
		||||
Ref: posts.user_id > users.id // Each post belongs to one user
 | 
			
		||||
Ref: comments.post_id > posts.id // Each comment belongs to one post
 | 
			
		||||
Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
 | 
			
		||||
    const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
 | 
			
		||||
    const { closeImportDBMLDialog } = useDialog();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState<string | undefined>();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const {
 | 
			
		||||
        addTables,
 | 
			
		||||
        addRelationships,
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        removeTables,
 | 
			
		||||
        removeRelationships,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { reorderTables } = useCanvas();
 | 
			
		||||
    const [reorder, setReorder] = useState(false);
 | 
			
		||||
    const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
    const decorationsCollection =
 | 
			
		||||
        useRef<monaco.editor.IEditorDecorationsCollection>();
 | 
			
		||||
 | 
			
		||||
    const handleEditorDidMount = (
 | 
			
		||||
        editor: monaco.editor.IStandaloneCodeEditor
 | 
			
		||||
    ) => {
 | 
			
		||||
        editorRef.current = editor;
 | 
			
		||||
        decorationsCollection.current = editor.createDecorationsCollection();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (reorder) {
 | 
			
		||||
            reorderTables({
 | 
			
		||||
                updateHistory: false,
 | 
			
		||||
            });
 | 
			
		||||
            setReorder(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [reorder, reorderTables]);
 | 
			
		||||
 | 
			
		||||
    const clearDecorations = useCallback(() => {
 | 
			
		||||
        clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const validateDBML = useCallback(
 | 
			
		||||
        async (content: string) => {
 | 
			
		||||
            // Clear previous errors
 | 
			
		||||
            setErrorMessage(undefined);
 | 
			
		||||
            clearDecorations();
 | 
			
		||||
 | 
			
		||||
            if (!content.trim()) return;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const preprocessedContent = preprocessDBML(content);
 | 
			
		||||
                const sanitizedContent = sanitizeDBML(preprocessedContent);
 | 
			
		||||
                const parser = new Parser();
 | 
			
		||||
                parser.parse(sanitizedContent, 'dbmlv2');
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                const parsedError = parseDBMLError(e);
 | 
			
		||||
                if (parsedError) {
 | 
			
		||||
                    setErrorMessage(
 | 
			
		||||
                        t('import_dbml_dialog.error.description') +
 | 
			
		||||
                            ` (1 error found - in line ${parsedError.line})`
 | 
			
		||||
                    );
 | 
			
		||||
                    highlightErrorLine({
 | 
			
		||||
                        error: parsedError,
 | 
			
		||||
                        model: editorRef.current?.getModel(),
 | 
			
		||||
                        editorDecorationsCollection:
 | 
			
		||||
                            decorationsCollection.current,
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    setErrorMessage(
 | 
			
		||||
                        e instanceof Error ? e.message : JSON.stringify(e)
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [clearDecorations, t]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
 | 
			
		||||
 | 
			
		||||
    // Set up debounced validation
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        debouncedValidateRef.current = debounce((value: string) => {
 | 
			
		||||
            validateDBML(value);
 | 
			
		||||
        }, 500);
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            debouncedValidateRef.current = null;
 | 
			
		||||
        };
 | 
			
		||||
    }, [validateDBML]);
 | 
			
		||||
 | 
			
		||||
    // Trigger validation when content changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (debouncedValidateRef.current) {
 | 
			
		||||
            debouncedValidateRef.current(dbmlContent);
 | 
			
		||||
        }
 | 
			
		||||
    }, [dbmlContent]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) {
 | 
			
		||||
            setErrorMessage(undefined);
 | 
			
		||||
            clearDecorations();
 | 
			
		||||
            setDBMLContent(initialDBML);
 | 
			
		||||
        }
 | 
			
		||||
    }, [dialog.open, initialDBML, clearDecorations]);
 | 
			
		||||
 | 
			
		||||
    const handleImport = useCallback(async () => {
 | 
			
		||||
        if (!dbmlContent.trim() || errorMessage) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const importedDiagram = await importDBMLToDiagram(dbmlContent);
 | 
			
		||||
            const tableIdsToRemove = tables
 | 
			
		||||
                .filter((table) =>
 | 
			
		||||
                    importedDiagram.tables?.some(
 | 
			
		||||
                        (t: DBTable) =>
 | 
			
		||||
                            t.name === table.name && t.schema === table.schema
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                .map((table) => table.id);
 | 
			
		||||
            // Find relationships that need to be removed
 | 
			
		||||
            const relationshipIdsToRemove = relationships
 | 
			
		||||
                .filter((relationship) => {
 | 
			
		||||
                    const sourceTable = tables.find(
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.id === relationship.sourceTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    const targetTable = tables.find(
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.id === relationship.targetTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    if (!sourceTable || !targetTable) return true;
 | 
			
		||||
                    const replacementSourceTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.name === sourceTable.name &&
 | 
			
		||||
                            table.schema === sourceTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
                    const replacementTargetTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table: DBTable) =>
 | 
			
		||||
                            table.name === targetTable.name &&
 | 
			
		||||
                            table.schema === targetTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
                    return replacementSourceTable || replacementTargetTable;
 | 
			
		||||
                })
 | 
			
		||||
                .map((relationship) => relationship.id);
 | 
			
		||||
 | 
			
		||||
            // Remove existing items
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                removeTables(tableIdsToRemove, { updateHistory: false }),
 | 
			
		||||
                removeRelationships(relationshipIdsToRemove, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Add new items
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                addTables(importedDiagram.tables ?? [], {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
                addRelationships(importedDiagram.relationships ?? [], {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
            setReorder(true);
 | 
			
		||||
            closeImportDBMLDialog();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            toast({
 | 
			
		||||
                title: t('import_dbml_dialog.error.title'),
 | 
			
		||||
                variant: 'destructive',
 | 
			
		||||
                description: (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <div>{t('import_dbml_dialog.error.description')}</div>
 | 
			
		||||
                        {e instanceof Error ? e.message : JSON.stringify(e)}
 | 
			
		||||
                    </>
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        dbmlContent,
 | 
			
		||||
        closeImportDBMLDialog,
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        removeTables,
 | 
			
		||||
        removeRelationships,
 | 
			
		||||
        addTables,
 | 
			
		||||
        addRelationships,
 | 
			
		||||
        errorMessage,
 | 
			
		||||
        toast,
 | 
			
		||||
        setReorder,
 | 
			
		||||
        t,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeImportDBMLDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
 | 
			
		||||
                showClose
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {withCreateEmptyDiagram
 | 
			
		||||
                            ? t('import_dbml_dialog.example_title')
 | 
			
		||||
                            : t('import_dbml_dialog.title')}
 | 
			
		||||
                    </DialogTitle>
 | 
			
		||||
                    <DialogDescription>
 | 
			
		||||
                        {t('import_dbml_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <DialogInternalContent>
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        <Editor
 | 
			
		||||
                            value={dbmlContent}
 | 
			
		||||
                            onChange={(value) => setDBMLContent(value || '')}
 | 
			
		||||
                            language="dbml"
 | 
			
		||||
                            onMount={handleEditorDidMount}
 | 
			
		||||
                            theme={
 | 
			
		||||
                                effectiveTheme === 'dark'
 | 
			
		||||
                                    ? 'dbml-dark'
 | 
			
		||||
                                    : 'dbml-light'
 | 
			
		||||
                            }
 | 
			
		||||
                            beforeMount={setupDBMLLanguage}
 | 
			
		||||
                            options={{
 | 
			
		||||
                                minimap: { enabled: false },
 | 
			
		||||
                                scrollBeyondLastLine: false,
 | 
			
		||||
                                automaticLayout: true,
 | 
			
		||||
                                glyphMargin: true,
 | 
			
		||||
                                lineNumbers: 'on',
 | 
			
		||||
                                scrollbar: {
 | 
			
		||||
                                    vertical: 'visible',
 | 
			
		||||
                                    horizontal: 'visible',
 | 
			
		||||
                                },
 | 
			
		||||
                            }}
 | 
			
		||||
                            className="size-full"
 | 
			
		||||
                        />
 | 
			
		||||
                    </Suspense>
 | 
			
		||||
                </DialogInternalContent>
 | 
			
		||||
                <DialogFooter>
 | 
			
		||||
                    <div className="flex w-full items-center justify-between">
 | 
			
		||||
                        <div className="flex items-center gap-4">
 | 
			
		||||
                            <DialogClose asChild>
 | 
			
		||||
                                <Button variant="secondary">
 | 
			
		||||
                                    {withCreateEmptyDiagram
 | 
			
		||||
                                        ? t('import_dbml_dialog.skip_and_empty')
 | 
			
		||||
                                        : t('import_dbml_dialog.cancel')}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </DialogClose>
 | 
			
		||||
                            {errorMessage ? (
 | 
			
		||||
                                <div className="flex items-center gap-1">
 | 
			
		||||
                                    <AlertCircle className="size-4 text-destructive" />
 | 
			
		||||
 | 
			
		||||
                                    <span className="text-xs text-destructive">
 | 
			
		||||
                                        {errorMessage ||
 | 
			
		||||
                                            t(
 | 
			
		||||
                                                'import_dbml_dialog.error.description'
 | 
			
		||||
                                            )}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            onClick={handleImport}
 | 
			
		||||
                            disabled={!dbmlContent.trim() || !!errorMessage}
 | 
			
		||||
                        >
 | 
			
		||||
                            {withCreateEmptyDiagram
 | 
			
		||||
                                ? t('import_dbml_dialog.show_example')
 | 
			
		||||
                                : t('import_dbml_dialog.import')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										328
									
								
								src/hooks/use-update-table-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/hooks/use-update-table-field.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,328 @@
 | 
			
		||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { useChartDB } from './use-chartdb';
 | 
			
		||||
import { useDebounce } from './use-debounce-v2';
 | 
			
		||||
import type { DBField, DBTable } from '@/lib/domain';
 | 
			
		||||
import type {
 | 
			
		||||
    SelectBoxOption,
 | 
			
		||||
    SelectBoxProps,
 | 
			
		||||
} from '@/components/select-box/select-box';
 | 
			
		||||
import {
 | 
			
		||||
    dataTypeDataToDataType,
 | 
			
		||||
    sortedDataTypeMap,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
 | 
			
		||||
 | 
			
		||||
const generateFieldRegexPatterns = (
 | 
			
		||||
    dataType: DataTypeData
 | 
			
		||||
): {
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
} => {
 | 
			
		||||
    if (!dataType.fieldAttributes) {
 | 
			
		||||
        return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const typeName = dataType.name;
 | 
			
		||||
    const fieldAttributes = dataType.fieldAttributes;
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.hasCharMaxLength) {
 | 
			
		||||
        if (fieldAttributes.hasCharMaxLengthOption) {
 | 
			
		||||
            return {
 | 
			
		||||
                regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
 | 
			
		||||
                extractRegex: /\((\d+|max)\)/i,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\(\\d+\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision && fieldAttributes.scale) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
 | 
			
		||||
            extractRegex: new RegExp(
 | 
			
		||||
                `${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
 | 
			
		||||
            ),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useUpdateTableField = (
 | 
			
		||||
    table: DBTable,
 | 
			
		||||
    field: DBField,
 | 
			
		||||
    customUpdateField?: (attrs: Partial<DBField>) => void
 | 
			
		||||
) => {
 | 
			
		||||
    const {
 | 
			
		||||
        databaseType,
 | 
			
		||||
        customTypes,
 | 
			
		||||
        updateField: chartDBUpdateField,
 | 
			
		||||
        removeField: chartDBRemoveField,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    // Local state for responsive UI
 | 
			
		||||
    const [localFieldName, setLocalFieldName] = useState(field.name);
 | 
			
		||||
    const [localNullable, setLocalNullable] = useState(field.nullable);
 | 
			
		||||
    const [localPrimaryKey, setLocalPrimaryKey] = useState(field.primaryKey);
 | 
			
		||||
 | 
			
		||||
    const lastFieldNameRef = useRef<string>(field.name);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (localFieldName === lastFieldNameRef.current) {
 | 
			
		||||
            lastFieldNameRef.current = field.name;
 | 
			
		||||
            setLocalFieldName(field.name);
 | 
			
		||||
        }
 | 
			
		||||
    }, [field.name, localFieldName]);
 | 
			
		||||
 | 
			
		||||
    // Update local state when field properties change externally
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLocalNullable(field.nullable);
 | 
			
		||||
        setLocalPrimaryKey(field.primaryKey);
 | 
			
		||||
    }, [field.nullable, field.primaryKey]);
 | 
			
		||||
 | 
			
		||||
    // Use custom updateField if provided, otherwise use the chartDB one
 | 
			
		||||
    const updateField = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            customUpdateField
 | 
			
		||||
                ? (
 | 
			
		||||
                      _tableId: string,
 | 
			
		||||
                      _fieldId: string,
 | 
			
		||||
                      attrs: Partial<DBField>
 | 
			
		||||
                  ) => customUpdateField(attrs)
 | 
			
		||||
                : chartDBUpdateField,
 | 
			
		||||
        [customUpdateField, chartDBUpdateField]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Calculate primary key fields for validation
 | 
			
		||||
    const primaryKeyFields = useMemo(() => {
 | 
			
		||||
        return table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
    }, [table.fields]);
 | 
			
		||||
 | 
			
		||||
    const primaryKeyCount = useMemo(
 | 
			
		||||
        () => primaryKeyFields.length,
 | 
			
		||||
        [primaryKeyFields.length]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Generate data type options for select box
 | 
			
		||||
    const dataFieldOptions = useMemo(() => {
 | 
			
		||||
        const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
 | 
			
		||||
            databaseType
 | 
			
		||||
        ].map((type) => {
 | 
			
		||||
            const regexPatterns = generateFieldRegexPatterns(type);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.id,
 | 
			
		||||
                regex: regexPatterns.regex,
 | 
			
		||||
                extractRegex: regexPatterns.extractRegex,
 | 
			
		||||
                group: customTypes?.length ? 'Standard Types' : undefined,
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!customTypes?.length) {
 | 
			
		||||
            return standardTypes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add custom types as options
 | 
			
		||||
        const customTypeOptions: SelectBoxOption[] = customTypes.map(
 | 
			
		||||
            (type) => ({
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.name,
 | 
			
		||||
                description:
 | 
			
		||||
                    type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
 | 
			
		||||
                group: 'Custom Types',
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return [...standardTypes, ...customTypeOptions];
 | 
			
		||||
    }, [databaseType, customTypes]);
 | 
			
		||||
 | 
			
		||||
    // Handle data type change
 | 
			
		||||
    const handleDataTypeChange = useCallback<
 | 
			
		||||
        NonNullable<SelectBoxProps['onChange']>
 | 
			
		||||
    >(
 | 
			
		||||
        (value, regexMatches) => {
 | 
			
		||||
            const dataType = sortedDataTypeMap[databaseType].find(
 | 
			
		||||
                (v) => v.id === value
 | 
			
		||||
            ) ?? {
 | 
			
		||||
                id: value as string,
 | 
			
		||||
                name: value as string,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let characterMaximumLength: string | undefined = undefined;
 | 
			
		||||
            let precision: number | undefined = undefined;
 | 
			
		||||
            let scale: number | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
            if (regexMatches?.length) {
 | 
			
		||||
                if (dataType?.fieldAttributes?.hasCharMaxLength) {
 | 
			
		||||
                    characterMaximumLength = regexMatches[1]?.toLowerCase();
 | 
			
		||||
                } else if (
 | 
			
		||||
                    dataType?.fieldAttributes?.precision &&
 | 
			
		||||
                    dataType?.fieldAttributes?.scale
 | 
			
		||||
                ) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                    scale = regexMatches[2]
 | 
			
		||||
                        ? parseInt(regexMatches[2])
 | 
			
		||||
                        : undefined;
 | 
			
		||||
                } else if (dataType?.fieldAttributes?.precision) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if (
 | 
			
		||||
                    dataType?.fieldAttributes?.hasCharMaxLength &&
 | 
			
		||||
                    field.characterMaximumLength
 | 
			
		||||
                ) {
 | 
			
		||||
                    characterMaximumLength = field.characterMaximumLength;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.precision && field.precision) {
 | 
			
		||||
                    precision = field.precision;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.scale && field.scale) {
 | 
			
		||||
                    scale = field.scale;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateField(table.id, field.id, {
 | 
			
		||||
                characterMaximumLength,
 | 
			
		||||
                precision,
 | 
			
		||||
                scale,
 | 
			
		||||
                increment: undefined,
 | 
			
		||||
                default: undefined,
 | 
			
		||||
                type: dataTypeDataToDataType(
 | 
			
		||||
                    dataType ?? {
 | 
			
		||||
                        id: value as string,
 | 
			
		||||
                        name: value as string,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            updateField,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            field.characterMaximumLength,
 | 
			
		||||
            field.precision,
 | 
			
		||||
            field.scale,
 | 
			
		||||
            field.id,
 | 
			
		||||
            table.id,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for field name
 | 
			
		||||
    const debouncedNameUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: string) => {
 | 
			
		||||
                if (value.trim() !== field.name) {
 | 
			
		||||
                    updateField(table.id, field.id, { name: value });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id, field.name]
 | 
			
		||||
        ),
 | 
			
		||||
        300 // 300ms debounce for text input
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for nullable toggle
 | 
			
		||||
    const debouncedNullableUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: boolean) => {
 | 
			
		||||
                updateField(table.id, field.id, { nullable: value });
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id]
 | 
			
		||||
        ),
 | 
			
		||||
        100 // 100ms debounce for toggle
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for primary key toggle
 | 
			
		||||
    const debouncedPrimaryKeyUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: boolean, primaryKeyCount: number) => {
 | 
			
		||||
                if (value) {
 | 
			
		||||
                    // When setting as primary key
 | 
			
		||||
                    const updates: Partial<DBField> = {
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                    };
 | 
			
		||||
                    // Only auto-set unique if this will be the only primary key
 | 
			
		||||
                    if (primaryKeyCount === 0) {
 | 
			
		||||
                        updates.unique = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    updateField(table.id, field.id, updates);
 | 
			
		||||
                } else {
 | 
			
		||||
                    // When removing primary key
 | 
			
		||||
                    updateField(table.id, field.id, {
 | 
			
		||||
                        primaryKey: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id]
 | 
			
		||||
        ),
 | 
			
		||||
        100 // 100ms debounce for toggle
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle primary key toggle with optimistic update
 | 
			
		||||
    const handlePrimaryKeyToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            setLocalPrimaryKey(value);
 | 
			
		||||
            debouncedPrimaryKeyUpdate(value, primaryKeyCount);
 | 
			
		||||
        },
 | 
			
		||||
        [primaryKeyCount, debouncedPrimaryKeyUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle nullable toggle with optimistic update
 | 
			
		||||
    const handleNullableToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            setLocalNullable(value);
 | 
			
		||||
            debouncedNullableUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedNullableUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle name change with optimistic update
 | 
			
		||||
    const handleNameChange = useCallback(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
            setLocalFieldName(value);
 | 
			
		||||
            debouncedNameUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedNameUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Utility function to generate field suffix for display
 | 
			
		||||
    const generateFieldSuffix = useCallback(
 | 
			
		||||
        (typeId?: string) => {
 | 
			
		||||
            return generateDBFieldSuffix(field, {
 | 
			
		||||
                databaseType,
 | 
			
		||||
                forceExtended: true,
 | 
			
		||||
                typeId,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [field, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeField = useCallback(() => {
 | 
			
		||||
        chartDBRemoveField(table.id, field.id);
 | 
			
		||||
    }, [chartDBRemoveField, table.id, field.id]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        dataFieldOptions,
 | 
			
		||||
        handleDataTypeChange,
 | 
			
		||||
        handlePrimaryKeyToggle,
 | 
			
		||||
        handleNullableToggle,
 | 
			
		||||
        handleNameChange,
 | 
			
		||||
        generateFieldSuffix,
 | 
			
		||||
        primaryKeyCount,
 | 
			
		||||
        fieldName: localFieldName,
 | 
			
		||||
        nullable: localNullable,
 | 
			
		||||
        primaryKey: localPrimaryKey,
 | 
			
		||||
        removeField,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										42
									
								
								src/hooks/use-update-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/hooks/use-update-table.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import { useCallback, useState, useEffect } from 'react';
 | 
			
		||||
import { useChartDB } from './use-chartdb';
 | 
			
		||||
import { useDebounce } from './use-debounce-v2';
 | 
			
		||||
import type { DBTable } from '@/lib/domain';
 | 
			
		||||
 | 
			
		||||
// Hook for updating table properties with debouncing for performance
 | 
			
		||||
export const useUpdateTable = (table: DBTable) => {
 | 
			
		||||
    const { updateTable: chartDBUpdateTable } = useChartDB();
 | 
			
		||||
    const [localTableName, setLocalTableName] = useState(table.name);
 | 
			
		||||
 | 
			
		||||
    // Debounced update function
 | 
			
		||||
    const debouncedUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: string) => {
 | 
			
		||||
                if (value.trim() && value.trim() !== table.name) {
 | 
			
		||||
                    chartDBUpdateTable(table.id, { name: value.trim() });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [chartDBUpdateTable, table.id, table.name]
 | 
			
		||||
        ),
 | 
			
		||||
        1000 // 1000ms debounce
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Update local state immediately for responsive UI
 | 
			
		||||
    const handleTableNameChange = useCallback(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
            setLocalTableName(value);
 | 
			
		||||
            debouncedUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Update local state when table name changes externally
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLocalTableName(table.name);
 | 
			
		||||
    }, [table.name]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        tableName: localTableName,
 | 
			
		||||
        handleTableNameChange,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -18,4 +18,7 @@
 | 
			
		||||
 | 
			
		||||
    .marker-definitions {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .nodrag {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'text', id: 'text', usageLevel: 1 },
 | 
			
		||||
    { name: 'boolean', id: 'boolean', usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
 | 
			
		||||
    { name: 'timestamptz', id: 'timestamptz', usageLevel: 1 },
 | 
			
		||||
    { name: 'date', id: 'date', usageLevel: 1 },
 | 
			
		||||
 | 
			
		||||
    // Level 2 - Second most common types
 | 
			
		||||
@@ -42,6 +43,7 @@ export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
        id: 'timestamp_with_time_zone',
 | 
			
		||||
        usageLevel: 2,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'int', id: 'int', usageLevel: 2 },
 | 
			
		||||
 | 
			
		||||
    // Less common types
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,9 @@ export const createFieldsFromMetadata = ({
 | 
			
		||||
            ...(col.precision?.scale ? { scale: col.precision.scale } : {}),
 | 
			
		||||
            ...(col.default ? { default: col.default } : {}),
 | 
			
		||||
            ...(col.collation ? { collation: col.collation } : {}),
 | 
			
		||||
            ...(col.is_identity !== undefined
 | 
			
		||||
                ? { increment: col.is_identity }
 | 
			
		||||
                : {}),
 | 
			
		||||
            createdAt: Date.now(),
 | 
			
		||||
            comments: col.comment ? col.comment : undefined,
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ export interface ColumnInfo {
 | 
			
		||||
    default?: string | null; // Default value for the column, nullable
 | 
			
		||||
    collation?: string | null;
 | 
			
		||||
    comment?: string | null;
 | 
			
		||||
    is_identity?: boolean; // Indicates if the column is auto-increment/identity
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ColumnInfoSchema: z.ZodType<ColumnInfo> = z.object({
 | 
			
		||||
@@ -35,4 +36,5 @@ export const ColumnInfoSchema: z.ZodType<ColumnInfo> = z.object({
 | 
			
		||||
    default: z.string().nullable().optional(),
 | 
			
		||||
    collation: z.string().nullable().optional(),
 | 
			
		||||
    comment: z.string().nullable().optional(),
 | 
			
		||||
    is_identity: z.boolean().optional(),
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,13 @@ cols AS (
 | 
			
		||||
                                            ',"default":"', null,
 | 
			
		||||
                                            '","collation":"', COALESCE(cols.COLLATION_NAME::TEXT, ''),
 | 
			
		||||
                                            '","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
 | 
			
		||||
                                            '"}')), ',') AS cols_metadata
 | 
			
		||||
                                            '","is_identity":', CASE 
 | 
			
		||||
                                                WHEN cols.is_identity = 'YES' THEN 'true'
 | 
			
		||||
                                                WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN 'true'
 | 
			
		||||
                                                WHEN cols.column_default LIKE 'unique_rowid()%' THEN 'true'
 | 
			
		||||
                                                ELSE 'false'
 | 
			
		||||
                                            END,
 | 
			
		||||
                                            '}')), ',') AS cols_metadata
 | 
			
		||||
    FROM information_schema.columns cols
 | 
			
		||||
    LEFT JOIN pg_catalog.pg_class c
 | 
			
		||||
        ON c.relname = cols.table_name
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,9 @@ SELECT CAST(CONCAT(
 | 
			
		||||
               ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
               ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
               ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
               '","collation":"', IFNULL(cols.collation_name, ''), '"}')
 | 
			
		||||
               '","collation":"', IFNULL(cols.collation_name, ''),
 | 
			
		||||
               '","is_identity":', IF(cols.extra LIKE '%auto_increment%', 'true', 'false'),
 | 
			
		||||
               '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        SELECT cols.table_schema,
 | 
			
		||||
               cols.table_name,
 | 
			
		||||
@@ -81,7 +83,8 @@ SELECT CAST(CONCAT(
 | 
			
		||||
               cols.ordinal_position,
 | 
			
		||||
               cols.is_nullable,
 | 
			
		||||
               cols.column_default,
 | 
			
		||||
               cols.collation_name
 | 
			
		||||
               cols.collation_name,
 | 
			
		||||
               cols.extra
 | 
			
		||||
        FROM information_schema.columns cols
 | 
			
		||||
        WHERE cols.table_schema = DATABASE()
 | 
			
		||||
    ) AS cols), ''),
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,9 @@ export const getMySQLQuery = (
 | 
			
		||||
                ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
                ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
                ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
                '","collation":"', IFNULL(cols.collation_name, ''), '"}'
 | 
			
		||||
                '","collation":"', IFNULL(cols.collation_name, ''),
 | 
			
		||||
                '","is_identity":', IF(cols.extra LIKE '%auto_increment%', 'true', 'false'),
 | 
			
		||||
                '}'
 | 
			
		||||
            )))))
 | 
			
		||||
), indexes as (
 | 
			
		||||
  (SELECT (@indexes:=NULL),
 | 
			
		||||
 
 | 
			
		||||
@@ -194,7 +194,12 @@ cols AS (
 | 
			
		||||
                                            ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
                                            '","collation":"', COALESCE(cols.COLLATION_NAME, ''),
 | 
			
		||||
                                            '","comment":"', ${withExtras ? withComments : withoutComments},
 | 
			
		||||
                                            '"}')), ',') AS cols_metadata
 | 
			
		||||
                                            '","is_identity":', CASE 
 | 
			
		||||
                                                WHEN cols.is_identity = 'YES' THEN 'true'
 | 
			
		||||
                                                WHEN cols.column_default IS NOT NULL AND cols.column_default LIKE 'nextval(%' THEN 'true'
 | 
			
		||||
                                                ELSE 'false'
 | 
			
		||||
                                            END,
 | 
			
		||||
                                            '}')), ',') AS cols_metadata
 | 
			
		||||
    FROM information_schema.columns cols
 | 
			
		||||
    LEFT JOIN pg_catalog.pg_class c
 | 
			
		||||
        ON c.relname = cols.table_name
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,13 @@ WITH fk_info AS (
 | 
			
		||||
                      END
 | 
			
		||||
                  ELSE null
 | 
			
		||||
              END,
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault}
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
              'is_identity', 
 | 
			
		||||
              CASE 
 | 
			
		||||
                  WHEN p.pk = 1 AND LOWER(p.type) LIKE '%int%' THEN json('true')
 | 
			
		||||
                  WHEN LOWER((SELECT sql FROM sqlite_master WHERE name = m.name)) LIKE '%' || p.name || '%autoincrement%' THEN json('true')
 | 
			
		||||
                  ELSE json('false')
 | 
			
		||||
              END
 | 
			
		||||
          )
 | 
			
		||||
      ) AS cols_metadata
 | 
			
		||||
  FROM
 | 
			
		||||
@@ -292,7 +298,13 @@ WITH fk_info AS (
 | 
			
		||||
                      END
 | 
			
		||||
                  ELSE null
 | 
			
		||||
              END,
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault}
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
              'is_identity', 
 | 
			
		||||
              CASE 
 | 
			
		||||
                  WHEN p.pk = 1 AND LOWER(p.type) LIKE '%int%' THEN json('true')
 | 
			
		||||
                  WHEN LOWER((SELECT sql FROM sqlite_master WHERE name = m.name)) LIKE '%' || p.name || '%autoincrement%' THEN json('true')
 | 
			
		||||
                  ELSE json('false')
 | 
			
		||||
              END
 | 
			
		||||
          )
 | 
			
		||||
      ) AS cols_metadata
 | 
			
		||||
  FROM
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,11 @@ cols AS (
 | 
			
		||||
                            WHEN cols.COLLATION_NAME IS NULL THEN 'null'
 | 
			
		||||
                            ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
 | 
			
		||||
                        END +
 | 
			
		||||
                        ', "is_identity": ' + CASE 
 | 
			
		||||
                            WHEN COLUMNPROPERTY(OBJECT_ID(cols.TABLE_SCHEMA + '.' + cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') = 1 
 | 
			
		||||
                            THEN 'true' 
 | 
			
		||||
                            ELSE 'false' 
 | 
			
		||||
                        END +
 | 
			
		||||
                    N'}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                ), N','
 | 
			
		||||
            ) +
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import {
 | 
			
		||||
    databaseTypesWithCommentSupport,
 | 
			
		||||
} from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DataType } from '../data-types/data-types';
 | 
			
		||||
import { dataTypeMap, type DataType } from '../data-types/data-types';
 | 
			
		||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
 | 
			
		||||
import { exportMSSQL } from './export-per-type/mssql';
 | 
			
		||||
import { exportPostgreSQL } from './export-per-type/postgresql';
 | 
			
		||||
@@ -13,6 +13,55 @@ import { exportSQLite } from './export-per-type/sqlite';
 | 
			
		||||
import { exportMySQL } from './export-per-type/mysql';
 | 
			
		||||
import { escapeSQLComment } from './export-per-type/common';
 | 
			
		||||
 | 
			
		||||
// Function to format default values with proper quoting
 | 
			
		||||
const formatDefaultValue = (value: string): string => {
 | 
			
		||||
    const trimmed = value.trim();
 | 
			
		||||
 | 
			
		||||
    // SQL keywords and function-like keywords that don't need quotes
 | 
			
		||||
    const keywords = [
 | 
			
		||||
        'TRUE',
 | 
			
		||||
        'FALSE',
 | 
			
		||||
        'NULL',
 | 
			
		||||
        'CURRENT_TIMESTAMP',
 | 
			
		||||
        'CURRENT_DATE',
 | 
			
		||||
        'CURRENT_TIME',
 | 
			
		||||
        'NOW',
 | 
			
		||||
        'GETDATE',
 | 
			
		||||
        'NEWID',
 | 
			
		||||
        'UUID',
 | 
			
		||||
    ];
 | 
			
		||||
    if (keywords.includes(trimmed.toUpperCase())) {
 | 
			
		||||
        return trimmed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Function calls (contain parentheses) don't need quotes
 | 
			
		||||
    if (trimmed.includes('(') && trimmed.includes(')')) {
 | 
			
		||||
        return trimmed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Numbers don't need quotes
 | 
			
		||||
    if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
 | 
			
		||||
        return trimmed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Already quoted strings - keep as is
 | 
			
		||||
    if (
 | 
			
		||||
        (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
 | 
			
		||||
        (trimmed.startsWith('"') && trimmed.endsWith('"'))
 | 
			
		||||
    ) {
 | 
			
		||||
        return trimmed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if it's a simple identifier (alphanumeric, no spaces) that might be a currency or enum
 | 
			
		||||
    // These typically don't have spaces and are short (< 10 chars)
 | 
			
		||||
    if (/^[A-Z][A-Z0-9_]*$/i.test(trimmed) && trimmed.length <= 10) {
 | 
			
		||||
        return trimmed; // Treat as unquoted identifier (e.g., EUR, USD)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Everything else needs to be quoted and escaped
 | 
			
		||||
    return `'${trimmed.replace(/'/g, "''")}'`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to simplify verbose data type names
 | 
			
		||||
const simplifyDataType = (typeName: string): string => {
 | 
			
		||||
    const typeMap: Record<string, string> = {};
 | 
			
		||||
@@ -314,11 +363,26 @@ export const exportBaseSQL = ({
 | 
			
		||||
                sqlScript += `(1)`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add precision and scale for numeric types
 | 
			
		||||
            if (field.precision && field.scale) {
 | 
			
		||||
                sqlScript += `(${field.precision}, ${field.scale})`;
 | 
			
		||||
            } else if (field.precision) {
 | 
			
		||||
                sqlScript += `(${field.precision})`;
 | 
			
		||||
            // Add precision and scale for numeric types only
 | 
			
		||||
            const precisionAndScaleTypes = dataTypeMap[targetDatabaseType]
 | 
			
		||||
                .filter(
 | 
			
		||||
                    (t) =>
 | 
			
		||||
                        t.fieldAttributes?.precision && t.fieldAttributes?.scale
 | 
			
		||||
                )
 | 
			
		||||
                .map((t) => t.name);
 | 
			
		||||
 | 
			
		||||
            const isNumericType = precisionAndScaleTypes.some(
 | 
			
		||||
                (t) =>
 | 
			
		||||
                    field.type.name.toLowerCase().includes(t) ||
 | 
			
		||||
                    typeName.toLowerCase().includes(t)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (isNumericType) {
 | 
			
		||||
                if (field.precision && field.scale) {
 | 
			
		||||
                    sqlScript += `(${field.precision}, ${field.scale})`;
 | 
			
		||||
                } else if (field.precision) {
 | 
			
		||||
                    sqlScript += `(${field.precision})`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle NOT NULL constraint
 | 
			
		||||
@@ -366,7 +430,19 @@ export const exportBaseSQL = ({
 | 
			
		||||
                        fieldDefault = `now()`;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    sqlScript += ` DEFAULT ${fieldDefault}`;
 | 
			
		||||
                    // Fix CURRENT_DATE() for PostgreSQL in DBML flow - PostgreSQL uses CURRENT_DATE without parentheses
 | 
			
		||||
                    if (
 | 
			
		||||
                        isDBMLFlow &&
 | 
			
		||||
                        targetDatabaseType === DatabaseType.POSTGRESQL
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if (fieldDefault.toUpperCase() === 'CURRENT_DATE()') {
 | 
			
		||||
                            fieldDefault = 'CURRENT_DATE';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Format default value with proper quoting
 | 
			
		||||
                    const formattedDefault = formatDefaultValue(fieldDefault);
 | 
			
		||||
                    sqlScript += ` DEFAULT ${formattedDefault}`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -454,10 +530,16 @@ export const exportBaseSQL = ({
 | 
			
		||||
                .join(', ');
 | 
			
		||||
 | 
			
		||||
            if (fieldNames) {
 | 
			
		||||
                const indexName =
 | 
			
		||||
                const rawIndexName =
 | 
			
		||||
                    table.schema && !isDBMLFlow
 | 
			
		||||
                        ? `${table.schema}_${index.name}`
 | 
			
		||||
                        : index.name;
 | 
			
		||||
                // Quote index name if it contains special characters
 | 
			
		||||
                // For DBML flow, also quote if contains special characters
 | 
			
		||||
                const needsQuoting = /[^a-zA-Z0-9_]/.test(rawIndexName);
 | 
			
		||||
                const indexName = needsQuoting
 | 
			
		||||
                    ? `"${rawIndexName}"`
 | 
			
		||||
                    : rawIndexName;
 | 
			
		||||
                sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +0,0 @@
 | 
			
		||||
import { describe, it } from 'vitest';
 | 
			
		||||
 | 
			
		||||
describe('node-sql-parser - CREATE TYPE handling', () => {
 | 
			
		||||
    it('should show exact parser error for CREATE TYPE', async () => {
 | 
			
		||||
        const { Parser } = await import('node-sql-parser');
 | 
			
		||||
        const parser = new Parser();
 | 
			
		||||
        const parserOpts = {
 | 
			
		||||
            database: 'PostgreSQL',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        console.log('\n=== Testing CREATE TYPE statement ===');
 | 
			
		||||
        const createTypeSQL = `CREATE TYPE spell_element AS ENUM ('fire', 'water', 'earth', 'air');`;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            parser.astify(createTypeSQL, parserOpts);
 | 
			
		||||
            console.log('CREATE TYPE parsed successfully');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log('CREATE TYPE parse error:', (error as Error).message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log('\n=== Testing CREATE EXTENSION statement ===');
 | 
			
		||||
        const createExtensionSQL = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            parser.astify(createExtensionSQL, parserOpts);
 | 
			
		||||
            console.log('CREATE EXTENSION parsed successfully');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                'CREATE EXTENSION parse error:',
 | 
			
		||||
                (error as Error).message
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log('\n=== Testing CREATE TABLE with custom type ===');
 | 
			
		||||
        const createTableWithTypeSQL = `CREATE TABLE wizards (
 | 
			
		||||
            id UUID PRIMARY KEY,
 | 
			
		||||
            element spell_element DEFAULT 'fire'
 | 
			
		||||
        );`;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            parser.astify(createTableWithTypeSQL, parserOpts);
 | 
			
		||||
            console.log('CREATE TABLE with custom type parsed successfully');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                'CREATE TABLE with custom type parse error:',
 | 
			
		||||
                (error as Error).message
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log('\n=== Testing CREATE TABLE with standard types only ===');
 | 
			
		||||
        const createTableStandardSQL = `CREATE TABLE wizards (
 | 
			
		||||
            id UUID PRIMARY KEY,
 | 
			
		||||
            element VARCHAR(20) DEFAULT 'fire'
 | 
			
		||||
        );`;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            parser.astify(createTableStandardSQL, parserOpts);
 | 
			
		||||
            console.log('CREATE TABLE with standard types parsed successfully');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                'CREATE TABLE with standard types parse error:',
 | 
			
		||||
                (error as Error).message
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,178 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { fromSQLite } from '../sqlite';
 | 
			
		||||
 | 
			
		||||
describe('SQLite Import Tests', () => {
 | 
			
		||||
    it('should parse SQLite script with sqlite_sequence table and all relationships', async () => {
 | 
			
		||||
        const sql = `
 | 
			
		||||
CREATE TABLE users (
 | 
			
		||||
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  name TEXT,
 | 
			
		||||
  age INTEGER
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE sqlite_sequence(name,seq);
 | 
			
		||||
CREATE TABLE products (
 | 
			
		||||
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  name TEXT,
 | 
			
		||||
  price REAL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE user_products (
 | 
			
		||||
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  user_id INTEGER NOT NULL,
 | 
			
		||||
  product_id INTEGER NOT NULL,
 | 
			
		||||
  purchased_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
  FOREIGN KEY (user_id) REFERENCES users(id),
 | 
			
		||||
  FOREIGN KEY (product_id) REFERENCES products(id)
 | 
			
		||||
);
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const result = await fromSQLite(sql);
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK TOTAL COUNTS =============
 | 
			
		||||
        // Should have exactly 4 tables
 | 
			
		||||
        expect(result.tables).toHaveLength(4);
 | 
			
		||||
 | 
			
		||||
        // Should have exactly 2 foreign key relationships
 | 
			
		||||
        expect(result.relationships).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK USERS TABLE =============
 | 
			
		||||
        const usersTable = result.tables.find((t) => t.name === 'users');
 | 
			
		||||
        expect(usersTable).toBeDefined();
 | 
			
		||||
        expect(usersTable?.columns).toHaveLength(3); // id, name, age
 | 
			
		||||
 | 
			
		||||
        // Check each column in users table
 | 
			
		||||
        expect(usersTable?.columns[0]).toMatchObject({
 | 
			
		||||
            name: 'id',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: true,
 | 
			
		||||
            increment: true,
 | 
			
		||||
            nullable: false,
 | 
			
		||||
        });
 | 
			
		||||
        expect(usersTable?.columns[1]).toMatchObject({
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'TEXT',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
        expect(usersTable?.columns[2]).toMatchObject({
 | 
			
		||||
            name: 'age',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK SQLITE_SEQUENCE TABLE =============
 | 
			
		||||
        const sqliteSequenceTable = result.tables.find(
 | 
			
		||||
            (t) => t.name === 'sqlite_sequence'
 | 
			
		||||
        );
 | 
			
		||||
        expect(sqliteSequenceTable).toBeDefined();
 | 
			
		||||
        expect(sqliteSequenceTable?.columns).toHaveLength(2); // name, seq
 | 
			
		||||
 | 
			
		||||
        // Check columns in sqlite_sequence table
 | 
			
		||||
        expect(sqliteSequenceTable?.columns[0]).toMatchObject({
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'TEXT', // Should default to TEXT when no type specified
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
        expect(sqliteSequenceTable?.columns[1]).toMatchObject({
 | 
			
		||||
            name: 'seq',
 | 
			
		||||
            type: 'TEXT', // Should default to TEXT when no type specified
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK PRODUCTS TABLE =============
 | 
			
		||||
        const productsTable = result.tables.find((t) => t.name === 'products');
 | 
			
		||||
        expect(productsTable).toBeDefined();
 | 
			
		||||
        expect(productsTable?.columns).toHaveLength(3); // id, name, price
 | 
			
		||||
 | 
			
		||||
        // Check each column in products table
 | 
			
		||||
        expect(productsTable?.columns[0]).toMatchObject({
 | 
			
		||||
            name: 'id',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: true,
 | 
			
		||||
            increment: true,
 | 
			
		||||
            nullable: false,
 | 
			
		||||
        });
 | 
			
		||||
        expect(productsTable?.columns[1]).toMatchObject({
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'TEXT',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
        expect(productsTable?.columns[2]).toMatchObject({
 | 
			
		||||
            name: 'price',
 | 
			
		||||
            type: 'REAL',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK USER_PRODUCTS TABLE =============
 | 
			
		||||
        const userProductsTable = result.tables.find(
 | 
			
		||||
            (t) => t.name === 'user_products'
 | 
			
		||||
        );
 | 
			
		||||
        expect(userProductsTable).toBeDefined();
 | 
			
		||||
        expect(userProductsTable?.columns).toHaveLength(4); // id, user_id, product_id, purchased_at
 | 
			
		||||
 | 
			
		||||
        // Check each column in user_products table
 | 
			
		||||
        expect(userProductsTable?.columns[0]).toMatchObject({
 | 
			
		||||
            name: 'id',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: true,
 | 
			
		||||
            increment: true,
 | 
			
		||||
            nullable: false,
 | 
			
		||||
        });
 | 
			
		||||
        expect(userProductsTable?.columns[1]).toMatchObject({
 | 
			
		||||
            name: 'user_id',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: false, // NOT NULL constraint
 | 
			
		||||
        });
 | 
			
		||||
        expect(userProductsTable?.columns[2]).toMatchObject({
 | 
			
		||||
            name: 'product_id',
 | 
			
		||||
            type: 'INTEGER',
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: false, // NOT NULL constraint
 | 
			
		||||
        });
 | 
			
		||||
        expect(userProductsTable?.columns[3]).toMatchObject({
 | 
			
		||||
            name: 'purchased_at',
 | 
			
		||||
            type: 'TIMESTAMP', // DATETIME should map to TIMESTAMP
 | 
			
		||||
            primaryKey: false,
 | 
			
		||||
            nullable: true,
 | 
			
		||||
            default: 'CURRENT_TIMESTAMP',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // ============= CHECK FOREIGN KEY RELATIONSHIPS =============
 | 
			
		||||
        // FK 1: user_products.user_id -> users.id
 | 
			
		||||
        const userIdFK = result.relationships.find(
 | 
			
		||||
            (r) =>
 | 
			
		||||
                r.sourceTable === 'user_products' &&
 | 
			
		||||
                r.sourceColumn === 'user_id' &&
 | 
			
		||||
                r.targetTable === 'users' &&
 | 
			
		||||
                r.targetColumn === 'id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(userIdFK).toBeDefined();
 | 
			
		||||
        expect(userIdFK).toMatchObject({
 | 
			
		||||
            sourceTable: 'user_products',
 | 
			
		||||
            sourceColumn: 'user_id',
 | 
			
		||||
            targetTable: 'users',
 | 
			
		||||
            targetColumn: 'id',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // FK 2: user_products.product_id -> products.id
 | 
			
		||||
        const productIdFK = result.relationships.find(
 | 
			
		||||
            (r) =>
 | 
			
		||||
                r.sourceTable === 'user_products' &&
 | 
			
		||||
                r.sourceColumn === 'product_id' &&
 | 
			
		||||
                r.targetTable === 'products' &&
 | 
			
		||||
                r.targetColumn === 'id'
 | 
			
		||||
        );
 | 
			
		||||
        expect(productIdFK).toBeDefined();
 | 
			
		||||
        expect(productIdFK).toMatchObject({
 | 
			
		||||
            sourceTable: 'user_products',
 | 
			
		||||
            sourceColumn: 'product_id',
 | 
			
		||||
            targetTable: 'products',
 | 
			
		||||
            targetColumn: 'id',
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -32,11 +32,11 @@ export async function fromSQLite(sqlContent: string): Promise<SQLParserResult> {
 | 
			
		||||
    const tableMap: Record<string, string> = {}; // Maps table name to its ID
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        // SPECIAL HANDLING: Direct line-by-line parser for SQLite DDL
 | 
			
		||||
        // This ensures we preserve the exact data types from the original DDL
 | 
			
		||||
        // SPECIAL HANDLING: Direct regex-based parser for SQLite DDL
 | 
			
		||||
        // This ensures we handle all SQLite-specific syntax including tables without types
 | 
			
		||||
        const directlyParsedTables = parseCreateTableStatements(sqlContent);
 | 
			
		||||
 | 
			
		||||
        // Check if we successfully parsed tables directly
 | 
			
		||||
        // Always try direct parsing first as it's more reliable for SQLite
 | 
			
		||||
        if (directlyParsedTables.length > 0) {
 | 
			
		||||
            // Map the direct parsing results to the expected SQLParserResult format
 | 
			
		||||
            directlyParsedTables.forEach((table) => {
 | 
			
		||||
@@ -56,8 +56,19 @@ export async function fromSQLite(sqlContent: string): Promise<SQLParserResult> {
 | 
			
		||||
            // Process foreign keys using the regex approach
 | 
			
		||||
            findForeignKeysUsingRegex(sqlContent, tableMap, relationships);
 | 
			
		||||
 | 
			
		||||
            // Return the result
 | 
			
		||||
            return { tables, relationships };
 | 
			
		||||
            // Create placeholder tables for any missing referenced tables
 | 
			
		||||
            addPlaceholderTablesForFKReferences(
 | 
			
		||||
                tables,
 | 
			
		||||
                relationships,
 | 
			
		||||
                tableMap
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Filter out any invalid relationships
 | 
			
		||||
            const validRelationships = relationships.filter((rel) => {
 | 
			
		||||
                return isValidForeignKeyRelationship(rel, tables);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return { tables, relationships: validRelationships };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Preprocess SQL to handle SQLite quoted identifiers
 | 
			
		||||
@@ -130,101 +141,182 @@ function parseCreateTableStatements(sqlContent: string): {
 | 
			
		||||
        columns: SQLColumn[];
 | 
			
		||||
    }[] = [];
 | 
			
		||||
 | 
			
		||||
    // Split SQL content into lines
 | 
			
		||||
    const lines = sqlContent.split('\n');
 | 
			
		||||
 | 
			
		||||
    let currentTable: { name: string; columns: SQLColumn[] } | null = null;
 | 
			
		||||
    let inCreateTable = false;
 | 
			
		||||
 | 
			
		||||
    // Process each line
 | 
			
		||||
    for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
        const line = lines[i].trim();
 | 
			
		||||
 | 
			
		||||
        // Skip empty lines and comments
 | 
			
		||||
        if (!line || line.startsWith('--')) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for CREATE TABLE statement
 | 
			
		||||
        if (line.toUpperCase().startsWith('CREATE TABLE')) {
 | 
			
		||||
            // Extract table name
 | 
			
		||||
            const tableNameMatch =
 | 
			
		||||
                /CREATE\s+TABLE\s+(?:if\s+not\s+exists\s+)?["'`]?(\w+)["'`]?/i.exec(
 | 
			
		||||
                    line
 | 
			
		||||
                );
 | 
			
		||||
            if (tableNameMatch && tableNameMatch[1]) {
 | 
			
		||||
                inCreateTable = true;
 | 
			
		||||
                currentTable = {
 | 
			
		||||
                    name: tableNameMatch[1],
 | 
			
		||||
                    columns: [],
 | 
			
		||||
                };
 | 
			
		||||
    // Remove comments before processing
 | 
			
		||||
    const cleanedSQL = sqlContent
 | 
			
		||||
        .split('\n')
 | 
			
		||||
        .map((line) => {
 | 
			
		||||
            const commentIndex = line.indexOf('--');
 | 
			
		||||
            if (commentIndex >= 0) {
 | 
			
		||||
                return line.substring(0, commentIndex);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Check for end of CREATE TABLE statement
 | 
			
		||||
        else if (inCreateTable && line.includes(');')) {
 | 
			
		||||
            if (currentTable) {
 | 
			
		||||
                tables.push(currentTable);
 | 
			
		||||
            return line;
 | 
			
		||||
        })
 | 
			
		||||
        .join('\n');
 | 
			
		||||
 | 
			
		||||
    // Match all CREATE TABLE statements including those without column definitions
 | 
			
		||||
    const createTableRegex =
 | 
			
		||||
        /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?\s*\(([^;]+?)\)\s*;/gis;
 | 
			
		||||
    let match;
 | 
			
		||||
 | 
			
		||||
    while ((match = createTableRegex.exec(cleanedSQL)) !== null) {
 | 
			
		||||
        const tableName = match[1];
 | 
			
		||||
        const tableBody = match[2].trim();
 | 
			
		||||
 | 
			
		||||
        const table: { name: string; columns: SQLColumn[] } = {
 | 
			
		||||
            name: tableName,
 | 
			
		||||
            columns: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Special case: sqlite_sequence or tables with columns but no types
 | 
			
		||||
        if (tableName === 'sqlite_sequence' || !tableBody.includes(' ')) {
 | 
			
		||||
            // Parse simple column list without types (e.g., "name,seq")
 | 
			
		||||
            const simpleColumns = tableBody.split(',').map((col) => col.trim());
 | 
			
		||||
            for (const colName of simpleColumns) {
 | 
			
		||||
                if (
 | 
			
		||||
                    colName &&
 | 
			
		||||
                    !colName.toUpperCase().startsWith('FOREIGN KEY') &&
 | 
			
		||||
                    !colName.toUpperCase().startsWith('PRIMARY KEY') &&
 | 
			
		||||
                    !colName.toUpperCase().startsWith('UNIQUE') &&
 | 
			
		||||
                    !colName.toUpperCase().startsWith('CHECK') &&
 | 
			
		||||
                    !colName.toUpperCase().startsWith('CONSTRAINT')
 | 
			
		||||
                ) {
 | 
			
		||||
                    table.columns.push({
 | 
			
		||||
                        name: colName.replace(/["'`]/g, ''),
 | 
			
		||||
                        type: 'TEXT', // Default to TEXT for untyped columns
 | 
			
		||||
                        nullable: true,
 | 
			
		||||
                        primaryKey: false,
 | 
			
		||||
                        unique: false,
 | 
			
		||||
                        default: '',
 | 
			
		||||
                        increment: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            inCreateTable = false;
 | 
			
		||||
            currentTable = null;
 | 
			
		||||
        }
 | 
			
		||||
        // Process column definitions inside CREATE TABLE
 | 
			
		||||
        else if (inCreateTable && currentTable && line.includes('"')) {
 | 
			
		||||
            // Column line pattern optimized for user's DDL format
 | 
			
		||||
            const columnPattern = /\s*["'`](\w+)["'`]\s+([A-Za-z0-9_]+)(.+)?/i;
 | 
			
		||||
            const match = columnPattern.exec(line);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Parse normal table with typed columns
 | 
			
		||||
            // Split by commas not inside parentheses
 | 
			
		||||
            const columnDefs = [];
 | 
			
		||||
            let current = '';
 | 
			
		||||
            let parenDepth = 0;
 | 
			
		||||
 | 
			
		||||
            if (match) {
 | 
			
		||||
                const columnName = match[1];
 | 
			
		||||
                const rawType = match[2].toUpperCase();
 | 
			
		||||
                const restOfLine = match[3] || '';
 | 
			
		||||
            for (let i = 0; i < tableBody.length; i++) {
 | 
			
		||||
                const char = tableBody[i];
 | 
			
		||||
                if (char === '(') parenDepth++;
 | 
			
		||||
                else if (char === ')') parenDepth--;
 | 
			
		||||
                else if (char === ',' && parenDepth === 0) {
 | 
			
		||||
                    columnDefs.push(current.trim());
 | 
			
		||||
                    current = '';
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                current += char;
 | 
			
		||||
            }
 | 
			
		||||
            if (current.trim()) {
 | 
			
		||||
                columnDefs.push(current.trim());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                // Determine column properties
 | 
			
		||||
                const isPrimaryKey = restOfLine
 | 
			
		||||
                    .toUpperCase()
 | 
			
		||||
                    .includes('PRIMARY KEY');
 | 
			
		||||
                const isNotNull = restOfLine.toUpperCase().includes('NOT NULL');
 | 
			
		||||
                const isUnique = restOfLine.toUpperCase().includes('UNIQUE');
 | 
			
		||||
            for (const columnDef of columnDefs) {
 | 
			
		||||
                const line = columnDef.trim();
 | 
			
		||||
 | 
			
		||||
                // Extract default value
 | 
			
		||||
                let defaultValue = '';
 | 
			
		||||
                const defaultMatch = /DEFAULT\s+([^,\s)]+)/i.exec(restOfLine);
 | 
			
		||||
                if (defaultMatch) {
 | 
			
		||||
                    defaultValue = defaultMatch[1];
 | 
			
		||||
                // Skip constraints
 | 
			
		||||
                if (
 | 
			
		||||
                    line.toUpperCase().startsWith('FOREIGN KEY') ||
 | 
			
		||||
                    line.toUpperCase().startsWith('PRIMARY KEY') ||
 | 
			
		||||
                    line.toUpperCase().startsWith('UNIQUE') ||
 | 
			
		||||
                    line.toUpperCase().startsWith('CHECK') ||
 | 
			
		||||
                    line.toUpperCase().startsWith('CONSTRAINT')
 | 
			
		||||
                ) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Map to appropriate SQLite storage class
 | 
			
		||||
                let columnType = rawType;
 | 
			
		||||
                if (rawType === 'INTEGER' || rawType === 'INT') {
 | 
			
		||||
                    columnType = 'INTEGER';
 | 
			
		||||
                } else if (
 | 
			
		||||
                    ['REAL', 'FLOAT', 'DOUBLE', 'NUMERIC', 'DECIMAL'].includes(
 | 
			
		||||
                        rawType
 | 
			
		||||
                    )
 | 
			
		||||
                ) {
 | 
			
		||||
                    columnType = 'REAL';
 | 
			
		||||
                } else if (rawType === 'BLOB' || rawType === 'BINARY') {
 | 
			
		||||
                    columnType = 'BLOB';
 | 
			
		||||
                } else if (
 | 
			
		||||
                    ['TIMESTAMP', 'DATETIME', 'DATE'].includes(rawType)
 | 
			
		||||
                ) {
 | 
			
		||||
                    columnType = 'TIMESTAMP';
 | 
			
		||||
                } else {
 | 
			
		||||
                    columnType = 'TEXT';
 | 
			
		||||
                }
 | 
			
		||||
                // Parse column: handle both quoted and unquoted identifiers
 | 
			
		||||
                // Pattern: [quotes]columnName[quotes] dataType [constraints]
 | 
			
		||||
                const columnPattern = /^["'`]?([\w]+)["'`]?\s+(\w+)(.*)$/i;
 | 
			
		||||
                const columnMatch = columnPattern.exec(line);
 | 
			
		||||
 | 
			
		||||
                // Add column to the table
 | 
			
		||||
                currentTable.columns.push({
 | 
			
		||||
                    name: columnName,
 | 
			
		||||
                    type: columnType,
 | 
			
		||||
                    nullable: !isNotNull,
 | 
			
		||||
                    primaryKey: isPrimaryKey,
 | 
			
		||||
                    unique: isUnique || isPrimaryKey,
 | 
			
		||||
                    default: defaultValue,
 | 
			
		||||
                    increment: isPrimaryKey && columnType === 'INTEGER',
 | 
			
		||||
                });
 | 
			
		||||
                if (columnMatch) {
 | 
			
		||||
                    const columnName = columnMatch[1];
 | 
			
		||||
                    const rawType = columnMatch[2].toUpperCase();
 | 
			
		||||
                    const restOfLine = columnMatch[3] || '';
 | 
			
		||||
                    const upperRest = restOfLine.toUpperCase();
 | 
			
		||||
 | 
			
		||||
                    // Determine column properties
 | 
			
		||||
                    const isPrimaryKey = upperRest.includes('PRIMARY KEY');
 | 
			
		||||
                    const isAutoIncrement = upperRest.includes('AUTOINCREMENT');
 | 
			
		||||
                    const isNotNull =
 | 
			
		||||
                        upperRest.includes('NOT NULL') || isPrimaryKey;
 | 
			
		||||
                    const isUnique =
 | 
			
		||||
                        upperRest.includes('UNIQUE') || isPrimaryKey;
 | 
			
		||||
 | 
			
		||||
                    // Extract default value
 | 
			
		||||
                    let defaultValue = '';
 | 
			
		||||
                    const defaultMatch = /DEFAULT\s+([^,)]+)/i.exec(restOfLine);
 | 
			
		||||
                    if (defaultMatch) {
 | 
			
		||||
                        defaultValue = defaultMatch[1].trim();
 | 
			
		||||
                        // Remove quotes if present
 | 
			
		||||
                        if (
 | 
			
		||||
                            (defaultValue.startsWith("'") &&
 | 
			
		||||
                                defaultValue.endsWith("'")) ||
 | 
			
		||||
                            (defaultValue.startsWith('"') &&
 | 
			
		||||
                                defaultValue.endsWith('"'))
 | 
			
		||||
                        ) {
 | 
			
		||||
                            defaultValue = defaultValue.slice(1, -1);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Map to appropriate SQLite storage class
 | 
			
		||||
                    let columnType = rawType;
 | 
			
		||||
                    if (rawType === 'INTEGER' || rawType === 'INT') {
 | 
			
		||||
                        columnType = 'INTEGER';
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        [
 | 
			
		||||
                            'REAL',
 | 
			
		||||
                            'FLOAT',
 | 
			
		||||
                            'DOUBLE',
 | 
			
		||||
                            'NUMERIC',
 | 
			
		||||
                            'DECIMAL',
 | 
			
		||||
                        ].includes(rawType)
 | 
			
		||||
                    ) {
 | 
			
		||||
                        columnType = 'REAL';
 | 
			
		||||
                    } else if (rawType === 'BLOB' || rawType === 'BINARY') {
 | 
			
		||||
                        columnType = 'BLOB';
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        ['TIMESTAMP', 'DATETIME', 'DATE', 'TIME'].includes(
 | 
			
		||||
                            rawType
 | 
			
		||||
                        )
 | 
			
		||||
                    ) {
 | 
			
		||||
                        columnType = 'TIMESTAMP';
 | 
			
		||||
                    } else if (
 | 
			
		||||
                        ['TEXT', 'VARCHAR', 'CHAR', 'CLOB', 'STRING'].includes(
 | 
			
		||||
                            rawType
 | 
			
		||||
                        ) ||
 | 
			
		||||
                        rawType.startsWith('VARCHAR') ||
 | 
			
		||||
                        rawType.startsWith('CHAR')
 | 
			
		||||
                    ) {
 | 
			
		||||
                        columnType = 'TEXT';
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Default to TEXT for unknown types
 | 
			
		||||
                        columnType = 'TEXT';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Add column to the table
 | 
			
		||||
                    table.columns.push({
 | 
			
		||||
                        name: columnName,
 | 
			
		||||
                        type: columnType,
 | 
			
		||||
                        nullable: !isNotNull,
 | 
			
		||||
                        primaryKey: isPrimaryKey,
 | 
			
		||||
                        unique: isUnique,
 | 
			
		||||
                        default: defaultValue,
 | 
			
		||||
                        increment:
 | 
			
		||||
                            isPrimaryKey &&
 | 
			
		||||
                            isAutoIncrement &&
 | 
			
		||||
                            columnType === 'INTEGER',
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (table.columns.length > 0 || tableName === 'sqlite_sequence') {
 | 
			
		||||
            tables.push(table);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return tables;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/lib/dbml/dbml-export/__tests__/cases/4.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/dbml/dbml-export/__tests__/cases/4.dbml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
Table "public"."orders" {
 | 
			
		||||
  "order_id" integer [pk, not null]
 | 
			
		||||
  "customer_id" integer [not null]
 | 
			
		||||
  "order_date" date [not null, default: `CURRENT_DATE`]
 | 
			
		||||
  "total_amount" numeric [not null, default: 0]
 | 
			
		||||
  "status" varchar(50) [not null, default: 'Pending']
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/dbml/dbml-export/__tests__/cases/4.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/dbml/dbml-export/__tests__/cases/4.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{"id":"6b81a1787207","name":"SQL Import (postgresql)","createdAt":"2025-09-15T08:46:26.747Z","updatedAt":"2025-09-17T11:32:13.876Z","databaseType":"postgresql","tables":[{"id":"5ytf0yj9etpmm7mhmhvpu8kfj","name":"orders","schema":"public","order":1,"fields":[{"id":"w7l77cy9hylvlitdovt4ktdmk","name":"order_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":true,"unique":false,"default":"","createdAt":1757925986747,"increment":true},{"id":"vz7747t5fxrb62v1eepmahv9v","name":"customer_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","createdAt":1757925986747,"increment":false},{"id":"geq9qy6sv4ozl2lg9fvcyzxpf","name":"order_date","type":{"name":"date","id":"date","usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"CURRENT_DATE()","createdAt":1757925986747,"increment":false},{"id":"z928n7umvpec79t2eif7kmde9","name":"total_amount","type":{"name":"numeric","id":"numeric","fieldAttributes":{"precision":{"max":999,"min":1,"default":10},"scale":{"max":999,"min":0,"default":2}}},"nullable":false,"primaryKey":false,"unique":false,"default":"0","createdAt":1757925986747,"increment":false},{"id":"7bkrd0rp1s17bi1lnle6pesc7","name":"status","type":{"name":"varchar","id":"varchar","fieldAttributes":{"hasCharMaxLength":true},"usageLevel":1},"nullable":false,"primaryKey":false,"unique":false,"default":"'Pending'","createdAt":1757925986747,"increment":false,"characterMaximumLength":"50"}],"indexes":[],"x":113,"y":747,"color":"#8eb7ff","isView":false,"createdAt":1757925986747,"diagramId":"6b81a1787207","parentAreaId":null}],"relationships":[],"dependencies":[],"storageMode":"project","lastProjectSavedAt":"2025-09-17T11:32:13.876Z","areas":[],"creationMethod":"imported","customTypes":[]}
 | 
			
		||||
							
								
								
									
										129
									
								
								src/lib/dbml/dbml-export/__tests__/cases/5.inline.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/lib/dbml/dbml-export/__tests__/cases/5.inline.dbml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
Enum "cbhpm_entradas_tipo" {
 | 
			
		||||
    "grupo"
 | 
			
		||||
    "subgrupo"
 | 
			
		||||
    "procedimento"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "cid_entradas_tipo" {
 | 
			
		||||
    "capitulo"
 | 
			
		||||
    "agrupamento"
 | 
			
		||||
    "categoria"
 | 
			
		||||
    "subcategoria"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "digital_signature_provider" {
 | 
			
		||||
    "soluti"
 | 
			
		||||
    "valid"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "impresso_posicao" {
 | 
			
		||||
    "start"
 | 
			
		||||
    "center"
 | 
			
		||||
    "end"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "otp_provider" {
 | 
			
		||||
    "clinic"
 | 
			
		||||
    "soluti_bird_id"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_cobranca" {
 | 
			
		||||
    "valor"
 | 
			
		||||
    "porte"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_contato_movel" {
 | 
			
		||||
    "celular"
 | 
			
		||||
    "telefone_residencial"
 | 
			
		||||
    "telefone_comercial"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_contrato" {
 | 
			
		||||
    "trial"
 | 
			
		||||
    "common"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_endereco" {
 | 
			
		||||
    "residencial"
 | 
			
		||||
    "comercial"
 | 
			
		||||
    "cobranca"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_espectro_autista" {
 | 
			
		||||
    "leve"
 | 
			
		||||
    "moderado"
 | 
			
		||||
    "severo"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_estado_civil" {
 | 
			
		||||
    "nao_infomado"
 | 
			
		||||
    "solteiro"
 | 
			
		||||
    "casado"
 | 
			
		||||
    "divorciado"
 | 
			
		||||
    "viuvo"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_etnia" {
 | 
			
		||||
    "nao_infomado"
 | 
			
		||||
    "branca"
 | 
			
		||||
    "preta"
 | 
			
		||||
    "parda"
 | 
			
		||||
    "amarela"
 | 
			
		||||
    "indigena"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_excecao" {
 | 
			
		||||
    "bloqueio"
 | 
			
		||||
    "compromisso"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_metodo_reajuste" {
 | 
			
		||||
    "percentual"
 | 
			
		||||
    "valor"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_pessoa" {
 | 
			
		||||
    "fisica"
 | 
			
		||||
    "juridica"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_procedimento" {
 | 
			
		||||
    "consulta"
 | 
			
		||||
    "exame_laboratorial"
 | 
			
		||||
    "exame_imagem"
 | 
			
		||||
    "procedimento_clinico"
 | 
			
		||||
    "procedimento_cirurgico"
 | 
			
		||||
    "terapia"
 | 
			
		||||
    "outros"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_relacionamento" {
 | 
			
		||||
    "pai"
 | 
			
		||||
    "mae"
 | 
			
		||||
    "conjuge"
 | 
			
		||||
    "filho_a"
 | 
			
		||||
    "tutor_legal"
 | 
			
		||||
    "contato_emergencia"
 | 
			
		||||
    "outro"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_sexo" {
 | 
			
		||||
    "nao_infomado"
 | 
			
		||||
    "masculino"
 | 
			
		||||
    "feminino"
 | 
			
		||||
    "intersexo"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Enum "tipo_status_agendamento" {
 | 
			
		||||
    "em espera"
 | 
			
		||||
    "faltou"
 | 
			
		||||
    "ok"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table "public"."organizacao_cfg_impressos" {
 | 
			
		||||
  "id_organizacao" integer [pk, not null, ref: < "public"."organizacao"."id"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table "public"."organizacao" {
 | 
			
		||||
  "id" integer [pk, not null]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/dbml/dbml-export/__tests__/cases/5.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/dbml/dbml-export/__tests__/cases/5.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -4,64 +4,66 @@ import { generateDBMLFromDiagram } from '../dbml-export';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
 | 
			
		||||
describe('DBML Export - Diagram Case 1 Tests', () => {
 | 
			
		||||
const testCase = (caseNumber: string) => {
 | 
			
		||||
    // Read the JSON file
 | 
			
		||||
    const jsonPath = path.join(__dirname, 'cases', `${caseNumber}.json`);
 | 
			
		||||
    const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
    // Parse the JSON and convert to diagram
 | 
			
		||||
    const diagram = diagramFromJSONInput(jsonContent);
 | 
			
		||||
 | 
			
		||||
    // Generate DBML from the diagram
 | 
			
		||||
    const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
    // Check for both regular and inline DBML files
 | 
			
		||||
    const regularDbmlPath = path.join(__dirname, 'cases', `${caseNumber}.dbml`);
 | 
			
		||||
    const inlineDbmlPath = path.join(
 | 
			
		||||
        __dirname,
 | 
			
		||||
        'cases',
 | 
			
		||||
        `${caseNumber}.inline.dbml`
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const hasRegularDbml = fs.existsSync(regularDbmlPath);
 | 
			
		||||
    const hasInlineDbml = fs.existsSync(inlineDbmlPath);
 | 
			
		||||
 | 
			
		||||
    // Test regular DBML if file exists
 | 
			
		||||
    if (hasRegularDbml) {
 | 
			
		||||
        const expectedRegularDBML = fs.readFileSync(regularDbmlPath, 'utf-8');
 | 
			
		||||
        expect(result.standardDbml).toBe(expectedRegularDBML);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test inline DBML if file exists
 | 
			
		||||
    if (hasInlineDbml) {
 | 
			
		||||
        const expectedInlineDBML = fs.readFileSync(inlineDbmlPath, 'utf-8');
 | 
			
		||||
        expect(result.inlineDbml).toBe(expectedInlineDBML);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure at least one DBML file exists
 | 
			
		||||
    if (!hasRegularDbml && !hasInlineDbml) {
 | 
			
		||||
        throw new Error(
 | 
			
		||||
            `No DBML file found for test case ${caseNumber}. Expected either ${caseNumber}.dbml or ${caseNumber}.inline.dbml`
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('DBML Export cases', () => {
 | 
			
		||||
    it('should handle case 1 diagram', { timeout: 30000 }, async () => {
 | 
			
		||||
        // Read the JSON file
 | 
			
		||||
        const jsonPath = path.join(__dirname, 'cases', '1.json');
 | 
			
		||||
        const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
        // Parse the JSON and convert to diagram
 | 
			
		||||
        const diagram = diagramFromJSONInput(jsonContent);
 | 
			
		||||
 | 
			
		||||
        // Generate DBML from the diagram
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
        const generatedDBML = result.standardDbml;
 | 
			
		||||
 | 
			
		||||
        // Read the expected DBML file
 | 
			
		||||
        const dbmlPath = path.join(__dirname, 'cases', '1.dbml');
 | 
			
		||||
        const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
        // Compare the generated DBML with the expected DBML
 | 
			
		||||
        expect(generatedDBML).toBe(expectedDBML);
 | 
			
		||||
        testCase('1');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle case 2 diagram', { timeout: 30000 }, async () => {
 | 
			
		||||
        // Read the JSON file
 | 
			
		||||
        const jsonPath = path.join(__dirname, 'cases', '2.json');
 | 
			
		||||
        const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
        // Parse the JSON and convert to diagram
 | 
			
		||||
        const diagram = diagramFromJSONInput(jsonContent);
 | 
			
		||||
 | 
			
		||||
        // Generate DBML from the diagram
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
        const generatedDBML = result.standardDbml;
 | 
			
		||||
 | 
			
		||||
        // Read the expected DBML file
 | 
			
		||||
        const dbmlPath = path.join(__dirname, 'cases', '2.dbml');
 | 
			
		||||
        const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
        // Compare the generated DBML with the expected DBML
 | 
			
		||||
        expect(generatedDBML).toBe(expectedDBML);
 | 
			
		||||
        testCase('2');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle case 3 diagram', { timeout: 30000 }, async () => {
 | 
			
		||||
        // Read the JSON file
 | 
			
		||||
        const jsonPath = path.join(__dirname, 'cases', '3.json');
 | 
			
		||||
        const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
 | 
			
		||||
        testCase('3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        // Parse the JSON and convert to diagram
 | 
			
		||||
        const diagram = diagramFromJSONInput(jsonContent);
 | 
			
		||||
    it('should handle case 4 diagram', { timeout: 30000 }, async () => {
 | 
			
		||||
        testCase('4');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        // Generate DBML from the diagram
 | 
			
		||||
        const result = generateDBMLFromDiagram(diagram);
 | 
			
		||||
        const generatedDBML = result.standardDbml;
 | 
			
		||||
 | 
			
		||||
        // Read the expected DBML file
 | 
			
		||||
        const dbmlPath = path.join(__dirname, 'cases', '3.dbml');
 | 
			
		||||
        const expectedDBML = fs.readFileSync(dbmlPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
        // Compare the generated DBML with the expected DBML
 | 
			
		||||
        expect(generatedDBML).toBe(expectedDBML);
 | 
			
		||||
    it('should handle case 5 diagram', { timeout: 30000 }, async () => {
 | 
			
		||||
        testCase('5');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import { exportBaseSQL } from '@/lib/data/sql-export/export-sql-script';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@@ -502,38 +501,35 @@ const convertToInlineRefs = (dbml: string): string => {
 | 
			
		||||
    return cleanedDbml;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to check for DBML reserved keywords
 | 
			
		||||
const isDBMLKeyword = (name: string): boolean => {
 | 
			
		||||
    const keywords = new Set([
 | 
			
		||||
        'YES',
 | 
			
		||||
        'NO',
 | 
			
		||||
        'TRUE',
 | 
			
		||||
        'FALSE',
 | 
			
		||||
        'NULL', // DBML reserved keywords (boolean literals)
 | 
			
		||||
    ]);
 | 
			
		||||
    return keywords.has(name.toUpperCase());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to check for SQL keywords (add more if needed)
 | 
			
		||||
const isSQLKeyword = (name: string): boolean => {
 | 
			
		||||
    const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Common SQL keywords
 | 
			
		||||
    return keywords.has(name.toUpperCase());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to remove duplicate relationships from the diagram
 | 
			
		||||
const deduplicateRelationships = (diagram: Diagram): Diagram => {
 | 
			
		||||
    if (!diagram.relationships) return diagram;
 | 
			
		||||
 | 
			
		||||
    const seenRelationships = new Set<string>();
 | 
			
		||||
    const seenBidirectional = new Set<string>();
 | 
			
		||||
    const uniqueRelationships = diagram.relationships.filter((rel) => {
 | 
			
		||||
        // Create a unique key based on the relationship endpoints
 | 
			
		||||
        const relationshipKey = `${rel.sourceTableId}-${rel.sourceFieldId}->${rel.targetTableId}-${rel.targetFieldId}`;
 | 
			
		||||
 | 
			
		||||
        // Create a normalized key that's the same for both directions
 | 
			
		||||
        const normalizedKey = [
 | 
			
		||||
            `${rel.sourceTableId}-${rel.sourceFieldId}`,
 | 
			
		||||
            `${rel.targetTableId}-${rel.targetFieldId}`,
 | 
			
		||||
        ]
 | 
			
		||||
            .sort()
 | 
			
		||||
            .join('<->');
 | 
			
		||||
 | 
			
		||||
        if (seenRelationships.has(relationshipKey)) {
 | 
			
		||||
            return false; // Skip duplicate
 | 
			
		||||
            return false; // Skip exact duplicate
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (seenBidirectional.has(normalizedKey)) {
 | 
			
		||||
            // This is a bidirectional relationship, skip the second one
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        seenRelationships.add(relationshipKey);
 | 
			
		||||
        seenBidirectional.add(normalizedKey);
 | 
			
		||||
        return true; // Keep unique relationship
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -543,48 +539,6 @@ const deduplicateRelationships = (diagram: Diagram): Diagram => {
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to append comment statements for renamed tables and fields
 | 
			
		||||
const appendRenameComments = (
 | 
			
		||||
    baseScript: string,
 | 
			
		||||
    sqlRenamedTables: Map<string, string>,
 | 
			
		||||
    fieldRenames: Array<{
 | 
			
		||||
        table: string;
 | 
			
		||||
        originalName: string;
 | 
			
		||||
        newName: string;
 | 
			
		||||
    }>,
 | 
			
		||||
    finalDiagramForExport: Diagram
 | 
			
		||||
): string => {
 | 
			
		||||
    let script = baseScript;
 | 
			
		||||
 | 
			
		||||
    // Append COMMENTS for tables renamed due to SQL keywords
 | 
			
		||||
    sqlRenamedTables.forEach((originalName, newName) => {
 | 
			
		||||
        const escapedOriginal = originalName.replace(/'/g, "\\'");
 | 
			
		||||
        // Find the table to get its schema
 | 
			
		||||
        const table = finalDiagramForExport.tables?.find(
 | 
			
		||||
            (t) => t.name === newName
 | 
			
		||||
        );
 | 
			
		||||
        const tableIdentifier = table?.schema
 | 
			
		||||
            ? `"${table.schema}"."${newName}"`
 | 
			
		||||
            : `"${newName}"`;
 | 
			
		||||
        script += `\nCOMMENT ON TABLE ${tableIdentifier} IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Append COMMENTS for fields renamed due to SQL keyword conflicts
 | 
			
		||||
    fieldRenames.forEach(({ table, originalName, newName }) => {
 | 
			
		||||
        const escapedOriginal = originalName.replace(/'/g, "\\'");
 | 
			
		||||
        // Find the table to get its schema
 | 
			
		||||
        const tableObj = finalDiagramForExport.tables?.find(
 | 
			
		||||
            (t) => t.name === table
 | 
			
		||||
        );
 | 
			
		||||
        const tableIdentifier = tableObj?.schema
 | 
			
		||||
            ? `"${tableObj.schema}"."${table}"`
 | 
			
		||||
            : `"${table}"`;
 | 
			
		||||
        script += `\nCOMMENT ON COLUMN ${tableIdentifier}."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return script;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Fix DBML formatting to ensure consistent display of char and varchar types
 | 
			
		||||
const normalizeCharTypeFormat = (dbml: string): string => {
 | 
			
		||||
    // Replace "char (N)" with "char(N)" to match varchar's formatting
 | 
			
		||||
@@ -778,9 +732,17 @@ const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
 | 
			
		||||
    return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function to extract only Ref statements from DBML
 | 
			
		||||
const extractRelationshipsDbml = (dbml: string): string => {
 | 
			
		||||
    const lines = dbml.split('\n');
 | 
			
		||||
    const refLines = lines.filter((line) => line.trim().startsWith('Ref '));
 | 
			
		||||
    return refLines.join('\n').trim();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface DBMLExportResult {
 | 
			
		||||
    standardDbml: string;
 | 
			
		||||
    inlineDbml: string;
 | 
			
		||||
    relationshipsDbml: string;
 | 
			
		||||
    error?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -843,105 +805,33 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
    // Sanitize field names ('from'/'to' in 'relation' table)
 | 
			
		||||
    const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
 | 
			
		||||
 | 
			
		||||
    // --- Final sanitization and renaming pass ---
 | 
			
		||||
    // Only rename keywords for PostgreSQL/SQLite
 | 
			
		||||
    // For other databases, we'll wrap problematic names in quotes instead
 | 
			
		||||
    const shouldRenameKeywords =
 | 
			
		||||
        diagram.databaseType === DatabaseType.POSTGRESQL ||
 | 
			
		||||
        diagram.databaseType === DatabaseType.SQLITE;
 | 
			
		||||
    const sqlRenamedTables = new Map<string, string>();
 | 
			
		||||
    const fieldRenames: Array<{
 | 
			
		||||
        table: string;
 | 
			
		||||
        originalName: string;
 | 
			
		||||
        newName: string;
 | 
			
		||||
    }> = [];
 | 
			
		||||
 | 
			
		||||
    // Simplified processing - just handle duplicate field names
 | 
			
		||||
    const processTable = (table: DBTable) => {
 | 
			
		||||
        const originalName = table.name;
 | 
			
		||||
        let safeTableName = originalName;
 | 
			
		||||
 | 
			
		||||
        // If name contains spaces or special characters, wrap in quotes
 | 
			
		||||
        if (/[^\w]/.test(originalName)) {
 | 
			
		||||
            safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Rename table if it's a keyword (PostgreSQL/SQLite only)
 | 
			
		||||
        if (
 | 
			
		||||
            shouldRenameKeywords &&
 | 
			
		||||
            (isDBMLKeyword(originalName) || isSQLKeyword(originalName))
 | 
			
		||||
        ) {
 | 
			
		||||
            const newName = `${originalName}_table`;
 | 
			
		||||
            sqlRenamedTables.set(newName, originalName);
 | 
			
		||||
            safeTableName = /[^\w]/.test(newName)
 | 
			
		||||
                ? `"${newName.replace(/"/g, '\\"')}"`
 | 
			
		||||
                : newName;
 | 
			
		||||
        }
 | 
			
		||||
        // For other databases, just quote DBML keywords
 | 
			
		||||
        else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
 | 
			
		||||
            safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const fieldNameCounts = new Map<string, number>();
 | 
			
		||||
        const processedFields = table.fields.map((field) => {
 | 
			
		||||
            let finalSafeName = field.name;
 | 
			
		||||
 | 
			
		||||
            // If field name contains spaces or special characters, wrap in quotes
 | 
			
		||||
            if (/[^\w]/.test(field.name)) {
 | 
			
		||||
                finalSafeName = `"${field.name.replace(/"/g, '\\"')}"`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle duplicate field names
 | 
			
		||||
            const count = fieldNameCounts.get(field.name) || 0;
 | 
			
		||||
            if (count > 0) {
 | 
			
		||||
                const newName = `${field.name}_${count + 1}`;
 | 
			
		||||
                finalSafeName = /[^\w]/.test(newName)
 | 
			
		||||
                    ? `"${newName.replace(/"/g, '\\"')}"`
 | 
			
		||||
                    : newName;
 | 
			
		||||
                return {
 | 
			
		||||
                    ...field,
 | 
			
		||||
                    name: newName,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            fieldNameCounts.set(field.name, count + 1);
 | 
			
		||||
 | 
			
		||||
            // Create sanitized field
 | 
			
		||||
            const sanitizedField: DBField = {
 | 
			
		||||
                ...field,
 | 
			
		||||
                name: finalSafeName,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Rename field if it's a keyword (PostgreSQL/SQLite only)
 | 
			
		||||
            if (
 | 
			
		||||
                shouldRenameKeywords &&
 | 
			
		||||
                (isDBMLKeyword(field.name) || isSQLKeyword(field.name))
 | 
			
		||||
            ) {
 | 
			
		||||
                const newFieldName = `${field.name}_field`;
 | 
			
		||||
                fieldRenames.push({
 | 
			
		||||
                    table: safeTableName,
 | 
			
		||||
                    originalName: field.name,
 | 
			
		||||
                    newName: newFieldName,
 | 
			
		||||
                });
 | 
			
		||||
                sanitizedField.name = /[^\w]/.test(newFieldName)
 | 
			
		||||
                    ? `"${newFieldName.replace(/"/g, '\\"')}"`
 | 
			
		||||
                    : newFieldName;
 | 
			
		||||
            }
 | 
			
		||||
            // For other databases, just quote DBML keywords
 | 
			
		||||
            else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
 | 
			
		||||
                sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return sanitizedField;
 | 
			
		||||
            return field;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            ...table,
 | 
			
		||||
            name: safeTableName,
 | 
			
		||||
            fields: processedFields,
 | 
			
		||||
            indexes: (table.indexes || [])
 | 
			
		||||
                .filter((index) => !index.isPrimaryKey) // Filter out PK indexes as they're handled separately
 | 
			
		||||
                .map((index) => ({
 | 
			
		||||
                    ...index,
 | 
			
		||||
                    name: index.name
 | 
			
		||||
                        ? /[^\w]/.test(index.name)
 | 
			
		||||
                            ? `"${index.name.replace(/"/g, '\\"')}"`
 | 
			
		||||
                            : index.name
 | 
			
		||||
                        : `idx_${Math.random().toString(36).substring(2, 8)}`,
 | 
			
		||||
                    name:
 | 
			
		||||
                        index.name ||
 | 
			
		||||
                        `idx_${Math.random().toString(36).substring(2, 8)}`,
 | 
			
		||||
                })),
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
@@ -979,19 +869,6 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
 | 
			
		||||
        baseScript = sanitizeSQLforDBML(baseScript);
 | 
			
		||||
 | 
			
		||||
        // Append comments for renamed tables and fields (PostgreSQL/SQLite only)
 | 
			
		||||
        if (
 | 
			
		||||
            shouldRenameKeywords &&
 | 
			
		||||
            (sqlRenamedTables.size > 0 || fieldRenames.length > 0)
 | 
			
		||||
        ) {
 | 
			
		||||
            baseScript = appendRenameComments(
 | 
			
		||||
                baseScript,
 | 
			
		||||
                sqlRenamedTables,
 | 
			
		||||
                fieldRenames,
 | 
			
		||||
                finalDiagramForExport
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        standard = fixArrayTypes(
 | 
			
		||||
            normalizeCharTypeFormat(
 | 
			
		||||
                fixMultilineTableNames(
 | 
			
		||||
@@ -1054,5 +931,13 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { standardDbml: standard, inlineDbml: inline, error: errorMsg };
 | 
			
		||||
    // Extract relationships DBML from standard output
 | 
			
		||||
    const relationshipsDbml = extractRelationshipsDbml(standard);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        standardDbml: standard,
 | 
			
		||||
        inlineDbml: inline,
 | 
			
		||||
        relationshipsDbml,
 | 
			
		||||
        error: errorMsg,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/lib/dbml/dbml-import/__tests__/cases/1.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/lib/dbml/dbml-import/__tests__/cases/1.dbml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
Table "public"."table_3"{
 | 
			
		||||
    "id" bigint [pk]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/dbml/dbml-import/__tests__/cases/1.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/dbml/dbml-import/__tests__/cases/1.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{"id":"mqqwkkodxt6p","name":"Diagram 3","createdAt":"2025-09-16T15:33:25.300Z","updatedAt":"2025-09-16T15:33:31.563Z","databaseType":"postgresql","tables":[{"id":"loyxg6mafzos5u971uirjs3zh","name":"table_3","schema":"","order":0,"fields":[{"id":"29e2p9bom0uxo1n0a9ze5auuy","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":true,"primaryKey":true,"unique":true,"createdAt":1758036805300}],"indexes":[{"id":"5gf0aeptch1uk1bxv0x89wxxe","name":"pk_table_3_id","fieldIds":["29e2p9bom0uxo1n0a9ze5auuy"],"unique":true,"isPrimaryKey":true,"createdAt":1758036811564}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758036805300,"diagramId":"mqqwkkodxt6p"}],"relationships":[],"dependencies":[],"areas":[],"customTypes":[]}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/lib/dbml/dbml-import/__tests__/cases/2.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/dbml/dbml-import/__tests__/cases/2.dbml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
Table "table_3" {
 | 
			
		||||
  "id" bigint [pk]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table "table_2" {
 | 
			
		||||
  "id" bigint [pk, not null, ref: < "table_3"."id"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/dbml/dbml-import/__tests__/cases/2.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/dbml/dbml-import/__tests__/cases/2.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{"id":"mqqwkkod6r09","name":"Diagram 10","createdAt":"2025-09-16T15:47:40.655Z","updatedAt":"2025-09-16T15:47:50.179Z","databaseType":"postgresql","tables":[{"id":"6xbco4ihmuiyv2heuw9fggbgx","name":"table_3","schema":"","order":0,"fields":[{"id":"rxftaey7uxvq5qg6ix1hbak1c","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":true,"primaryKey":true,"unique":true,"createdAt":1758037660654}],"indexes":[{"id":"vsyjjaq2l58urkh9qm2g9hqhd","name":"pk_table_3_id","fieldIds":["rxftaey7uxvq5qg6ix1hbak1c"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660654}],"x":0,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660654,"diagramId":"mqqwkkod6r09"},{"id":"klu6k5ntddcxfdsu0fsfcwbiw","name":"table_2","schema":"","order":1,"fields":[{"id":"qq2415tivmtvun8vd727d9mr2","name":"id","type":{"name":"bigint","id":"bigint","usageLevel":2},"nullable":false,"primaryKey":true,"unique":true,"createdAt":1758037660655}],"indexes":[{"id":"cvv7sgmq07i9y54lz9a97nah5","name":"pk_table_2_id","fieldIds":["qq2415tivmtvun8vd727d9mr2"],"unique":true,"isPrimaryKey":true,"createdAt":1758037660655}],"x":300,"y":0,"color":"#8eb7ff","isView":false,"createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"relationships":[{"id":"yw2pbcumsabuncc6rjnp3n87t","name":"table_3_id_table_2_id","sourceSchema":"","targetSchema":"","sourceTableId":"6xbco4ihmuiyv2heuw9fggbgx","targetTableId":"klu6k5ntddcxfdsu0fsfcwbiw","sourceFieldId":"rxftaey7uxvq5qg6ix1hbak1c","targetFieldId":"qq2415tivmtvun8vd727d9mr2","sourceCardinality":"one","targetCardinality":"one","createdAt":1758037660655,"diagramId":"mqqwkkod6r09"}],"dependencies":[],"areas":[],"customTypes":[]}
 | 
			
		||||
							
								
								
									
										345
									
								
								src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								src/lib/dbml/dbml-import/__tests__/dbml-import-cases.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,345 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { importDBMLToDiagram } from '../dbml-import';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
// Type for field map entries
 | 
			
		||||
interface FieldMapEntry {
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    fieldName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to compare field properties (excluding IDs and timestamps)
 | 
			
		||||
function expectFieldsMatch(
 | 
			
		||||
    actualFields: DBField[],
 | 
			
		||||
    expectedFields: DBField[]
 | 
			
		||||
): void {
 | 
			
		||||
    expect(actualFields).toHaveLength(expectedFields.length);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < actualFields.length; i++) {
 | 
			
		||||
        const actual = actualFields[i];
 | 
			
		||||
        const expected = expectedFields[i];
 | 
			
		||||
 | 
			
		||||
        // Compare field properties (excluding ID and createdAt)
 | 
			
		||||
        expect(actual.name).toBe(expected.name);
 | 
			
		||||
 | 
			
		||||
        // Handle type comparison (could be string or object with name property)
 | 
			
		||||
        if (typeof expected.type === 'object' && expected.type?.name) {
 | 
			
		||||
            expect(actual.type?.name).toBe(expected.type.name);
 | 
			
		||||
        } else if (typeof expected.type === 'string') {
 | 
			
		||||
            expect(actual.type?.name).toBe(expected.type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Boolean flags with defaults
 | 
			
		||||
        expect(actual.primaryKey).toBe(expected.primaryKey || false);
 | 
			
		||||
        expect(actual.unique).toBe(expected.unique || false);
 | 
			
		||||
        expect(actual.nullable).toBe(expected.nullable ?? true);
 | 
			
		||||
 | 
			
		||||
        // Optional boolean flag
 | 
			
		||||
        if (expected.increment !== undefined) {
 | 
			
		||||
            expect(actual.increment).toBe(expected.increment);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Optional string/number properties
 | 
			
		||||
        if (expected.characterMaximumLength !== undefined) {
 | 
			
		||||
            expect(actual.characterMaximumLength).toBe(
 | 
			
		||||
                expected.characterMaximumLength
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (expected.precision !== undefined) {
 | 
			
		||||
            expect(actual.precision).toBe(expected.precision);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (expected.scale !== undefined) {
 | 
			
		||||
            expect(actual.scale).toBe(expected.scale);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (expected.default !== undefined) {
 | 
			
		||||
            expect(actual.default).toBe(expected.default);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (expected.collation !== undefined) {
 | 
			
		||||
            expect(actual.collation).toBe(expected.collation);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (expected.comments !== undefined) {
 | 
			
		||||
            expect(actual.comments).toBe(expected.comments);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to compare table properties (excluding IDs)
 | 
			
		||||
function expectTablesMatch(
 | 
			
		||||
    actualTables: DBTable[],
 | 
			
		||||
    expectedTables: DBTable[],
 | 
			
		||||
    databaseType: DatabaseType
 | 
			
		||||
): void {
 | 
			
		||||
    expect(actualTables).toHaveLength(expectedTables.length);
 | 
			
		||||
 | 
			
		||||
    // Sort tables by name for consistent comparison
 | 
			
		||||
    const sortedActual = [...actualTables].sort((a, b) =>
 | 
			
		||||
        a.name.localeCompare(b.name)
 | 
			
		||||
    );
 | 
			
		||||
    const sortedExpected = [...expectedTables].sort((a, b) =>
 | 
			
		||||
        a.name.localeCompare(b.name)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < sortedActual.length; i++) {
 | 
			
		||||
        const actual = sortedActual[i];
 | 
			
		||||
        const expected = sortedExpected[i];
 | 
			
		||||
 | 
			
		||||
        // Compare table properties (excluding ID and position)
 | 
			
		||||
        expect(actual.name).toBe(expected.name);
 | 
			
		||||
 | 
			
		||||
        // Schema comparison - handle differences in how schemas are represented
 | 
			
		||||
        if (expected.schema) {
 | 
			
		||||
            const defaultSchema = defaultSchemas[databaseType];
 | 
			
		||||
            if (defaultSchema && expected.schema === defaultSchema) {
 | 
			
		||||
                // DBML parser might not include default schema or might handle it differently
 | 
			
		||||
                expect(
 | 
			
		||||
                    actual.schema === expected.schema ||
 | 
			
		||||
                        actual.schema === '' ||
 | 
			
		||||
                        actual.schema === undefined
 | 
			
		||||
                ).toBeTruthy();
 | 
			
		||||
            } else {
 | 
			
		||||
                expect(actual.schema).toBe(expected.schema);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Compare fields
 | 
			
		||||
        expectFieldsMatch(actual.fields, expected.fields);
 | 
			
		||||
 | 
			
		||||
        // Check indexes exist for tables with primary keys
 | 
			
		||||
        const hasPrimaryKeyField = actual.fields.some((f) => f.primaryKey);
 | 
			
		||||
        if (hasPrimaryKeyField) {
 | 
			
		||||
            expect(actual.indexes).toBeDefined();
 | 
			
		||||
            expect(actual.indexes.length).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
            const pkIndex = actual.indexes.find((idx) => idx.isPrimaryKey);
 | 
			
		||||
            expect(pkIndex).toBeDefined();
 | 
			
		||||
            expect(pkIndex?.unique).toBe(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check comments if present
 | 
			
		||||
        if (expected.comments !== undefined) {
 | 
			
		||||
            expect(actual.comments).toBe(expected.comments);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to compare relationships (excluding IDs)
 | 
			
		||||
function expectRelationshipsMatch(
 | 
			
		||||
    actualRelationships: DBRelationship[],
 | 
			
		||||
    expectedRelationships: DBRelationship[],
 | 
			
		||||
    actualTables: DBTable[],
 | 
			
		||||
    expectedTables: DBTable[]
 | 
			
		||||
): void {
 | 
			
		||||
    expect(actualRelationships).toHaveLength(expectedRelationships.length);
 | 
			
		||||
 | 
			
		||||
    // Create lookup maps for table and field names by ID
 | 
			
		||||
    const expectedTableMap = new Map(expectedTables.map((t) => [t.id, t.name]));
 | 
			
		||||
    const actualTableMap = new Map(actualTables.map((t) => [t.id, t.name]));
 | 
			
		||||
 | 
			
		||||
    const expectedFieldMap = new Map<string, FieldMapEntry>();
 | 
			
		||||
    const actualFieldMap = new Map<string, FieldMapEntry>();
 | 
			
		||||
 | 
			
		||||
    expectedTables.forEach((table) => {
 | 
			
		||||
        table.fields.forEach((field) => {
 | 
			
		||||
            expectedFieldMap.set(field.id, {
 | 
			
		||||
                tableName: table.name,
 | 
			
		||||
                fieldName: field.name,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    actualTables.forEach((table) => {
 | 
			
		||||
        table.fields.forEach((field) => {
 | 
			
		||||
            actualFieldMap.set(field.id, {
 | 
			
		||||
                tableName: table.name,
 | 
			
		||||
                fieldName: field.name,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Sort relationships for consistent comparison
 | 
			
		||||
    const sortRelationships = (
 | 
			
		||||
        rels: DBRelationship[],
 | 
			
		||||
        tableMap: Map<string, string>,
 | 
			
		||||
        fieldMap: Map<string, FieldMapEntry>
 | 
			
		||||
    ) => {
 | 
			
		||||
        return [...rels].sort((a, b) => {
 | 
			
		||||
            const aSourceTable = tableMap.get(a.sourceTableId) || '';
 | 
			
		||||
            const bSourceTable = tableMap.get(b.sourceTableId) || '';
 | 
			
		||||
            const aTargetTable = tableMap.get(a.targetTableId) || '';
 | 
			
		||||
            const bTargetTable = tableMap.get(b.targetTableId) || '';
 | 
			
		||||
 | 
			
		||||
            const tableCompare =
 | 
			
		||||
                aSourceTable.localeCompare(bSourceTable) ||
 | 
			
		||||
                aTargetTable.localeCompare(bTargetTable);
 | 
			
		||||
            if (tableCompare !== 0) return tableCompare;
 | 
			
		||||
 | 
			
		||||
            const aSourceField = fieldMap.get(a.sourceFieldId)?.fieldName || '';
 | 
			
		||||
            const bSourceField = fieldMap.get(b.sourceFieldId)?.fieldName || '';
 | 
			
		||||
            const aTargetField = fieldMap.get(a.targetFieldId)?.fieldName || '';
 | 
			
		||||
            const bTargetField = fieldMap.get(b.targetFieldId)?.fieldName || '';
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                aSourceField.localeCompare(bSourceField) ||
 | 
			
		||||
                aTargetField.localeCompare(bTargetField)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const sortedActual = sortRelationships(
 | 
			
		||||
        actualRelationships,
 | 
			
		||||
        actualTableMap,
 | 
			
		||||
        actualFieldMap
 | 
			
		||||
    );
 | 
			
		||||
    const sortedExpected = sortRelationships(
 | 
			
		||||
        expectedRelationships,
 | 
			
		||||
        expectedTableMap,
 | 
			
		||||
        expectedFieldMap
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < sortedActual.length; i++) {
 | 
			
		||||
        const actual = sortedActual[i];
 | 
			
		||||
        const expected = sortedExpected[i];
 | 
			
		||||
 | 
			
		||||
        // Get table and field names for comparison
 | 
			
		||||
        const actualSourceTable = actualTableMap.get(actual.sourceTableId);
 | 
			
		||||
        const actualTargetTable = actualTableMap.get(actual.targetTableId);
 | 
			
		||||
        const expectedSourceTable = expectedTableMap.get(
 | 
			
		||||
            expected.sourceTableId
 | 
			
		||||
        );
 | 
			
		||||
        const expectedTargetTable = expectedTableMap.get(
 | 
			
		||||
            expected.targetTableId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const actualSourceField = actualFieldMap.get(actual.sourceFieldId);
 | 
			
		||||
        const actualTargetField = actualFieldMap.get(actual.targetFieldId);
 | 
			
		||||
        const expectedSourceField = expectedFieldMap.get(
 | 
			
		||||
            expected.sourceFieldId
 | 
			
		||||
        );
 | 
			
		||||
        const expectedTargetField = expectedFieldMap.get(
 | 
			
		||||
            expected.targetFieldId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Compare relationship by table and field names
 | 
			
		||||
        expect(actualSourceTable).toBe(expectedSourceTable);
 | 
			
		||||
        expect(actualTargetTable).toBe(expectedTargetTable);
 | 
			
		||||
        expect(actualSourceField?.fieldName).toBe(
 | 
			
		||||
            expectedSourceField?.fieldName
 | 
			
		||||
        );
 | 
			
		||||
        expect(actualTargetField?.fieldName).toBe(
 | 
			
		||||
            expectedTargetField?.fieldName
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Compare cardinality
 | 
			
		||||
        expect(actual.sourceCardinality).toBe(expected.sourceCardinality);
 | 
			
		||||
        expect(actual.targetCardinality).toBe(expected.targetCardinality);
 | 
			
		||||
 | 
			
		||||
        // Compare relationship name if present
 | 
			
		||||
        if (expected.name !== undefined) {
 | 
			
		||||
            expect(actual.name).toBe(expected.name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Main test helper function
 | 
			
		||||
async function testDBMLImportCase(caseNumber: string): Promise<void> {
 | 
			
		||||
    // Read the DBML file
 | 
			
		||||
    const dbmlPath = path.join(__dirname, 'cases', `${caseNumber}.dbml`);
 | 
			
		||||
    const dbmlContent = fs.readFileSync(dbmlPath, 'utf-8');
 | 
			
		||||
 | 
			
		||||
    // Read the expected JSON file
 | 
			
		||||
    const jsonPath = path.join(__dirname, 'cases', `${caseNumber}.json`);
 | 
			
		||||
    const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
 | 
			
		||||
    const expectedData = JSON.parse(jsonContent);
 | 
			
		||||
 | 
			
		||||
    // Import DBML to diagram
 | 
			
		||||
    const result = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
        databaseType: expectedData.databaseType || DatabaseType.POSTGRESQL,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Check basic diagram properties
 | 
			
		||||
    expect(result.name).toBe('DBML Import'); // Name is always 'DBML Import'
 | 
			
		||||
    expect(result.databaseType).toBe(expectedData.databaseType);
 | 
			
		||||
 | 
			
		||||
    // Check tables and fields
 | 
			
		||||
    expectTablesMatch(
 | 
			
		||||
        result.tables || [],
 | 
			
		||||
        expectedData.tables || [],
 | 
			
		||||
        expectedData.databaseType || DatabaseType.POSTGRESQL
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Check relationships
 | 
			
		||||
    expectRelationshipsMatch(
 | 
			
		||||
        result.relationships || [],
 | 
			
		||||
        expectedData.relationships || [],
 | 
			
		||||
        result.tables || [],
 | 
			
		||||
        expectedData.tables || []
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('DBML Import cases', () => {
 | 
			
		||||
    it('should handle case 1 - simple table with pk and unique', async () => {
 | 
			
		||||
        await testDBMLImportCase('1');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle case 2 - tables with relationships', async () => {
 | 
			
		||||
        await testDBMLImportCase('2');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle table with default values', async () => {
 | 
			
		||||
        const dbmlContent = `Table "public"."products" {
 | 
			
		||||
  "id" bigint [pk, not null]
 | 
			
		||||
  "name" varchar(255) [not null]
 | 
			
		||||
  "price" decimal(10,2) [not null, default: 0]
 | 
			
		||||
  "is_active" boolean [not null, default: true]
 | 
			
		||||
  "status" varchar(50) [not null, default: "deprecated"]
 | 
			
		||||
  "description" varchar(100) [default: \`complex "value" with quotes\`]
 | 
			
		||||
  "created_at" timestamp [not null, default: "now()"]
 | 
			
		||||
 | 
			
		||||
  Indexes {
 | 
			
		||||
    (name) [name: "idx_products_name"]
 | 
			
		||||
  }
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
        const result = await importDBMLToDiagram(dbmlContent, {
 | 
			
		||||
            databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(result.tables).toHaveLength(1);
 | 
			
		||||
        const table = result.tables![0];
 | 
			
		||||
        expect(table.name).toBe('products');
 | 
			
		||||
        expect(table.fields).toHaveLength(7);
 | 
			
		||||
 | 
			
		||||
        // Check numeric default (0)
 | 
			
		||||
        const priceField = table.fields.find((f) => f.name === 'price');
 | 
			
		||||
        expect(priceField?.default).toBe('0');
 | 
			
		||||
 | 
			
		||||
        // Check boolean default (true)
 | 
			
		||||
        const isActiveField = table.fields.find((f) => f.name === 'is_active');
 | 
			
		||||
        expect(isActiveField?.default).toBe('true');
 | 
			
		||||
 | 
			
		||||
        // Check string default with all quotes removed
 | 
			
		||||
        const statusField = table.fields.find((f) => f.name === 'status');
 | 
			
		||||
        expect(statusField?.default).toBe('deprecated');
 | 
			
		||||
 | 
			
		||||
        // Check backtick string - all quotes removed
 | 
			
		||||
        const descField = table.fields.find((f) => f.name === 'description');
 | 
			
		||||
        expect(descField?.default).toBe('complex value with quotes');
 | 
			
		||||
 | 
			
		||||
        // Check function default with all quotes removed
 | 
			
		||||
        const createdAtField = table.fields.find(
 | 
			
		||||
            (f) => f.name === 'created_at'
 | 
			
		||||
        );
 | 
			
		||||
        expect(createdAtField?.default).toBe('now()');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										149
									
								
								src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
 | 
			
		||||
// This test verifies the DBML integration without UI components
 | 
			
		||||
describe('DBML Integration Tests', () => {
 | 
			
		||||
    it('should handle DBML import in create diagram flow', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
Table users {
 | 
			
		||||
  id uuid [pk, not null]
 | 
			
		||||
  email varchar [unique, not null]
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table posts {
 | 
			
		||||
  id uuid [pk]
 | 
			
		||||
  title varchar
 | 
			
		||||
  content text
 | 
			
		||||
  user_id uuid [ref: > users.id]
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table comments {
 | 
			
		||||
  id uuid [pk]
 | 
			
		||||
  content text
 | 
			
		||||
  post_id uuid [ref: > posts.id]
 | 
			
		||||
  user_id uuid [ref: > users.id]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This will be ignored
 | 
			
		||||
TableGroup "Content" {
 | 
			
		||||
  posts
 | 
			
		||||
  comments
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This will be ignored too
 | 
			
		||||
Note test_note {
 | 
			
		||||
  'This is a test note'
 | 
			
		||||
}`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent);
 | 
			
		||||
 | 
			
		||||
        // Verify basic structure
 | 
			
		||||
        expect(diagram).toBeDefined();
 | 
			
		||||
        expect(diagram.tables).toHaveLength(3);
 | 
			
		||||
        expect(diagram.relationships).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
        // Verify tables
 | 
			
		||||
        const tableNames = diagram.tables?.map((t) => t.name).sort();
 | 
			
		||||
        expect(tableNames).toEqual(['comments', 'posts', 'users']);
 | 
			
		||||
 | 
			
		||||
        // Verify users table
 | 
			
		||||
        const usersTable = diagram.tables?.find((t) => t.name === 'users');
 | 
			
		||||
        expect(usersTable).toBeDefined();
 | 
			
		||||
        expect(usersTable?.fields).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
        const emailField = usersTable?.fields.find((f) => f.name === 'email');
 | 
			
		||||
        expect(emailField?.unique).toBe(true);
 | 
			
		||||
        expect(emailField?.nullable).toBe(false);
 | 
			
		||||
 | 
			
		||||
        // Verify relationships
 | 
			
		||||
        // There should be 3 relationships total
 | 
			
		||||
        expect(diagram.relationships).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
        // Find the relationship from users to posts (DBML ref is: posts.user_id > users.id)
 | 
			
		||||
        // This creates a relationship FROM users TO posts (one user has many posts)
 | 
			
		||||
        const postsTable = diagram.tables?.find((t) => t.name === 'posts');
 | 
			
		||||
        const usersTableId = usersTable?.id;
 | 
			
		||||
 | 
			
		||||
        const userPostRelation = diagram.relationships?.find(
 | 
			
		||||
            (r) =>
 | 
			
		||||
                r.sourceTableId === usersTableId &&
 | 
			
		||||
                r.targetTableId === postsTable?.id
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(userPostRelation).toBeDefined();
 | 
			
		||||
        expect(userPostRelation?.sourceCardinality).toBe('one');
 | 
			
		||||
        expect(userPostRelation?.targetCardinality).toBe('many');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle DBML with special features', async () => {
 | 
			
		||||
        const dbmlContent = `
 | 
			
		||||
// Enum will be converted to varchar
 | 
			
		||||
Table users {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  status enum
 | 
			
		||||
  tags text[] // Array will be converted to text
 | 
			
		||||
  favorite_product_id int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table products [headercolor: #FF0000] {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  name varchar
 | 
			
		||||
  price decimal(10,2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Ref: products.id < users.favorite_product_id`;
 | 
			
		||||
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent);
 | 
			
		||||
 | 
			
		||||
        expect(diagram.tables).toHaveLength(2);
 | 
			
		||||
 | 
			
		||||
        // Check enum conversion
 | 
			
		||||
        const usersTable = diagram.tables?.find((t) => t.name === 'users');
 | 
			
		||||
        const statusField = usersTable?.fields.find((f) => f.name === 'status');
 | 
			
		||||
        expect(statusField?.type.id).toBe('varchar');
 | 
			
		||||
 | 
			
		||||
        // Check array type conversion
 | 
			
		||||
        const tagsField = usersTable?.fields.find((f) => f.name === 'tags');
 | 
			
		||||
        expect(tagsField?.type.id).toBe('text');
 | 
			
		||||
 | 
			
		||||
        // Check that header color was removed
 | 
			
		||||
        const productsTable = diagram.tables?.find(
 | 
			
		||||
            (t) => t.name === 'products'
 | 
			
		||||
        );
 | 
			
		||||
        expect(productsTable).toBeDefined();
 | 
			
		||||
        expect(productsTable?.name).toBe('products');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle empty or invalid DBML gracefully', async () => {
 | 
			
		||||
        // Empty DBML
 | 
			
		||||
        const emptyDiagram = await importDBMLToDiagram('');
 | 
			
		||||
        expect(emptyDiagram.tables).toHaveLength(0);
 | 
			
		||||
        expect(emptyDiagram.relationships).toHaveLength(0);
 | 
			
		||||
 | 
			
		||||
        // Only comments
 | 
			
		||||
        const commentDiagram = await importDBMLToDiagram('// Just a comment');
 | 
			
		||||
        expect(commentDiagram.tables).toHaveLength(0);
 | 
			
		||||
        expect(commentDiagram.relationships).toHaveLength(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should preserve diagram metadata when importing DBML', async () => {
 | 
			
		||||
        const dbmlContent = `Table test {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
}`;
 | 
			
		||||
        const diagram = await importDBMLToDiagram(dbmlContent);
 | 
			
		||||
 | 
			
		||||
        // Default values
 | 
			
		||||
        expect(diagram.name).toBe('DBML Import');
 | 
			
		||||
        expect(diagram.databaseType).toBe(DatabaseType.GENERIC);
 | 
			
		||||
 | 
			
		||||
        // These can be overridden by the dialog
 | 
			
		||||
        diagram.name = 'My Custom Diagram';
 | 
			
		||||
        diagram.databaseType = DatabaseType.POSTGRESQL;
 | 
			
		||||
 | 
			
		||||
        expect(diagram.name).toBe('My Custom Diagram');
 | 
			
		||||
        expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -180,7 +180,7 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
 | 
			
		||||
            expect(artifactsTable?.schema).toBe(''); // No schema = empty string
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should rename reserved keywords for PostgreSQL', async () => {
 | 
			
		||||
        it('should handle reserved keywords for PostgreSQL', async () => {
 | 
			
		||||
            const dbmlContent = `
 | 
			
		||||
                Table "magic_items" {
 | 
			
		||||
                    "id" bigint [pk]
 | 
			
		||||
@@ -197,10 +197,9 @@ describe('DBML Schema Handling - Fantasy Realm Database', () => {
 | 
			
		||||
 | 
			
		||||
            const exported = generateDBMLFromDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
            // For PostgreSQL, keywords should be renamed in export
 | 
			
		||||
            expect(exported.standardDbml).toContain('Order_field');
 | 
			
		||||
            expect(exported.standardDbml).toContain('Yes_field');
 | 
			
		||||
            expect(exported.standardDbml).toContain('No_field');
 | 
			
		||||
            expect(exported.standardDbml).toContain('Order');
 | 
			
		||||
            expect(exported.standardDbml).toContain('Yes');
 | 
			
		||||
            expect(exported.standardDbml).toContain('No');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ import {
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export const defaultDBMLDiagramName = 'DBML Import';
 | 
			
		||||
 | 
			
		||||
// Preprocess DBML to handle unsupported features
 | 
			
		||||
export const preprocessDBML = (content: string): string => {
 | 
			
		||||
    let processed = content;
 | 
			
		||||
@@ -87,6 +89,7 @@ interface DBMLField {
 | 
			
		||||
    precision?: number | null;
 | 
			
		||||
    scale?: number | null;
 | 
			
		||||
    note?: string | { value: string } | null;
 | 
			
		||||
    default?: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLIndexColumn {
 | 
			
		||||
@@ -196,7 +199,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
        if (!dbmlContent.trim()) {
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                name: defaultDBMLDiagramName,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
@@ -214,7 +217,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
        if (!sanitizedContent.trim()) {
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                name: defaultDBMLDiagramName,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
@@ -229,7 +232,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
        if (!parsedData.schemas || parsedData.schemas.length === 0) {
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateDiagramId(),
 | 
			
		||||
                name: 'DBML Import',
 | 
			
		||||
                name: defaultDBMLDiagramName,
 | 
			
		||||
                databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
                tables: [],
 | 
			
		||||
                relationships: [],
 | 
			
		||||
@@ -332,6 +335,20 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                        schema: schemaName,
 | 
			
		||||
                        note: table.note,
 | 
			
		||||
                        fields: table.fields.map((field): DBMLField => {
 | 
			
		||||
                            // Extract default value and remove all quotes
 | 
			
		||||
                            let defaultValue: string | undefined;
 | 
			
		||||
                            if (
 | 
			
		||||
                                field.dbdefault !== undefined &&
 | 
			
		||||
                                field.dbdefault !== null
 | 
			
		||||
                            ) {
 | 
			
		||||
                                const rawDefault = String(
 | 
			
		||||
                                    field.dbdefault.value
 | 
			
		||||
                                );
 | 
			
		||||
                                // Remove ALL quotes (single, double, backticks) to clean the value
 | 
			
		||||
                                // The SQL export layer will handle adding proper quotes when needed
 | 
			
		||||
                                defaultValue = rawDefault.replace(/['"`]/g, '');
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return {
 | 
			
		||||
                                name: field.name,
 | 
			
		||||
                                type: field.type,
 | 
			
		||||
@@ -340,6 +357,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                                not_null: field.not_null,
 | 
			
		||||
                                increment: field.increment,
 | 
			
		||||
                                note: field.note,
 | 
			
		||||
                                default: defaultValue,
 | 
			
		||||
                                ...getFieldExtraAttributes(field, allEnums),
 | 
			
		||||
                            } satisfies DBMLField;
 | 
			
		||||
                        }),
 | 
			
		||||
@@ -480,12 +498,13 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
                    }),
 | 
			
		||||
                    nullable: !field.not_null,
 | 
			
		||||
                    primaryKey: field.pk || false,
 | 
			
		||||
                    unique: field.unique || false,
 | 
			
		||||
                    unique: field.unique || field.pk || false, // Primary keys are always unique
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                    characterMaximumLength: field.characterMaximumLength,
 | 
			
		||||
                    precision: field.precision,
 | 
			
		||||
                    scale: field.scale,
 | 
			
		||||
                    ...(fieldComment ? { comments: fieldComment } : {}),
 | 
			
		||||
                    ...(field.default ? { default: field.default } : {}),
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@@ -734,7 +753,7 @@ export const importDBMLToDiagram = async (
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            id: generateDiagramId(),
 | 
			
		||||
            name: 'DBML Import',
 | 
			
		||||
            name: defaultDBMLDiagramName,
 | 
			
		||||
            databaseType: options?.databaseType ?? DatabaseType.GENERIC,
 | 
			
		||||
            tables,
 | 
			
		||||
            relationships,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/lib/dbml/dbml-import/verify-dbml.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/lib/dbml/dbml-import/verify-dbml.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import { Parser } from '@dbml/core';
 | 
			
		||||
import { preprocessDBML, sanitizeDBML } from './dbml-import';
 | 
			
		||||
import type { DBMLError } from './dbml-import-error';
 | 
			
		||||
import { parseDBMLError } from './dbml-import-error';
 | 
			
		||||
 | 
			
		||||
export const verifyDBML = (
 | 
			
		||||
    content: string
 | 
			
		||||
):
 | 
			
		||||
    | {
 | 
			
		||||
          hasError: true;
 | 
			
		||||
          error: unknown;
 | 
			
		||||
          parsedError?: DBMLError;
 | 
			
		||||
          errorText: string;
 | 
			
		||||
      }
 | 
			
		||||
    | {
 | 
			
		||||
          hasError: false;
 | 
			
		||||
      } => {
 | 
			
		||||
    try {
 | 
			
		||||
        const preprocessedContent = preprocessDBML(content);
 | 
			
		||||
        const sanitizedContent = sanitizeDBML(preprocessedContent);
 | 
			
		||||
        const parser = new Parser();
 | 
			
		||||
        parser.parse(sanitizedContent, 'dbmlv2');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        const parsedError = parseDBMLError(e);
 | 
			
		||||
        if (parsedError) {
 | 
			
		||||
            return {
 | 
			
		||||
                hasError: true,
 | 
			
		||||
                parsedError: parsedError,
 | 
			
		||||
                error: e,
 | 
			
		||||
                errorText: parsedError.message,
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            if (e instanceof Error) {
 | 
			
		||||
                return {
 | 
			
		||||
                    hasError: true,
 | 
			
		||||
                    error: e,
 | 
			
		||||
                    errorText: e.message,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                hasError: true,
 | 
			
		||||
                error: e,
 | 
			
		||||
                errorText: JSON.stringify(e),
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        hasError: false,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										77
									
								
								src/lib/domain/diff/area-diff.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/lib/domain/diff/area-diff.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
import type { Area } from '../area';
 | 
			
		||||
 | 
			
		||||
export type AreaDiffAttribute = keyof Pick<
 | 
			
		||||
    Area,
 | 
			
		||||
    'name' | 'color' | 'x' | 'y' | 'width' | 'height'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
const areaDiffAttributeSchema: z.ZodType<AreaDiffAttribute> = z.union([
 | 
			
		||||
    z.literal('name'),
 | 
			
		||||
    z.literal('color'),
 | 
			
		||||
    z.literal('x'),
 | 
			
		||||
    z.literal('y'),
 | 
			
		||||
    z.literal('width'),
 | 
			
		||||
    z.literal('height'),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export interface AreaDiffChanged {
 | 
			
		||||
    object: 'area';
 | 
			
		||||
    type: 'changed';
 | 
			
		||||
    areaId: string;
 | 
			
		||||
    attribute: AreaDiffAttribute;
 | 
			
		||||
    oldValue?: string | number | null;
 | 
			
		||||
    newValue?: string | number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AreaDiffChangedSchema: z.ZodType<AreaDiffChanged> = z.object({
 | 
			
		||||
    object: z.literal('area'),
 | 
			
		||||
    type: z.literal('changed'),
 | 
			
		||||
    areaId: z.string(),
 | 
			
		||||
    attribute: areaDiffAttributeSchema,
 | 
			
		||||
    oldValue: z.union([z.string(), z.number(), z.null()]).optional(),
 | 
			
		||||
    newValue: z.union([z.string(), z.number(), z.null()]).optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface AreaDiffRemoved {
 | 
			
		||||
    object: 'area';
 | 
			
		||||
    type: 'removed';
 | 
			
		||||
    areaId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AreaDiffRemovedSchema: z.ZodType<AreaDiffRemoved> = z.object({
 | 
			
		||||
    object: z.literal('area'),
 | 
			
		||||
    type: z.literal('removed'),
 | 
			
		||||
    areaId: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface AreaDiffAdded<T = Area> {
 | 
			
		||||
    object: 'area';
 | 
			
		||||
    type: 'added';
 | 
			
		||||
    areaAdded: T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createAreaDiffAddedSchema = <T = Area>(
 | 
			
		||||
    areaSchema: z.ZodType<T>
 | 
			
		||||
): z.ZodType<AreaDiffAdded<T>> => {
 | 
			
		||||
    return z.object({
 | 
			
		||||
        object: z.literal('area'),
 | 
			
		||||
        type: z.literal('added'),
 | 
			
		||||
        areaAdded: areaSchema,
 | 
			
		||||
    }) as z.ZodType<AreaDiffAdded<T>>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AreaDiff<T = Area> =
 | 
			
		||||
    | AreaDiffChanged
 | 
			
		||||
    | AreaDiffRemoved
 | 
			
		||||
    | AreaDiffAdded<T>;
 | 
			
		||||
 | 
			
		||||
export const createAreaDiffSchema = <T = Area>(
 | 
			
		||||
    areaSchema: z.ZodType<T>
 | 
			
		||||
): z.ZodType<AreaDiff<T>> => {
 | 
			
		||||
    return z.union([
 | 
			
		||||
        AreaDiffChangedSchema,
 | 
			
		||||
        AreaDiffRemovedSchema,
 | 
			
		||||
        createAreaDiffAddedSchema(areaSchema),
 | 
			
		||||
    ]) as z.ZodType<AreaDiff<T>>;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										883
									
								
								src/lib/domain/diff/diff-check/__tests__/diff-check.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										883
									
								
								src/lib/domain/diff/diff-check/__tests__/diff-check.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,883 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { generateDiff } from '../diff-check';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { TableDiffChanged } from '../../table-diff';
 | 
			
		||||
import type { FieldDiffChanged } from '../../field-diff';
 | 
			
		||||
import type { AreaDiffChanged } from '../../area-diff';
 | 
			
		||||
 | 
			
		||||
// Helper function to create a mock diagram
 | 
			
		||||
function createMockDiagram(overrides?: Partial<Diagram>): Diagram {
 | 
			
		||||
    return {
 | 
			
		||||
        id: 'diagram-1',
 | 
			
		||||
        name: 'Test Diagram',
 | 
			
		||||
        databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        tables: [],
 | 
			
		||||
        relationships: [],
 | 
			
		||||
        areas: [],
 | 
			
		||||
        createdAt: new Date(),
 | 
			
		||||
        updatedAt: new Date(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to create a mock table
 | 
			
		||||
function createMockTable(overrides?: Partial<DBTable>): DBTable {
 | 
			
		||||
    return {
 | 
			
		||||
        id: 'table-1',
 | 
			
		||||
        name: 'users',
 | 
			
		||||
        fields: [],
 | 
			
		||||
        indexes: [],
 | 
			
		||||
        x: 0,
 | 
			
		||||
        y: 0,
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    } as DBTable;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to create a mock field
 | 
			
		||||
function createMockField(overrides?: Partial<DBField>): DBField {
 | 
			
		||||
    return {
 | 
			
		||||
        id: 'field-1',
 | 
			
		||||
        name: 'id',
 | 
			
		||||
        type: { id: 'integer', name: 'integer' },
 | 
			
		||||
        primaryKey: false,
 | 
			
		||||
        nullable: true,
 | 
			
		||||
        unique: false,
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    } as DBField;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to create a mock relationship
 | 
			
		||||
function createMockRelationship(
 | 
			
		||||
    overrides?: Partial<DBRelationship>
 | 
			
		||||
): DBRelationship {
 | 
			
		||||
    return {
 | 
			
		||||
        id: 'rel-1',
 | 
			
		||||
        sourceTableId: 'table-1',
 | 
			
		||||
        targetTableId: 'table-2',
 | 
			
		||||
        sourceFieldId: 'field-1',
 | 
			
		||||
        targetFieldId: 'field-2',
 | 
			
		||||
        type: 'one-to-many',
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    } as DBRelationship;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to create a mock area
 | 
			
		||||
function createMockArea(overrides?: Partial<Area>): Area {
 | 
			
		||||
    return {
 | 
			
		||||
        id: 'area-1',
 | 
			
		||||
        name: 'Main Area',
 | 
			
		||||
        x: 0,
 | 
			
		||||
        y: 0,
 | 
			
		||||
        width: 100,
 | 
			
		||||
        height: 100,
 | 
			
		||||
        color: 'blue',
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    } as Area;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('generateDiff', () => {
 | 
			
		||||
    describe('Basic Table Diffing', () => {
 | 
			
		||||
        it('should detect added tables', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({ tables: [] });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable()],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('table-table-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('added');
 | 
			
		||||
            expect(result.changedTables.has('table-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect removed tables', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable()],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({ tables: [] });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('table-table-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('removed');
 | 
			
		||||
            expect(result.changedTables.has('table-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect table name changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ name: 'customers' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('table-name-table-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('changed');
 | 
			
		||||
            expect((diff as TableDiffChanged)?.attribute).toBe('name');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect table position changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ x: 0, y: 0 })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ x: 100, y: 200 })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        tables: ['name', 'comments', 'color', 'x', 'y'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(2);
 | 
			
		||||
            expect(result.diffMap.has('table-x-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('table-y-table-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect table width changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ width: 150 })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ width: 250 })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        tables: ['width'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('table-width-table-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('changed');
 | 
			
		||||
            expect((diff as TableDiffChanged)?.attribute).toBe('width');
 | 
			
		||||
            expect((diff as TableDiffChanged)?.oldValue).toBe(150);
 | 
			
		||||
            expect((diff as TableDiffChanged)?.newValue).toBe(250);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect multiple table dimension changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ x: 0, y: 0, width: 100 })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ x: 50, y: 75, width: 200 })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        tables: ['x', 'y', 'width'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(3);
 | 
			
		||||
            expect(result.diffMap.has('table-x-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('table-y-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('table-width-table-1')).toBe(true);
 | 
			
		||||
 | 
			
		||||
            const widthDiff = result.diffMap.get('table-width-table-1');
 | 
			
		||||
            expect(widthDiff?.type).toBe('changed');
 | 
			
		||||
            expect((widthDiff as TableDiffChanged)?.oldValue).toBe(100);
 | 
			
		||||
            expect((widthDiff as TableDiffChanged)?.newValue).toBe(200);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Field Diffing', () => {
 | 
			
		||||
        it('should detect added fields', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ fields: [] })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [createMockField()],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('field-field-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('added');
 | 
			
		||||
            expect(result.changedFields.has('field-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect removed fields', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [createMockField()],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ fields: [] })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('field-field-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('removed');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect field type changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createMockField({
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createMockField({
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('field-type-field-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('changed');
 | 
			
		||||
            expect((diff as FieldDiffChanged)?.attribute).toBe('type');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Relationship Diffing', () => {
 | 
			
		||||
        it('should detect added relationships', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({ relationships: [] });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                relationships: [createMockRelationship()],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('relationship-rel-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('added');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect removed relationships', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                relationships: [createMockRelationship()],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({ relationships: [] });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('relationship-rel-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('removed');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Area Diffing', () => {
 | 
			
		||||
        it('should detect added areas when includeAreas is true', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({ areas: [] });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea()],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('area-area-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('added');
 | 
			
		||||
            expect(result.changedAreas.has('area-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should not detect area changes when includeAreas is false', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({ areas: [] });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea()],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: false,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect area width changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea({ width: 100 })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea({ width: 200 })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        areas: ['width'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('area-width-area-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('changed');
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.attribute).toBe('width');
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.oldValue).toBe(100);
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.newValue).toBe(200);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect area height changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea({ height: 100 })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                areas: [createMockArea({ height: 300 })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        areas: ['height'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const diff = result.diffMap.get('area-height-area-1');
 | 
			
		||||
            expect(diff).toBeDefined();
 | 
			
		||||
            expect(diff?.type).toBe('changed');
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.attribute).toBe('height');
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.oldValue).toBe(100);
 | 
			
		||||
            expect((diff as AreaDiffChanged)?.newValue).toBe(300);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect multiple area dimension changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                areas: [
 | 
			
		||||
                    createMockArea({ x: 0, y: 0, width: 100, height: 100 }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                areas: [
 | 
			
		||||
                    createMockArea({ x: 50, y: 50, width: 200, height: 300 }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        areas: ['x', 'y', 'width', 'height'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(4);
 | 
			
		||||
            expect(result.diffMap.has('area-x-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-y-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-width-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-height-area-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Custom Matchers', () => {
 | 
			
		||||
        it('should use custom table matcher to match by name', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-2', name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    matchers: {
 | 
			
		||||
                        table: (table, tables) =>
 | 
			
		||||
                            tables.find((t) => t.name === table.name),
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should not detect any changes since tables match by name
 | 
			
		||||
            expect(result.diffMap.size).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect changes when custom matcher finds no match', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-2', name: 'customers' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    matchers: {
 | 
			
		||||
                        table: (table, tables) =>
 | 
			
		||||
                            tables.find((t) => t.name === table.name),
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should detect both added and removed since names don't match
 | 
			
		||||
            expect(result.diffMap.size).toBe(2);
 | 
			
		||||
            expect(result.diffMap.has('table-table-1')).toBe(true); // removed
 | 
			
		||||
            expect(result.diffMap.has('table-table-2')).toBe(true); // added
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should use custom field matcher to match by name', () => {
 | 
			
		||||
            const field1 = createMockField({
 | 
			
		||||
                id: 'field-1',
 | 
			
		||||
                name: 'email',
 | 
			
		||||
                nullable: true,
 | 
			
		||||
            });
 | 
			
		||||
            const field2 = createMockField({
 | 
			
		||||
                id: 'field-2',
 | 
			
		||||
                name: 'email',
 | 
			
		||||
                nullable: false,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', fields: [field1] })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', fields: [field2] })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    matchers: {
 | 
			
		||||
                        field: (field, fields) =>
 | 
			
		||||
                            fields.find((f) => f.name === field.name),
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // With name-based matching, field-1 should match field-2 by name
 | 
			
		||||
            // and detect the nullable change
 | 
			
		||||
            const nullableChange = result.diffMap.get('field-nullable-field-1');
 | 
			
		||||
            expect(nullableChange).toBeDefined();
 | 
			
		||||
            expect(nullableChange?.type).toBe('changed');
 | 
			
		||||
            expect((nullableChange as FieldDiffChanged)?.attribute).toBe(
 | 
			
		||||
                'nullable'
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should use case-insensitive custom matcher', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', name: 'Users' })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-2', name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    matchers: {
 | 
			
		||||
                        table: (table, tables) =>
 | 
			
		||||
                            tables.find(
 | 
			
		||||
                                (t) =>
 | 
			
		||||
                                    t.name.toLowerCase() ===
 | 
			
		||||
                                    table.name.toLowerCase()
 | 
			
		||||
                            ),
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // With case-insensitive name matching, the tables are matched
 | 
			
		||||
            // but the name case difference is still detected as a change
 | 
			
		||||
            expect(result.diffMap.size).toBe(1);
 | 
			
		||||
            const nameChange = result.diffMap.get('table-name-table-1');
 | 
			
		||||
            expect(nameChange).toBeDefined();
 | 
			
		||||
            expect(nameChange?.type).toBe('changed');
 | 
			
		||||
            expect((nameChange as TableDiffChanged)?.attribute).toBe('name');
 | 
			
		||||
            expect((nameChange as TableDiffChanged)?.oldValue).toBe('Users');
 | 
			
		||||
            expect((nameChange as TableDiffChanged)?.newValue).toBe('users');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Filtering Options', () => {
 | 
			
		||||
        it('should only check specified change types', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1', name: 'users' })],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-2', name: 'products' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    changeTypes: {
 | 
			
		||||
                        tables: ['added'], // Only check for added tables
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should only detect added table (table-2)
 | 
			
		||||
            const addedTables = Array.from(result.diffMap.values()).filter(
 | 
			
		||||
                (diff) => diff.type === 'added' && diff.object === 'table'
 | 
			
		||||
            );
 | 
			
		||||
            expect(addedTables.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
            // Should not detect removed table (table-1)
 | 
			
		||||
            const removedTables = Array.from(result.diffMap.values()).filter(
 | 
			
		||||
                (diff) => diff.type === 'removed' && diff.object === 'table'
 | 
			
		||||
            );
 | 
			
		||||
            expect(removedTables.length).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should only check specified attributes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        color: 'blue',
 | 
			
		||||
                        comments: 'old comment',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        name: 'customers',
 | 
			
		||||
                        color: 'red',
 | 
			
		||||
                        comments: 'new comment',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        tables: ['name'], // Only check name changes
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should only detect name change
 | 
			
		||||
            const nameChanges = Array.from(result.diffMap.values()).filter(
 | 
			
		||||
                (diff) =>
 | 
			
		||||
                    diff.type === 'changed' &&
 | 
			
		||||
                    diff.attribute === 'name' &&
 | 
			
		||||
                    diff.object === 'table'
 | 
			
		||||
            );
 | 
			
		||||
            expect(nameChanges.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
            // Should not detect color or comments changes
 | 
			
		||||
            const otherChanges = Array.from(result.diffMap.values()).filter(
 | 
			
		||||
                (diff) =>
 | 
			
		||||
                    diff.type === 'changed' &&
 | 
			
		||||
                    (diff.attribute === 'color' ||
 | 
			
		||||
                        diff.attribute === 'comments') &&
 | 
			
		||||
                    diff.object === 'table'
 | 
			
		||||
            );
 | 
			
		||||
            expect(otherChanges.length).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should respect include flags', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [createMockField()],
 | 
			
		||||
                        indexes: [{ id: 'idx-1', name: 'idx' } as DBIndex],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        fields: [],
 | 
			
		||||
                        indexes: [],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeFields: false,
 | 
			
		||||
                    includeIndexes: true,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should only detect index removal, not field removal
 | 
			
		||||
            expect(result.diffMap.has('index-idx-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('field-field-1')).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Complex Scenarios', () => {
 | 
			
		||||
        it('should detect all dimensional changes for tables and areas', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        x: 0,
 | 
			
		||||
                        y: 0,
 | 
			
		||||
                        width: 100,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                areas: [
 | 
			
		||||
                    createMockArea({
 | 
			
		||||
                        id: 'area-1',
 | 
			
		||||
                        x: 0,
 | 
			
		||||
                        y: 0,
 | 
			
		||||
                        width: 200,
 | 
			
		||||
                        height: 150,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        x: 10,
 | 
			
		||||
                        y: 20,
 | 
			
		||||
                        width: 120,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                areas: [
 | 
			
		||||
                    createMockArea({
 | 
			
		||||
                        id: 'area-1',
 | 
			
		||||
                        x: 25,
 | 
			
		||||
                        y: 35,
 | 
			
		||||
                        width: 250,
 | 
			
		||||
                        height: 175,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        tables: ['x', 'y', 'width'],
 | 
			
		||||
                        areas: ['x', 'y', 'width', 'height'],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Table dimensional changes
 | 
			
		||||
            expect(result.diffMap.has('table-x-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('table-y-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('table-width-table-1')).toBe(true);
 | 
			
		||||
 | 
			
		||||
            // Area dimensional changes
 | 
			
		||||
            expect(result.diffMap.has('area-x-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-y-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-width-area-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-height-area-1')).toBe(true);
 | 
			
		||||
 | 
			
		||||
            // Verify the correct values
 | 
			
		||||
            const tableWidthDiff = result.diffMap.get('table-width-table-1');
 | 
			
		||||
            expect((tableWidthDiff as TableDiffChanged)?.oldValue).toBe(100);
 | 
			
		||||
            expect((tableWidthDiff as TableDiffChanged)?.newValue).toBe(120);
 | 
			
		||||
 | 
			
		||||
            const areaWidthDiff = result.diffMap.get('area-width-area-1');
 | 
			
		||||
            expect((areaWidthDiff as AreaDiffChanged)?.oldValue).toBe(200);
 | 
			
		||||
            expect((areaWidthDiff as AreaDiffChanged)?.newValue).toBe(250);
 | 
			
		||||
 | 
			
		||||
            const areaHeightDiff = result.diffMap.get('area-height-area-1');
 | 
			
		||||
            expect((areaHeightDiff as AreaDiffChanged)?.oldValue).toBe(150);
 | 
			
		||||
            expect((areaHeightDiff as AreaDiffChanged)?.newValue).toBe(175);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle multiple simultaneous changes', () => {
 | 
			
		||||
            const oldDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createMockField({ id: 'field-1', name: 'id' }),
 | 
			
		||||
                            createMockField({ id: 'field-2', name: 'email' }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-2',
 | 
			
		||||
                        name: 'products',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [createMockRelationship()],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const newDiagram = createMockDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-1',
 | 
			
		||||
                        name: 'customers', // Changed name
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createMockField({ id: 'field-1', name: 'id' }),
 | 
			
		||||
                            // Removed field-2
 | 
			
		||||
                            createMockField({ id: 'field-3', name: 'name' }), // Added field
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                    // Removed table-2
 | 
			
		||||
                    createMockTable({
 | 
			
		||||
                        id: 'table-3',
 | 
			
		||||
                        name: 'orders', // Added table
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [], // Removed relationship
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: oldDiagram,
 | 
			
		||||
                newDiagram,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Verify all changes are detected
 | 
			
		||||
            expect(result.diffMap.has('table-name-table-1')).toBe(true); // Table name change
 | 
			
		||||
            expect(result.diffMap.has('field-field-2')).toBe(true); // Removed field
 | 
			
		||||
            expect(result.diffMap.has('field-field-3')).toBe(true); // Added field
 | 
			
		||||
            expect(result.diffMap.has('table-table-2')).toBe(true); // Removed table
 | 
			
		||||
            expect(result.diffMap.has('table-table-3')).toBe(true); // Added table
 | 
			
		||||
            expect(result.diffMap.has('relationship-rel-1')).toBe(true); // Removed relationship
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle empty diagrams', () => {
 | 
			
		||||
            const emptyDiagram1 = createMockDiagram();
 | 
			
		||||
            const emptyDiagram2 = createMockDiagram();
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: emptyDiagram1,
 | 
			
		||||
                newDiagram: emptyDiagram2,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(result.diffMap.size).toBe(0);
 | 
			
		||||
            expect(result.changedTables.size).toBe(0);
 | 
			
		||||
            expect(result.changedFields.size).toBe(0);
 | 
			
		||||
            expect(result.changedAreas.size).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle diagrams with undefined collections', () => {
 | 
			
		||||
            const diagram1 = createMockDiagram({
 | 
			
		||||
                tables: undefined,
 | 
			
		||||
                relationships: undefined,
 | 
			
		||||
                areas: undefined,
 | 
			
		||||
            });
 | 
			
		||||
            const diagram2 = createMockDiagram({
 | 
			
		||||
                tables: [createMockTable({ id: 'table-1' })],
 | 
			
		||||
                relationships: [createMockRelationship({ id: 'rel-1' })],
 | 
			
		||||
                areas: [createMockArea({ id: 'area-1' })],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = generateDiff({
 | 
			
		||||
                diagram: diagram1,
 | 
			
		||||
                newDiagram: diagram2,
 | 
			
		||||
                options: {
 | 
			
		||||
                    includeAreas: true,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Should detect all as added
 | 
			
		||||
            expect(result.diffMap.has('table-table-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('relationship-rel-1')).toBe(true);
 | 
			
		||||
            expect(result.diffMap.has('area-area-1')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,36 +8,43 @@ import type { RelationshipDiff } from './relationship-diff';
 | 
			
		||||
import { createRelationshipDiffSchema } from './relationship-diff';
 | 
			
		||||
import type { TableDiff } from './table-diff';
 | 
			
		||||
import { createTableDiffSchema } from './table-diff';
 | 
			
		||||
import type { DBField, DBIndex, DBRelationship, DBTable } from '..';
 | 
			
		||||
import type { AreaDiff } from './area-diff';
 | 
			
		||||
import { createAreaDiffSchema } from './area-diff';
 | 
			
		||||
import type { DBField, DBIndex, DBRelationship, DBTable, Area } from '..';
 | 
			
		||||
 | 
			
		||||
export type ChartDBDiff<
 | 
			
		||||
    TTable = DBTable,
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
> =
 | 
			
		||||
    | TableDiff<TTable>
 | 
			
		||||
    | FieldDiff<TField>
 | 
			
		||||
    | IndexDiff<TIndex>
 | 
			
		||||
    | RelationshipDiff<TRelationship>;
 | 
			
		||||
    | RelationshipDiff<TRelationship>
 | 
			
		||||
    | AreaDiff<TArea>;
 | 
			
		||||
 | 
			
		||||
export const createChartDBDiffSchema = <
 | 
			
		||||
    TTable = DBTable,
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
>(
 | 
			
		||||
    tableSchema: z.ZodType<TTable>,
 | 
			
		||||
    fieldSchema: z.ZodType<TField>,
 | 
			
		||||
    indexSchema: z.ZodType<TIndex>,
 | 
			
		||||
    relationshipSchema: z.ZodType<TRelationship>
 | 
			
		||||
): z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship>> => {
 | 
			
		||||
    relationshipSchema: z.ZodType<TRelationship>,
 | 
			
		||||
    areaSchema: z.ZodType<TArea>
 | 
			
		||||
): z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>> => {
 | 
			
		||||
    return z.union([
 | 
			
		||||
        createTableDiffSchema(tableSchema),
 | 
			
		||||
        createFieldDiffSchema(fieldSchema),
 | 
			
		||||
        createIndexDiffSchema(indexSchema),
 | 
			
		||||
        createRelationshipDiffSchema(relationshipSchema),
 | 
			
		||||
    ]) as z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
 | 
			
		||||
        createAreaDiffSchema(areaSchema),
 | 
			
		||||
    ]) as z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffMap<
 | 
			
		||||
@@ -45,18 +52,21 @@ export type DiffMap<
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
> = Map<string, ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
> = Map<string, ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
 | 
			
		||||
 | 
			
		||||
export type DiffObject<
 | 
			
		||||
    TTable = DBTable,
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
> =
 | 
			
		||||
    | TableDiff<TTable>['object']
 | 
			
		||||
    | FieldDiff<TField>['object']
 | 
			
		||||
    | IndexDiff<TIndex>['object']
 | 
			
		||||
    | RelationshipDiff<TRelationship>['object'];
 | 
			
		||||
    | RelationshipDiff<TRelationship>['object']
 | 
			
		||||
    | AreaDiff<TArea>['object'];
 | 
			
		||||
 | 
			
		||||
type ExtractDiffKind<T> = T extends { object: infer O; type: infer Type }
 | 
			
		||||
    ? T extends { attribute: infer A }
 | 
			
		||||
@@ -69,16 +79,18 @@ export type DiffKind<
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
> = ExtractDiffKind<ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
> = ExtractDiffKind<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
 | 
			
		||||
 | 
			
		||||
export const isDiffOfKind = <
 | 
			
		||||
    TTable = DBTable,
 | 
			
		||||
    TField = DBField,
 | 
			
		||||
    TIndex = DBIndex,
 | 
			
		||||
    TRelationship = DBRelationship,
 | 
			
		||||
    TArea = Area,
 | 
			
		||||
>(
 | 
			
		||||
    diff: ChartDBDiff<TTable, TField, TIndex, TRelationship>,
 | 
			
		||||
    kind: DiffKind<TTable, TField, TIndex, TRelationship>
 | 
			
		||||
    diff: ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>,
 | 
			
		||||
    kind: DiffKind<TTable, TField, TIndex, TRelationship, TArea>
 | 
			
		||||
): boolean => {
 | 
			
		||||
    if ('attribute' in kind) {
 | 
			
		||||
        return (
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,16 @@ import type { DBTable } from '../db-table';
 | 
			
		||||
 | 
			
		||||
export type TableDiffAttribute = keyof Pick<
 | 
			
		||||
    DBTable,
 | 
			
		||||
    'name' | 'comments' | 'color'
 | 
			
		||||
    'name' | 'comments' | 'color' | 'x' | 'y' | 'width'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
const tableDiffAttributeSchema: z.ZodType<TableDiffAttribute> = z.union([
 | 
			
		||||
    z.literal('name'),
 | 
			
		||||
    z.literal('comments'),
 | 
			
		||||
    z.literal('color'),
 | 
			
		||||
    z.literal('x'),
 | 
			
		||||
    z.literal('y'),
 | 
			
		||||
    z.literal('width'),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export interface TableDiffChanged {
 | 
			
		||||
@@ -17,8 +20,8 @@ export interface TableDiffChanged {
 | 
			
		||||
    type: 'changed';
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attribute: TableDiffAttribute;
 | 
			
		||||
    oldValue?: string | null;
 | 
			
		||||
    newValue?: string | null;
 | 
			
		||||
    oldValue?: string | number | null;
 | 
			
		||||
    newValue?: string | number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableDiffChangedSchema: z.ZodType<TableDiffChanged> = z.object({
 | 
			
		||||
@@ -26,8 +29,8 @@ export const TableDiffChangedSchema: z.ZodType<TableDiffChanged> = z.object({
 | 
			
		||||
    type: z.literal('changed'),
 | 
			
		||||
    tableId: z.string(),
 | 
			
		||||
    attribute: tableDiffAttributeSchema,
 | 
			
		||||
    oldValue: z.string().or(z.null()).optional(),
 | 
			
		||||
    newValue: z.string().or(z.null()).optional(),
 | 
			
		||||
    oldValue: z.union([z.string(), z.number(), z.null()]).optional(),
 | 
			
		||||
    newValue: z.union([z.string(), z.number(), z.null()]).optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface TableDiffRemoved {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										157
									
								
								src/lib/import-method/__tests__/detect-import-type.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/lib/import-method/__tests__/detect-import-type.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
import { describe, it, expect } from 'vitest';
 | 
			
		||||
import { detectImportMethod } from '../detect-import-method';
 | 
			
		||||
 | 
			
		||||
describe('detectImportMethod', () => {
 | 
			
		||||
    describe('DBML detection', () => {
 | 
			
		||||
        it('should detect DBML with Table definition', () => {
 | 
			
		||||
            const content = `Table users {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  name varchar
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with Ref definition', () => {
 | 
			
		||||
            const content = `Table posts {
 | 
			
		||||
  user_id int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Ref: posts.user_id > users.id`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with pk attribute', () => {
 | 
			
		||||
            const content = `id integer [pk]`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with ref attribute', () => {
 | 
			
		||||
            const content = `user_id int [ref: > users.id]`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with Enum definition', () => {
 | 
			
		||||
            const content = `Enum status {
 | 
			
		||||
  active
 | 
			
		||||
  inactive
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with TableGroup', () => {
 | 
			
		||||
            const content = `TableGroup commerce {
 | 
			
		||||
  users
 | 
			
		||||
  orders
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DBML with Note', () => {
 | 
			
		||||
            const content = `Note project_note {
 | 
			
		||||
  'This is a note about the project'
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should prioritize DBML over SQL when both patterns exist', () => {
 | 
			
		||||
            const content = `CREATE TABLE test (id int);
 | 
			
		||||
Table users {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('SQL DDL detection', () => {
 | 
			
		||||
        it('should detect CREATE TABLE statement', () => {
 | 
			
		||||
            const content = `CREATE TABLE users (
 | 
			
		||||
  id INT PRIMARY KEY,
 | 
			
		||||
  name VARCHAR(255)
 | 
			
		||||
);`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect ALTER TABLE statement', () => {
 | 
			
		||||
            const content = `ALTER TABLE users ADD COLUMN email VARCHAR(255);`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DROP TABLE statement', () => {
 | 
			
		||||
            const content = `DROP TABLE IF EXISTS users;`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect CREATE INDEX statement', () => {
 | 
			
		||||
            const content = `CREATE INDEX idx_users_email ON users(email);`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect multiple DDL statements', () => {
 | 
			
		||||
            const content = `CREATE TABLE users (id INT);
 | 
			
		||||
CREATE TABLE posts (id INT);
 | 
			
		||||
ALTER TABLE posts ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect DDL case-insensitively', () => {
 | 
			
		||||
            const content = `create table users (id int);`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('ddl');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('JSON detection', () => {
 | 
			
		||||
        it('should detect JSON object', () => {
 | 
			
		||||
            const content = `{
 | 
			
		||||
  "tables": [],
 | 
			
		||||
  "relationships": []
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('query');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect JSON array', () => {
 | 
			
		||||
            const content = `[
 | 
			
		||||
  {"name": "users"},
 | 
			
		||||
  {"name": "posts"}
 | 
			
		||||
]`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('query');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect minified JSON', () => {
 | 
			
		||||
            const content = `{"tables":[],"relationships":[]}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('query');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should detect JSON with whitespace', () => {
 | 
			
		||||
            const content = `   {
 | 
			
		||||
    "data": true
 | 
			
		||||
}   `;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('query');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('edge cases', () => {
 | 
			
		||||
        it('should return null for empty content', () => {
 | 
			
		||||
            expect(detectImportMethod('')).toBeNull();
 | 
			
		||||
            expect(detectImportMethod('   ')).toBeNull();
 | 
			
		||||
            expect(detectImportMethod('\n\n')).toBeNull();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return null for unrecognized content', () => {
 | 
			
		||||
            const content = `This is just some random text
 | 
			
		||||
that doesn't match any pattern`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBeNull();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle content with special characters', () => {
 | 
			
		||||
            const content = `Table users {
 | 
			
		||||
  name varchar // Special chars: áéíóú
 | 
			
		||||
}`;
 | 
			
		||||
            expect(detectImportMethod(content)).toBe('dbml');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle malformed JSON gracefully', () => {
 | 
			
		||||
            const content = `{ "incomplete": `;
 | 
			
		||||
            expect(detectImportMethod(content)).toBeNull();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										59
									
								
								src/lib/import-method/detect-import-method.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/lib/import-method/detect-import-method.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import type { ImportMethod } from './import-method';
 | 
			
		||||
 | 
			
		||||
export const detectImportMethod = (content: string): ImportMethod | null => {
 | 
			
		||||
    if (!content || content.trim().length === 0) return null;
 | 
			
		||||
 | 
			
		||||
    const upperContent = content.toUpperCase();
 | 
			
		||||
 | 
			
		||||
    // Check for DBML patterns first (case sensitive)
 | 
			
		||||
    const dbmlPatterns = [
 | 
			
		||||
        /^Table\s+\w+\s*{/m,
 | 
			
		||||
        /^Ref:\s*\w+/m,
 | 
			
		||||
        /^Enum\s+\w+\s*{/m,
 | 
			
		||||
        /^TableGroup\s+/m,
 | 
			
		||||
        /^Note\s+\w+\s*{/m,
 | 
			
		||||
        /\[pk\]/,
 | 
			
		||||
        /\[ref:\s*[<>-]/,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const hasDBMLPatterns = dbmlPatterns.some((pattern) =>
 | 
			
		||||
        pattern.test(content)
 | 
			
		||||
    );
 | 
			
		||||
    if (hasDBMLPatterns) return 'dbml';
 | 
			
		||||
 | 
			
		||||
    // Common SQL DDL keywords
 | 
			
		||||
    const ddlKeywords = [
 | 
			
		||||
        'CREATE TABLE',
 | 
			
		||||
        'ALTER TABLE',
 | 
			
		||||
        'DROP TABLE',
 | 
			
		||||
        'CREATE INDEX',
 | 
			
		||||
        'CREATE VIEW',
 | 
			
		||||
        'CREATE PROCEDURE',
 | 
			
		||||
        'CREATE FUNCTION',
 | 
			
		||||
        'CREATE SCHEMA',
 | 
			
		||||
        'CREATE DATABASE',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Check for SQL DDL patterns
 | 
			
		||||
    const hasDDLKeywords = ddlKeywords.some((keyword) =>
 | 
			
		||||
        upperContent.includes(keyword)
 | 
			
		||||
    );
 | 
			
		||||
    if (hasDDLKeywords) return 'ddl';
 | 
			
		||||
 | 
			
		||||
    // Check if it looks like JSON
 | 
			
		||||
    try {
 | 
			
		||||
        // Just check structure, don't need full parse for detection
 | 
			
		||||
        if (
 | 
			
		||||
            (content.trim().startsWith('{') && content.trim().endsWith('}')) ||
 | 
			
		||||
            (content.trim().startsWith('[') && content.trim().endsWith(']'))
 | 
			
		||||
        ) {
 | 
			
		||||
            return 'query';
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        // Not valid JSON, might be partial
 | 
			
		||||
        console.error('Error detecting content type:', error);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If we can't confidently detect, return null
 | 
			
		||||
    return null;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/import-method/import-method.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/import-method/import-method.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export type ImportMethod = 'query' | 'ddl' | 'dbml';
 | 
			
		||||
							
								
								
									
										687
									
								
								src/lib/utils/__tests__/apply-ids.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										687
									
								
								src/lib/utils/__tests__/apply-ids.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,687 @@
 | 
			
		||||
import { describe, expect, it } from 'vitest';
 | 
			
		||||
import { applyIds } from '../apply-ids';
 | 
			
		||||
import {
 | 
			
		||||
    DatabaseType,
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type Diagram,
 | 
			
		||||
    type DBTable,
 | 
			
		||||
    type DBField,
 | 
			
		||||
    type DBIndex,
 | 
			
		||||
    type DBRelationship,
 | 
			
		||||
    type DBDependency,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '../../domain';
 | 
			
		||||
 | 
			
		||||
describe('applyIds', () => {
 | 
			
		||||
    const createBaseDiagram = (overrides?: Partial<Diagram>): Diagram => ({
 | 
			
		||||
        id: 'diagram1',
 | 
			
		||||
        name: 'Test Diagram',
 | 
			
		||||
        databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
        createdAt: new Date(),
 | 
			
		||||
        updatedAt: new Date(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createTable = (overrides: Partial<DBTable>): DBTable => ({
 | 
			
		||||
        id: 'table-1',
 | 
			
		||||
        name: 'table',
 | 
			
		||||
        schema: 'public',
 | 
			
		||||
        x: 0,
 | 
			
		||||
        y: 0,
 | 
			
		||||
        fields: [],
 | 
			
		||||
        indexes: [],
 | 
			
		||||
        color: '#000000',
 | 
			
		||||
        comments: null,
 | 
			
		||||
        isView: false,
 | 
			
		||||
        createdAt: Date.now(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createField = (overrides: Partial<DBField>): DBField => ({
 | 
			
		||||
        id: 'field-1',
 | 
			
		||||
        name: 'field',
 | 
			
		||||
        type: { id: 'integer', name: 'integer' },
 | 
			
		||||
        primaryKey: false,
 | 
			
		||||
        nullable: false,
 | 
			
		||||
        unique: false,
 | 
			
		||||
        comments: null,
 | 
			
		||||
        collation: null,
 | 
			
		||||
        createdAt: Date.now(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createIndex = (overrides: Partial<DBIndex>): DBIndex => ({
 | 
			
		||||
        id: 'index-1',
 | 
			
		||||
        name: 'index',
 | 
			
		||||
        unique: false,
 | 
			
		||||
        fieldIds: [],
 | 
			
		||||
        createdAt: Date.now(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createRelationship = (
 | 
			
		||||
        overrides: Partial<DBRelationship>
 | 
			
		||||
    ): DBRelationship => ({
 | 
			
		||||
        id: 'rel-1',
 | 
			
		||||
        name: 'relationship',
 | 
			
		||||
        sourceTableId: 'table-1',
 | 
			
		||||
        sourceFieldId: 'field-1',
 | 
			
		||||
        targetTableId: 'table-2',
 | 
			
		||||
        targetFieldId: 'field-2',
 | 
			
		||||
        sourceCardinality: 'many',
 | 
			
		||||
        targetCardinality: 'one',
 | 
			
		||||
        createdAt: Date.now(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createDependency = (
 | 
			
		||||
        overrides: Partial<DBDependency>
 | 
			
		||||
    ): DBDependency => ({
 | 
			
		||||
        id: 'dep-1',
 | 
			
		||||
        tableId: 'table-1',
 | 
			
		||||
        dependentTableId: 'table-2',
 | 
			
		||||
        createdAt: Date.now(),
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createCustomType = (
 | 
			
		||||
        overrides: Partial<DBCustomType>
 | 
			
		||||
    ): DBCustomType => ({
 | 
			
		||||
        id: 'type-1',
 | 
			
		||||
        name: 'custom_type',
 | 
			
		||||
        schema: 'public',
 | 
			
		||||
        kind: DBCustomTypeKind.enum,
 | 
			
		||||
        values: [],
 | 
			
		||||
        ...overrides,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('table ID mapping', () => {
 | 
			
		||||
        it('should preserve table IDs when tables match by name and schema', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-2',
 | 
			
		||||
                        name: 'posts',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        comments: 'Users table',
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-2',
 | 
			
		||||
                        name: 'posts',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 200,
 | 
			
		||||
                        y: 200,
 | 
			
		||||
                        color: '#00ff00',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(2);
 | 
			
		||||
            expect(result.tables?.[0].id).toBe('source-table-1');
 | 
			
		||||
            expect(result.tables?.[0].name).toBe('users');
 | 
			
		||||
            expect(result.tables?.[0].x).toBe(100); // Should keep target's position
 | 
			
		||||
            expect(result.tables?.[0].color).toBe('#ff0000'); // Should keep target's color
 | 
			
		||||
            expect(result.tables?.[1].id).toBe('source-table-2');
 | 
			
		||||
            expect(result.tables?.[1].name).toBe('posts');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should keep target table IDs when no matching source table exists', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'orders',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(1);
 | 
			
		||||
            expect(result.tables?.[0].id).toBe('target-table-1'); // Should keep target ID
 | 
			
		||||
            expect(result.tables?.[0].name).toBe('orders');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('field ID mapping', () => {
 | 
			
		||||
        it('should preserve field IDs when fields match by name within the same table', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-2',
 | 
			
		||||
                                name: 'email',
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                nullable: false,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                                comments: 'Primary key',
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-2',
 | 
			
		||||
                                name: 'email',
 | 
			
		||||
                                type: { id: 'text', name: 'text' },
 | 
			
		||||
                                primaryKey: false,
 | 
			
		||||
                                nullable: true,
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables?.[0].fields).toHaveLength(2);
 | 
			
		||||
            expect(result.tables?.[0].fields[0].id).toBe('source-field-1');
 | 
			
		||||
            expect(result.tables?.[0].fields[0].name).toBe('id');
 | 
			
		||||
            expect(result.tables?.[0].fields[0].type.id).toBe('bigint'); // Should keep target's type
 | 
			
		||||
            expect(result.tables?.[0].fields[1].id).toBe('source-field-2');
 | 
			
		||||
            expect(result.tables?.[0].fields[1].name).toBe('email');
 | 
			
		||||
            expect(result.tables?.[0].fields[1].nullable).toBe(true); // Should keep target's nullable
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('index ID mapping', () => {
 | 
			
		||||
        it('should preserve index IDs and update field references', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-1',
 | 
			
		||||
                                name: 'email',
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [
 | 
			
		||||
                            createIndex({
 | 
			
		||||
                                id: 'source-index-1',
 | 
			
		||||
                                name: 'idx_email',
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                                fieldIds: ['source-field-1'],
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-1',
 | 
			
		||||
                                name: 'email',
 | 
			
		||||
                                type: { id: 'text', name: 'text' },
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                        indexes: [
 | 
			
		||||
                            createIndex({
 | 
			
		||||
                                id: 'target-index-1',
 | 
			
		||||
                                name: 'idx_email',
 | 
			
		||||
                                unique: false,
 | 
			
		||||
                                fieldIds: ['target-field-1'],
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables?.[0].indexes).toHaveLength(1);
 | 
			
		||||
            expect(result.tables?.[0].indexes[0].id).toBe('source-index-1');
 | 
			
		||||
            expect(result.tables?.[0].indexes[0].fieldIds).toEqual([
 | 
			
		||||
                'source-field-1',
 | 
			
		||||
            ]); // Should update field reference
 | 
			
		||||
            expect(result.tables?.[0].indexes[0].unique).toBe(false); // Should keep target's unique setting
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('relationship ID mapping', () => {
 | 
			
		||||
        it('should preserve relationship IDs and update table/field references', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-2',
 | 
			
		||||
                        name: 'posts',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-2',
 | 
			
		||||
                                name: 'user_id',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [
 | 
			
		||||
                    createRelationship({
 | 
			
		||||
                        id: 'source-rel-1',
 | 
			
		||||
                        name: 'fk_posts_users',
 | 
			
		||||
                        sourceTableId: 'source-table-2',
 | 
			
		||||
                        sourceFieldId: 'source-field-2',
 | 
			
		||||
                        targetTableId: 'source-table-1',
 | 
			
		||||
                        targetFieldId: 'source-field-1',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-2',
 | 
			
		||||
                        name: 'posts',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 200,
 | 
			
		||||
                        y: 200,
 | 
			
		||||
                        color: '#00ff00',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-2',
 | 
			
		||||
                                name: 'user_id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                nullable: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                relationships: [
 | 
			
		||||
                    createRelationship({
 | 
			
		||||
                        id: 'target-rel-1',
 | 
			
		||||
                        name: 'fk_posts_users',
 | 
			
		||||
                        sourceTableId: 'target-table-2',
 | 
			
		||||
                        sourceFieldId: 'target-field-2',
 | 
			
		||||
                        targetTableId: 'target-table-1',
 | 
			
		||||
                        targetFieldId: 'target-field-1',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.relationships).toHaveLength(1);
 | 
			
		||||
            expect(result.relationships?.[0].id).toBe('source-rel-1');
 | 
			
		||||
            expect(result.relationships?.[0].sourceTableId).toBe(
 | 
			
		||||
                'source-table-2'
 | 
			
		||||
            );
 | 
			
		||||
            expect(result.relationships?.[0].sourceFieldId).toBe(
 | 
			
		||||
                'source-field-2'
 | 
			
		||||
            );
 | 
			
		||||
            expect(result.relationships?.[0].targetTableId).toBe(
 | 
			
		||||
                'source-table-1'
 | 
			
		||||
            );
 | 
			
		||||
            expect(result.relationships?.[0].targetFieldId).toBe(
 | 
			
		||||
                'source-field-1'
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('dependency ID mapping', () => {
 | 
			
		||||
        it('should preserve dependency IDs and update table references', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-2',
 | 
			
		||||
                        name: 'user_view',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        isView: true,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                dependencies: [
 | 
			
		||||
                    createDependency({
 | 
			
		||||
                        id: 'source-dep-1',
 | 
			
		||||
                        tableId: 'source-table-2',
 | 
			
		||||
                        dependentTableId: 'source-table-1',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-2',
 | 
			
		||||
                        name: 'user_view',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 200,
 | 
			
		||||
                        y: 200,
 | 
			
		||||
                        color: '#00ff00',
 | 
			
		||||
                        isView: true,
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
                dependencies: [
 | 
			
		||||
                    createDependency({
 | 
			
		||||
                        id: 'target-dep-1',
 | 
			
		||||
                        tableId: 'target-table-2',
 | 
			
		||||
                        dependentTableId: 'target-table-1',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.dependencies).toHaveLength(1);
 | 
			
		||||
            expect(result.dependencies?.[0].id).toBe('source-dep-1');
 | 
			
		||||
            expect(result.dependencies?.[0].tableId).toBe('source-table-2');
 | 
			
		||||
            expect(result.dependencies?.[0].dependentTableId).toBe(
 | 
			
		||||
                'source-table-1'
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('custom type ID mapping', () => {
 | 
			
		||||
        it('should preserve custom type IDs when types match by name and schema', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                customTypes: [
 | 
			
		||||
                    createCustomType({
 | 
			
		||||
                        id: 'source-type-1',
 | 
			
		||||
                        name: 'user_role',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        values: ['admin', 'user', 'guest'],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                customTypes: [
 | 
			
		||||
                    createCustomType({
 | 
			
		||||
                        id: 'target-type-1',
 | 
			
		||||
                        name: 'user_role',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        values: ['admin', 'user', 'guest', 'moderator'],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.customTypes).toHaveLength(1);
 | 
			
		||||
            expect(result.customTypes?.[0].id).toBe('source-type-1');
 | 
			
		||||
            expect(result.customTypes?.[0].values).toEqual([
 | 
			
		||||
                'admin',
 | 
			
		||||
                'user',
 | 
			
		||||
                'guest',
 | 
			
		||||
                'moderator',
 | 
			
		||||
            ]); // Should keep target's values
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('complex scenarios', () => {
 | 
			
		||||
        it('should handle partial matches correctly', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'source-field-2',
 | 
			
		||||
                                name: 'email',
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'bigint', name: 'bigint' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-3',
 | 
			
		||||
                                name: 'username',
 | 
			
		||||
                                type: { id: 'varchar', name: 'varchar' },
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-2',
 | 
			
		||||
                        name: 'posts',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 200,
 | 
			
		||||
                        y: 200,
 | 
			
		||||
                        color: '#00ff00',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables).toHaveLength(2);
 | 
			
		||||
            expect(result.tables?.[0].id).toBe('source-table-1');
 | 
			
		||||
            expect(result.tables?.[0].fields).toHaveLength(2);
 | 
			
		||||
            expect(result.tables?.[0].fields[0].id).toBe('source-field-1'); // Matched field
 | 
			
		||||
            expect(result.tables?.[0].fields[1].id).toBe('target-field-3'); // Unmatched field keeps target ID
 | 
			
		||||
            expect(result.tables?.[1].id).toBe('target-table-2'); // Unmatched table keeps target ID
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle different schemas correctly', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'auth',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result.tables?.[0].id).toBe('target-table-1'); // Different schemas, no match
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle empty diagrams', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram();
 | 
			
		||||
            const targetDiagram = createBaseDiagram();
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            expect(result).toEqual(targetDiagram);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return target diagram unchanged when source has no matching entities', () => {
 | 
			
		||||
            const sourceDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'source-table-1',
 | 
			
		||||
                        name: 'products',
 | 
			
		||||
                        schema: 'inventory',
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const targetDiagram = createBaseDiagram({
 | 
			
		||||
                tables: [
 | 
			
		||||
                    createTable({
 | 
			
		||||
                        id: 'target-table-1',
 | 
			
		||||
                        name: 'users',
 | 
			
		||||
                        schema: 'public',
 | 
			
		||||
                        x: 100,
 | 
			
		||||
                        y: 100,
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        fields: [
 | 
			
		||||
                            createField({
 | 
			
		||||
                                id: 'target-field-1',
 | 
			
		||||
                                name: 'id',
 | 
			
		||||
                                type: { id: 'integer', name: 'integer' },
 | 
			
		||||
                                primaryKey: true,
 | 
			
		||||
                                unique: true,
 | 
			
		||||
                            }),
 | 
			
		||||
                        ],
 | 
			
		||||
                    }),
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const result = applyIds({ sourceDiagram, targetDiagram });
 | 
			
		||||
 | 
			
		||||
            // Should keep all target IDs since nothing matches
 | 
			
		||||
            expect(result.tables?.[0].id).toBe('target-table-1');
 | 
			
		||||
            expect(result.tables?.[0].fields[0].id).toBe('target-field-1');
 | 
			
		||||
            expect(result.tables?.[0].name).toBe('users');
 | 
			
		||||
            expect(result.tables?.[0].schema).toBe('public');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										328
									
								
								src/lib/utils/apply-ids.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/lib/utils/apply-ids.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,328 @@
 | 
			
		||||
import { defaultSchemas } from '../data/default-schemas';
 | 
			
		||||
import type { DBCustomType, DBTable, Diagram } from '../domain';
 | 
			
		||||
 | 
			
		||||
const createTableKey = ({
 | 
			
		||||
    table,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${table.schema ?? defaultSchema ?? ''}::${table.name}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createFieldKey = ({
 | 
			
		||||
    table,
 | 
			
		||||
    fieldName,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    fieldName: string;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${fieldName}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createIndexKey = ({
 | 
			
		||||
    table,
 | 
			
		||||
    indexName,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    indexName: string;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${indexName}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createRelationshipKey = ({
 | 
			
		||||
    relationshipName,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    relationshipName: string;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${defaultSchema ?? ''}::${relationshipName}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createDependencyKey = ({
 | 
			
		||||
    table,
 | 
			
		||||
    dependentTable,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    dependentTable: DBTable;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${dependentTable.schema ?? defaultSchema ?? ''}::${dependentTable.name}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createCustomTypeKey = ({
 | 
			
		||||
    customType,
 | 
			
		||||
    defaultSchema,
 | 
			
		||||
}: {
 | 
			
		||||
    customType: DBCustomType;
 | 
			
		||||
    defaultSchema?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
    return `${customType.schema ?? defaultSchema ?? ''}::${customType.name}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const applyIds = ({
 | 
			
		||||
    sourceDiagram,
 | 
			
		||||
    targetDiagram,
 | 
			
		||||
}: {
 | 
			
		||||
    sourceDiagram: Diagram;
 | 
			
		||||
    targetDiagram: Diagram;
 | 
			
		||||
}): Diagram => {
 | 
			
		||||
    // Create a mapping of old IDs to new IDs
 | 
			
		||||
    const tablesIdMapping = new Map<string, string>();
 | 
			
		||||
    const fieldsIdMapping = new Map<string, string>();
 | 
			
		||||
    const indexesIdMapping = new Map<string, string>();
 | 
			
		||||
    const relationshipsIdMapping = new Map<string, string>();
 | 
			
		||||
    const dependenciesIdMapping = new Map<string, string>();
 | 
			
		||||
    const customTypesIdMapping = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
    const sourceDefaultSchema = defaultSchemas[sourceDiagram.databaseType];
 | 
			
		||||
    const targetDefaultSchema = defaultSchemas[targetDiagram.databaseType];
 | 
			
		||||
 | 
			
		||||
    // build idMapping
 | 
			
		||||
    sourceDiagram?.tables?.forEach((sourceTable) => {
 | 
			
		||||
        const sourceKey = createTableKey({
 | 
			
		||||
            table: sourceTable,
 | 
			
		||||
            defaultSchema: sourceDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        tablesIdMapping.set(sourceKey, sourceTable.id);
 | 
			
		||||
 | 
			
		||||
        sourceTable.fields.forEach((field) => {
 | 
			
		||||
            const fieldKey = createFieldKey({
 | 
			
		||||
                table: sourceTable,
 | 
			
		||||
                fieldName: field.name,
 | 
			
		||||
                defaultSchema: sourceDefaultSchema,
 | 
			
		||||
            });
 | 
			
		||||
            fieldsIdMapping.set(fieldKey, field.id);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        sourceTable.indexes.forEach((index) => {
 | 
			
		||||
            const indexKey = createIndexKey({
 | 
			
		||||
                table: sourceTable,
 | 
			
		||||
                indexName: index.name,
 | 
			
		||||
                defaultSchema: sourceDefaultSchema,
 | 
			
		||||
            });
 | 
			
		||||
            indexesIdMapping.set(indexKey, index.id);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    sourceDiagram.relationships?.forEach((relationship) => {
 | 
			
		||||
        const relationshipKey = createRelationshipKey({
 | 
			
		||||
            relationshipName: relationship.name,
 | 
			
		||||
            defaultSchema: sourceDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        relationshipsIdMapping.set(relationshipKey, relationship.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    sourceDiagram.dependencies?.forEach((dependency) => {
 | 
			
		||||
        const table = sourceDiagram.tables?.find(
 | 
			
		||||
            (t) => t.id === dependency.tableId
 | 
			
		||||
        );
 | 
			
		||||
        const dependentTable = sourceDiagram.tables?.find(
 | 
			
		||||
            (t) => t.id === dependency.dependentTableId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!table || !dependentTable) return;
 | 
			
		||||
 | 
			
		||||
        const dependencyKey = createDependencyKey({
 | 
			
		||||
            table,
 | 
			
		||||
            dependentTable,
 | 
			
		||||
            defaultSchema: sourceDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        dependenciesIdMapping.set(dependencyKey, dependency.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    sourceDiagram.customTypes?.forEach((customType) => {
 | 
			
		||||
        const customTypeKey = createCustomTypeKey({
 | 
			
		||||
            customType,
 | 
			
		||||
            defaultSchema: sourceDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        customTypesIdMapping.set(customTypeKey, customType.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Map current ID -> new ID for target diagram entities
 | 
			
		||||
    const targetTableIdMapping = new Map<string, string>();
 | 
			
		||||
    const targetFieldIdMapping = new Map<string, string>();
 | 
			
		||||
    const targetIndexIdMapping = new Map<string, string>();
 | 
			
		||||
    const targetRelationshipIdMapping = new Map<string, string>();
 | 
			
		||||
    const targetDependencyIdMapping = new Map<string, string>();
 | 
			
		||||
    const targetCustomTypeIdMapping = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
    targetDiagram?.tables?.forEach((targetTable) => {
 | 
			
		||||
        const targetKey = createTableKey({
 | 
			
		||||
            table: targetTable,
 | 
			
		||||
            defaultSchema: targetDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        const newId = tablesIdMapping.get(targetKey);
 | 
			
		||||
        if (newId) {
 | 
			
		||||
            targetTableIdMapping.set(targetTable.id, newId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        targetTable.fields.forEach((field) => {
 | 
			
		||||
            const fieldKey = createFieldKey({
 | 
			
		||||
                table: targetTable,
 | 
			
		||||
                fieldName: field.name,
 | 
			
		||||
                defaultSchema: targetDefaultSchema,
 | 
			
		||||
            });
 | 
			
		||||
            const newFieldId = fieldsIdMapping.get(fieldKey);
 | 
			
		||||
            if (newFieldId) {
 | 
			
		||||
                targetFieldIdMapping.set(field.id, newFieldId);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        targetTable.indexes.forEach((index) => {
 | 
			
		||||
            const indexKey = createIndexKey({
 | 
			
		||||
                table: targetTable,
 | 
			
		||||
                indexName: index.name,
 | 
			
		||||
                defaultSchema: targetDefaultSchema,
 | 
			
		||||
            });
 | 
			
		||||
            const newIndexId = indexesIdMapping.get(indexKey);
 | 
			
		||||
            if (newIndexId) {
 | 
			
		||||
                targetIndexIdMapping.set(index.id, newIndexId);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    targetDiagram.relationships?.forEach((relationship) => {
 | 
			
		||||
        const relationshipKey = createRelationshipKey({
 | 
			
		||||
            relationshipName: relationship.name,
 | 
			
		||||
            defaultSchema: targetDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        const newId = relationshipsIdMapping.get(relationshipKey);
 | 
			
		||||
        if (newId) {
 | 
			
		||||
            targetRelationshipIdMapping.set(relationship.id, newId);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    targetDiagram.dependencies?.forEach((dependency) => {
 | 
			
		||||
        const table = targetDiagram.tables?.find(
 | 
			
		||||
            (t) => t.id === dependency.tableId
 | 
			
		||||
        );
 | 
			
		||||
        const dependentTable = targetDiagram.tables?.find(
 | 
			
		||||
            (t) => t.id === dependency.dependentTableId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!table || !dependentTable) return;
 | 
			
		||||
 | 
			
		||||
        const dependencyKey = createDependencyKey({
 | 
			
		||||
            table,
 | 
			
		||||
            dependentTable,
 | 
			
		||||
            defaultSchema: targetDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const newId = dependenciesIdMapping.get(dependencyKey);
 | 
			
		||||
        if (newId) {
 | 
			
		||||
            targetDependencyIdMapping.set(dependency.id, newId);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    targetDiagram.customTypes?.forEach((customType) => {
 | 
			
		||||
        const customTypeKey = createCustomTypeKey({
 | 
			
		||||
            customType,
 | 
			
		||||
            defaultSchema: targetDefaultSchema,
 | 
			
		||||
        });
 | 
			
		||||
        const newId = customTypesIdMapping.get(customTypeKey);
 | 
			
		||||
        if (newId) {
 | 
			
		||||
            targetCustomTypeIdMapping.set(customType.id, newId);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Apply the ID mappings to create the final diagram
 | 
			
		||||
    const result: Diagram = {
 | 
			
		||||
        ...targetDiagram,
 | 
			
		||||
        tables: targetDiagram.tables?.map((table) => {
 | 
			
		||||
            const newTableId = targetTableIdMapping.get(table.id) ?? table.id;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...table,
 | 
			
		||||
                id: newTableId,
 | 
			
		||||
                fields: table.fields.map((field) => {
 | 
			
		||||
                    const newFieldId =
 | 
			
		||||
                        targetFieldIdMapping.get(field.id) ?? field.id;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...field,
 | 
			
		||||
                        id: newFieldId,
 | 
			
		||||
                    };
 | 
			
		||||
                }),
 | 
			
		||||
                indexes: table.indexes.map((index) => {
 | 
			
		||||
                    const newIndexId =
 | 
			
		||||
                        targetIndexIdMapping.get(index.id) ?? index.id;
 | 
			
		||||
 | 
			
		||||
                    // Update field IDs in index
 | 
			
		||||
                    const updatedFieldIds = index.fieldIds.map((fieldId) => {
 | 
			
		||||
                        return targetFieldIdMapping.get(fieldId) ?? fieldId;
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...index,
 | 
			
		||||
                        id: newIndexId,
 | 
			
		||||
                        fieldIds: updatedFieldIds,
 | 
			
		||||
                    };
 | 
			
		||||
                }),
 | 
			
		||||
            };
 | 
			
		||||
        }),
 | 
			
		||||
        relationships: targetDiagram.relationships?.map((relationship) => {
 | 
			
		||||
            const newRelationshipId =
 | 
			
		||||
                targetRelationshipIdMapping.get(relationship.id) ??
 | 
			
		||||
                relationship.id;
 | 
			
		||||
 | 
			
		||||
            // Update table and field IDs in relationships
 | 
			
		||||
            const newSourceTableId =
 | 
			
		||||
                targetTableIdMapping.get(relationship.sourceTableId) ??
 | 
			
		||||
                relationship.sourceTableId;
 | 
			
		||||
            const newTargetTableId =
 | 
			
		||||
                targetTableIdMapping.get(relationship.targetTableId) ??
 | 
			
		||||
                relationship.targetTableId;
 | 
			
		||||
            const newSourceFieldId =
 | 
			
		||||
                targetFieldIdMapping.get(relationship.sourceFieldId) ??
 | 
			
		||||
                relationship.sourceFieldId;
 | 
			
		||||
            const newTargetFieldId =
 | 
			
		||||
                targetFieldIdMapping.get(relationship.targetFieldId) ??
 | 
			
		||||
                relationship.targetFieldId;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...relationship,
 | 
			
		||||
                id: newRelationshipId,
 | 
			
		||||
                sourceTableId: newSourceTableId,
 | 
			
		||||
                targetTableId: newTargetTableId,
 | 
			
		||||
                sourceFieldId: newSourceFieldId,
 | 
			
		||||
                targetFieldId: newTargetFieldId,
 | 
			
		||||
            };
 | 
			
		||||
        }),
 | 
			
		||||
        dependencies: targetDiagram.dependencies?.map((dependency) => {
 | 
			
		||||
            const newDependencyId =
 | 
			
		||||
                targetDependencyIdMapping.get(dependency.id) ?? dependency.id;
 | 
			
		||||
            const newTableId =
 | 
			
		||||
                targetTableIdMapping.get(dependency.tableId) ??
 | 
			
		||||
                dependency.tableId;
 | 
			
		||||
            const newDependentTableId =
 | 
			
		||||
                targetTableIdMapping.get(dependency.dependentTableId) ??
 | 
			
		||||
                dependency.dependentTableId;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...dependency,
 | 
			
		||||
                id: newDependencyId,
 | 
			
		||||
                tableId: newTableId,
 | 
			
		||||
                dependentTableId: newDependentTableId,
 | 
			
		||||
            };
 | 
			
		||||
        }),
 | 
			
		||||
        customTypes: targetDiagram.customTypes?.map((customType) => {
 | 
			
		||||
            const newCustomTypeId =
 | 
			
		||||
                targetCustomTypeIdMapping.get(customType.id) ?? customType.id;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                ...customType,
 | 
			
		||||
                id: newCustomTypeId,
 | 
			
		||||
            };
 | 
			
		||||
        }),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { calcTableHeight } from '@/lib/domain/db-table';
 | 
			
		||||
import { calcTableHeight, MIN_TABLE_SIZE } from '@/lib/domain/db-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if a table is inside an area based on their positions and dimensions
 | 
			
		||||
 */
 | 
			
		||||
const isTableInsideArea = (table: DBTable, area: Area): boolean => {
 | 
			
		||||
export const isTableInsideArea = (table: DBTable, area: Area): boolean => {
 | 
			
		||||
    // Get table dimensions (assuming default width if not specified)
 | 
			
		||||
    const tableWidth = table.width ?? 224; // MIN_TABLE_SIZE from db-table.ts
 | 
			
		||||
    const tableWidth = table.width ?? MIN_TABLE_SIZE;
 | 
			
		||||
    const tableHeight = calcTableHeight(table);
 | 
			
		||||
 | 
			
		||||
    // Check if table's top-left corner is inside the area
 | 
			
		||||
@@ -33,7 +33,10 @@ const isTableInsideArea = (table: DBTable, area: Area): boolean => {
 | 
			
		||||
/**
 | 
			
		||||
 * Find which area contains a table
 | 
			
		||||
 */
 | 
			
		||||
const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
 | 
			
		||||
export const findContainingArea = (
 | 
			
		||||
    table: DBTable,
 | 
			
		||||
    areas: Area[]
 | 
			
		||||
): Area | null => {
 | 
			
		||||
    // Sort areas by order (if available) to prioritize top-most areas
 | 
			
		||||
    const sortedAreas = [...areas].sort(
 | 
			
		||||
        (a, b) => (b.order ?? 0) - (a.order ?? 0)
 | 
			
		||||
							
								
								
									
										3
									
								
								src/lib/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/lib/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export * from './utils';
 | 
			
		||||
export * from './apply-ids';
 | 
			
		||||
export * from './area-utils';
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
import {
 | 
			
		||||
    ContextMenu,
 | 
			
		||||
    ContextMenuContent,
 | 
			
		||||
    ContextMenuItem,
 | 
			
		||||
    ContextMenuTrigger,
 | 
			
		||||
} from '@/components/context-menu/context-menu';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { Pencil, Trash2 } from 'lucide-react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
export interface AreaNodeContextMenuProps {
 | 
			
		||||
    area: Area;
 | 
			
		||||
    onEditName?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AreaNodeContextMenu: React.FC<
 | 
			
		||||
    React.PropsWithChildren<AreaNodeContextMenuProps>
 | 
			
		||||
> = ({ children, area, onEditName }) => {
 | 
			
		||||
    const { removeArea, readonly } = useChartDB();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const removeAreaHandler = useCallback(() => {
 | 
			
		||||
        removeArea(area.id);
 | 
			
		||||
    }, [removeArea, area.id]);
 | 
			
		||||
 | 
			
		||||
    if (!isDesktop || readonly) {
 | 
			
		||||
        return <>{children}</>;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <ContextMenu>
 | 
			
		||||
            <ContextMenuTrigger>{children}</ContextMenuTrigger>
 | 
			
		||||
            <ContextMenuContent>
 | 
			
		||||
                {onEditName ? (
 | 
			
		||||
                    <ContextMenuItem
 | 
			
		||||
                        onClick={onEditName}
 | 
			
		||||
                        className="flex justify-between gap-3"
 | 
			
		||||
                    >
 | 
			
		||||
                        <span>Edit Area Name</span>
 | 
			
		||||
                        <Pencil className="size-3.5" />
 | 
			
		||||
                    </ContextMenuItem>
 | 
			
		||||
                ) : null}
 | 
			
		||||
                <ContextMenuItem
 | 
			
		||||
                    onClick={removeAreaHandler}
 | 
			
		||||
                    className="flex justify-between gap-3"
 | 
			
		||||
                >
 | 
			
		||||
                    <span>Delete Area</span>
 | 
			
		||||
                    <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                </ContextMenuItem>
 | 
			
		||||
            </ContextMenuContent>
 | 
			
		||||
        </ContextMenu>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -12,9 +12,11 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { Check, GripVertical } from 'lucide-react';
 | 
			
		||||
import { Check, GripVertical, Pencil } from 'lucide-react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { AreaNodeContextMenu } from './area-node-context-menu';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
 | 
			
		||||
export type AreaNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
@@ -56,85 +58,102 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
 | 
			
		||||
        useKeyPressEvent('Enter', editAreaName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const { setEditTableModeTable } = useCanvas();
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex h-full flex-col rounded-md border-2 shadow-sm',
 | 
			
		||||
                    selected ? 'border-pink-600' : 'border-transparent'
 | 
			
		||||
                )}
 | 
			
		||||
                style={{
 | 
			
		||||
                    backgroundColor: `${area.color}15`,
 | 
			
		||||
                    borderColor: selected ? undefined : area.color,
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                    if (e.detail === 2) {
 | 
			
		||||
                        openAreaInEditor();
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
            <AreaNodeContextMenu
 | 
			
		||||
                area={area}
 | 
			
		||||
                onEditName={() => setEditMode(true)}
 | 
			
		||||
            >
 | 
			
		||||
                {!readonly ? (
 | 
			
		||||
                    <NodeResizer
 | 
			
		||||
                        isVisible={focused}
 | 
			
		||||
                        lineClassName="!border-4 !border-transparent"
 | 
			
		||||
                        handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
 | 
			
		||||
                        minHeight={100}
 | 
			
		||||
                        minWidth={100}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
                <div className="group flex h-8 items-center justify-between rounded-t-md px-2">
 | 
			
		||||
                    <div className="flex w-full items-center gap-1">
 | 
			
		||||
                        <GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
 | 
			
		||||
                <div
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'flex h-full flex-col rounded-md border-2 shadow-sm',
 | 
			
		||||
                        selected ? 'border-pink-600' : 'border-transparent'
 | 
			
		||||
                    )}
 | 
			
		||||
                    style={{
 | 
			
		||||
                        backgroundColor: `${area.color}15`,
 | 
			
		||||
                        borderColor: selected ? undefined : area.color,
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        setEditTableModeTable(null);
 | 
			
		||||
                        if (e.detail === 2) {
 | 
			
		||||
                            openAreaInEditor();
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    {!readonly ? (
 | 
			
		||||
                        <NodeResizer
 | 
			
		||||
                            isVisible={focused}
 | 
			
		||||
                            lineClassName="!border-4 !border-transparent"
 | 
			
		||||
                            handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
 | 
			
		||||
                            minHeight={100}
 | 
			
		||||
                            minWidth={100}
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <div className="group flex h-8 items-center justify-between rounded-t-md px-2">
 | 
			
		||||
                        <div className="flex w-full items-center gap-1">
 | 
			
		||||
                            <GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
 | 
			
		||||
 | 
			
		||||
                        {editMode && !readonly ? (
 | 
			
		||||
                            <div className="flex w-full items-center">
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    ref={inputRef}
 | 
			
		||||
                                    autoFocus
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    placeholder={area.name}
 | 
			
		||||
                                    value={areaName}
 | 
			
		||||
                                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        setAreaName(e.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
 | 
			
		||||
                                />
 | 
			
		||||
                            {editMode && !readonly ? (
 | 
			
		||||
                                <div className="flex w-full items-center">
 | 
			
		||||
                                    <Input
 | 
			
		||||
                                        ref={inputRef}
 | 
			
		||||
                                        autoFocus
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        placeholder={area.name}
 | 
			
		||||
                                        value={areaName}
 | 
			
		||||
                                        onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                        onChange={(e) =>
 | 
			
		||||
                                            setAreaName(e.target.value)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="ml-1 size-6 p-0 hover:bg-white/20"
 | 
			
		||||
                                        onClick={editAreaName}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Check className="size-3.5 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ) : !readonly ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <div
 | 
			
		||||
                                            className="text-editable truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
 | 
			
		||||
                                            onDoubleClick={enterEditMode}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {area.name}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
 | 
			
		||||
                                    {area.name}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {!editMode && !readonly && (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="ml-1 size-6 p-0 hover:bg-white/20"
 | 
			
		||||
                                    onClick={editAreaName}
 | 
			
		||||
                                    className="ml-auto size-5 p-0 opacity-0 transition-opacity hover:bg-white/20 group-hover:opacity-100"
 | 
			
		||||
                                    onClick={enterEditMode}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Check className="size-3.5 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                    <Pencil className="size-3 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : !readonly ? (
 | 
			
		||||
                            <Tooltip>
 | 
			
		||||
                                <TooltipTrigger asChild>
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        className="text-editable max-w-[200px] cursor-text truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
 | 
			
		||||
                                        onDoubleClick={enterEditMode}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {area.name}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </TooltipTrigger>
 | 
			
		||||
                                <TooltipContent>
 | 
			
		||||
                                    {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
 | 
			
		||||
                                {area.name}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="flex-1" />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1" />
 | 
			
		||||
            </div>
 | 
			
		||||
            </AreaNodeContextMenu>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,92 +13,96 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Table, Workflow, Group, View } from 'lucide-react';
 | 
			
		||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { createTable, readonly, createArea } = useChartDB();
 | 
			
		||||
    const { createTable, readonly, createArea, databaseType } = useChartDB();
 | 
			
		||||
    const { schemasDisplayed } = useDiagramFilter();
 | 
			
		||||
    const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
 | 
			
		||||
    const { openCreateRelationshipDialog } = useDialog();
 | 
			
		||||
    const { screenToFlowPosition } = useReactFlow();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { showDBViews } = useLocalConfig();
 | 
			
		||||
    const { setEditTableModeTable } = useCanvas();
 | 
			
		||||
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const createTableHandler = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
 | 
			
		||||
        async (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
 | 
			
		||||
            const position = screenToFlowPosition({
 | 
			
		||||
                x: event.clientX,
 | 
			
		||||
                y: event.clientY,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (schemasDisplayed.length > 1) {
 | 
			
		||||
                openTableSchemaDialog({
 | 
			
		||||
                    onConfirm: ({ schema }) =>
 | 
			
		||||
                        createTable({
 | 
			
		||||
                            x: position.x,
 | 
			
		||||
                            y: position.y,
 | 
			
		||||
                            schema: schema.name,
 | 
			
		||||
                        }),
 | 
			
		||||
                    schemas: schemasDisplayed,
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                const schema =
 | 
			
		||||
                    schemasDisplayed?.length === 1
 | 
			
		||||
                        ? schemasDisplayed[0]?.name
 | 
			
		||||
                        : undefined;
 | 
			
		||||
                createTable({
 | 
			
		||||
                    x: position.x,
 | 
			
		||||
                    y: position.y,
 | 
			
		||||
                    schema,
 | 
			
		||||
                });
 | 
			
		||||
            // Auto-select schema with priority: default schema > first displayed schema > undefined
 | 
			
		||||
            let schema: string | undefined = undefined;
 | 
			
		||||
            if (schemasDisplayed.length > 0) {
 | 
			
		||||
                const defaultSchemaName = defaultSchemas[databaseType];
 | 
			
		||||
                const defaultSchemaInList = schemasDisplayed.find(
 | 
			
		||||
                    (s) => s.name === defaultSchemaName
 | 
			
		||||
                );
 | 
			
		||||
                schema = defaultSchemaInList
 | 
			
		||||
                    ? defaultSchemaInList.name
 | 
			
		||||
                    : schemasDisplayed[0]?.name;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newTable = await createTable({
 | 
			
		||||
                x: position.x,
 | 
			
		||||
                y: position.y,
 | 
			
		||||
                schema,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (newTable) {
 | 
			
		||||
                setEditTableModeTable({ tableId: newTable.id });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            createTable,
 | 
			
		||||
            screenToFlowPosition,
 | 
			
		||||
            openTableSchemaDialog,
 | 
			
		||||
            schemasDisplayed,
 | 
			
		||||
            setEditTableModeTable,
 | 
			
		||||
            databaseType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createViewHandler = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
 | 
			
		||||
        async (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
 | 
			
		||||
            const position = screenToFlowPosition({
 | 
			
		||||
                x: event.clientX,
 | 
			
		||||
                y: event.clientY,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (schemasDisplayed.length > 1) {
 | 
			
		||||
                openTableSchemaDialog({
 | 
			
		||||
                    onConfirm: ({ schema }) =>
 | 
			
		||||
                        createTable({
 | 
			
		||||
                            x: position.x,
 | 
			
		||||
                            y: position.y,
 | 
			
		||||
                            schema: schema.name,
 | 
			
		||||
                            isView: true,
 | 
			
		||||
                        }),
 | 
			
		||||
                    schemas: schemasDisplayed,
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                const schema =
 | 
			
		||||
                    schemasDisplayed?.length === 1
 | 
			
		||||
                        ? schemasDisplayed[0]?.name
 | 
			
		||||
                        : undefined;
 | 
			
		||||
                createTable({
 | 
			
		||||
                    x: position.x,
 | 
			
		||||
                    y: position.y,
 | 
			
		||||
                    schema,
 | 
			
		||||
                    isView: true,
 | 
			
		||||
                });
 | 
			
		||||
            // Auto-select schema with priority: default schema > first displayed schema > undefined
 | 
			
		||||
            let schema: string | undefined = undefined;
 | 
			
		||||
            if (schemasDisplayed.length > 0) {
 | 
			
		||||
                const defaultSchemaName = defaultSchemas[databaseType];
 | 
			
		||||
                const defaultSchemaInList = schemasDisplayed.find(
 | 
			
		||||
                    (s) => s.name === defaultSchemaName
 | 
			
		||||
                );
 | 
			
		||||
                schema = defaultSchemaInList
 | 
			
		||||
                    ? defaultSchemaInList.name
 | 
			
		||||
                    : schemasDisplayed[0]?.name;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newView = await createTable({
 | 
			
		||||
                x: position.x,
 | 
			
		||||
                y: position.y,
 | 
			
		||||
                schema,
 | 
			
		||||
                isView: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (newView) {
 | 
			
		||||
                setEditTableModeTable({ tableId: newView.id });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            createTable,
 | 
			
		||||
            screenToFlowPosition,
 | 
			
		||||
            openTableSchemaDialog,
 | 
			
		||||
            schemasDisplayed,
 | 
			
		||||
            setEditTableModeTable,
 | 
			
		||||
            databaseType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,11 @@ import {
 | 
			
		||||
import '@xyflow/react/dist/style.css';
 | 
			
		||||
import equal from 'fast-deep-equal';
 | 
			
		||||
import type { TableNodeType } from './table-node/table-node';
 | 
			
		||||
import { TableNode } from './table-node/table-node';
 | 
			
		||||
import {
 | 
			
		||||
    TABLE_RELATIONSHIP_SOURCE_HANDLE_ID_PREFIX,
 | 
			
		||||
    TABLE_RELATIONSHIP_TARGET_HANDLE_ID_PREFIX,
 | 
			
		||||
    TableNode,
 | 
			
		||||
} from './table-node/table-node';
 | 
			
		||||
import type { RelationshipEdgeType } from './relationship-edge/relationship-edge';
 | 
			
		||||
import { RelationshipEdge } from './relationship-edge/relationship-edge';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
@@ -79,7 +83,24 @@ import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import type { AreaNodeType } from './area-node/area-node';
 | 
			
		||||
import { AreaNode } from './area-node/area-node';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { updateTablesParentAreas, getTablesInArea } from './area-utils';
 | 
			
		||||
import type { TempCursorNodeType } from './temp-cursor-node/temp-cursor-node';
 | 
			
		||||
import {
 | 
			
		||||
    TEMP_CURSOR_HANDLE_ID,
 | 
			
		||||
    TEMP_CURSOR_NODE_ID,
 | 
			
		||||
    TempCursorNode,
 | 
			
		||||
} from './temp-cursor-node/temp-cursor-node';
 | 
			
		||||
import type { TempFloatingEdgeType } from './temp-floating-edge/temp-floating-edge';
 | 
			
		||||
import {
 | 
			
		||||
    TEMP_FLOATING_EDGE_ID,
 | 
			
		||||
    TempFloatingEdge,
 | 
			
		||||
} from './temp-floating-edge/temp-floating-edge';
 | 
			
		||||
import type { CreateRelationshipNodeType } from './create-relationship-node/create-relationship-node';
 | 
			
		||||
import { CreateRelationshipNode } from './create-relationship-node/create-relationship-node';
 | 
			
		||||
import { ConnectionLine } from './connection-line/connection-line';
 | 
			
		||||
import {
 | 
			
		||||
    updateTablesParentAreas,
 | 
			
		||||
    getTablesInArea,
 | 
			
		||||
} from '@/lib/utils/area-utils';
 | 
			
		||||
import { CanvasFilter } from './canvas-filter/canvas-filter';
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
			
		||||
import { ShowAllButton } from './show-all-button';
 | 
			
		||||
@@ -89,24 +110,35 @@ import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-f
 | 
			
		||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
import { useClickAway } from 'react-use';
 | 
			
		||||
 | 
			
		||||
const HIGHLIGHTED_EDGE_Z_INDEX = 1;
 | 
			
		||||
const DEFAULT_EDGE_Z_INDEX = 0;
 | 
			
		||||
 | 
			
		||||
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
 | 
			
		||||
export type EdgeType =
 | 
			
		||||
    | RelationshipEdgeType
 | 
			
		||||
    | DependencyEdgeType
 | 
			
		||||
    | TempFloatingEdgeType;
 | 
			
		||||
 | 
			
		||||
export type NodeType = TableNodeType | AreaNodeType;
 | 
			
		||||
export type NodeType =
 | 
			
		||||
    | TableNodeType
 | 
			
		||||
    | AreaNodeType
 | 
			
		||||
    | TempCursorNodeType
 | 
			
		||||
    | CreateRelationshipNodeType;
 | 
			
		||||
 | 
			
		||||
type AddEdgeParams = Parameters<typeof addEdge<EdgeType>>[0];
 | 
			
		||||
 | 
			
		||||
const edgeTypes: EdgeTypes = {
 | 
			
		||||
    'relationship-edge': RelationshipEdge,
 | 
			
		||||
    'dependency-edge': DependencyEdge,
 | 
			
		||||
    'temp-floating-edge': TempFloatingEdge,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nodeTypes: NodeTypes = {
 | 
			
		||||
    table: TableNode,
 | 
			
		||||
    area: AreaNode,
 | 
			
		||||
    'temp-cursor': TempCursorNode,
 | 
			
		||||
    'create-relationship': CreateRelationshipNode,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const initialEdges: EdgeType[] = [];
 | 
			
		||||
@@ -119,12 +151,14 @@ const tableToTableNode = (
 | 
			
		||||
        filterLoading,
 | 
			
		||||
        showDBViews,
 | 
			
		||||
        forceShow,
 | 
			
		||||
        isRelationshipCreatingTarget = false,
 | 
			
		||||
    }: {
 | 
			
		||||
        filter?: DiagramFilter;
 | 
			
		||||
        databaseType: DatabaseType;
 | 
			
		||||
        filterLoading: boolean;
 | 
			
		||||
        showDBViews?: boolean;
 | 
			
		||||
        forceShow?: boolean;
 | 
			
		||||
        isRelationshipCreatingTarget?: boolean;
 | 
			
		||||
    }
 | 
			
		||||
): TableNodeType => {
 | 
			
		||||
    // Always use absolute position for now
 | 
			
		||||
@@ -152,6 +186,7 @@ const tableToTableNode = (
 | 
			
		||||
        data: {
 | 
			
		||||
            table,
 | 
			
		||||
            isOverlapping: false,
 | 
			
		||||
            isRelationshipCreatingTarget,
 | 
			
		||||
        },
 | 
			
		||||
        width: table.width ?? MIN_TABLE_SIZE,
 | 
			
		||||
        hidden,
 | 
			
		||||
@@ -196,6 +231,9 @@ const areaToAreaNode = (
 | 
			
		||||
        width: area.width,
 | 
			
		||||
        height: area.height,
 | 
			
		||||
        zIndex: -10,
 | 
			
		||||
        style: {
 | 
			
		||||
            zIndex: -10,
 | 
			
		||||
        },
 | 
			
		||||
        hidden: !hasVisibleTable || filterLoading,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -244,6 +282,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
        overlapGraph,
 | 
			
		||||
        showFilter,
 | 
			
		||||
        setShowFilter,
 | 
			
		||||
        setEditTableModeTable,
 | 
			
		||||
        tempFloatingEdge,
 | 
			
		||||
        endFloatingEdgeCreation,
 | 
			
		||||
        hoveringTableId,
 | 
			
		||||
        hideCreateRelationshipNode,
 | 
			
		||||
    } = useCanvas();
 | 
			
		||||
    const { filter, loading: filterLoading } = useDiagramFilter();
 | 
			
		||||
    const { checkIfNewTable } = useDiff();
 | 
			
		||||
@@ -265,6 +308,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                filterLoading,
 | 
			
		||||
                showDBViews,
 | 
			
		||||
                forceShow: shouldForceShowTable(table.id),
 | 
			
		||||
                isRelationshipCreatingTarget: false,
 | 
			
		||||
            })
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
@@ -273,6 +317,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
 | 
			
		||||
    const [snapToGridEnabled, setSnapToGridEnabled] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const [cursorPosition, setCursorPosition] = useState<{
 | 
			
		||||
        x: number;
 | 
			
		||||
        y: number;
 | 
			
		||||
    } | null>(null);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setIsInitialLoadingNodes(true);
 | 
			
		||||
    }, [initialTables]);
 | 
			
		||||
@@ -285,6 +334,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                filterLoading,
 | 
			
		||||
                showDBViews,
 | 
			
		||||
                forceShow: shouldForceShowTable(table.id),
 | 
			
		||||
                isRelationshipCreatingTarget: false,
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        if (equal(initialNodes, nodes)) {
 | 
			
		||||
@@ -390,58 +440,62 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
            // Check if any edge needs updating
 | 
			
		||||
            let hasChanges = false;
 | 
			
		||||
 | 
			
		||||
            const newEdges = prevEdges.map((edge): EdgeType => {
 | 
			
		||||
                const shouldBeHighlighted =
 | 
			
		||||
                    selectedRelationshipIdsSet.has(edge.id) ||
 | 
			
		||||
                    selectedTableIdsSet.has(edge.source) ||
 | 
			
		||||
                    selectedTableIdsSet.has(edge.target);
 | 
			
		||||
            const newEdges = prevEdges
 | 
			
		||||
                .filter((e) => e.type !== 'temp-floating-edge')
 | 
			
		||||
                .map((edge): EdgeType => {
 | 
			
		||||
                    const shouldBeHighlighted =
 | 
			
		||||
                        selectedRelationshipIdsSet.has(edge.id) ||
 | 
			
		||||
                        selectedTableIdsSet.has(edge.source) ||
 | 
			
		||||
                        selectedTableIdsSet.has(edge.target);
 | 
			
		||||
 | 
			
		||||
                const currentHighlighted = edge.data?.highlighted ?? false;
 | 
			
		||||
                const currentAnimated = edge.animated ?? false;
 | 
			
		||||
                const currentZIndex = edge.zIndex ?? 0;
 | 
			
		||||
                    const currentHighlighted =
 | 
			
		||||
                        (edge as Exclude<EdgeType, TempFloatingEdgeType>).data
 | 
			
		||||
                            ?.highlighted ?? false;
 | 
			
		||||
                    const currentAnimated = edge.animated ?? false;
 | 
			
		||||
                    const currentZIndex = edge.zIndex ?? 0;
 | 
			
		||||
 | 
			
		||||
                // Skip if no changes needed
 | 
			
		||||
                if (
 | 
			
		||||
                    currentHighlighted === shouldBeHighlighted &&
 | 
			
		||||
                    currentAnimated === shouldBeHighlighted &&
 | 
			
		||||
                    currentZIndex ===
 | 
			
		||||
                        (shouldBeHighlighted
 | 
			
		||||
                            ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                            : DEFAULT_EDGE_Z_INDEX)
 | 
			
		||||
                ) {
 | 
			
		||||
                    return edge;
 | 
			
		||||
                }
 | 
			
		||||
                    // Skip if no changes needed
 | 
			
		||||
                    if (
 | 
			
		||||
                        currentHighlighted === shouldBeHighlighted &&
 | 
			
		||||
                        currentAnimated === shouldBeHighlighted &&
 | 
			
		||||
                        currentZIndex ===
 | 
			
		||||
                            (shouldBeHighlighted
 | 
			
		||||
                                ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                                : DEFAULT_EDGE_Z_INDEX)
 | 
			
		||||
                    ) {
 | 
			
		||||
                        return edge;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                hasChanges = true;
 | 
			
		||||
                    hasChanges = true;
 | 
			
		||||
 | 
			
		||||
                if (edge.type === 'dependency-edge') {
 | 
			
		||||
                    const dependencyEdge = edge as DependencyEdgeType;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...dependencyEdge,
 | 
			
		||||
                        data: {
 | 
			
		||||
                            ...dependencyEdge.data!,
 | 
			
		||||
                            highlighted: shouldBeHighlighted,
 | 
			
		||||
                        },
 | 
			
		||||
                        animated: shouldBeHighlighted,
 | 
			
		||||
                        zIndex: shouldBeHighlighted
 | 
			
		||||
                            ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                            : DEFAULT_EDGE_Z_INDEX,
 | 
			
		||||
                    };
 | 
			
		||||
                } else {
 | 
			
		||||
                    const relationshipEdge = edge as RelationshipEdgeType;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...relationshipEdge,
 | 
			
		||||
                        data: {
 | 
			
		||||
                            ...relationshipEdge.data!,
 | 
			
		||||
                            highlighted: shouldBeHighlighted,
 | 
			
		||||
                        },
 | 
			
		||||
                        animated: shouldBeHighlighted,
 | 
			
		||||
                        zIndex: shouldBeHighlighted
 | 
			
		||||
                            ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                            : DEFAULT_EDGE_Z_INDEX,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
                    if (edge.type === 'dependency-edge') {
 | 
			
		||||
                        const dependencyEdge = edge as DependencyEdgeType;
 | 
			
		||||
                        return {
 | 
			
		||||
                            ...dependencyEdge,
 | 
			
		||||
                            data: {
 | 
			
		||||
                                ...dependencyEdge.data!,
 | 
			
		||||
                                highlighted: shouldBeHighlighted,
 | 
			
		||||
                            },
 | 
			
		||||
                            animated: shouldBeHighlighted,
 | 
			
		||||
                            zIndex: shouldBeHighlighted
 | 
			
		||||
                                ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                                : DEFAULT_EDGE_Z_INDEX,
 | 
			
		||||
                        };
 | 
			
		||||
                    } else {
 | 
			
		||||
                        const relationshipEdge = edge as RelationshipEdgeType;
 | 
			
		||||
                        return {
 | 
			
		||||
                            ...relationshipEdge,
 | 
			
		||||
                            data: {
 | 
			
		||||
                                ...relationshipEdge.data!,
 | 
			
		||||
                                highlighted: shouldBeHighlighted,
 | 
			
		||||
                            },
 | 
			
		||||
                            animated: shouldBeHighlighted,
 | 
			
		||||
                            zIndex: shouldBeHighlighted
 | 
			
		||||
                                ? HIGHLIGHTED_EDGE_Z_INDEX
 | 
			
		||||
                                : DEFAULT_EDGE_Z_INDEX,
 | 
			
		||||
                        };
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            return hasChanges ? newEdges : prevEdges;
 | 
			
		||||
        });
 | 
			
		||||
@@ -459,6 +513,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                        filterLoading,
 | 
			
		||||
                        showDBViews,
 | 
			
		||||
                        forceShow: shouldForceShowTable(table.id),
 | 
			
		||||
                        isRelationshipCreatingTarget: false,
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    // Check if table uses the highlighted custom type
 | 
			
		||||
@@ -488,6 +543,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                        filterLoading,
 | 
			
		||||
                    })
 | 
			
		||||
                ),
 | 
			
		||||
                ...prevNodes.filter(
 | 
			
		||||
                    (n) =>
 | 
			
		||||
                        n.type === 'temp-cursor' ||
 | 
			
		||||
                        n.type === 'create-relationship'
 | 
			
		||||
                ),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            // Check if nodes actually changed
 | 
			
		||||
@@ -512,6 +572,37 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
        shouldForceShowTable,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Surgical update for relationship creation target highlighting
 | 
			
		||||
    // This avoids expensive full node recalculation when only the visual state changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setNodes((nds) => {
 | 
			
		||||
            let hasChanges = false;
 | 
			
		||||
            const updatedNodes = nds.map((node) => {
 | 
			
		||||
                if (node.type !== 'table') return node;
 | 
			
		||||
 | 
			
		||||
                const shouldBeTarget =
 | 
			
		||||
                    !!tempFloatingEdge?.sourceNodeId &&
 | 
			
		||||
                    node.id !== tempFloatingEdge.sourceNodeId;
 | 
			
		||||
                const isCurrentlyTarget =
 | 
			
		||||
                    node.data.isRelationshipCreatingTarget ?? false;
 | 
			
		||||
 | 
			
		||||
                if (shouldBeTarget !== isCurrentlyTarget) {
 | 
			
		||||
                    hasChanges = true;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...node,
 | 
			
		||||
                        data: {
 | 
			
		||||
                            ...node.data,
 | 
			
		||||
                            isRelationshipCreatingTarget: shouldBeTarget,
 | 
			
		||||
                        },
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                return node;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return hasChanges ? updatedNodes : nds;
 | 
			
		||||
        });
 | 
			
		||||
    }, [tempFloatingEdge?.sourceNodeId, setNodes]);
 | 
			
		||||
 | 
			
		||||
    const prevFilter = useRef<DiagramFilter | undefined>(undefined);
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!equal(filter, prevFilter.current)) {
 | 
			
		||||
@@ -1213,6 +1304,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
        setTimeout(() => setHighlightOverlappingTables(false), 600);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const containerRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
    const exitEditTableMode = useCallback(
 | 
			
		||||
        () => setEditTableModeTable(null),
 | 
			
		||||
        [setEditTableModeTable]
 | 
			
		||||
    );
 | 
			
		||||
    useClickAway(containerRef, exitEditTableMode);
 | 
			
		||||
    useClickAway(containerRef, hideCreateRelationshipNode);
 | 
			
		||||
 | 
			
		||||
    const shiftPressed = useKeyPress('Shift');
 | 
			
		||||
    const operatingSystem = getOperatingSystem();
 | 
			
		||||
 | 
			
		||||
@@ -1228,15 +1327,134 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle mouse move to update cursor position for floating edge
 | 
			
		||||
    const { screenToFlowPosition } = useReactFlow();
 | 
			
		||||
    const rafIdRef = useRef<number>();
 | 
			
		||||
    const handleMouseMove = useCallback(
 | 
			
		||||
        (event: React.MouseEvent) => {
 | 
			
		||||
            if (tempFloatingEdge) {
 | 
			
		||||
                // Throttle using requestAnimationFrame
 | 
			
		||||
                if (rafIdRef.current) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                rafIdRef.current = requestAnimationFrame(() => {
 | 
			
		||||
                    const position = screenToFlowPosition({
 | 
			
		||||
                        x: event.clientX,
 | 
			
		||||
                        y: event.clientY,
 | 
			
		||||
                    });
 | 
			
		||||
                    setCursorPosition(position);
 | 
			
		||||
                    rafIdRef.current = undefined;
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [tempFloatingEdge, screenToFlowPosition]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Cleanup RAF on unmount
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        return () => {
 | 
			
		||||
            if (rafIdRef.current) {
 | 
			
		||||
                cancelAnimationFrame(rafIdRef.current);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    // Handle escape key to cancel floating edge creation and close relationship node
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const handleEscape = (event: KeyboardEvent) => {
 | 
			
		||||
            if (event.key === 'Escape') {
 | 
			
		||||
                if (tempFloatingEdge) {
 | 
			
		||||
                    endFloatingEdgeCreation();
 | 
			
		||||
                    setCursorPosition(null);
 | 
			
		||||
                }
 | 
			
		||||
                // Also close CreateRelationshipNode if present
 | 
			
		||||
                hideCreateRelationshipNode();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        document.addEventListener('keydown', handleEscape);
 | 
			
		||||
        return () => document.removeEventListener('keydown', handleEscape);
 | 
			
		||||
    }, [tempFloatingEdge, endFloatingEdgeCreation, hideCreateRelationshipNode]);
 | 
			
		||||
 | 
			
		||||
    // Add temporary invisible node at cursor position and edge
 | 
			
		||||
    const nodesWithCursor = useMemo(() => {
 | 
			
		||||
        if (!tempFloatingEdge || !cursorPosition) {
 | 
			
		||||
            return nodes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tempNode: TempCursorNodeType = {
 | 
			
		||||
            id: TEMP_CURSOR_NODE_ID,
 | 
			
		||||
            type: 'temp-cursor',
 | 
			
		||||
            position: cursorPosition,
 | 
			
		||||
            data: {},
 | 
			
		||||
            draggable: false,
 | 
			
		||||
            selectable: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return [...nodes, tempNode];
 | 
			
		||||
    }, [nodes, tempFloatingEdge, cursorPosition]);
 | 
			
		||||
 | 
			
		||||
    const edgesWithFloating = useMemo(() => {
 | 
			
		||||
        if (!tempFloatingEdge || !cursorPosition) return edges;
 | 
			
		||||
 | 
			
		||||
        let target = TEMP_CURSOR_NODE_ID;
 | 
			
		||||
        let targetHandle: string | undefined = TEMP_CURSOR_HANDLE_ID;
 | 
			
		||||
 | 
			
		||||
        if (tempFloatingEdge.targetNodeId) {
 | 
			
		||||
            target = tempFloatingEdge.targetNodeId;
 | 
			
		||||
            targetHandle = `${TABLE_RELATIONSHIP_TARGET_HANDLE_ID_PREFIX}${tempFloatingEdge.targetNodeId}`;
 | 
			
		||||
        } else if (
 | 
			
		||||
            hoveringTableId &&
 | 
			
		||||
            hoveringTableId !== tempFloatingEdge.sourceNodeId
 | 
			
		||||
        ) {
 | 
			
		||||
            target = hoveringTableId;
 | 
			
		||||
            targetHandle = `${TABLE_RELATIONSHIP_TARGET_HANDLE_ID_PREFIX}${hoveringTableId}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tempEdge: TempFloatingEdgeType = {
 | 
			
		||||
            id: TEMP_FLOATING_EDGE_ID,
 | 
			
		||||
            source: tempFloatingEdge.sourceNodeId,
 | 
			
		||||
            sourceHandle: `${TABLE_RELATIONSHIP_SOURCE_HANDLE_ID_PREFIX}${tempFloatingEdge.sourceNodeId}`,
 | 
			
		||||
            target,
 | 
			
		||||
            targetHandle,
 | 
			
		||||
            type: 'temp-floating-edge',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return [...edges, tempEdge];
 | 
			
		||||
    }, [edges, tempFloatingEdge, cursorPosition, hoveringTableId]);
 | 
			
		||||
 | 
			
		||||
    const onPaneClickHandler = useCallback(() => {
 | 
			
		||||
        if (tempFloatingEdge) {
 | 
			
		||||
            endFloatingEdgeCreation();
 | 
			
		||||
            setCursorPosition(null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Close CreateRelationshipNode if it exists
 | 
			
		||||
        hideCreateRelationshipNode();
 | 
			
		||||
 | 
			
		||||
        // Exit edit table mode
 | 
			
		||||
        exitEditTableMode();
 | 
			
		||||
    }, [
 | 
			
		||||
        tempFloatingEdge,
 | 
			
		||||
        exitEditTableMode,
 | 
			
		||||
        endFloatingEdgeCreation,
 | 
			
		||||
        hideCreateRelationshipNode,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <CanvasContextMenu>
 | 
			
		||||
            <div className="relative flex h-full" id="canvas">
 | 
			
		||||
            <div
 | 
			
		||||
                className="relative flex h-full"
 | 
			
		||||
                id="canvas"
 | 
			
		||||
                ref={containerRef}
 | 
			
		||||
                onMouseMove={handleMouseMove}
 | 
			
		||||
            >
 | 
			
		||||
                <ReactFlow
 | 
			
		||||
                    onlyRenderVisibleElements
 | 
			
		||||
                    colorMode={effectiveTheme}
 | 
			
		||||
                    className="canvas-cursor-default nodes-animated"
 | 
			
		||||
                    nodes={nodes}
 | 
			
		||||
                    edges={edges}
 | 
			
		||||
                    nodes={nodesWithCursor}
 | 
			
		||||
                    edges={edgesWithFloating}
 | 
			
		||||
                    onNodesChange={onNodesChangeHandler}
 | 
			
		||||
                    onEdgesChange={onEdgesChangeHandler}
 | 
			
		||||
                    maxZoom={5}
 | 
			
		||||
@@ -1255,6 +1473,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
                    panOnScroll={scrollAction === 'pan'}
 | 
			
		||||
                    snapToGrid={shiftPressed || snapToGridEnabled}
 | 
			
		||||
                    snapGrid={[20, 20]}
 | 
			
		||||
                    onPaneClick={onPaneClickHandler}
 | 
			
		||||
                    connectionLineComponent={ConnectionLine}
 | 
			
		||||
                >
 | 
			
		||||
                    <Controls
 | 
			
		||||
                        position="top-left"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { ConnectionLineComponentProps } from '@xyflow/react';
 | 
			
		||||
import { getSmoothStepPath, Position } from '@xyflow/react';
 | 
			
		||||
import type { NodeType } from '../canvas';
 | 
			
		||||
 | 
			
		||||
export const ConnectionLine: React.FC<
 | 
			
		||||
    ConnectionLineComponentProps<NodeType>
 | 
			
		||||
> = ({ fromX, fromY, toX, toY, fromPosition, toPosition }) => {
 | 
			
		||||
    const [edgePath] = getSmoothStepPath({
 | 
			
		||||
        sourceX: fromX,
 | 
			
		||||
        sourceY: fromY,
 | 
			
		||||
        sourcePosition: fromPosition ?? Position.Right,
 | 
			
		||||
        targetX: toX,
 | 
			
		||||
        targetY: toY,
 | 
			
		||||
        targetPosition: toPosition ?? Position.Left,
 | 
			
		||||
        borderRadius: 14,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <g>
 | 
			
		||||
            <path
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="#ec4899"
 | 
			
		||||
                strokeWidth={2}
 | 
			
		||||
                strokeDasharray="5,5"
 | 
			
		||||
                d={edgePath}
 | 
			
		||||
            />
 | 
			
		||||
            <circle
 | 
			
		||||
                cx={toX}
 | 
			
		||||
                cy={toY}
 | 
			
		||||
                fill="#fff"
 | 
			
		||||
                r={3}
 | 
			
		||||
                stroke="#ec4899"
 | 
			
		||||
                strokeWidth={1.5}
 | 
			
		||||
            />
 | 
			
		||||
        </g>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,321 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import type { NodeProps, Node } from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import type { SelectBoxOption } from '@/components/select-box/select-box';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { ArrowRight, X } from 'lucide-react';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { generateId } from '@/lib/utils';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
 | 
			
		||||
export const CREATE_RELATIONSHIP_NODE_ID = '__create-relationship-node__';
 | 
			
		||||
 | 
			
		||||
const CREATE_NEW_FIELD_VALUE = 'CREATE_NEW';
 | 
			
		||||
 | 
			
		||||
export type CreateRelationshipNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
        sourceTableId: string;
 | 
			
		||||
        targetTableId: string;
 | 
			
		||||
    },
 | 
			
		||||
    'create-relationship'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const CreateRelationshipNode: React.FC<
 | 
			
		||||
    NodeProps<CreateRelationshipNodeType>
 | 
			
		||||
> = React.memo(({ data }) => {
 | 
			
		||||
    const { sourceTableId, targetTableId } = data;
 | 
			
		||||
    const { getTable, createRelationship, databaseType, addField } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const { hideCreateRelationshipNode } = useCanvas();
 | 
			
		||||
    const { setEdges } = useReactFlow();
 | 
			
		||||
    const { openRelationshipFromSidebar } = useLayout();
 | 
			
		||||
    const [targetFieldId, setTargetFieldId] = useState<string | undefined>();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState('');
 | 
			
		||||
    const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
    const [selectOpen, setSelectOpen] = useState(false);
 | 
			
		||||
    const [searchTerm, setSearchTerm] = useState<string>('');
 | 
			
		||||
 | 
			
		||||
    const sourceTable = useMemo(
 | 
			
		||||
        () => getTable(sourceTableId),
 | 
			
		||||
        [sourceTableId, getTable]
 | 
			
		||||
    );
 | 
			
		||||
    const targetTable = useMemo(
 | 
			
		||||
        () => getTable(targetTableId),
 | 
			
		||||
        [targetTableId, getTable]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Get the PK field from source table
 | 
			
		||||
    const sourcePKField = useMemo(() => {
 | 
			
		||||
        if (!sourceTable) return null;
 | 
			
		||||
        return (
 | 
			
		||||
            sourceTable.fields.find((f) => f.primaryKey) ||
 | 
			
		||||
            sourceTable.fields[0]
 | 
			
		||||
        );
 | 
			
		||||
    }, [sourceTable]);
 | 
			
		||||
 | 
			
		||||
    // Get compatible target fields (FK columns)
 | 
			
		||||
    // Reset state when source or target table changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setTargetFieldId(undefined);
 | 
			
		||||
        setSearchTerm('');
 | 
			
		||||
        setErrorMessage('');
 | 
			
		||||
        setSelectOpen(true);
 | 
			
		||||
    }, [sourceTableId, targetTableId]);
 | 
			
		||||
 | 
			
		||||
    const targetFieldOptions = useMemo(() => {
 | 
			
		||||
        if (!targetTable || !sourcePKField) return [];
 | 
			
		||||
 | 
			
		||||
        const compatibleFields = targetTable.fields
 | 
			
		||||
            .filter((field) =>
 | 
			
		||||
                areFieldTypesCompatible(
 | 
			
		||||
                    sourcePKField.type,
 | 
			
		||||
                    field.type,
 | 
			
		||||
                    databaseType
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            .map(
 | 
			
		||||
                (field) =>
 | 
			
		||||
                    ({
 | 
			
		||||
                        label: field.name,
 | 
			
		||||
                        value: field.id,
 | 
			
		||||
                        description: `(${field.type.name})`,
 | 
			
		||||
                    }) as SelectBoxOption
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        // Add option to create a new field if user typed a custom name
 | 
			
		||||
        if (
 | 
			
		||||
            searchTerm &&
 | 
			
		||||
            !compatibleFields.find(
 | 
			
		||||
                (f) => f.label.toLowerCase() === searchTerm.toLowerCase()
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            compatibleFields.push({
 | 
			
		||||
                label: `Create "${searchTerm}"`,
 | 
			
		||||
                value: CREATE_NEW_FIELD_VALUE,
 | 
			
		||||
                description: `(${sourcePKField.type.name})`,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return compatibleFields;
 | 
			
		||||
    }, [targetTable, sourcePKField, databaseType, searchTerm]);
 | 
			
		||||
 | 
			
		||||
    // Auto-select first compatible field OR pre-populate suggested name
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (targetFieldOptions.length > 0 && !targetFieldId) {
 | 
			
		||||
            setTargetFieldId(targetFieldOptions[0].value as string);
 | 
			
		||||
        } else if (
 | 
			
		||||
            targetFieldOptions.length === 0 &&
 | 
			
		||||
            !searchTerm &&
 | 
			
		||||
            sourceTable &&
 | 
			
		||||
            sourcePKField
 | 
			
		||||
        ) {
 | 
			
		||||
            // No compatible fields - suggest a field name based on source table + PK field
 | 
			
		||||
            const suggestedName =
 | 
			
		||||
                sourcePKField.name.toLowerCase() === 'id'
 | 
			
		||||
                    ? `${sourceTable.name}_${sourcePKField.name}`
 | 
			
		||||
                    : sourcePKField.name;
 | 
			
		||||
            setSearchTerm(suggestedName);
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        targetFieldOptions.length,
 | 
			
		||||
        sourceTable,
 | 
			
		||||
        sourcePKField,
 | 
			
		||||
        searchTerm,
 | 
			
		||||
        targetFieldId,
 | 
			
		||||
        targetFieldOptions,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Auto-open the select immediately and trigger animation
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setSelectOpen(true);
 | 
			
		||||
        const rafId = requestAnimationFrame(() => {
 | 
			
		||||
            setIsVisible(true);
 | 
			
		||||
        });
 | 
			
		||||
        return () => cancelAnimationFrame(rafId);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleCreate = useCallback(async () => {
 | 
			
		||||
        if (!sourcePKField) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            let finalTargetFieldId = targetFieldId;
 | 
			
		||||
 | 
			
		||||
            // If user selected "CREATE_NEW", create the field first
 | 
			
		||||
            if (targetFieldId === CREATE_NEW_FIELD_VALUE && searchTerm) {
 | 
			
		||||
                const newField: DBField = {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: searchTerm,
 | 
			
		||||
                    type: sourcePKField.type,
 | 
			
		||||
                    unique: false,
 | 
			
		||||
                    nullable: true,
 | 
			
		||||
                    primaryKey: false,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    await addField(targetTableId, newField);
 | 
			
		||||
                    finalTargetFieldId = newField.id;
 | 
			
		||||
                } catch (fieldError) {
 | 
			
		||||
                    console.error('Failed to create field:', fieldError);
 | 
			
		||||
                    setErrorMessage('Failed to create new field');
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!finalTargetFieldId) {
 | 
			
		||||
                setErrorMessage('Please select a target field');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const relationship = await createRelationship({
 | 
			
		||||
                sourceTableId,
 | 
			
		||||
                sourceFieldId: sourcePKField.id,
 | 
			
		||||
                targetTableId,
 | 
			
		||||
                targetFieldId: finalTargetFieldId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            setEdges((edges) =>
 | 
			
		||||
                edges.map((edge) =>
 | 
			
		||||
                    edge.id === relationship.id
 | 
			
		||||
                        ? { ...edge, selected: true }
 | 
			
		||||
                        : { ...edge, selected: false }
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            openRelationshipFromSidebar(relationship.id);
 | 
			
		||||
            hideCreateRelationshipNode();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(error);
 | 
			
		||||
            setErrorMessage('Failed to create relationship');
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        sourcePKField,
 | 
			
		||||
        targetFieldId,
 | 
			
		||||
        searchTerm,
 | 
			
		||||
        sourceTableId,
 | 
			
		||||
        targetTableId,
 | 
			
		||||
        createRelationship,
 | 
			
		||||
        addField,
 | 
			
		||||
        setEdges,
 | 
			
		||||
        openRelationshipFromSidebar,
 | 
			
		||||
        hideCreateRelationshipNode,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Note: Escape key handling is done in canvas.tsx to avoid duplicate listeners
 | 
			
		||||
 | 
			
		||||
    if (!sourceTable || !targetTable || !sourcePKField) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
            className={cn(
 | 
			
		||||
                'pointer-events-auto flex cursor-auto flex-col rounded-lg border border-slate-300 bg-white shadow-xl transition-all duration-150 ease-out dark:border-slate-600 dark:bg-slate-800',
 | 
			
		||||
                {
 | 
			
		||||
                    'scale-100 opacity-100': isVisible,
 | 
			
		||||
                    'scale-95 opacity-0': !isVisible,
 | 
			
		||||
                }
 | 
			
		||||
            )}
 | 
			
		||||
            style={{
 | 
			
		||||
                minWidth: '380px',
 | 
			
		||||
                maxWidth: '420px',
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={(e) => e.stopPropagation()}
 | 
			
		||||
        >
 | 
			
		||||
            {/* Header - draggable */}
 | 
			
		||||
            <div className="flex cursor-move items-center justify-between gap-2 rounded-t-[7px] border-b bg-sky-600 px-3 py-1 dark:border-slate-600 dark:bg-sky-800">
 | 
			
		||||
                <div className="text-xs font-semibold text-white">
 | 
			
		||||
                    Create Relationship
 | 
			
		||||
                </div>
 | 
			
		||||
                <Button
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    className="size-6 p-0 text-white hover:bg-white/20 hover:text-white dark:hover:bg-white/10"
 | 
			
		||||
                    onClick={hideCreateRelationshipNode}
 | 
			
		||||
                >
 | 
			
		||||
                    <X className="size-4" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {/* Content */}
 | 
			
		||||
            <div className="nodrag flex flex-col gap-3 p-3">
 | 
			
		||||
                <div className="flex flex-row gap-2">
 | 
			
		||||
                    {/* PK Column (Source) */}
 | 
			
		||||
                    <div className="flex flex-1 flex-col gap-1.5">
 | 
			
		||||
                        <label className="text-xs font-medium text-slate-600 dark:text-slate-300">
 | 
			
		||||
                            From (PK)
 | 
			
		||||
                        </label>
 | 
			
		||||
                        <div className="flex h-7 items-center rounded-md border border-slate-200 bg-slate-50 px-2.5 text-sm font-medium text-slate-700 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-200">
 | 
			
		||||
                            {sourcePKField.name}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div className="text-xs text-slate-500 dark:text-slate-400">
 | 
			
		||||
                            {sourceTable.name}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Arrow indicator */}
 | 
			
		||||
                    <div className="flex items-center">
 | 
			
		||||
                        <ArrowRight className="size-3.5 text-slate-400 dark:text-slate-500" />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* FK Column (Target) */}
 | 
			
		||||
                    <div className="flex flex-1 flex-col gap-1.5">
 | 
			
		||||
                        <label className="text-xs font-medium text-slate-600 dark:text-slate-300">
 | 
			
		||||
                            To (FK)
 | 
			
		||||
                        </label>
 | 
			
		||||
                        <SelectBox
 | 
			
		||||
                            className="flex h-7 min-h-0 w-full dark:border-slate-200"
 | 
			
		||||
                            popoverClassName="!z-[1001]"
 | 
			
		||||
                            options={targetFieldOptions}
 | 
			
		||||
                            placeholder="Select field..."
 | 
			
		||||
                            inputPlaceholder="Search or Create..."
 | 
			
		||||
                            value={targetFieldId}
 | 
			
		||||
                            onChange={(value) => {
 | 
			
		||||
                                setTargetFieldId(value as string);
 | 
			
		||||
                            }}
 | 
			
		||||
                            emptyPlaceholder="No compatible fields"
 | 
			
		||||
                            onSearchChange={setSearchTerm}
 | 
			
		||||
                            open={selectOpen}
 | 
			
		||||
                            onOpenChange={setSelectOpen}
 | 
			
		||||
                        />
 | 
			
		||||
                        <div className="text-xs text-slate-500 dark:text-slate-400">
 | 
			
		||||
                            {targetTable.name}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {errorMessage && (
 | 
			
		||||
                    <div className="rounded-md bg-red-50 p-2 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
 | 
			
		||||
                        {errorMessage}
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                {targetFieldOptions.length === 0 && (
 | 
			
		||||
                    <div className="rounded-md bg-yellow-50 p-2 text-xs text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400">
 | 
			
		||||
                        No compatible fields found in target table
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {/* Footer */}
 | 
			
		||||
            <div className="flex cursor-move items-center justify-end gap-2 rounded-b-lg border-t border-slate-200 bg-slate-50 px-3 py-2 dark:border-slate-600 dark:bg-slate-900">
 | 
			
		||||
                <Button
 | 
			
		||||
                    disabled={!targetFieldId || targetFieldOptions.length === 0}
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onClick={handleCreate}
 | 
			
		||||
                    variant="default"
 | 
			
		||||
                    className="h-7 bg-sky-600 px-3 text-xs text-white hover:bg-sky-700 dark:bg-sky-800 dark:text-white dark:hover:bg-sky-900"
 | 
			
		||||
                >
 | 
			
		||||
                    Create
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
CreateRelationshipNode.displayName = 'CreateRelationshipNode';
 | 
			
		||||
@@ -0,0 +1,180 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import { KeyRound, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { generateDBFieldSuffix, type DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBTable } from '@/lib/domain';
 | 
			
		||||
import { useUpdateTableField } from '@/hooks/use-update-table-field';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { TableFieldToggle } from './table-field-toggle';
 | 
			
		||||
 | 
			
		||||
export interface TableEditModeFieldProps {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    field: DBField;
 | 
			
		||||
    focused?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
 | 
			
		||||
    ({ table, field, focused = false }) => {
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const [showHighlight, setShowHighlight] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
            dataFieldOptions,
 | 
			
		||||
            handleDataTypeChange,
 | 
			
		||||
            handlePrimaryKeyToggle,
 | 
			
		||||
            handleNullableToggle,
 | 
			
		||||
            handleNameChange,
 | 
			
		||||
            generateFieldSuffix,
 | 
			
		||||
            fieldName,
 | 
			
		||||
            nullable,
 | 
			
		||||
            primaryKey,
 | 
			
		||||
            removeField,
 | 
			
		||||
        } = useUpdateTableField(table, field);
 | 
			
		||||
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
        // Animate the highlight after mount if focused
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            if (focused) {
 | 
			
		||||
                const timer = setTimeout(() => {
 | 
			
		||||
                    setShowHighlight(true);
 | 
			
		||||
                    inputRef.current?.select();
 | 
			
		||||
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        setShowHighlight(false);
 | 
			
		||||
                    }, 2000);
 | 
			
		||||
                }, 200); // Small delay for the animation to be noticeable
 | 
			
		||||
 | 
			
		||||
                return () => clearTimeout(timer);
 | 
			
		||||
            } else {
 | 
			
		||||
                setShowHighlight(false);
 | 
			
		||||
            }
 | 
			
		||||
        }, [focused]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex flex-1 flex-row justify-between gap-2 p-1 transition-colors duration-1000 ease-out',
 | 
			
		||||
                    {
 | 
			
		||||
                        'bg-sky-100 dark:bg-sky-950': showHighlight,
 | 
			
		||||
                    }
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                <div className="flex flex-1 items-center justify-start gap-1 overflow-hidden">
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span className="min-w-0 flex-1">
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    ref={inputRef}
 | 
			
		||||
                                    className="h-8 w-full !truncate bg-background focus-visible:ring-0"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    placeholder={t(
 | 
			
		||||
                                        'side_panel.tables_section.table.field_name'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    value={fieldName}
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        handleNameChange(e.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    autoFocus={focused}
 | 
			
		||||
                                />
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>{fieldName}</TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger
 | 
			
		||||
                            className="flex h-8 min-w-0 flex-1"
 | 
			
		||||
                            asChild
 | 
			
		||||
                        >
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <SelectBox
 | 
			
		||||
                                    className="flex h-8 min-h-8 w-full bg-background"
 | 
			
		||||
                                    popoverClassName="min-w-[200px]"
 | 
			
		||||
                                    options={dataFieldOptions}
 | 
			
		||||
                                    placeholder={t(
 | 
			
		||||
                                        'side_panel.tables_section.table.field_type'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    value={field.type.id}
 | 
			
		||||
                                    valueSuffix={generateDBFieldSuffix(field)}
 | 
			
		||||
                                    optionSuffix={(option) =>
 | 
			
		||||
                                        generateFieldSuffix(option.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    onChange={handleDataTypeChange}
 | 
			
		||||
                                    emptyPlaceholder={t(
 | 
			
		||||
                                        'side_panel.tables_section.table.no_types_found'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    commandOnClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                    commandOnMouseDown={(e) =>
 | 
			
		||||
                                        e.stopPropagation()
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {field.type.name}
 | 
			
		||||
                            {field.characterMaximumLength
 | 
			
		||||
                                ? `(${field.characterMaximumLength})`
 | 
			
		||||
                                : ''}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex shrink-0 items-center justify-end gap-1">
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <TableFieldToggle
 | 
			
		||||
                                    pressed={nullable}
 | 
			
		||||
                                    onPressedChange={handleNullableToggle}
 | 
			
		||||
                                >
 | 
			
		||||
                                    N
 | 
			
		||||
                                </TableFieldToggle>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('side_panel.tables_section.table.nullable')}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <TableFieldToggle
 | 
			
		||||
                                    pressed={primaryKey}
 | 
			
		||||
                                    onPressedChange={handlePrimaryKeyToggle}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <KeyRound className="h-3.5" />
 | 
			
		||||
                                </TableFieldToggle>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('side_panel.tables_section.table.primary_key')}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <TableFieldToggle onPressedChange={removeField}>
 | 
			
		||||
                                    <Trash2 className="h-3.5 text-red-700" />
 | 
			
		||||
                                </TableFieldToggle>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t(
 | 
			
		||||
                                'side_panel.tables_section.table.field_actions.delete_field'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TableEditModeField.displayName = 'TableEditModeField';
 | 
			
		||||
@@ -0,0 +1,344 @@
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import type { DBTable } from '@/lib/domain';
 | 
			
		||||
import { FileType2, X, SquarePlus } from 'lucide-react';
 | 
			
		||||
import React, {
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useState,
 | 
			
		||||
    useRef,
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useMemo,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { TableEditModeField } from './table-edit-mode-field';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { ColorPicker } from '@/components/color-picker/color-picker';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useUpdateTable } from '@/hooks/use-update-table';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import type { SelectBoxOption } from '@/components/select-box/select-box';
 | 
			
		||||
import {
 | 
			
		||||
    databasesWithSchemas,
 | 
			
		||||
    schemaNameToSchemaId,
 | 
			
		||||
} from '@/lib/domain/db-schema';
 | 
			
		||||
import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
export interface TableEditModeProps {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
    color: string;
 | 
			
		||||
    focusFieldId?: string;
 | 
			
		||||
    onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
 | 
			
		||||
    ({ table, color, focusFieldId: focusFieldIdProp, onClose }) => {
 | 
			
		||||
        const containerRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
        const scrollAreaRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
        const fieldRefs = useRef<Map<string, HTMLDivElement>>(new Map());
 | 
			
		||||
        const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
        const { createField, updateTable, schemas, databaseType } =
 | 
			
		||||
            useChartDB();
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const { tableName, handleTableNameChange } = useUpdateTable(table);
 | 
			
		||||
        const [focusFieldId, setFocusFieldId] = useState<string | undefined>(
 | 
			
		||||
            focusFieldIdProp
 | 
			
		||||
        );
 | 
			
		||||
        const inputRef = useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
        // Schema-related state
 | 
			
		||||
        const [isCreatingNewSchema, setIsCreatingNewSchema] = useState(false);
 | 
			
		||||
        const [newSchemaName, setNewSchemaName] = useState('');
 | 
			
		||||
        const [selectedSchemaId, setSelectedSchemaId] = useState<string>(() =>
 | 
			
		||||
            table.schema ? schemaNameToSchemaId(table.schema) : ''
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Sync selectedSchemaId when table.schema changes
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            setSelectedSchemaId(
 | 
			
		||||
                table.schema ? schemaNameToSchemaId(table.schema) : ''
 | 
			
		||||
            );
 | 
			
		||||
        }, [table.schema]);
 | 
			
		||||
 | 
			
		||||
        const supportsSchemas = useMemo(
 | 
			
		||||
            () => databasesWithSchemas.includes(databaseType),
 | 
			
		||||
            [databaseType]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const defaultSchemaName = useMemo(
 | 
			
		||||
            () => defaultSchemas?.[databaseType],
 | 
			
		||||
            [databaseType]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const schemaOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                schemas.map((schema) => ({
 | 
			
		||||
                    value: schema.id,
 | 
			
		||||
                    label: schema.name,
 | 
			
		||||
                })),
 | 
			
		||||
            [schemas]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            setFocusFieldId(focusFieldIdProp);
 | 
			
		||||
            if (!focusFieldIdProp) {
 | 
			
		||||
                inputRef.current?.select();
 | 
			
		||||
            }
 | 
			
		||||
        }, [focusFieldIdProp]);
 | 
			
		||||
 | 
			
		||||
        // Callback to store field refs
 | 
			
		||||
        const setFieldRef = useCallback((fieldId: string) => {
 | 
			
		||||
            return (element: HTMLDivElement | null) => {
 | 
			
		||||
                if (element) {
 | 
			
		||||
                    fieldRefs.current.set(fieldId, element);
 | 
			
		||||
                } else {
 | 
			
		||||
                    fieldRefs.current.delete(fieldId);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            // Trigger animation after mount
 | 
			
		||||
            requestAnimationFrame(() => {
 | 
			
		||||
                setIsVisible(true);
 | 
			
		||||
            });
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        const scrollToFieldId = useCallback((fieldId: string) => {
 | 
			
		||||
            const fieldElement = fieldRefs.current.get(fieldId);
 | 
			
		||||
            if (fieldElement) {
 | 
			
		||||
                fieldElement.scrollIntoView({
 | 
			
		||||
                    behavior: 'smooth',
 | 
			
		||||
                    block: 'center',
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        // Scroll to focused field when component mounts
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            if (focusFieldId) {
 | 
			
		||||
                scrollToFieldId(focusFieldId);
 | 
			
		||||
            }
 | 
			
		||||
        }, [focusFieldId, scrollToFieldId]);
 | 
			
		||||
 | 
			
		||||
        // Handle wheel events: allow zoom to pass through, but handle scroll locally
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            const handleWheel = (e: WheelEvent) => {
 | 
			
		||||
                // If Ctrl or Cmd is pressed, it's a zoom gesture - let it pass through to canvas
 | 
			
		||||
                if (e.ctrlKey || e.metaKey) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Otherwise, it's a scroll - stop propagation to prevent canvas panning
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const scrollArea = scrollAreaRef.current;
 | 
			
		||||
            if (scrollArea) {
 | 
			
		||||
                // Use passive: false to allow preventDefault if needed
 | 
			
		||||
                scrollArea.addEventListener('wheel', handleWheel, {
 | 
			
		||||
                    passive: false,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return () => {
 | 
			
		||||
                    scrollArea.removeEventListener('wheel', handleWheel);
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        const handleAddField = useCallback(async () => {
 | 
			
		||||
            const field = await createField(table.id);
 | 
			
		||||
 | 
			
		||||
            if (field.id) {
 | 
			
		||||
                setFocusFieldId(field.id);
 | 
			
		||||
            }
 | 
			
		||||
        }, [createField, table.id]);
 | 
			
		||||
 | 
			
		||||
        const handleColorChange = useCallback(
 | 
			
		||||
            (newColor: string) => {
 | 
			
		||||
                updateTable(table.id, { color: newColor });
 | 
			
		||||
            },
 | 
			
		||||
            [updateTable, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const handleSchemaChange = useCallback(
 | 
			
		||||
            (schemaId: string) => {
 | 
			
		||||
                const schema = schemas.find((s) => s.id === schemaId);
 | 
			
		||||
                if (schema) {
 | 
			
		||||
                    updateTable(table.id, { schema: schema.name });
 | 
			
		||||
                    setSelectedSchemaId(schemaId);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [schemas, updateTable, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const handleCreateNewSchema = useCallback(() => {
 | 
			
		||||
            if (newSchemaName.trim()) {
 | 
			
		||||
                const trimmedName = newSchemaName.trim();
 | 
			
		||||
                const newSchema: DBSchema = {
 | 
			
		||||
                    id: schemaNameToSchemaId(trimmedName),
 | 
			
		||||
                    name: trimmedName,
 | 
			
		||||
                    tableCount: 0,
 | 
			
		||||
                };
 | 
			
		||||
                updateTable(table.id, { schema: newSchema.name });
 | 
			
		||||
                setSelectedSchemaId(newSchema.id);
 | 
			
		||||
                setIsCreatingNewSchema(false);
 | 
			
		||||
                setNewSchemaName('');
 | 
			
		||||
            }
 | 
			
		||||
        }, [newSchemaName, updateTable, table.id]);
 | 
			
		||||
 | 
			
		||||
        const handleToggleSchemaMode = useCallback(() => {
 | 
			
		||||
            if (isCreatingNewSchema && newSchemaName.trim()) {
 | 
			
		||||
                // If we're leaving create mode with a value, create the schema
 | 
			
		||||
                handleCreateNewSchema();
 | 
			
		||||
            } else {
 | 
			
		||||
                // Otherwise just toggle modes
 | 
			
		||||
                setIsCreatingNewSchema(!isCreatingNewSchema);
 | 
			
		||||
                setNewSchemaName('');
 | 
			
		||||
            }
 | 
			
		||||
        }, [isCreatingNewSchema, newSchemaName, handleCreateNewSchema]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                ref={containerRef}
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex z-50 border-slate-500 dark:border-slate-700 flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-lg absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-auto transition-all duration-100 ease-out',
 | 
			
		||||
                    {
 | 
			
		||||
                        'opacity-100 scale-100': isVisible,
 | 
			
		||||
                        'opacity-0 scale-95': !isVisible,
 | 
			
		||||
                    }
 | 
			
		||||
                )}
 | 
			
		||||
                style={{
 | 
			
		||||
                    minHeight: '300px',
 | 
			
		||||
                    minWidth: '350px',
 | 
			
		||||
                    height: 'max(calc(100% + 48px), 200px)',
 | 
			
		||||
                    width: 'max(calc(100% + 48px), 300px)',
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={(e) => e.stopPropagation()}
 | 
			
		||||
            >
 | 
			
		||||
                <div
 | 
			
		||||
                    className="h-2 cursor-move rounded-t-[6px]"
 | 
			
		||||
                    style={{ backgroundColor: color }}
 | 
			
		||||
                ></div>
 | 
			
		||||
                <div className="group flex h-9 cursor-move items-center justify-between gap-2 bg-slate-200 px-2 dark:bg-slate-900">
 | 
			
		||||
                    <div className="flex min-w-0 flex-1 items-center gap-2">
 | 
			
		||||
                        {supportsSchemas && !isCreatingNewSchema && (
 | 
			
		||||
                            <SelectBox
 | 
			
		||||
                                options={schemaOptions}
 | 
			
		||||
                                value={selectedSchemaId}
 | 
			
		||||
                                onChange={(value) =>
 | 
			
		||||
                                    handleSchemaChange(value as string)
 | 
			
		||||
                                }
 | 
			
		||||
                                placeholder={
 | 
			
		||||
                                    defaultSchemaName || 'Select schema'
 | 
			
		||||
                                }
 | 
			
		||||
                                className="h-6 min-h-6 w-20 shrink-0 rounded-sm border-slate-600 bg-background py-0 pl-2 pr-0.5 text-sm"
 | 
			
		||||
                                popoverClassName="w-[200px]"
 | 
			
		||||
                                commandOnMouseDown={(e) => e.stopPropagation()}
 | 
			
		||||
                                commandOnClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                footerButtons={
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        size="sm"
 | 
			
		||||
                                        className="w-full justify-center rounded-none text-xs"
 | 
			
		||||
                                        onClick={(e) => {
 | 
			
		||||
                                            e.stopPropagation();
 | 
			
		||||
                                            handleToggleSchemaMode();
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <SquarePlus className="!size-3.5" />
 | 
			
		||||
                                        Create new schema
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        )}
 | 
			
		||||
                        {supportsSchemas && isCreatingNewSchema && (
 | 
			
		||||
                            <Input
 | 
			
		||||
                                value={newSchemaName}
 | 
			
		||||
                                onChange={(e) =>
 | 
			
		||||
                                    setNewSchemaName(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                placeholder={`Enter schema name${defaultSchemaName ? ` (e.g. ${defaultSchemaName})` : ''}`}
 | 
			
		||||
                                className="h-6 w-28 shrink-0 rounded-sm border-slate-600 bg-background text-sm"
 | 
			
		||||
                                onKeyDown={(e) => {
 | 
			
		||||
                                    if (e.key === 'Enter') {
 | 
			
		||||
                                        handleCreateNewSchema();
 | 
			
		||||
                                    } else if (e.key === 'Escape') {
 | 
			
		||||
                                        handleToggleSchemaMode();
 | 
			
		||||
                                    }
 | 
			
		||||
                                }}
 | 
			
		||||
                                onBlur={handleToggleSchemaMode}
 | 
			
		||||
                                autoFocus
 | 
			
		||||
                            />
 | 
			
		||||
                        )}
 | 
			
		||||
                        <Input
 | 
			
		||||
                            ref={inputRef}
 | 
			
		||||
                            className="h-6 flex-1 rounded-sm border-slate-600 bg-background text-sm"
 | 
			
		||||
                            placeholder="Table name"
 | 
			
		||||
                            value={tableName}
 | 
			
		||||
                            onChange={(e) =>
 | 
			
		||||
                                handleTableNameChange(e.target.value)
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="ghost"
 | 
			
		||||
                        size="sm"
 | 
			
		||||
                        className="size-6 p-0 hover:bg-slate-300 dark:hover:bg-slate-700"
 | 
			
		||||
                        onClick={onClose}
 | 
			
		||||
                    >
 | 
			
		||||
                        <X className="size-4" />
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <ScrollArea ref={scrollAreaRef} className="nodrag flex-1 p-2">
 | 
			
		||||
                    {table.fields.map((field) => (
 | 
			
		||||
                        <div key={field.id} ref={setFieldRef(field.id)}>
 | 
			
		||||
                            <TableEditModeField
 | 
			
		||||
                                table={table}
 | 
			
		||||
                                field={field}
 | 
			
		||||
                                focused={focusFieldId === field.id}
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </ScrollArea>
 | 
			
		||||
 | 
			
		||||
                <Separator />
 | 
			
		||||
                <div className="flex cursor-move items-center justify-between p-2">
 | 
			
		||||
                    <div className="flex items-center gap-2">
 | 
			
		||||
                        {!table.isView ? (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <ColorPicker
 | 
			
		||||
                                    color={color}
 | 
			
		||||
                                    onChange={handleColorChange}
 | 
			
		||||
                                    popoverOnMouseDown={(e) =>
 | 
			
		||||
                                        e.stopPropagation()
 | 
			
		||||
                                    }
 | 
			
		||||
                                    popoverOnClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                />
 | 
			
		||||
                            </>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <div />
 | 
			
		||||
                        )}
 | 
			
		||||
                        <Button
 | 
			
		||||
                            variant="outline"
 | 
			
		||||
                            className="h-8 p-2 text-xs"
 | 
			
		||||
                            onClick={handleAddField}
 | 
			
		||||
                        >
 | 
			
		||||
                            <FileType2 className="mr-1 h-4" />
 | 
			
		||||
                            {t('side_panel.tables_section.table.add_field')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <span className="text-xs font-medium text-muted-foreground">
 | 
			
		||||
                        {table.fields.length}{' '}
 | 
			
		||||
                        {t('side_panel.tables_section.table.fields')}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TableEditMode.displayName = 'TableEditMode';
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Toggle } from '@/components/toggle/toggle';
 | 
			
		||||
 | 
			
		||||
export const TableFieldToggle = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof Toggle>,
 | 
			
		||||
    React.ComponentPropsWithoutRef<typeof Toggle>
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Toggle
 | 
			
		||||
            {...props}
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            variant="default"
 | 
			
		||||
            className="h-8 w-[32px] p-2 text-xs text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
TableFieldToggle.displayName = Toggle.displayName;
 | 
			
		||||
@@ -12,7 +12,7 @@ import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { Copy, Pencil, Trash2, Workflow } from 'lucide-react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
 | 
			
		||||
export interface TableNodeContextMenuProps {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
@@ -22,34 +22,59 @@ export const TableNodeContextMenu: React.FC<
 | 
			
		||||
    React.PropsWithChildren<TableNodeContextMenuProps>
 | 
			
		||||
> = ({ children, table }) => {
 | 
			
		||||
    const { removeTable, readonly, createTable } = useChartDB();
 | 
			
		||||
    const { openTableFromSidebar } = useLayout();
 | 
			
		||||
    const { closeAllTablesInSidebar } = useLayout();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const { openCreateRelationshipDialog } = useDialog();
 | 
			
		||||
    const { setEditTableModeTable, startFloatingEdgeCreation } = useCanvas();
 | 
			
		||||
 | 
			
		||||
    const duplicateTableHandler = useCallback(() => {
 | 
			
		||||
        const clonedTable = cloneTable(table);
 | 
			
		||||
    const duplicateTableHandler: React.MouseEventHandler<HTMLDivElement> =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                const clonedTable = cloneTable(table);
 | 
			
		||||
 | 
			
		||||
        clonedTable.name = `${clonedTable.name}_copy`;
 | 
			
		||||
        clonedTable.x += 30;
 | 
			
		||||
        clonedTable.y += 50;
 | 
			
		||||
                clonedTable.name = `${clonedTable.name}_copy`;
 | 
			
		||||
                clonedTable.x += 30;
 | 
			
		||||
                clonedTable.y += 50;
 | 
			
		||||
 | 
			
		||||
        createTable(clonedTable);
 | 
			
		||||
    }, [createTable, table]);
 | 
			
		||||
                createTable(clonedTable);
 | 
			
		||||
            },
 | 
			
		||||
            [createTable, table]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const editTableHandler = useCallback(() => {
 | 
			
		||||
        openTableFromSidebar(table.id);
 | 
			
		||||
    }, [openTableFromSidebar, table.id]);
 | 
			
		||||
    const editTableHandler: React.MouseEventHandler<HTMLDivElement> =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                if (readonly) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
    const removeTableHandler = useCallback(() => {
 | 
			
		||||
        removeTable(table.id);
 | 
			
		||||
    }, [removeTable, table.id]);
 | 
			
		||||
                closeAllTablesInSidebar();
 | 
			
		||||
                setEditTableModeTable({ tableId: table.id });
 | 
			
		||||
            },
 | 
			
		||||
            [table.id, setEditTableModeTable, closeAllTablesInSidebar, readonly]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const addRelationshipHandler = useCallback(() => {
 | 
			
		||||
        openCreateRelationshipDialog({
 | 
			
		||||
            sourceTableId: table.id,
 | 
			
		||||
        });
 | 
			
		||||
    }, [openCreateRelationshipDialog, table.id]);
 | 
			
		||||
    const removeTableHandler: React.MouseEventHandler<HTMLDivElement> =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                removeTable(table.id);
 | 
			
		||||
            },
 | 
			
		||||
            [removeTable, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const addRelationshipHandler: React.MouseEventHandler<HTMLDivElement> =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                startFloatingEdgeCreation({
 | 
			
		||||
                    sourceNodeId: table.id,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [startFloatingEdgeCreation, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    if (!isDesktop || readonly) {
 | 
			
		||||
        return <>{children}</>;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,12 @@ import {
 | 
			
		||||
} from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
    Check,
 | 
			
		||||
    KeyRound,
 | 
			
		||||
    MessageCircleMore,
 | 
			
		||||
    SquareDot,
 | 
			
		||||
    SquareMinus,
 | 
			
		||||
    SquarePlus,
 | 
			
		||||
    Trash2,
 | 
			
		||||
    Pencil,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { generateDBFieldSuffix, type DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
@@ -29,14 +28,14 @@ import {
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import {
 | 
			
		||||
    BOTTOM_SOURCE_HANDLE_ID_PREFIX,
 | 
			
		||||
    TOP_SOURCE_HANDLE_ID_PREFIX,
 | 
			
		||||
} from './table-node-dependency-indicator';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
 | 
			
		||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
 | 
			
		||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
 | 
			
		||||
@@ -78,16 +77,7 @@ const arePropsEqual = (
 | 
			
		||||
 | 
			
		||||
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
    ({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
 | 
			
		||||
        const {
 | 
			
		||||
            removeField,
 | 
			
		||||
            relationships,
 | 
			
		||||
            readonly,
 | 
			
		||||
            updateField,
 | 
			
		||||
            highlightedCustomType,
 | 
			
		||||
        } = useChartDB();
 | 
			
		||||
        const [editMode, setEditMode] = useState(false);
 | 
			
		||||
        const [fieldName, setFieldName] = useState(field.name);
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
        const { relationships, readonly, highlightedCustomType } = useChartDB();
 | 
			
		||||
 | 
			
		||||
        const updateNodeInternals = useUpdateNodeInternals();
 | 
			
		||||
        const connection = useConnection();
 | 
			
		||||
@@ -152,23 +142,6 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            }
 | 
			
		||||
        }, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
 | 
			
		||||
 | 
			
		||||
        const editFieldName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
            if (fieldName.trim()) {
 | 
			
		||||
                updateField(tableNodeId, field.id, { name: fieldName.trim() });
 | 
			
		||||
            }
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
        }, [fieldName, field.id, updateField, editMode, tableNodeId]);
 | 
			
		||||
 | 
			
		||||
        const abortEdit = useCallback(() => {
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
            setFieldName(field.name);
 | 
			
		||||
        }, [field.name]);
 | 
			
		||||
 | 
			
		||||
        useClickAway(inputRef, editFieldName);
 | 
			
		||||
        useKeyPressEvent('Enter', editFieldName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
            checkIfFieldRemoved,
 | 
			
		||||
            checkIfNewField,
 | 
			
		||||
@@ -186,13 +159,17 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
        const [diffState, setDiffState] = useState<{
 | 
			
		||||
            isDiffFieldRemoved: boolean;
 | 
			
		||||
            isDiffNewField: boolean;
 | 
			
		||||
            fieldDiffChangedName: string | null;
 | 
			
		||||
            fieldDiffChangedType: DBField['type'] | null;
 | 
			
		||||
            fieldDiffChangedNullable: boolean | null;
 | 
			
		||||
            fieldDiffChangedCharacterMaximumLength: string | null;
 | 
			
		||||
            fieldDiffChangedScale: number | null;
 | 
			
		||||
            fieldDiffChangedPrecision: number | null;
 | 
			
		||||
            fieldDiffChangedPrimaryKey: boolean | null;
 | 
			
		||||
            fieldDiffChangedName: ReturnType<typeof getFieldNewName>;
 | 
			
		||||
            fieldDiffChangedType: ReturnType<typeof getFieldNewType>;
 | 
			
		||||
            fieldDiffChangedNullable: ReturnType<typeof getFieldNewNullable>;
 | 
			
		||||
            fieldDiffChangedCharacterMaximumLength: ReturnType<
 | 
			
		||||
                typeof getFieldNewCharacterMaximumLength
 | 
			
		||||
            >;
 | 
			
		||||
            fieldDiffChangedScale: ReturnType<typeof getFieldNewScale>;
 | 
			
		||||
            fieldDiffChangedPrecision: ReturnType<typeof getFieldNewPrecision>;
 | 
			
		||||
            fieldDiffChangedPrimaryKey: ReturnType<
 | 
			
		||||
                typeof getFieldNewPrimaryKey
 | 
			
		||||
            >;
 | 
			
		||||
            isDiffFieldChanged: boolean;
 | 
			
		||||
        }>({
 | 
			
		||||
            isDiffFieldRemoved: false,
 | 
			
		||||
@@ -272,17 +249,32 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            fieldDiffChangedPrecision,
 | 
			
		||||
        } = diffState;
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = useCallback((e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        const isCustomTypeHighlighted = useMemo(() => {
 | 
			
		||||
            if (!highlightedCustomType) return false;
 | 
			
		||||
            return field.type.name === highlightedCustomType.name;
 | 
			
		||||
        }, [highlightedCustomType, field.type.name]);
 | 
			
		||||
        const { showFieldAttributes } = useLocalConfig();
 | 
			
		||||
 | 
			
		||||
        const { closeAllTablesInSidebar } = useLayout();
 | 
			
		||||
        const { setEditTableModeTable } = useCanvas();
 | 
			
		||||
        const openEditTableOnField = useCallback(() => {
 | 
			
		||||
            if (readonly) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            closeAllTablesInSidebar();
 | 
			
		||||
            setEditTableModeTable({
 | 
			
		||||
                tableId: tableNodeId,
 | 
			
		||||
                fieldId: field.id,
 | 
			
		||||
            });
 | 
			
		||||
        }, [
 | 
			
		||||
            setEditTableModeTable,
 | 
			
		||||
            closeAllTablesInSidebar,
 | 
			
		||||
            tableNodeId,
 | 
			
		||||
            field.id,
 | 
			
		||||
            readonly,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
@@ -354,7 +346,6 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        'flex items-center gap-1 min-w-0 flex-1 text-left',
 | 
			
		||||
                        {
 | 
			
		||||
                            'font-semibold': field.primaryKey || field.unique,
 | 
			
		||||
                            'w-full': editMode,
 | 
			
		||||
                        }
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
@@ -365,54 +356,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                    ) : isDiffFieldChanged && !isSummaryOnly ? (
 | 
			
		||||
                        <SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {editMode && !readonly ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Input
 | 
			
		||||
                                ref={inputRef}
 | 
			
		||||
                                onBlur={editFieldName}
 | 
			
		||||
                                placeholder={field.name}
 | 
			
		||||
                                autoFocus
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={fieldName}
 | 
			
		||||
                                onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                onChange={(e) => setFieldName(e.target.value)}
 | 
			
		||||
                                className="h-5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                onClick={editFieldName}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Check className="size-4" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <span
 | 
			
		||||
                            className={cn('truncate min-w-0', {
 | 
			
		||||
                                'text-red-800 font-normal dark:text-red-200':
 | 
			
		||||
                                    isDiffFieldRemoved,
 | 
			
		||||
                                'text-green-800 font-normal dark:text-green-200':
 | 
			
		||||
                                    isDiffNewField,
 | 
			
		||||
                                'text-sky-800 font-normal dark:text-sky-200':
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                    !isSummaryOnly &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isDiffNewField,
 | 
			
		||||
                            })}
 | 
			
		||||
                            onDoubleClick={enterEditMode}
 | 
			
		||||
                        >
 | 
			
		||||
                            {fieldDiffChangedName ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {field.name}{' '}
 | 
			
		||||
                                    <span className="font-medium">→</span>{' '}
 | 
			
		||||
                                    {fieldDiffChangedName}
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                field.name
 | 
			
		||||
                            )}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {field.comments && !editMode ? (
 | 
			
		||||
 | 
			
		||||
                    <span
 | 
			
		||||
                        className={cn('truncate min-w-0', {
 | 
			
		||||
                            'text-red-800 font-normal dark:text-red-200':
 | 
			
		||||
                                isDiffFieldRemoved,
 | 
			
		||||
                            'text-green-800 font-normal dark:text-green-200':
 | 
			
		||||
                                isDiffNewField,
 | 
			
		||||
                            'text-sky-800 font-normal dark:text-sky-200':
 | 
			
		||||
                                isDiffFieldChanged &&
 | 
			
		||||
                                !isSummaryOnly &&
 | 
			
		||||
                                !isDiffFieldRemoved &&
 | 
			
		||||
                                !isDiffNewField,
 | 
			
		||||
                        })}
 | 
			
		||||
                    >
 | 
			
		||||
                        {fieldDiffChangedName ? (
 | 
			
		||||
                            <>
 | 
			
		||||
                                {fieldDiffChangedName.old}{' '}
 | 
			
		||||
                                <span className="font-medium">→</span>{' '}
 | 
			
		||||
                                {fieldDiffChangedName.new}
 | 
			
		||||
                            </>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            field.name
 | 
			
		||||
                        )}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    {field.comments ? (
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger asChild>
 | 
			
		||||
                                <div className="shrink-0 cursor-pointer text-muted-foreground">
 | 
			
		||||
@@ -423,37 +391,13 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
                {editMode ? null : (
 | 
			
		||||
                    <div className="ml-2 flex shrink-0 items-center justify-end gap-1.5">
 | 
			
		||||
                        {(field.primaryKey &&
 | 
			
		||||
                            fieldDiffChangedPrimaryKey === null) ||
 | 
			
		||||
                        fieldDiffChangedPrimaryKey ? (
 | 
			
		||||
                            <div
 | 
			
		||||
                                className={cn(
 | 
			
		||||
                                    'text-muted-foreground',
 | 
			
		||||
                                    !readonly ? 'group-hover:hidden' : '',
 | 
			
		||||
                                    isDiffFieldRemoved
 | 
			
		||||
                                        ? 'text-red-800 dark:text-red-200'
 | 
			
		||||
                                        : '',
 | 
			
		||||
                                    isDiffNewField
 | 
			
		||||
                                        ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                        : '',
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                        !isSummaryOnly &&
 | 
			
		||||
                                        !isDiffFieldRemoved &&
 | 
			
		||||
                                        !isDiffNewField
 | 
			
		||||
                                        ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                        : ''
 | 
			
		||||
                                )}
 | 
			
		||||
                            >
 | 
			
		||||
                                <KeyRound size={14} />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
 | 
			
		||||
                <div className="ml-2 flex shrink-0 items-center justify-end gap-1.5">
 | 
			
		||||
                    {(field.primaryKey && !fieldDiffChangedPrimaryKey?.old) ||
 | 
			
		||||
                    fieldDiffChangedPrimaryKey?.new ? (
 | 
			
		||||
                        <div
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'content-center text-right text-xs text-muted-foreground overflow-hidden max-w-[8rem]',
 | 
			
		||||
                                field.primaryKey ? 'min-w-0' : 'min-w-[3rem]',
 | 
			
		||||
                                'text-muted-foreground',
 | 
			
		||||
                                !readonly ? 'group-hover:hidden' : '',
 | 
			
		||||
                                isDiffFieldRemoved
 | 
			
		||||
                                    ? 'text-red-800 dark:text-red-200'
 | 
			
		||||
@@ -462,74 +406,100 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                                    ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                    : '',
 | 
			
		||||
                                isDiffFieldChanged &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isSummaryOnly &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isDiffNewField
 | 
			
		||||
                                    ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                    : ''
 | 
			
		||||
                            )}
 | 
			
		||||
                        >
 | 
			
		||||
                            <span className="block truncate">
 | 
			
		||||
                                {fieldDiffChangedType ? (
 | 
			
		||||
                                    <>
 | 
			
		||||
                                        <span className="line-through">
 | 
			
		||||
                                            {field.type.name.split(' ')[0]}
 | 
			
		||||
                                        </span>{' '}
 | 
			
		||||
                            <KeyRound size={14} />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
 | 
			
		||||
                    <div
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            'content-center text-right text-xs text-muted-foreground overflow-hidden max-w-[8rem]',
 | 
			
		||||
                            field.primaryKey ? 'min-w-0' : 'min-w-[3rem]',
 | 
			
		||||
                            !readonly ? 'group-hover:hidden' : '',
 | 
			
		||||
                            isDiffFieldRemoved
 | 
			
		||||
                                ? 'text-red-800 dark:text-red-200'
 | 
			
		||||
                                : '',
 | 
			
		||||
                            isDiffNewField
 | 
			
		||||
                                ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                : '',
 | 
			
		||||
                            isDiffFieldChanged &&
 | 
			
		||||
                                !isDiffFieldRemoved &&
 | 
			
		||||
                                !isSummaryOnly &&
 | 
			
		||||
                                !isDiffNewField
 | 
			
		||||
                                ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                : ''
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                        <span className="block truncate">
 | 
			
		||||
                            {fieldDiffChangedType ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <span className="line-through">
 | 
			
		||||
                                        {
 | 
			
		||||
                                            fieldDiffChangedType.name.split(
 | 
			
		||||
                                            fieldDiffChangedType.old.name.split(
 | 
			
		||||
                                                ' '
 | 
			
		||||
                                            )[0]
 | 
			
		||||
                                        }
 | 
			
		||||
                                    </>
 | 
			
		||||
                                    </span>{' '}
 | 
			
		||||
                                    {
 | 
			
		||||
                                        fieldDiffChangedType.new.name.split(
 | 
			
		||||
                                            ' '
 | 
			
		||||
                                        )[0]
 | 
			
		||||
                                    }
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                `${field.type.name.split(' ')[0]}${
 | 
			
		||||
                                    showFieldAttributes
 | 
			
		||||
                                        ? generateDBFieldSuffix({
 | 
			
		||||
                                              ...field,
 | 
			
		||||
                                              ...{
 | 
			
		||||
                                                  precision:
 | 
			
		||||
                                                      fieldDiffChangedPrecision?.new ??
 | 
			
		||||
                                                      field.precision,
 | 
			
		||||
                                                  scale:
 | 
			
		||||
                                                      fieldDiffChangedScale?.new ??
 | 
			
		||||
                                                      field.scale,
 | 
			
		||||
                                                  characterMaximumLength:
 | 
			
		||||
                                                      fieldDiffChangedCharacterMaximumLength?.new ??
 | 
			
		||||
                                                      field.characterMaximumLength,
 | 
			
		||||
                                              },
 | 
			
		||||
                                          })
 | 
			
		||||
                                        : ''
 | 
			
		||||
                                }`
 | 
			
		||||
                            )}
 | 
			
		||||
                            {fieldDiffChangedNullable ? (
 | 
			
		||||
                                fieldDiffChangedNullable.new ? (
 | 
			
		||||
                                    <span className="font-semibold">?</span>
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    `${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 ? (
 | 
			
		||||
                                    '?'
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    ''
 | 
			
		||||
                                )}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {readonly ? null : (
 | 
			
		||||
                            <div className="hidden flex-row group-hover:flex">
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 hover:bg-primary-foreground"
 | 
			
		||||
                                    onClick={(e) => {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        removeField(tableNodeId, field.id);
 | 
			
		||||
                                    }}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
                                    <span className="line-through">?</span>
 | 
			
		||||
                                )
 | 
			
		||||
                            ) : field.nullable ? (
 | 
			
		||||
                                '?'
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                ''
 | 
			
		||||
                            )}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
                    {readonly ? null : (
 | 
			
		||||
                        <div className="hidden flex-row group-hover:flex">
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 hover:bg-primary-foreground"
 | 
			
		||||
                                onClick={(e) => {
 | 
			
		||||
                                    e.stopPropagation();
 | 
			
		||||
                                    openEditTableOnField();
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Pencil className="!size-3.5 text-pink-600" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,13 @@ import React, {
 | 
			
		||||
    useEffect,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import type { NodeProps, Node } from '@xyflow/react';
 | 
			
		||||
import { NodeResizer, useConnection, useStore } from '@xyflow/react';
 | 
			
		||||
import {
 | 
			
		||||
    NodeResizer,
 | 
			
		||||
    useConnection,
 | 
			
		||||
    useStore,
 | 
			
		||||
    Handle,
 | 
			
		||||
    Position,
 | 
			
		||||
} from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
    ChevronsLeftRight,
 | 
			
		||||
@@ -14,7 +20,6 @@ import {
 | 
			
		||||
    Table2,
 | 
			
		||||
    ChevronDown,
 | 
			
		||||
    ChevronUp,
 | 
			
		||||
    Check,
 | 
			
		||||
    CircleDotDashed,
 | 
			
		||||
    SquareDot,
 | 
			
		||||
    SquarePlus,
 | 
			
		||||
@@ -38,8 +43,6 @@ import { TableNodeContextMenu } from './table-node-context-menu';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
 | 
			
		||||
import type { EdgeType } from '../canvas';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
@@ -47,6 +50,11 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
import { TableNodeStatus } from './table-node-status/table-node-status';
 | 
			
		||||
import { TableEditMode } from './table-edit-mode/table-edit-mode';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
 | 
			
		||||
export const TABLE_RELATIONSHIP_SOURCE_HANDLE_ID_PREFIX = 'table_rel_source_';
 | 
			
		||||
export const TABLE_RELATIONSHIP_TARGET_HANDLE_ID_PREFIX = 'table_rel_target_';
 | 
			
		||||
 | 
			
		||||
export type TableNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
@@ -55,6 +63,7 @@ export type TableNodeType = Node<
 | 
			
		||||
        highlightOverlappingTables?: boolean;
 | 
			
		||||
        hasHighlightedCustomType?: boolean;
 | 
			
		||||
        highlightTable?: boolean;
 | 
			
		||||
        isRelationshipCreatingTarget?: boolean;
 | 
			
		||||
    },
 | 
			
		||||
    'table'
 | 
			
		||||
>;
 | 
			
		||||
@@ -70,17 +79,40 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
            highlightOverlappingTables,
 | 
			
		||||
            hasHighlightedCustomType,
 | 
			
		||||
            highlightTable,
 | 
			
		||||
            isRelationshipCreatingTarget,
 | 
			
		||||
        },
 | 
			
		||||
    }) => {
 | 
			
		||||
        const { updateTable, relationships, readonly } = useChartDB();
 | 
			
		||||
        const edges = useStore((store) => store.edges) as EdgeType[];
 | 
			
		||||
        const { openTableFromSidebar, selectSidebarSection } = useLayout();
 | 
			
		||||
        const {
 | 
			
		||||
            openTableFromSidebar,
 | 
			
		||||
            selectSidebarSection,
 | 
			
		||||
            closeAllTablesInSidebar,
 | 
			
		||||
        } = useLayout();
 | 
			
		||||
        const [expanded, setExpanded] = useState(table.expanded ?? false);
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const [editMode, setEditMode] = useState(false);
 | 
			
		||||
        const [tableName, setTableName] = useState(table.name);
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
        const [isHovering, setIsHovering] = useState(false);
 | 
			
		||||
        const {
 | 
			
		||||
            setEditTableModeTable,
 | 
			
		||||
            editTableModeTable,
 | 
			
		||||
            setHoveringTableId,
 | 
			
		||||
            showCreateRelationshipNode,
 | 
			
		||||
            tempFloatingEdge,
 | 
			
		||||
        } = useCanvas();
 | 
			
		||||
 | 
			
		||||
        // Get edit mode state directly from context
 | 
			
		||||
        const editTableMode = useMemo(
 | 
			
		||||
            () => editTableModeTable?.tableId === table.id,
 | 
			
		||||
            [editTableModeTable, table.id]
 | 
			
		||||
        );
 | 
			
		||||
        const editTableModeFieldId = useMemo(
 | 
			
		||||
            () => (editTableMode ? editTableModeTable?.fieldId : null),
 | 
			
		||||
            [editTableMode, editTableModeTable]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Store the initial field count when entering edit mode to keep table height fixed
 | 
			
		||||
        const [editModeInitialFieldCount, setEditModeInitialFieldCount] =
 | 
			
		||||
            useState<number | null>(null);
 | 
			
		||||
 | 
			
		||||
        const connection = useConnection();
 | 
			
		||||
 | 
			
		||||
@@ -101,6 +133,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
 | 
			
		||||
        const fields = useMemo(() => table.fields, [table.fields]);
 | 
			
		||||
 | 
			
		||||
        // Effect to manage field count when entering/exiting edit mode
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            if (editTableMode && editModeInitialFieldCount === null) {
 | 
			
		||||
                // Entering edit mode - capture current field count
 | 
			
		||||
                setEditModeInitialFieldCount(fields.length);
 | 
			
		||||
            } else if (!editTableMode && editModeInitialFieldCount !== null) {
 | 
			
		||||
                // Exiting edit mode - reset
 | 
			
		||||
                setEditModeInitialFieldCount(null);
 | 
			
		||||
            }
 | 
			
		||||
        }, [editTableMode, fields.length, editModeInitialFieldCount]);
 | 
			
		||||
 | 
			
		||||
        const tableChangedName = useMemo(
 | 
			
		||||
            () => getTableNewName({ tableId: table.id }),
 | 
			
		||||
            [getTableNewName, table.id]
 | 
			
		||||
@@ -112,7 +155,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
        );
 | 
			
		||||
        const tableColor = useMemo(() => {
 | 
			
		||||
            if (tableChangedColor) {
 | 
			
		||||
                return tableChangedColor;
 | 
			
		||||
                return tableChangedColor.new;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return table.color;
 | 
			
		||||
@@ -235,14 +278,20 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
        }, [relationships]);
 | 
			
		||||
 | 
			
		||||
        const visibleFields = useMemo(() => {
 | 
			
		||||
            if (expanded || fields.length <= TABLE_MINIMIZED_FIELDS) {
 | 
			
		||||
                return fields;
 | 
			
		||||
            // If in edit mode, use the initial field count to keep consistent height
 | 
			
		||||
            const fieldsToConsider =
 | 
			
		||||
                editTableMode && editModeInitialFieldCount !== null
 | 
			
		||||
                    ? fields.slice(0, editModeInitialFieldCount)
 | 
			
		||||
                    : fields;
 | 
			
		||||
 | 
			
		||||
            if (expanded || fieldsToConsider.length <= TABLE_MINIMIZED_FIELDS) {
 | 
			
		||||
                return fieldsToConsider;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const mustDisplayedFields: DBField[] = [];
 | 
			
		||||
            const nonMustDisplayedFields: DBField[] = [];
 | 
			
		||||
 | 
			
		||||
            for (const field of fields) {
 | 
			
		||||
            for (const field of fieldsToConsider) {
 | 
			
		||||
                if (relatedFieldIds.has(field.id) || field.primaryKey) {
 | 
			
		||||
                    mustDisplayedFields.push(field);
 | 
			
		||||
                } else {
 | 
			
		||||
@@ -269,46 +318,33 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                ...visibleMustDisplayedFields,
 | 
			
		||||
                ...visibleNonMustDisplayedFields,
 | 
			
		||||
            ]);
 | 
			
		||||
            const result = fields.filter((field) =>
 | 
			
		||||
            const result = fieldsToConsider.filter((field) =>
 | 
			
		||||
                visibleFieldsSet.has(field)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }, [expanded, fields, relatedFieldIds]);
 | 
			
		||||
        }, [
 | 
			
		||||
            expanded,
 | 
			
		||||
            fields,
 | 
			
		||||
            relatedFieldIds,
 | 
			
		||||
            editTableMode,
 | 
			
		||||
            editModeInitialFieldCount,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        const editTableName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
            if (tableName.trim()) {
 | 
			
		||||
                updateTable(table.id, { name: tableName.trim() });
 | 
			
		||||
            }
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
        }, [tableName, table.id, updateTable, editMode]);
 | 
			
		||||
 | 
			
		||||
        const abortEdit = useCallback(() => {
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
            setTableName(table.name);
 | 
			
		||||
        }, [table.name]);
 | 
			
		||||
 | 
			
		||||
        useClickAway(inputRef, editTableName);
 | 
			
		||||
        useKeyPressEvent('Enter', editTableName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = useCallback((e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
        React.useEffect(() => {
 | 
			
		||||
            if (table.name.trim()) {
 | 
			
		||||
                setTableName(table.name.trim());
 | 
			
		||||
            }
 | 
			
		||||
        }, [table.name]);
 | 
			
		||||
        const isPartOfCreatingRelationship = useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                tempFloatingEdge?.sourceNodeId === id ||
 | 
			
		||||
                (isRelationshipCreatingTarget &&
 | 
			
		||||
                    tempFloatingEdge?.targetNodeId === id) ||
 | 
			
		||||
                isHovering,
 | 
			
		||||
            [tempFloatingEdge, id, isRelationshipCreatingTarget, isHovering]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const tableClassName = useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                cn(
 | 
			
		||||
                    'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
 | 
			
		||||
                    selected || isTarget
 | 
			
		||||
                    selected || isTarget || isPartOfCreatingRelationship
 | 
			
		||||
                        ? 'border-pink-600'
 | 
			
		||||
                        : 'border-slate-500 dark:border-slate-700',
 | 
			
		||||
                    isOverlapping
 | 
			
		||||
@@ -337,7 +373,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                        : '',
 | 
			
		||||
                    isDiffTableRemoved
 | 
			
		||||
                        ? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
 | 
			
		||||
                        : ''
 | 
			
		||||
                        : editTableMode
 | 
			
		||||
                          ? 'invisible'
 | 
			
		||||
                          : ''
 | 
			
		||||
                ),
 | 
			
		||||
            [
 | 
			
		||||
                selected,
 | 
			
		||||
@@ -350,20 +388,72 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                isDiffNewTable,
 | 
			
		||||
                isDiffTableRemoved,
 | 
			
		||||
                isTarget,
 | 
			
		||||
                editTableMode,
 | 
			
		||||
                isPartOfCreatingRelationship,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const enterEditTableMode = useCallback(() => {
 | 
			
		||||
            if (readonly) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            closeAllTablesInSidebar();
 | 
			
		||||
            setEditTableModeTable({ tableId: table.id });
 | 
			
		||||
        }, [
 | 
			
		||||
            table.id,
 | 
			
		||||
            setEditTableModeTable,
 | 
			
		||||
            closeAllTablesInSidebar,
 | 
			
		||||
            readonly,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        const exitEditTableMode = useCallback(() => {
 | 
			
		||||
            setEditTableModeTable(null);
 | 
			
		||||
        }, [setEditTableModeTable]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <TableNodeContextMenu table={table}>
 | 
			
		||||
                {editTableMode ? (
 | 
			
		||||
                    <TableEditMode
 | 
			
		||||
                        table={table}
 | 
			
		||||
                        color={tableColor}
 | 
			
		||||
                        focusFieldId={editTableModeFieldId ?? undefined}
 | 
			
		||||
                        onClose={() => {
 | 
			
		||||
                            exitEditTableMode();
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
                <div
 | 
			
		||||
                    className={tableClassName}
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        if (e.detail === 2) {
 | 
			
		||||
                            openTableInEditor();
 | 
			
		||||
                        if (e.detail === 2 && !readonly) {
 | 
			
		||||
                            e.stopPropagation();
 | 
			
		||||
                            enterEditTableMode();
 | 
			
		||||
                        } else if (e.detail === 1 && !readonly) {
 | 
			
		||||
                            // Handle single click
 | 
			
		||||
                            if (
 | 
			
		||||
                                isRelationshipCreatingTarget &&
 | 
			
		||||
                                tempFloatingEdge
 | 
			
		||||
                            ) {
 | 
			
		||||
                                e.stopPropagation();
 | 
			
		||||
                                showCreateRelationshipNode({
 | 
			
		||||
                                    sourceTableId:
 | 
			
		||||
                                        tempFloatingEdge.sourceNodeId,
 | 
			
		||||
                                    targetTableId: table.id,
 | 
			
		||||
                                    x: e.clientX,
 | 
			
		||||
                                    y: e.clientY,
 | 
			
		||||
                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                    onMouseEnter={() => setIsHovering(true)}
 | 
			
		||||
                    onMouseLeave={() => setIsHovering(false)}
 | 
			
		||||
                    onMouseEnter={() => {
 | 
			
		||||
                        setIsHovering(true);
 | 
			
		||||
                        setHoveringTableId(table.id);
 | 
			
		||||
                    }}
 | 
			
		||||
                    onMouseLeave={() => {
 | 
			
		||||
                        setIsHovering(false);
 | 
			
		||||
                        setHoveringTableId(null);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <NodeResizer
 | 
			
		||||
                        isVisible={focused}
 | 
			
		||||
@@ -373,6 +463,25 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                        shouldResize={(event) => event.dy === 0}
 | 
			
		||||
                        handleClassName="!hidden"
 | 
			
		||||
                    />
 | 
			
		||||
                    {/* Center handle for floating edge creation */}
 | 
			
		||||
                    {!readonly ? (
 | 
			
		||||
                        <Handle
 | 
			
		||||
                            id={`${TABLE_RELATIONSHIP_SOURCE_HANDLE_ID_PREFIX}${table.id}`}
 | 
			
		||||
                            type="source"
 | 
			
		||||
                            position={Position.Top}
 | 
			
		||||
                            className="!invisible !left-1/2 !top-1/2 !h-1 !w-1 !-translate-x-1/2 !-translate-y-1/2 !transform"
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {/* Target handle covering entire table for floating edge creation */}
 | 
			
		||||
                    {!readonly ? (
 | 
			
		||||
                        <Handle
 | 
			
		||||
                            id={`${TABLE_RELATIONSHIP_TARGET_HANDLE_ID_PREFIX}${table.id}`}
 | 
			
		||||
                            type="target"
 | 
			
		||||
                            position={Position.Top}
 | 
			
		||||
                            className="!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0"
 | 
			
		||||
                            isConnectable={isRelationshipCreatingTarget}
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <TableNodeDependencyIndicator
 | 
			
		||||
                        table={table}
 | 
			
		||||
                        focused={focused}
 | 
			
		||||
@@ -435,13 +544,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                            {tableChangedName ? (
 | 
			
		||||
                                <Label className="flex h-5 items-center 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">
 | 
			
		||||
                                    <span className="truncate">
 | 
			
		||||
                                        {table.name}
 | 
			
		||||
                                        {tableChangedName.old}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span className="mx-1 font-semibold">
 | 
			
		||||
                                        →
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span className="truncate">
 | 
			
		||||
                                        {tableChangedName}
 | 
			
		||||
                                        {tableChangedName.new}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : isDiffNewTable ? (
 | 
			
		||||
@@ -456,47 +565,14 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                                <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>
 | 
			
		||||
                            ) : editMode && !readonly ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <Input
 | 
			
		||||
                                        ref={inputRef}
 | 
			
		||||
                                        onBlur={editTableName}
 | 
			
		||||
                                        placeholder={table.name}
 | 
			
		||||
                                        autoFocus
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        value={tableName}
 | 
			
		||||
                                        onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                        onChange={(e) =>
 | 
			
		||||
                                            setTableName(e.target.value)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        className="h-6 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                        onClick={editTableName}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Check className="size-4" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <Label
 | 
			
		||||
                                            className="text-editable truncate px-2 py-0.5 text-sm font-bold"
 | 
			
		||||
                                            onDoubleClick={enterEditMode}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {table.name}
 | 
			
		||||
                                        </Label>
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                                <Label className="truncate px-2 py-0.5 text-sm font-bold">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div className="hidden shrink-0 flex-row group-hover:flex">
 | 
			
		||||
                            {readonly || editMode ? null : (
 | 
			
		||||
                            {readonly ? null : (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
@@ -505,30 +581,28 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                                    <CircleDotDashed className="size-4" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {editMode ? null : (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                    onClick={
 | 
			
		||||
                                        table.width !== MAX_TABLE_SIZE
 | 
			
		||||
                                            ? expandTable
 | 
			
		||||
                                            : shrinkTable
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    {table.width !== MAX_TABLE_SIZE ? (
 | 
			
		||||
                                        <ChevronsLeftRight className="size-4" />
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        <ChevronsRightLeft className="size-4" />
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            )}
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                onClick={
 | 
			
		||||
                                    table.width !== MAX_TABLE_SIZE
 | 
			
		||||
                                        ? expandTable
 | 
			
		||||
                                        : shrinkTable
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {table.width !== MAX_TABLE_SIZE ? (
 | 
			
		||||
                                    <ChevronsLeftRight className="size-4" />
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <ChevronsRightLeft className="size-4" />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div
 | 
			
		||||
                        className="transition-[max-height] duration-200 ease-in-out"
 | 
			
		||||
                        style={{
 | 
			
		||||
                            maxHeight: expanded
 | 
			
		||||
                                ? `${fields.length * 2}rem` // h-8 per field
 | 
			
		||||
                                ? `${(editTableMode && editModeInitialFieldCount !== null ? editModeInitialFieldCount : fields.length) * 2}rem` // h-8 per field
 | 
			
		||||
                                : `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
@@ -544,7 +618,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {fields.length > TABLE_MINIMIZED_FIELDS && (
 | 
			
		||||
                    {(editTableMode && editModeInitialFieldCount !== null
 | 
			
		||||
                        ? editModeInitialFieldCount
 | 
			
		||||
                        : fields.length) > TABLE_MINIMIZED_FIELDS && (
 | 
			
		||||
                        <div
 | 
			
		||||
                            className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
 | 
			
		||||
                            onClick={toggleExpand}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { type NodeProps, type Node, Handle, Position } from '@xyflow/react';
 | 
			
		||||
 | 
			
		||||
export const TEMP_CURSOR_NODE_ID = '__temp_cursor_node__';
 | 
			
		||||
export const TEMP_CURSOR_HANDLE_ID = '__temp-cursor-target__';
 | 
			
		||||
 | 
			
		||||
export type TempCursorNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
        // Empty data object - this is just a cursor position marker
 | 
			
		||||
    },
 | 
			
		||||
    'temp-cursor'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const TempCursorNode: React.FC<NodeProps<TempCursorNodeType>> =
 | 
			
		||||
    React.memo(() => {
 | 
			
		||||
        // Invisible node that just serves as a connection point
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                style={{
 | 
			
		||||
                    width: 1,
 | 
			
		||||
                    height: 1,
 | 
			
		||||
                    opacity: 0,
 | 
			
		||||
                    pointerEvents: 'none',
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <Handle
 | 
			
		||||
                    id={TEMP_CURSOR_HANDLE_ID}
 | 
			
		||||
                    className="!invisible"
 | 
			
		||||
                    position={Position.Right}
 | 
			
		||||
                    type="target"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
TempCursorNode.displayName = 'TempCursorNode';
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { Edge, EdgeProps } from '@xyflow/react';
 | 
			
		||||
import { getSmoothStepPath, Position } from '@xyflow/react';
 | 
			
		||||
 | 
			
		||||
export const TEMP_FLOATING_EDGE_ID = '__temp_floating_edge__';
 | 
			
		||||
 | 
			
		||||
export type TempFloatingEdgeType = Edge<
 | 
			
		||||
    {
 | 
			
		||||
        // No relationship data - this is a temporary visual edge
 | 
			
		||||
    },
 | 
			
		||||
    'temp-floating-edge'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const TempFloatingEdge: React.FC<EdgeProps<TempFloatingEdgeType>> =
 | 
			
		||||
    React.memo(
 | 
			
		||||
        ({
 | 
			
		||||
            id,
 | 
			
		||||
            sourceX,
 | 
			
		||||
            sourceY,
 | 
			
		||||
            targetX,
 | 
			
		||||
            targetY,
 | 
			
		||||
            sourcePosition = Position.Right,
 | 
			
		||||
            targetPosition = Position.Left,
 | 
			
		||||
        }) => {
 | 
			
		||||
            const [edgePath] = getSmoothStepPath({
 | 
			
		||||
                sourceX,
 | 
			
		||||
                sourceY,
 | 
			
		||||
                sourcePosition,
 | 
			
		||||
                targetX,
 | 
			
		||||
                targetY,
 | 
			
		||||
                targetPosition,
 | 
			
		||||
                borderRadius: 14,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                <g>
 | 
			
		||||
                    <path
 | 
			
		||||
                        id={id}
 | 
			
		||||
                        fill="none"
 | 
			
		||||
                        stroke="#ec4899"
 | 
			
		||||
                        strokeWidth={2}
 | 
			
		||||
                        strokeDasharray="5,5"
 | 
			
		||||
                        d={edgePath}
 | 
			
		||||
                        style={{
 | 
			
		||||
                            pointerEvents: 'none',
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <circle
 | 
			
		||||
                        cx={targetX}
 | 
			
		||||
                        cy={targetY}
 | 
			
		||||
                        fill="#fff"
 | 
			
		||||
                        r={3}
 | 
			
		||||
                        stroke="#ec4899"
 | 
			
		||||
                        strokeWidth={1.5}
 | 
			
		||||
                        style={{
 | 
			
		||||
                            pointerEvents: 'none',
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                </g>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
TempFloatingEdge.displayName = 'TempFloatingEdge';
 | 
			
		||||
@@ -5,6 +5,7 @@ import React, {
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { useDebounceFn } from 'ahooks';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
@@ -25,7 +26,6 @@ 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,
 | 
			
		||||
@@ -113,6 +113,17 @@ export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
    const { hideLoader, showLoader } = useFullScreenLoader();
 | 
			
		||||
    const emphasisTimeoutRef = useRef<NodeJS.Timeout>();
 | 
			
		||||
    const readOnlyDisposableRef = useRef<monaco.IDisposable>();
 | 
			
		||||
    const currentDiagramRef = useRef<Diagram>(currentDiagram);
 | 
			
		||||
    const originalDiagramRef = useRef<Diagram | null>(originalDiagram);
 | 
			
		||||
 | 
			
		||||
    // Keep refs updated
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        currentDiagramRef.current = currentDiagram;
 | 
			
		||||
    }, [currentDiagram]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        originalDiagramRef.current = originalDiagram;
 | 
			
		||||
    }, [originalDiagram]);
 | 
			
		||||
 | 
			
		||||
    // --- Check for empty field name warnings only on mount ---
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -190,7 +201,7 @@ export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const sourceDiagram: Diagram =
 | 
			
		||||
                    originalDiagram ?? currentDiagram;
 | 
			
		||||
                    originalDiagramRef.current ?? currentDiagramRef.current;
 | 
			
		||||
 | 
			
		||||
                const targetDiagram: Diagram = {
 | 
			
		||||
                    ...sourceDiagram,
 | 
			
		||||
@@ -204,9 +215,9 @@ export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
                    targetDiagram,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (originalDiagram) {
 | 
			
		||||
                if (originalDiagramRef.current) {
 | 
			
		||||
                    resetDiff();
 | 
			
		||||
                    loadDiagramFromData(originalDiagram);
 | 
			
		||||
                    loadDiagramFromData(originalDiagramRef.current);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                calculateDiff({
 | 
			
		||||
@@ -232,18 +243,12 @@ export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            t,
 | 
			
		||||
            originalDiagram,
 | 
			
		||||
            currentDiagram,
 | 
			
		||||
            resetDiff,
 | 
			
		||||
            loadDiagramFromData,
 | 
			
		||||
            calculateDiff,
 | 
			
		||||
            databaseType,
 | 
			
		||||
        ]
 | 
			
		||||
        [t, resetDiff, loadDiagramFromData, calculateDiff, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const debouncedShowDiff = useDebounce(showDiff, 1000);
 | 
			
		||||
    const { run: debouncedShowDiff } = useDebounceFn(showDiff, {
 | 
			
		||||
        wait: 1000,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!isEditMode || !editedDbml) {
 | 
			
		||||
@@ -359,8 +364,10 @@ export const TableDBML: React.FC<TableDBMLProps> = () => {
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: 'View',
 | 
			
		||||
                                    icon: PencilOff,
 | 
			
		||||
                                    onClick: () =>
 | 
			
		||||
                                        setIsEditMode((prev) => !prev),
 | 
			
		||||
                                    onClick: () => {
 | 
			
		||||
                                        resetDiff();
 | 
			
		||||
                                        setIsEditMode((prev) => !prev);
 | 
			
		||||
                                    },
 | 
			
		||||
                                },
 | 
			
		||||
                            ]
 | 
			
		||||
                          : [
 | 
			
		||||
 
 | 
			
		||||
@@ -173,9 +173,11 @@ export const RelationshipListItemContent: React.FC<
 | 
			
		||||
                                <SelectItem value="many_to_one">
 | 
			
		||||
                                    {t('relationship_type.many_to_one')}
 | 
			
		||||
                                </SelectItem>
 | 
			
		||||
                                <SelectItem value="many_to_many">
 | 
			
		||||
                                    {t('relationship_type.many_to_many')}
 | 
			
		||||
                                </SelectItem>
 | 
			
		||||
                                {relationshipType === 'many_to_many' ? (
 | 
			
		||||
                                    <SelectItem value="many_to_many">
 | 
			
		||||
                                        {t('relationship_type.many_to_many')}
 | 
			
		||||
                                    </SelectItem>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
                            </SelectGroup>
 | 
			
		||||
                        </SelectContent>
 | 
			
		||||
                    </Select>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,9 @@
 | 
			
		||||
import React, { useCallback, useMemo } from 'react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { GripVertical, KeyRound } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { generateDBFieldSuffix, type DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    dataTypeDataToDataType,
 | 
			
		||||
    sortedDataTypeMap,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { useUpdateTableField } from '@/hooks/use-update-table-field';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
@@ -17,10 +13,6 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { TableFieldToggle } from './table-field-toggle';
 | 
			
		||||
import { useSortable } from '@dnd-kit/sortable';
 | 
			
		||||
import { CSS } from '@dnd-kit/utilities';
 | 
			
		||||
import type {
 | 
			
		||||
    SelectBoxOption,
 | 
			
		||||
    SelectBoxProps,
 | 
			
		||||
} from '@/components/select-box/select-box';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import { TableFieldPopover } from './table-field-modal/table-field-modal';
 | 
			
		||||
import type { DBTable } from '@/lib/domain';
 | 
			
		||||
@@ -32,213 +24,35 @@ export interface TableFieldProps {
 | 
			
		||||
    removeField: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generateFieldRegexPatterns = (
 | 
			
		||||
    dataType: DataTypeData
 | 
			
		||||
): {
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
} => {
 | 
			
		||||
    if (!dataType.fieldAttributes) {
 | 
			
		||||
        return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const typeName = dataType.name;
 | 
			
		||||
    const fieldAttributes = dataType.fieldAttributes;
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.hasCharMaxLength) {
 | 
			
		||||
        if (fieldAttributes.hasCharMaxLengthOption) {
 | 
			
		||||
            return {
 | 
			
		||||
                regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
 | 
			
		||||
                extractRegex: /\((\d+|max)\)/i,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\(\\d+\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision && fieldAttributes.scale) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
 | 
			
		||||
            extractRegex: new RegExp(
 | 
			
		||||
                `${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
 | 
			
		||||
            ),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
    table,
 | 
			
		||||
    field,
 | 
			
		||||
    updateField,
 | 
			
		||||
    removeField,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { databaseType, customTypes, readonly } = useChartDB();
 | 
			
		||||
    const { databaseType, readonly } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    // Only calculate primary key fields, not just count
 | 
			
		||||
    const primaryKeyFields = useMemo(() => {
 | 
			
		||||
        return table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
    }, [table.fields]);
 | 
			
		||||
 | 
			
		||||
    const primaryKeyCount = primaryKeyFields.length;
 | 
			
		||||
 | 
			
		||||
    const { attributes, listeners, setNodeRef, transform, transition } =
 | 
			
		||||
        useSortable({ id: field.id });
 | 
			
		||||
 | 
			
		||||
    const dataFieldOptions = useMemo(() => {
 | 
			
		||||
        const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
 | 
			
		||||
            databaseType
 | 
			
		||||
        ].map((type) => {
 | 
			
		||||
            const regexPatterns = generateFieldRegexPatterns(type);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.id,
 | 
			
		||||
                regex: regexPatterns.regex,
 | 
			
		||||
                extractRegex: regexPatterns.extractRegex,
 | 
			
		||||
                group: customTypes?.length ? 'Standard Types' : undefined,
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!customTypes?.length) {
 | 
			
		||||
            return standardTypes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add custom types as options
 | 
			
		||||
        const customTypeOptions: SelectBoxOption[] = customTypes.map(
 | 
			
		||||
            (type) => ({
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.name,
 | 
			
		||||
                description:
 | 
			
		||||
                    type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
 | 
			
		||||
                group: 'Custom Types',
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return [...standardTypes, ...customTypeOptions];
 | 
			
		||||
    }, [databaseType, customTypes]);
 | 
			
		||||
 | 
			
		||||
    const onChangeDataType = useCallback<
 | 
			
		||||
        NonNullable<SelectBoxProps['onChange']>
 | 
			
		||||
    >(
 | 
			
		||||
        (value, regexMatches) => {
 | 
			
		||||
            const dataType = sortedDataTypeMap[databaseType].find(
 | 
			
		||||
                (v) => v.id === value
 | 
			
		||||
            ) ?? {
 | 
			
		||||
                id: value as string,
 | 
			
		||||
                name: value as string,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let characterMaximumLength: string | undefined = undefined;
 | 
			
		||||
            let precision: number | undefined = undefined;
 | 
			
		||||
            let scale: number | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
            if (regexMatches?.length) {
 | 
			
		||||
                if (dataType?.fieldAttributes?.hasCharMaxLength) {
 | 
			
		||||
                    characterMaximumLength = regexMatches[1]?.toLowerCase();
 | 
			
		||||
                } else if (
 | 
			
		||||
                    dataType?.fieldAttributes?.precision &&
 | 
			
		||||
                    dataType?.fieldAttributes?.scale
 | 
			
		||||
                ) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                    scale = regexMatches[2]
 | 
			
		||||
                        ? parseInt(regexMatches[2])
 | 
			
		||||
                        : undefined;
 | 
			
		||||
                } else if (dataType?.fieldAttributes?.precision) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if (
 | 
			
		||||
                    dataType?.fieldAttributes?.hasCharMaxLength &&
 | 
			
		||||
                    field.characterMaximumLength
 | 
			
		||||
                ) {
 | 
			
		||||
                    characterMaximumLength = field.characterMaximumLength;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.precision && field.precision) {
 | 
			
		||||
                    precision = field.precision;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.scale && field.scale) {
 | 
			
		||||
                    scale = field.scale;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateField({
 | 
			
		||||
                characterMaximumLength,
 | 
			
		||||
                precision,
 | 
			
		||||
                scale,
 | 
			
		||||
                increment: undefined,
 | 
			
		||||
                default: undefined,
 | 
			
		||||
                type: dataTypeDataToDataType(
 | 
			
		||||
                    dataType ?? {
 | 
			
		||||
                        id: value as string,
 | 
			
		||||
                        name: value as string,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            updateField,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            field.characterMaximumLength,
 | 
			
		||||
            field.precision,
 | 
			
		||||
            field.scale,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
    const {
 | 
			
		||||
        dataFieldOptions,
 | 
			
		||||
        handleDataTypeChange,
 | 
			
		||||
        handlePrimaryKeyToggle,
 | 
			
		||||
        handleNullableToggle,
 | 
			
		||||
        handleNameChange,
 | 
			
		||||
        generateFieldSuffix,
 | 
			
		||||
        fieldName,
 | 
			
		||||
        nullable,
 | 
			
		||||
        primaryKey,
 | 
			
		||||
    } = useUpdateTableField(table, field, updateField);
 | 
			
		||||
 | 
			
		||||
    const style = {
 | 
			
		||||
        transform: CSS.Translate.toString(transform),
 | 
			
		||||
        transition,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handlePrimaryKeyToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            if (value) {
 | 
			
		||||
                // When setting as primary key
 | 
			
		||||
                const updates: Partial<DBField> = {
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                };
 | 
			
		||||
                // Only auto-set unique if this will be the only primary key
 | 
			
		||||
                if (primaryKeyCount === 0) {
 | 
			
		||||
                    updates.unique = true;
 | 
			
		||||
                }
 | 
			
		||||
                updateField(updates);
 | 
			
		||||
            } else {
 | 
			
		||||
                // When removing primary key
 | 
			
		||||
                updateField({
 | 
			
		||||
                    primaryKey: false,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [primaryKeyCount, updateField]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleNullableToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            updateField({ nullable: value });
 | 
			
		||||
        },
 | 
			
		||||
        [updateField]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleNameChange = useCallback(
 | 
			
		||||
        (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
            updateField({ name: e.target.value });
 | 
			
		||||
        },
 | 
			
		||||
        [updateField]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
            className="flex flex-1 touch-none flex-row justify-between gap-2 p-1"
 | 
			
		||||
@@ -264,8 +78,10 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                                placeholder={t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_name'
 | 
			
		||||
                                )}
 | 
			
		||||
                                value={field.name}
 | 
			
		||||
                                onChange={handleNameChange}
 | 
			
		||||
                                value={fieldName}
 | 
			
		||||
                                onChange={(e) =>
 | 
			
		||||
                                    handleNameChange(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                readOnly={readonly}
 | 
			
		||||
                            />
 | 
			
		||||
                        </span>
 | 
			
		||||
@@ -285,13 +101,9 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                                value={field.type.id}
 | 
			
		||||
                                valueSuffix={generateDBFieldSuffix(field)}
 | 
			
		||||
                                optionSuffix={(option) =>
 | 
			
		||||
                                    generateDBFieldSuffix(field, {
 | 
			
		||||
                                        databaseType,
 | 
			
		||||
                                        forceExtended: true,
 | 
			
		||||
                                        typeId: option.value,
 | 
			
		||||
                                    })
 | 
			
		||||
                                    generateFieldSuffix(option.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                onChange={onChangeDataType}
 | 
			
		||||
                                onChange={handleDataTypeChange}
 | 
			
		||||
                                emptyPlaceholder={t(
 | 
			
		||||
                                    'side_panel.tables_section.table.no_types_found'
 | 
			
		||||
                                )}
 | 
			
		||||
@@ -312,7 +124,7 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                    <TooltipTrigger asChild>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <TableFieldToggle
 | 
			
		||||
                                pressed={field.nullable}
 | 
			
		||||
                                pressed={nullable}
 | 
			
		||||
                                onPressedChange={handleNullableToggle}
 | 
			
		||||
                                disabled={readonly}
 | 
			
		||||
                            >
 | 
			
		||||
@@ -328,7 +140,7 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                    <TooltipTrigger asChild>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <TableFieldToggle
 | 
			
		||||
                                pressed={field.primaryKey}
 | 
			
		||||
                                pressed={primaryKey}
 | 
			
		||||
                                onPressedChange={handlePrimaryKeyToggle}
 | 
			
		||||
                                disabled={readonly}
 | 
			
		||||
                            >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Check } from 'lucide-react';
 | 
			
		||||
import { Check, Pencil } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
@@ -32,22 +32,39 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
    }, [diagramName]);
 | 
			
		||||
 | 
			
		||||
    const editDiagramName = useCallback(() => {
 | 
			
		||||
        if (!editMode) return;
 | 
			
		||||
        if (editedDiagramName.trim()) {
 | 
			
		||||
            updateDiagramName(editedDiagramName.trim());
 | 
			
		||||
        }
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
    }, [editedDiagramName, updateDiagramName, editMode]);
 | 
			
		||||
    }, [editedDiagramName, updateDiagramName]);
 | 
			
		||||
 | 
			
		||||
    // Handle click outside to save and exit edit mode
 | 
			
		||||
    useClickAway(inputRef, editDiagramName);
 | 
			
		||||
 | 
			
		||||
    useKeyPressEvent('Enter', editDiagramName);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = (
 | 
			
		||||
        event: React.MouseEvent<HTMLHeadingElement, MouseEvent>
 | 
			
		||||
    ) => {
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
        setEditMode(true);
 | 
			
		||||
    };
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (editMode) {
 | 
			
		||||
            // Small delay to ensure the input is rendered
 | 
			
		||||
            const timeoutId = setTimeout(() => {
 | 
			
		||||
                if (inputRef.current) {
 | 
			
		||||
                    inputRef.current.focus();
 | 
			
		||||
                    inputRef.current.select();
 | 
			
		||||
                }
 | 
			
		||||
            }, 50); // Slightly longer delay to ensure DOM is ready
 | 
			
		||||
 | 
			
		||||
            return () => clearTimeout(timeoutId);
 | 
			
		||||
        }
 | 
			
		||||
    }, [editMode]);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
            setEditedDiagramName(diagramName);
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        },
 | 
			
		||||
        [diagramName]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="group">
 | 
			
		||||
@@ -80,11 +97,16 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                                onChange={(e) =>
 | 
			
		||||
                                    setEditedDiagramName(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                className="ml-1 h-7 focus-visible:ring-0"
 | 
			
		||||
                                className="h-7 max-w-[300px] focus-visible:ring-0"
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    width: `${
 | 
			
		||||
                                        editedDiagramName.length * 8 + 30
 | 
			
		||||
                                    }px`,
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
 | 
			
		||||
                                className="ml-1 flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
 | 
			
		||||
                                onClick={editDiagramName}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Check />
 | 
			
		||||
@@ -97,7 +119,7 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                                    <h1
 | 
			
		||||
                                        className={cn(
 | 
			
		||||
                                            labelVariants(),
 | 
			
		||||
                                            'group-hover:underline'
 | 
			
		||||
                                            'group-hover:underline max-w-[300px] truncate'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                        onDoubleClick={(e) => {
 | 
			
		||||
                                            enterEditMode(e);
 | 
			
		||||
@@ -110,6 +132,16 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                                    {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="ml-1 hidden size-5 p-0 hover:bg-background/50 group-hover:flex"
 | 
			
		||||
                                onClick={enterEditMode}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Pencil
 | 
			
		||||
                                    strokeWidth="1.5"
 | 
			
		||||
                                    className="!size-3.5 text-slate-600 dark:text-slate-400"
 | 
			
		||||
                                />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,6 @@ export const Menu: React.FC<MenuProps> = () => {
 | 
			
		||||
        openExportImageDialog,
 | 
			
		||||
        openExportDiagramDialog,
 | 
			
		||||
        openImportDiagramDialog,
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    } = useDialog();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const { setTheme, theme } = useTheme();
 | 
			
		||||
@@ -185,9 +184,6 @@ export const Menu: React.FC<MenuProps> = () => {
 | 
			
		||||
                            <MenubarItem onClick={openImportDiagramDialog}>
 | 
			
		||||
                                .json
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={() => openImportDBMLDialog()}>
 | 
			
		||||
                                .dbml
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user