mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-10-30 19:43:54 +00:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
			v1.14.0
			...
			jf/add_edi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7382626b92 | ||
|  | 6f6b59c74f | ||
|  | 4f1a378762 | ||
|  | 1a6688e85e | ||
|  | 5e81c1848a | ||
|  | 2bd9ca25b2 | ||
|  | b016a70691 | ||
|  | a0fb1ed08b | ||
|  | ffddcdcc98 | ||
|  | fe9ef275b8 | ||
|  | df89f0b6b9 | ||
|  | 534d2858af | ||
|  | 2a64deebb8 | ||
|  | e5e1d59327 | ||
|  | aa290615ca | ||
|  | ec6e46fe81 | ||
|  | ac128d67de | ||
|  | 07937a2f51 | ||
|  | d8e0bc7db8 | ||
|  | 1ce265781b | ||
|  | 60c5675cbf | ||
|  | 66b086378c | ||
|  | abd2a6ccbe | ||
|  | 459c5f1ce3 | ||
|  | 44be48ff3a | ||
|  | ad8e34483f | ||
|  | 215d57979d | ||
|  | ec3719ebce | ||
|  | 0a5874a69b | ||
|  | 7e0fdd1595 | ||
|  | 2531a7023f | ||
|  | 73daf0df21 | ||
|  | c77c983989 | ||
|  | 0aaa451479 | ||
|  | b697e26170 | ||
|  | 04d91c67b1 | ||
|  | d0dee84970 | ||
|  | b4ccfcdcde | ||
|  | 1759b0b9f2 | ||
|  | ab4845c772 | ||
|  | 0545b41140 | ||
|  | 4520f8b1f7 | ||
|  | 712bdf5b95 | ||
|  | d7c9536272 | ||
|  | 815a52f192 | ||
|  | f1a4298362 | ||
|  | b8f2141bd2 | ||
|  | eaebe34768 | ||
|  | 0d623a86b1 | ||
|  | 19fd94c6bd | ||
|  | 0da3caeeac | ||
|  | cb2ba66233 | ||
|  | 8a2267281b | ||
|  | 41ba251377 | ||
|  | e9c5442d9d | ||
|  | 4f1d3295c0 | ||
|  | 5936500ca0 | ||
|  | 43fc1d7fc2 | 
							
								
								
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -7,7 +7,7 @@ on: | ||||
|  | ||||
| permissions: | ||||
|   actions: write | ||||
|   contents: write # this can be 'read' if the signatures are in remote repository | ||||
|   contents: read | ||||
|   pull-requests: write | ||||
|   statuses: write | ||||
|  | ||||
|   | ||||
							
								
								
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,55 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0)) | ||||
| * **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671)) | ||||
| * handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872)) | ||||
|  | ||||
| ## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103)) | ||||
| * **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4)) | ||||
| * include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992)) | ||||
| * **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006)) | ||||
| * support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026)) | ||||
| * **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037)) | ||||
| * **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f)) | ||||
| * **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de)) | ||||
| * DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3)) | ||||
| * filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02)) | ||||
| * **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d)) | ||||
| * **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22)) | ||||
| * for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2)) | ||||
| * improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1)) | ||||
| * initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9)) | ||||
| * **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195)) | ||||
| * merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015)) | ||||
| * move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51)) | ||||
| * open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e)) | ||||
| * preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771)) | ||||
| * prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b)) | ||||
| * remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c)) | ||||
| * reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20)) | ||||
| * **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268)) | ||||
| * set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925)) | ||||
| * show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb)) | ||||
| * **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4)) | ||||
| * **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca)) | ||||
| * **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a)) | ||||
| * update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4)) | ||||
|  | ||||
| ## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								index.html
									
									
									
									
									
								
							| @@ -4,8 +4,9 @@ | ||||
|         <meta charset="UTF-8" /> | ||||
|         <link rel="icon" type="image/svg+xml" href="/favicon.ico" /> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|         <meta name="robots" content="max-image-preview:large" /> | ||||
|         <meta name="robots" content="noindex, max-image-preview:large" /> | ||||
|         <title>ChartDB - Create & Visualize Database Schema Diagrams</title> | ||||
|         <link rel="canonical" href="https://chartdb.io" /> | ||||
|         <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||||
|         <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||||
|         <link | ||||
| @@ -15,14 +16,19 @@ | ||||
|         <script src="/config.js"></script> | ||||
|         <script> | ||||
|             // Load analytics only if not disabled | ||||
|             (function() { | ||||
|                 const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') || | ||||
|                                         (typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true'); | ||||
|             (function () { | ||||
|                 const disableAnalytics = | ||||
|                     (window.env && window.env.DISABLE_ANALYTICS === 'true') || | ||||
|                     (typeof process !== 'undefined' && | ||||
|                         process.env && | ||||
|                         process.env.VITE_DISABLE_ANALYTICS === 'true'); | ||||
|  | ||||
|                 if (!disableAnalytics) { | ||||
|                     const script = document.createElement('script'); | ||||
|                     script.src = 'https://cdn.usefathom.com/script.js'; | ||||
|                     script.setAttribute('data-site', 'PRHIVBNN'); | ||||
|                     script.setAttribute('data-canonical', 'false'); | ||||
|                     script.setAttribute('data-spa', 'auto'); | ||||
|                     script.defer = true; | ||||
|                     document.head.appendChild(script); | ||||
|                 } | ||||
|   | ||||
							
								
								
									
										869
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										869
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|     "name": "chartdb", | ||||
|     "private": true, | ||||
|     "version": "1.14.0", | ||||
|     "version": "1.15.1", | ||||
|     "type": "module", | ||||
|     "scripts": { | ||||
|         "dev": "vite", | ||||
| @@ -17,7 +17,7 @@ | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@ai-sdk/openai": "^0.0.51", | ||||
|         "@dbml/core": "^3.9.5", | ||||
|         "@dbml/core": "^3.13.9", | ||||
|         "@dnd-kit/sortable": "^8.0.0", | ||||
|         "@monaco-editor/react": "^4.6.0", | ||||
|         "@radix-ui/react-accordion": "^1.2.0", | ||||
| @@ -26,22 +26,22 @@ | ||||
|         "@radix-ui/react-checkbox": "^1.1.1", | ||||
|         "@radix-ui/react-collapsible": "^1.1.0", | ||||
|         "@radix-ui/react-context-menu": "^2.2.1", | ||||
|         "@radix-ui/react-dialog": "^1.1.6", | ||||
|         "@radix-ui/react-dialog": "^1.1.14", | ||||
|         "@radix-ui/react-dropdown-menu": "^2.1.1", | ||||
|         "@radix-ui/react-hover-card": "^1.1.1", | ||||
|         "@radix-ui/react-icons": "^1.3.0", | ||||
|         "@radix-ui/react-icons": "^1.3.2", | ||||
|         "@radix-ui/react-label": "^2.1.0", | ||||
|         "@radix-ui/react-menubar": "^1.1.1", | ||||
|         "@radix-ui/react-popover": "^1.1.1", | ||||
|         "@radix-ui/react-scroll-area": "1.2.0", | ||||
|         "@radix-ui/react-select": "^2.1.1", | ||||
|         "@radix-ui/react-separator": "^1.1.2", | ||||
|         "@radix-ui/react-separator": "^1.1.7", | ||||
|         "@radix-ui/react-slot": "^1.2.3", | ||||
|         "@radix-ui/react-tabs": "^1.1.0", | ||||
|         "@radix-ui/react-toast": "^1.2.1", | ||||
|         "@radix-ui/react-toggle": "^1.1.0", | ||||
|         "@radix-ui/react-toggle-group": "^1.1.0", | ||||
|         "@radix-ui/react-tooltip": "^1.1.8", | ||||
|         "@radix-ui/react-tooltip": "^1.2.7", | ||||
|         "@uidotdev/usehooks": "^2.4.1", | ||||
|         "@xyflow/react": "^12.8.2", | ||||
|         "ahooks": "^3.8.1", | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| User-agent: * | ||||
| Allow: / | ||||
| Disallow: / | ||||
|  | ||||
| Sitemap: https://app.chartdb.io/sitemap.xml | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { cva } from 'class-variance-authority'; | ||||
|  | ||||
| export const buttonVariants = cva( | ||||
|     'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', | ||||
|     'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', | ||||
|     { | ||||
|         variants: { | ||||
|             variant: { | ||||
|   | ||||
							
								
								
									
										137
									
								
								src/components/button/button-with-alternatives.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/components/button/button-with-alternatives.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| import React from 'react'; | ||||
| import { ChevronDownIcon } from '@radix-ui/react-icons'; | ||||
| import { Slot } from '@radix-ui/react-slot'; | ||||
| import { type VariantProps } from 'class-variance-authority'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { buttonVariants } from './button-variants'; | ||||
| import { | ||||
|     DropdownMenu, | ||||
|     DropdownMenuContent, | ||||
|     DropdownMenuItem, | ||||
|     DropdownMenuTrigger, | ||||
| } from '@/components/dropdown-menu/dropdown-menu'; | ||||
| import { | ||||
|     Tooltip, | ||||
|     TooltipContent, | ||||
|     TooltipTrigger, | ||||
| } from '@/components/tooltip/tooltip'; | ||||
|  | ||||
| export interface ButtonAlternative { | ||||
|     label: string; | ||||
|     onClick: () => void; | ||||
|     disabled?: boolean; | ||||
|     icon?: React.ReactNode; | ||||
|     className?: string; | ||||
|     tooltip?: string; | ||||
| } | ||||
|  | ||||
| export interface ButtonWithAlternativesProps | ||||
|     extends React.ButtonHTMLAttributes<HTMLButtonElement>, | ||||
|         VariantProps<typeof buttonVariants> { | ||||
|     asChild?: boolean; | ||||
|     alternatives: Array<ButtonAlternative>; | ||||
|     dropdownTriggerClassName?: string; | ||||
|     chevronDownIconClassName?: string; | ||||
| } | ||||
|  | ||||
| const ButtonWithAlternatives = React.forwardRef< | ||||
|     HTMLButtonElement, | ||||
|     ButtonWithAlternativesProps | ||||
| >( | ||||
|     ( | ||||
|         { | ||||
|             className, | ||||
|             variant, | ||||
|             size, | ||||
|             asChild = false, | ||||
|             alternatives, | ||||
|             children, | ||||
|             onClick, | ||||
|             dropdownTriggerClassName, | ||||
|             chevronDownIconClassName, | ||||
|             ...props | ||||
|         }, | ||||
|         ref | ||||
|     ) => { | ||||
|         const Comp = asChild ? Slot : 'button'; | ||||
|         const hasAlternatives = (alternatives?.length ?? 0) > 0; | ||||
|  | ||||
|         return ( | ||||
|             <div className="inline-flex items-stretch"> | ||||
|                 <Comp | ||||
|                     className={cn( | ||||
|                         buttonVariants({ variant, size }), | ||||
|                         { 'rounded-r-none': hasAlternatives }, | ||||
|                         className | ||||
|                     )} | ||||
|                     ref={ref} | ||||
|                     onClick={onClick} | ||||
|                     {...props} | ||||
|                 > | ||||
|                     {children} | ||||
|                 </Comp> | ||||
|                 {hasAlternatives ? ( | ||||
|                     <DropdownMenu> | ||||
|                         <DropdownMenuTrigger asChild> | ||||
|                             <button | ||||
|                                 className={cn( | ||||
|                                     buttonVariants({ variant, size }), | ||||
|                                     'rounded-l-none border-l border-l-primary/5 px-2 min-w-0', | ||||
|                                     className?.includes('h-') && | ||||
|                                         className.match(/h-\d+/)?.[0], | ||||
|                                     className?.includes('text-') && | ||||
|                                         className.match(/text-\w+/)?.[0], | ||||
|                                     dropdownTriggerClassName | ||||
|                                 )} | ||||
|                                 type="button" | ||||
|                             > | ||||
|                                 <ChevronDownIcon | ||||
|                                     className={cn( | ||||
|                                         'size-4 shrink-0', | ||||
|                                         chevronDownIconClassName | ||||
|                                     )} | ||||
|                                 /> | ||||
|                             </button> | ||||
|                         </DropdownMenuTrigger> | ||||
|                         <DropdownMenuContent align="end"> | ||||
|                             {alternatives.map((alternative, index) => { | ||||
|                                 const menuItem = ( | ||||
|                                     <DropdownMenuItem | ||||
|                                         key={index} | ||||
|                                         onClick={alternative.onClick} | ||||
|                                         disabled={alternative.disabled} | ||||
|                                         className={cn(alternative.className)} | ||||
|                                     > | ||||
|                                         <span className="flex w-full items-center justify-between gap-2"> | ||||
|                                             {alternative.label} | ||||
|                                             {alternative.icon} | ||||
|                                         </span> | ||||
|                                     </DropdownMenuItem> | ||||
|                                 ); | ||||
|  | ||||
|                                 if (alternative.tooltip) { | ||||
|                                     return ( | ||||
|                                         <Tooltip key={index}> | ||||
|                                             <TooltipTrigger asChild> | ||||
|                                                 {menuItem} | ||||
|                                             </TooltipTrigger> | ||||
|                                             <TooltipContent side="left"> | ||||
|                                                 {alternative.tooltip} | ||||
|                                             </TooltipContent> | ||||
|                                         </Tooltip> | ||||
|                                     ); | ||||
|                                 } | ||||
|  | ||||
|                                 return menuItem; | ||||
|                             })} | ||||
|                         </DropdownMenuContent> | ||||
|                     </DropdownMenu> | ||||
|                 ) : null} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| ); | ||||
| ButtonWithAlternatives.displayName = 'ButtonWithAlternatives'; | ||||
|  | ||||
| export { ButtonWithAlternatives }; | ||||
| @@ -5,21 +5,33 @@ import { | ||||
|     PopoverTrigger, | ||||
| } from '@/components/popover/popover'; | ||||
| import { colorOptions } from '@/lib/colors'; | ||||
| import { cn } from '@/lib/utils'; | ||||
|  | ||||
| export interface ColorPickerProps { | ||||
|     color: string; | ||||
|     onChange: (color: string) => void; | ||||
|     disabled?: boolean; | ||||
| } | ||||
|  | ||||
| export const ColorPicker = React.forwardRef< | ||||
|     React.ElementRef<typeof PopoverTrigger>, | ||||
|     ColorPickerProps | ||||
| >(({ color, onChange }, ref) => { | ||||
| >(({ color, onChange, disabled }, ref) => { | ||||
|     return ( | ||||
|         <Popover> | ||||
|             <PopoverTrigger asChild ref={ref}> | ||||
|             <PopoverTrigger | ||||
|                 asChild | ||||
|                 ref={ref} | ||||
|                 disabled={disabled} | ||||
|                 {...(disabled ? { onClick: (e) => e.preventDefault() } : {})} | ||||
|             > | ||||
|                 <div | ||||
|                     className="h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md" | ||||
|                     className={cn( | ||||
|                         'h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md', | ||||
|                         { | ||||
|                             'hover:shadow-none cursor-default': disabled, | ||||
|                         } | ||||
|                     )} | ||||
|                     style={{ | ||||
|                         backgroundColor: color, | ||||
|                     }} | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { ScrollArea } from '../scroll-area/scroll-area'; | ||||
| import { ChevronLeft } from 'lucide-react'; | ||||
|  | ||||
| const Dialog = DialogPrimitive.Root; | ||||
|  | ||||
| @@ -32,28 +33,75 @@ const DialogContent = React.forwardRef< | ||||
|     React.ElementRef<typeof DialogPrimitive.Content>, | ||||
|     React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { | ||||
|         showClose?: boolean; | ||||
|         showBack?: boolean; | ||||
|         backButtonClassName?: string; | ||||
|         blurBackground?: boolean; | ||||
|         forceOverlay?: boolean; | ||||
|         onBackClick?: () => void; | ||||
|     } | ||||
| >(({ className, children, showClose, ...props }, ref) => ( | ||||
|     <DialogPortal> | ||||
|         <DialogOverlay /> | ||||
|         <DialogPrimitive.Content | ||||
|             ref={ref} | ||||
|             className={cn( | ||||
|                 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
|         > | ||||
|             {children} | ||||
|             {showClose && ( | ||||
|                 <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||||
|                     <Cross2Icon className="size-4" /> | ||||
|                     <span className="sr-only">Close</span> | ||||
|                 </DialogPrimitive.Close> | ||||
|             )} | ||||
|         </DialogPrimitive.Content> | ||||
|     </DialogPortal> | ||||
| )); | ||||
| >( | ||||
|     ( | ||||
|         { | ||||
|             className, | ||||
|             children, | ||||
|             showClose, | ||||
|             showBack, | ||||
|             onBackClick, | ||||
|             backButtonClassName, | ||||
|             blurBackground, | ||||
|             forceOverlay, | ||||
|             ...props | ||||
|         }, | ||||
|         ref | ||||
|     ) => ( | ||||
|         <DialogPortal> | ||||
|             {forceOverlay ? ( | ||||
|                 <div | ||||
|                     className={cn( | ||||
|                         'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', | ||||
|                         { | ||||
|                             'bg-black/80': !blurBackground, | ||||
|                             'bg-black/30 backdrop-blur-sm': blurBackground, | ||||
|                         } | ||||
|                     )} | ||||
|                     data-state="open" | ||||
|                 /> | ||||
|             ) : null} | ||||
|             <DialogOverlay | ||||
|                 className={cn({ | ||||
|                     'bg-black/30 backdrop-blur-sm': blurBackground, | ||||
|                 })} | ||||
|             /> | ||||
|             <DialogPrimitive.Content | ||||
|                 ref={ref} | ||||
|                 className={cn( | ||||
|                     'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | ||||
|                     className | ||||
|                 )} | ||||
|                 {...props} | ||||
|             > | ||||
|                 {children} | ||||
|                 {showBack && ( | ||||
|                     <button | ||||
|                         onClick={() => onBackClick?.()} | ||||
|                         className={cn( | ||||
|                             'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground', | ||||
|                             backButtonClassName | ||||
|                         )} | ||||
|                     > | ||||
|                         <ChevronLeft className="size-4" /> | ||||
|                     </button> | ||||
|                 )} | ||||
|                 {showClose && ( | ||||
|                     <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||||
|                         <Cross2Icon className="size-4" /> | ||||
|                         <span className="sr-only">Close</span> | ||||
|                     </DialogPrimitive.Close> | ||||
|                 )} | ||||
|             </DialogPrimitive.Content> | ||||
|         </DialogPortal> | ||||
|     ) | ||||
| ); | ||||
| DialogContent.displayName = DialogPrimitive.Content.displayName; | ||||
|  | ||||
| const DialogHeader = ({ | ||||
|   | ||||
| @@ -2,16 +2,13 @@ import React from 'react'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
|  | ||||
| export interface InputProps | ||||
|     extends React.InputHTMLAttributes<HTMLInputElement> {} | ||||
|  | ||||
| const Input = React.forwardRef<HTMLInputElement, InputProps>( | ||||
| const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( | ||||
|     ({ className, type, ...props }, ref) => { | ||||
|         return ( | ||||
|             <input | ||||
|                 type={type} | ||||
|                 className={cn( | ||||
|                     'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', | ||||
|                     'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', | ||||
|                     className | ||||
|                 )} | ||||
|                 ref={ref} | ||||
|   | ||||
| @@ -27,6 +27,7 @@ export interface SelectBoxOption { | ||||
|     regex?: string; | ||||
|     extractRegex?: RegExp; | ||||
|     group?: string; | ||||
|     icon?: React.ReactNode; | ||||
| } | ||||
|  | ||||
| export interface SelectBoxProps { | ||||
| @@ -53,6 +54,8 @@ export interface SelectBoxProps { | ||||
|     open?: boolean; | ||||
|     onOpenChange?: (open: boolean) => void; | ||||
|     popoverClassName?: string; | ||||
|     readonly?: boolean; | ||||
|     footerButtons?: React.ReactNode; | ||||
| } | ||||
|  | ||||
| export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
| @@ -78,6 +81,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|             open, | ||||
|             onOpenChange: setOpen, | ||||
|             popoverClassName, | ||||
|             readonly, | ||||
|             footerButtons, | ||||
|         }, | ||||
|         ref | ||||
|     ) => { | ||||
| @@ -94,6 +99,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                 setOpen?.(isOpen); | ||||
|                 setIsOpen(isOpen); | ||||
|  | ||||
|                 if (isOpen) { | ||||
|                     setSearchTerm(''); | ||||
|                 } | ||||
|  | ||||
|                 setTimeout(() => (document.body.style.pointerEvents = ''), 500); | ||||
|             }, | ||||
|             [setOpen] | ||||
| @@ -148,18 +157,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                             className={`inline-flex min-w-0 shrink-0 items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${oneLine ? 'mx-0.5' : ''}`} | ||||
|                         > | ||||
|                             <span>{option.label}</span> | ||||
|                             <span | ||||
|                                 onClick={(e) => { | ||||
|                                     e.preventDefault(); | ||||
|                                     handleSelect(option.value); | ||||
|                                 }} | ||||
|                                 className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground" | ||||
|                             > | ||||
|                                 <Cross2Icon /> | ||||
|                             </span> | ||||
|                             {!readonly ? ( | ||||
|                                 <span | ||||
|                                     onClick={(e) => { | ||||
|                                         e.preventDefault(); | ||||
|                                         handleSelect(option.value); | ||||
|                                     }} | ||||
|                                     className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground" | ||||
|                                 > | ||||
|                                     <Cross2Icon /> | ||||
|                                 </span> | ||||
|                             ) : null} | ||||
|                         </span> | ||||
|                     )), | ||||
|             [options, value, handleSelect, oneLine, keepOrder] | ||||
|             [options, value, handleSelect, oneLine, keepOrder, readonly] | ||||
|         ); | ||||
|  | ||||
|         const isAllSelected = React.useMemo( | ||||
| @@ -246,6 +257,11 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                             </div> | ||||
|                         )} | ||||
|                         <div className="flex flex-1 items-center truncate"> | ||||
|                             {option.icon ? ( | ||||
|                                 <span className="mr-2 shrink-0"> | ||||
|                                     {option.icon} | ||||
|                                 </span> | ||||
|                             ) : null} | ||||
|                             <span> | ||||
|                                 {isRegexMatch ? searchTerm : option.label} | ||||
|                                 {!isRegexMatch && optionSuffix | ||||
| @@ -280,7 +296,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                 <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}> | ||||
|                     <div | ||||
|                         className={cn( | ||||
|                             `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`, | ||||
|                             `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''} ${readonly ? 'pointer-events-none' : ''}`, | ||||
|                             className | ||||
|                         )} | ||||
|                     > | ||||
| @@ -439,6 +455,9 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                             </div> | ||||
|                         </ScrollArea> | ||||
|                     </Command> | ||||
|                     {footerButtons ? ( | ||||
|                         <div className="border-t">{footerButtons}</div> | ||||
|                     ) : null} | ||||
|                 </PopoverContent> | ||||
|             </Popover> | ||||
|         ); | ||||
|   | ||||
| @@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; | ||||
| const SIDEBAR_WIDTH = '16rem'; | ||||
| const SIDEBAR_WIDTH_MOBILE = '18rem'; | ||||
| const SIDEBAR_WIDTH_ICON = '3rem'; | ||||
| const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem'; | ||||
| const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; | ||||
|  | ||||
| type SidebarContext = { | ||||
| @@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef< | ||||
|                             { | ||||
|                                 '--sidebar-width': SIDEBAR_WIDTH, | ||||
|                                 '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, | ||||
|                                 '--sidebar-width-icon-extended': | ||||
|                                     SIDEBAR_WIDTH_ICON_EXTENDED, | ||||
|                                 ...style, | ||||
|                             } as React.CSSProperties | ||||
|                         } | ||||
| @@ -166,7 +169,7 @@ const Sidebar = React.forwardRef< | ||||
|     React.ComponentProps<'div'> & { | ||||
|         side?: 'left' | 'right'; | ||||
|         variant?: 'sidebar' | 'floating' | 'inset'; | ||||
|         collapsible?: 'offcanvas' | 'icon' | 'none'; | ||||
|         collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none'; | ||||
|     } | ||||
| >( | ||||
|     ( | ||||
| @@ -245,8 +248,8 @@ const Sidebar = React.forwardRef< | ||||
|                         'group-data-[collapsible=offcanvas]:w-0', | ||||
|                         'group-data-[side=right]:rotate-180', | ||||
|                         variant === 'floating' || variant === 'inset' | ||||
|                             ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]' | ||||
|                             ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]' | ||||
|                     )} | ||||
|                 /> | ||||
|                 <div | ||||
| @@ -257,8 +260,8 @@ const Sidebar = React.forwardRef< | ||||
|                             : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', | ||||
|                         // Adjust the padding for floating and inset variants. | ||||
|                         variant === 'floating' || variant === 'inset' | ||||
|                             ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l', | ||||
|                             ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l', | ||||
|                         className | ||||
|                     )} | ||||
|                     {...props} | ||||
| @@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef< | ||||
|             ref={ref} | ||||
|             data-sidebar="content" | ||||
|             className={cn( | ||||
|                 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', | ||||
|                 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef< | ||||
|             className={cn( | ||||
|                 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|                 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', | ||||
|                 'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef< | ||||
|                 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|                 // Increases the hit area of the button on mobile. | ||||
|                 'after:absolute after:-inset-2 after:md:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef< | ||||
| SidebarMenuItem.displayName = 'SidebarMenuItem'; | ||||
|  | ||||
| const sidebarMenuButtonVariants = cva( | ||||
|     'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|     'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|     { | ||||
|         variants: { | ||||
|             variant: { | ||||
| @@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef< | ||||
|                 'peer-data-[size=sm]/menu-button:top-1', | ||||
|                 'peer-data-[size=default]/menu-button:top-1.5', | ||||
|                 'peer-data-[size=lg]/menu-button:top-2.5', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 showOnHover && | ||||
|                     'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', | ||||
|                 className | ||||
| @@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef< | ||||
|                 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', | ||||
|                 size === 'sm' && 'text-xs', | ||||
|                 size === 'md' && 'text-sm', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
|   | ||||
| @@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef< | ||||
|     React.ElementRef<typeof TooltipPrimitive.Content>, | ||||
|     React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> | ||||
| >(({ className, sideOffset = 4, ...props }, ref) => ( | ||||
|     // <TooltipPrimitive.Portal> | ||||
|     <TooltipPrimitive.Content | ||||
|         ref={ref} | ||||
|         sideOffset={sideOffset} | ||||
|         className={cn( | ||||
|             'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | ||||
|             'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]', | ||||
|             className | ||||
|         )} | ||||
|         {...props} | ||||
|     /> | ||||
|     // </TooltipPrimitive.Portal> | ||||
| )); | ||||
| TooltipContent.displayName = TooltipPrimitive.Content.displayName; | ||||
|  | ||||
|   | ||||
| @@ -1,27 +1,55 @@ | ||||
| import React, { type ReactNode, useCallback, useState } from 'react'; | ||||
| import React, { | ||||
|     type ReactNode, | ||||
|     useCallback, | ||||
|     useState, | ||||
|     useEffect, | ||||
|     useRef, | ||||
| } from 'react'; | ||||
| import { canvasContext } from './canvas-context'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { | ||||
|     adjustTablePositions, | ||||
|     shouldShowTablesBySchemaFilter, | ||||
| } from '@/lib/domain/db-table'; | ||||
| import { adjustTablePositions } from '@/lib/domain/db-table'; | ||||
| import { useReactFlow } from '@xyflow/react'; | ||||
| import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils'; | ||||
| import type { Graph } from '@/lib/graph'; | ||||
| import { createGraph } from '@/lib/graph'; | ||||
| import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter'; | ||||
| import { filterTable } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
|  | ||||
| interface CanvasProviderProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
|  | ||||
| export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|     const { tables, relationships, updateTablesState, filteredSchemas } = | ||||
|         useChartDB(); | ||||
|     const { | ||||
|         tables, | ||||
|         relationships, | ||||
|         updateTablesState, | ||||
|         databaseType, | ||||
|         areas, | ||||
|         diagramId, | ||||
|     } = useChartDB(); | ||||
|     const { filter, loading: filterLoading } = useDiagramFilter(); | ||||
|     const { fitView } = useReactFlow(); | ||||
|     const [overlapGraph, setOverlapGraph] = | ||||
|         useState<Graph<string>>(createGraph()); | ||||
|  | ||||
|     const [showFilter, setShowFilter] = useState(false); | ||||
|     const diagramIdActiveFilterRef = useRef<string>(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (filterLoading) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (diagramIdActiveFilterRef.current === diagramId) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         diagramIdActiveFilterRef.current = diagramId; | ||||
|  | ||||
|         setShowFilter(true); | ||||
|     }, [filterLoading, diagramId]); | ||||
|  | ||||
|     const reorderTables = useCallback( | ||||
|         ( | ||||
| @@ -32,9 +60,19 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|             const newTables = adjustTablePositions({ | ||||
|                 relationships, | ||||
|                 tables: tables.filter((table) => | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|                     filterTable({ | ||||
|                         table: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ), | ||||
|                 mode: 'all', // Use 'all' mode for manual reordering | ||||
|                 areas, | ||||
|                 mode: 'all', | ||||
|             }); | ||||
|  | ||||
|             const updatedOverlapGraph = findOverlappingTables({ | ||||
| @@ -69,7 +107,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|                 }); | ||||
|             }, 500); | ||||
|         }, | ||||
|         [filteredSchemas, relationships, tables, updateTablesState, fitView] | ||||
|         [ | ||||
|             filter, | ||||
|             relationships, | ||||
|             tables, | ||||
|             updateTablesState, | ||||
|             fitView, | ||||
|             databaseType, | ||||
|             areas, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|   | ||||
| @@ -81,9 +81,6 @@ export interface ChartDBContext { | ||||
|     highlightedCustomType?: DBCustomType; | ||||
|     highlightCustomTypeId: (id?: string) => void; | ||||
|  | ||||
|     filteredSchemas?: string[]; | ||||
|     filterSchemas: (schemaIds: string[]) => void; | ||||
|  | ||||
|     // General operations | ||||
|     updateDiagramId: (id: string) => Promise<void>; | ||||
|     updateDiagramName: ( | ||||
| @@ -284,11 +281,6 @@ export interface ChartDBContext { | ||||
|         customType: Partial<DBCustomType>, | ||||
|         options?: { updateHistory: boolean } | ||||
|     ) => Promise<void>; | ||||
|  | ||||
|     // Filters | ||||
|     hiddenTableIds?: string[]; | ||||
|     addHiddenTableId: (tableId: string) => Promise<void>; | ||||
|     removeHiddenTableId: (tableId: string) => Promise<void>; | ||||
| } | ||||
|  | ||||
| export const chartDBContext = createContext<ChartDBContext>({ | ||||
| @@ -302,8 +294,6 @@ export const chartDBContext = createContext<ChartDBContext>({ | ||||
|     customTypes: [], | ||||
|     schemas: [], | ||||
|     highlightCustomTypeId: emptyFn, | ||||
|     filteredSchemas: [], | ||||
|     filterSchemas: emptyFn, | ||||
|     currentDiagram: { | ||||
|         id: '', | ||||
|         name: '', | ||||
| @@ -386,9 +376,4 @@ export const chartDBContext = createContext<ChartDBContext>({ | ||||
|     removeCustomType: emptyFn, | ||||
|     removeCustomTypes: emptyFn, | ||||
|     updateCustomType: emptyFn, | ||||
|  | ||||
|     // Filters | ||||
|     hiddenTableIds: [], | ||||
|     addHiddenTableId: emptyFn, | ||||
|     removeHiddenTableId: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import React, { useCallback, useMemo, useState } from 'react'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import { deepCopy, generateId } from '@/lib/utils'; | ||||
| import { randomColor } from '@/lib/colors'; | ||||
| import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors'; | ||||
| import type { ChartDBContext, ChartDBEvent } from './chartdb-context'; | ||||
| import { chartDBContext } from './chartdb-context'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
| import type { DBIndex } from '@/lib/domain/db-index'; | ||||
| import { | ||||
|     getTableIndexesWithPrimaryKey, | ||||
|     type DBIndex, | ||||
| } from '@/lib/domain/db-index'; | ||||
| import type { DBRelationship } from '@/lib/domain/db-relationship'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| @@ -17,7 +20,6 @@ import { | ||||
|     databasesWithSchemas, | ||||
|     schemaNameToSchemaId, | ||||
| } from '@/lib/domain/db-schema'; | ||||
| import { useLocalConfig } from '@/hooks/use-local-config'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
| import { useEventEmitter } from 'ahooks'; | ||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||
| @@ -29,7 +31,6 @@ import { | ||||
|     DBCustomTypeKind, | ||||
|     type DBCustomType, | ||||
| } from '@/lib/domain/db-custom-type'; | ||||
| import { useConfig } from '@/hooks/use-config'; | ||||
|  | ||||
| export interface ChartDBProviderProps { | ||||
|     diagram?: Diagram; | ||||
| @@ -40,17 +41,11 @@ export const ChartDBProvider: React.FC< | ||||
|     React.PropsWithChildren<ChartDBProviderProps> | ||||
| > = ({ children, diagram, readonly: readonlyProp }) => { | ||||
|     const { hasDiff } = useDiff(); | ||||
|     const dbStorage = useStorage(); | ||||
|     let db = dbStorage; | ||||
|     const storageDB = useStorage(); | ||||
|     const events = useEventEmitter<ChartDBEvent>(); | ||||
|     const { setSchemasFilter, schemasFilter } = useLocalConfig(); | ||||
|     const { addUndoAction, resetRedoStack, resetUndoStack } = | ||||
|         useRedoUndoStack(); | ||||
|     const { | ||||
|         getHiddenTablesForDiagram, | ||||
|         hideTableForDiagram, | ||||
|         unhideTableForDiagram, | ||||
|     } = useConfig(); | ||||
|  | ||||
|     const [diagramId, setDiagramId] = useState(''); | ||||
|     const [diagramName, setDiagramName] = useState(''); | ||||
|     const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date()); | ||||
| @@ -72,7 +67,7 @@ export const ChartDBProvider: React.FC< | ||||
|     const [customTypes, setCustomTypes] = useState<DBCustomType[]>( | ||||
|         diagram?.customTypes ?? [] | ||||
|     ); | ||||
|     const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]); | ||||
|  | ||||
|     const { events: diffEvents } = useDiff(); | ||||
|  | ||||
|     const [highlightedCustomTypeId, setHighlightedCustomTypeId] = | ||||
| @@ -96,25 +91,16 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|     diffEvents.useSubscription(diffCalculatedHandler); | ||||
|  | ||||
|     // Sync hiddenTableIds with config | ||||
|     useEffect(() => { | ||||
|         if (diagramId) { | ||||
|             const hiddenTables = getHiddenTablesForDiagram(diagramId); | ||||
|             setHiddenTableIds(hiddenTables); | ||||
|         } | ||||
|     }, [diagramId, getHiddenTablesForDiagram]); | ||||
|  | ||||
|     const defaultSchemaName = defaultSchemas[databaseType]; | ||||
|     const defaultSchemaName = useMemo( | ||||
|         () => defaultSchemas[databaseType], | ||||
|         [databaseType] | ||||
|     ); | ||||
|  | ||||
|     const readonly = useMemo( | ||||
|         () => readonlyProp ?? hasDiff ?? false, | ||||
|         [readonlyProp, hasDiff] | ||||
|     ); | ||||
|  | ||||
|     if (readonly) { | ||||
|         db = storageInitialValue; | ||||
|     } | ||||
|  | ||||
|     const schemas = useMemo( | ||||
|         () => | ||||
|             databasesWithSchemas.includes(databaseType) | ||||
| @@ -125,9 +111,11 @@ export const ChartDBProvider: React.FC< | ||||
|                               .filter((schema) => !!schema) as string[] | ||||
|                       ), | ||||
|                   ] | ||||
|                       .sort((a, b) => | ||||
|                           a === defaultSchemaName ? -1 : a.localeCompare(b) | ||||
|                       ) | ||||
|                       .sort((a, b) => { | ||||
|                           if (a === defaultSchemaName) return -1; | ||||
|                           if (b === defaultSchemaName) return 1; | ||||
|                           return a.localeCompare(b); | ||||
|                       }) | ||||
|                       .map( | ||||
|                           (schema): DBSchema => ({ | ||||
|                               id: schemaNameToSchemaId(schema), | ||||
| @@ -141,34 +129,11 @@ export const ChartDBProvider: React.FC< | ||||
|         [tables, defaultSchemaName, databaseType] | ||||
|     ); | ||||
|  | ||||
|     const filterSchemas: ChartDBContext['filterSchemas'] = useCallback( | ||||
|         (schemaIds) => { | ||||
|             setSchemasFilter((prev) => ({ | ||||
|                 ...prev, | ||||
|                 [diagramId]: schemaIds, | ||||
|             })); | ||||
|         }, | ||||
|         [diagramId, setSchemasFilter] | ||||
|     const db = useMemo( | ||||
|         () => (readonly ? storageInitialValue : storageDB), | ||||
|         [storageDB, readonly] | ||||
|     ); | ||||
|  | ||||
|     const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => { | ||||
|         if (schemas.length === 0) { | ||||
|             return undefined; | ||||
|         } | ||||
|  | ||||
|         const schemasFilterFromCache = | ||||
|             (schemasFilter[diagramId] ?? []).length === 0 | ||||
|                 ? undefined // in case of empty filter, skip cache | ||||
|                 : schemasFilter[diagramId]; | ||||
|  | ||||
|         return ( | ||||
|             schemasFilterFromCache ?? [ | ||||
|                 schemas.find((s) => s.name === defaultSchemaName)?.id ?? | ||||
|                     schemas[0]?.id, | ||||
|             ] | ||||
|         ); | ||||
|     }, [schemasFilter, diagramId, schemas, defaultSchemaName]); | ||||
|  | ||||
|     const currentDiagram: Diagram = useMemo( | ||||
|         () => ({ | ||||
|             id: diagramId, | ||||
| @@ -380,12 +345,17 @@ export const ChartDBProvider: React.FC< | ||||
|                     }, | ||||
|                 ], | ||||
|                 indexes: [], | ||||
|                 color: randomColor(), | ||||
|                 color: attributes?.isView ? viewColor : defaultTableColor, | ||||
|                 createdAt: Date.now(), | ||||
|                 isView: false, | ||||
|                 order: tables.length, | ||||
|                 ...attributes, | ||||
|             }; | ||||
|  | ||||
|             table.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                 table, | ||||
|             }); | ||||
|  | ||||
|             await addTable(table); | ||||
|  | ||||
|             return table; | ||||
| @@ -677,17 +647,30 @@ export const ChartDBProvider: React.FC< | ||||
|             options = { updateHistory: true } | ||||
|         ) => { | ||||
|             const prevField = getField(tableId, fieldId); | ||||
|  | ||||
|             const updateTableFn = (table: DBTable) => { | ||||
|                 const updatedTable: DBTable = { | ||||
|                     ...table, | ||||
|                     fields: table.fields.map((f) => | ||||
|                         f.id === fieldId ? { ...f, ...field } : f | ||||
|                     ), | ||||
|                 } satisfies DBTable; | ||||
|  | ||||
|                 updatedTable.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                     table: updatedTable, | ||||
|                 }); | ||||
|  | ||||
|                 return updatedTable; | ||||
|             }; | ||||
|  | ||||
|             setTables((tables) => | ||||
|                 tables.map((table) => | ||||
|                     table.id === tableId | ||||
|                         ? { | ||||
|                               ...table, | ||||
|                               fields: table.fields.map((f) => | ||||
|                                   f.id === fieldId ? { ...f, ...field } : f | ||||
|                               ), | ||||
|                           } | ||||
|                         : table | ||||
|                 ) | ||||
|                 tables.map((table) => { | ||||
|                     if (table.id === tableId) { | ||||
|                         return updateTableFn(table); | ||||
|                     } | ||||
|  | ||||
|                     return table; | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             const table = await db.getTable({ diagramId, id: tableId }); | ||||
| @@ -702,10 +685,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 db.updateTable({ | ||||
|                     id: tableId, | ||||
|                     attributes: { | ||||
|                         ...table, | ||||
|                         fields: table.fields.map((f) => | ||||
|                             f.id === fieldId ? { ...f, ...field } : f | ||||
|                         ), | ||||
|                         ...updateTableFn(table), | ||||
|                     }, | ||||
|                 }), | ||||
|             ]); | ||||
| @@ -732,19 +712,29 @@ export const ChartDBProvider: React.FC< | ||||
|             fieldId: string, | ||||
|             options = { updateHistory: true } | ||||
|         ) => { | ||||
|             const updateTableFn = (table: DBTable) => { | ||||
|                 const updatedTable: DBTable = { | ||||
|                     ...table, | ||||
|                     fields: table.fields.filter((f) => f.id !== fieldId), | ||||
|                 } satisfies DBTable; | ||||
|  | ||||
|                 updatedTable.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                     table: updatedTable, | ||||
|                 }); | ||||
|  | ||||
|                 return updatedTable; | ||||
|             }; | ||||
|  | ||||
|             const fields = getTable(tableId)?.fields ?? []; | ||||
|             const prevField = getField(tableId, fieldId); | ||||
|             setTables((tables) => | ||||
|                 tables.map((table) => | ||||
|                     table.id === tableId | ||||
|                         ? { | ||||
|                               ...table, | ||||
|                               fields: table.fields.filter( | ||||
|                                   (f) => f.id !== fieldId | ||||
|                               ), | ||||
|                           } | ||||
|                         : table | ||||
|                 ) | ||||
|                 tables.map((table) => { | ||||
|                     if (table.id === tableId) { | ||||
|                         return updateTableFn(table); | ||||
|                     } | ||||
|  | ||||
|                     return table; | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             events.emit({ | ||||
| @@ -768,8 +758,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 db.updateTable({ | ||||
|                     id: tableId, | ||||
|                     attributes: { | ||||
|                         ...table, | ||||
|                         fields: table.fields.filter((f) => f.id !== fieldId), | ||||
|                         ...updateTableFn(table), | ||||
|                     }, | ||||
|                 }), | ||||
|             ]); | ||||
| @@ -1125,12 +1114,15 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|                 const sourceFieldName = sourceField?.name ?? ''; | ||||
|  | ||||
|                 const targetTable = getTable(targetTableId); | ||||
|                 const targetTableSchema = targetTable?.schema; | ||||
|  | ||||
|                 const relationship: DBRelationship = { | ||||
|                     id: generateId(), | ||||
|                     name: `${sourceTableName}_${sourceFieldName}_fk`, | ||||
|                     sourceSchema: sourceTable?.schema, | ||||
|                     sourceTableId, | ||||
|                     targetSchema: sourceTable?.schema, | ||||
|                     targetSchema: targetTableSchema, | ||||
|                     targetTableId, | ||||
|                     sourceFieldId, | ||||
|                     targetFieldId, | ||||
| @@ -1452,7 +1444,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 y: 0, | ||||
|                 width: 300, | ||||
|                 height: 200, | ||||
|                 color: randomColor(), | ||||
|                 color: defaultAreaColor, | ||||
|                 ...attributes, | ||||
|             }; | ||||
|  | ||||
| @@ -1588,17 +1580,17 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|     const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback( | ||||
|         async (diagram, options) => { | ||||
|             const st = options?.forceUpdateStorage ? dbStorage : db; | ||||
|             const st = options?.forceUpdateStorage ? storageDB : db; | ||||
|             await st.deleteDiagram(diagram.id); | ||||
|             await st.addDiagram({ diagram }); | ||||
|             loadDiagramFromData(diagram); | ||||
|         }, | ||||
|         [db, dbStorage, loadDiagramFromData] | ||||
|         [db, storageDB, loadDiagramFromData] | ||||
|     ); | ||||
|  | ||||
|     const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( | ||||
|         async (diagramId: string) => { | ||||
|             const diagram = await db.getDiagram(diagramId, { | ||||
|             const diagram = await storageDB.getDiagram(diagramId, { | ||||
|                 includeRelationships: true, | ||||
|                 includeTables: true, | ||||
|                 includeDependencies: true, | ||||
| @@ -1612,7 +1604,7 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|             return diagram; | ||||
|         }, | ||||
|         [db, loadDiagramFromData] | ||||
|         [storageDB, loadDiagramFromData] | ||||
|     ); | ||||
|  | ||||
|     // Custom type operations | ||||
| @@ -1759,29 +1751,6 @@ export const ChartDBProvider: React.FC< | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback( | ||||
|         async (tableId: string) => { | ||||
|             if (!hiddenTableIds.includes(tableId)) { | ||||
|                 setHiddenTableIds((prev) => [...prev, tableId]); | ||||
|                 await hideTableForDiagram(diagramId, tableId); | ||||
|             } | ||||
|         }, | ||||
|         [hiddenTableIds, diagramId, hideTableForDiagram] | ||||
|     ); | ||||
|  | ||||
|     const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] = | ||||
|         useCallback( | ||||
|             async (tableId: string) => { | ||||
|                 if (hiddenTableIds.includes(tableId)) { | ||||
|                     setHiddenTableIds((prev) => | ||||
|                         prev.filter((id) => id !== tableId) | ||||
|                     ); | ||||
|                     await unhideTableForDiagram(diagramId, tableId); | ||||
|                 } | ||||
|             }, | ||||
|             [hiddenTableIds, diagramId, unhideTableForDiagram] | ||||
|         ); | ||||
|  | ||||
|     return ( | ||||
|         <chartDBContext.Provider | ||||
|             value={{ | ||||
| @@ -1794,10 +1763,8 @@ export const ChartDBProvider: React.FC< | ||||
|                 areas, | ||||
|                 currentDiagram, | ||||
|                 schemas, | ||||
|                 filteredSchemas, | ||||
|                 events, | ||||
|                 readonly, | ||||
|                 filterSchemas, | ||||
|                 updateDiagramData, | ||||
|                 updateDiagramId, | ||||
|                 updateDiagramName, | ||||
| @@ -1855,9 +1822,6 @@ export const ChartDBProvider: React.FC< | ||||
|                 removeCustomType, | ||||
|                 removeCustomTypes, | ||||
|                 updateCustomType, | ||||
|                 hiddenTableIds, | ||||
|                 addHiddenTableId, | ||||
|                 removeHiddenTableId, | ||||
|                 highlightCustomTypeId, | ||||
|                 highlightedCustomType, | ||||
|             }} | ||||
|   | ||||
| @@ -8,23 +8,9 @@ export interface ConfigContext { | ||||
|         config?: Partial<ChartDBConfig>; | ||||
|         updateFn?: (config: ChartDBConfig) => ChartDBConfig; | ||||
|     }) => Promise<void>; | ||||
|     getHiddenTablesForDiagram: (diagramId: string) => string[]; | ||||
|     setHiddenTablesForDiagram: ( | ||||
|         diagramId: string, | ||||
|         hiddenTableIds: string[] | ||||
|     ) => Promise<void>; | ||||
|     hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>; | ||||
|     unhideTableForDiagram: ( | ||||
|         diagramId: string, | ||||
|         tableId: string | ||||
|     ) => Promise<void>; | ||||
| } | ||||
|  | ||||
| export const ConfigContext = createContext<ConfigContext>({ | ||||
|     config: undefined, | ||||
|     updateConfig: emptyFn, | ||||
|     getHiddenTablesForDiagram: () => [], | ||||
|     setHiddenTablesForDiagram: emptyFn, | ||||
|     hideTableForDiagram: emptyFn, | ||||
|     unhideTableForDiagram: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { ConfigContext } from './config-context'; | ||||
|  | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| @@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const { getConfig, updateConfig: updateDataConfig } = useStorage(); | ||||
|     const [config, setConfig] = React.useState<ChartDBConfig | undefined>(); | ||||
|     const [config, setConfig] = useState<ChartDBConfig | undefined>(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const loadConfig = async () => { | ||||
| @@ -44,84 +44,11 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         return promise; | ||||
|     }; | ||||
|  | ||||
|     const getHiddenTablesForDiagram = (diagramId: string): string[] => { | ||||
|         return config?.hiddenTablesByDiagram?.[diagramId] ?? []; | ||||
|     }; | ||||
|  | ||||
|     const setHiddenTablesForDiagram = async ( | ||||
|         diagramId: string, | ||||
|         hiddenTableIds: string[] | ||||
|     ): Promise<void> => { | ||||
|         return updateConfig({ | ||||
|             updateFn: (currentConfig) => ({ | ||||
|                 ...currentConfig, | ||||
|                 hiddenTablesByDiagram: { | ||||
|                     ...currentConfig.hiddenTablesByDiagram, | ||||
|                     [diagramId]: hiddenTableIds, | ||||
|                 }, | ||||
|             }), | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     const hideTableForDiagram = async ( | ||||
|         diagramId: string, | ||||
|         tableId: string | ||||
|     ): Promise<void> => { | ||||
|         return updateConfig({ | ||||
|             updateFn: (currentConfig) => { | ||||
|                 const currentHiddenTables = | ||||
|                     currentConfig.hiddenTablesByDiagram?.[diagramId] ?? []; | ||||
|                 if (currentHiddenTables.includes(tableId)) { | ||||
|                     return currentConfig; // Already hidden, no change needed | ||||
|                 } | ||||
|  | ||||
|                 return { | ||||
|                     ...currentConfig, | ||||
|                     hiddenTablesByDiagram: { | ||||
|                         ...currentConfig.hiddenTablesByDiagram, | ||||
|                         [diagramId]: [...currentHiddenTables, tableId], | ||||
|                     }, | ||||
|                 }; | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     const unhideTableForDiagram = async ( | ||||
|         diagramId: string, | ||||
|         tableId: string | ||||
|     ): Promise<void> => { | ||||
|         return updateConfig({ | ||||
|             updateFn: (currentConfig) => { | ||||
|                 const currentHiddenTables = | ||||
|                     currentConfig.hiddenTablesByDiagram?.[diagramId] ?? []; | ||||
|                 const filteredTables = currentHiddenTables.filter( | ||||
|                     (id) => id !== tableId | ||||
|                 ); | ||||
|  | ||||
|                 if (filteredTables.length === currentHiddenTables.length) { | ||||
|                     return currentConfig; // Not hidden, no change needed | ||||
|                 } | ||||
|  | ||||
|                 return { | ||||
|                     ...currentConfig, | ||||
|                     hiddenTablesByDiagram: { | ||||
|                         ...currentConfig.hiddenTablesByDiagram, | ||||
|                         [diagramId]: filteredTables, | ||||
|                     }, | ||||
|                 }; | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <ConfigContext.Provider | ||||
|             value={{ | ||||
|                 config, | ||||
|                 updateConfig, | ||||
|                 getHiddenTablesForDiagram, | ||||
|                 setHiddenTablesForDiagram, | ||||
|                 hideTableForDiagram, | ||||
|                 unhideTableForDiagram, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| import type { DBSchema } from '@/lib/domain'; | ||||
| import type { | ||||
|     DiagramFilter, | ||||
|     FilterTableInfo, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
| import { createContext } from 'react'; | ||||
|  | ||||
| export interface DiagramFilterContext { | ||||
|     filter?: DiagramFilter; | ||||
|     loading: boolean; | ||||
|  | ||||
|     hasActiveFilter: boolean; | ||||
|     schemasDisplayed: DBSchema[]; | ||||
|  | ||||
|     clearSchemaIdsFilter: () => void; | ||||
|     clearTableIdsFilter: () => void; | ||||
|  | ||||
|     setTableIdsFilterEmpty: () => void; | ||||
|  | ||||
|     // reset | ||||
|     resetFilter: () => void; | ||||
|  | ||||
|     toggleSchemaFilter: (schemaId: string) => void; | ||||
|     toggleTableFilter: (tableId: string) => void; | ||||
|     addSchemaToFilter: (schemaId: string) => void; | ||||
|     addTablesToFilter: (attrs: { | ||||
|         tableIds?: string[]; | ||||
|         filterCallback?: (table: FilterTableInfo) => boolean; | ||||
|     }) => void; | ||||
|     removeTablesFromFilter: (attrs: { | ||||
|         tableIds?: string[]; | ||||
|         filterCallback?: (table: FilterTableInfo) => boolean; | ||||
|     }) => void; | ||||
| } | ||||
|  | ||||
| export const diagramFilterContext = createContext<DiagramFilterContext>({ | ||||
|     hasActiveFilter: false, | ||||
|     clearSchemaIdsFilter: emptyFn, | ||||
|     clearTableIdsFilter: emptyFn, | ||||
|     setTableIdsFilterEmpty: emptyFn, | ||||
|     resetFilter: emptyFn, | ||||
|     toggleSchemaFilter: emptyFn, | ||||
|     toggleTableFilter: emptyFn, | ||||
|     addSchemaToFilter: emptyFn, | ||||
|     schemasDisplayed: [], | ||||
|     addTablesToFilter: emptyFn, | ||||
|     removeTablesFromFilter: emptyFn, | ||||
|     loading: false, | ||||
| }); | ||||
							
								
								
									
										559
									
								
								src/context/diagram-filter-context/diagram-filter-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								src/context/diagram-filter-context/diagram-filter-provider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,559 @@ | ||||
| import React, { | ||||
|     useCallback, | ||||
|     useEffect, | ||||
|     useMemo, | ||||
|     useRef, | ||||
|     useState, | ||||
| } from 'react'; | ||||
| import type { DiagramFilterContext } from './diagram-filter-context'; | ||||
| import { diagramFilterContext } from './diagram-filter-context'; | ||||
| import type { | ||||
|     DiagramFilter, | ||||
|     FilterTableInfo, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { | ||||
|     reduceFilter, | ||||
|     spreadFilterTables, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { filterTable } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
| import type { ChartDBEvent } from '../chartdb-context/chartdb-context'; | ||||
|  | ||||
| export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const { diagramId, tables, schemas, databaseType, events } = useChartDB(); | ||||
|     const { getDiagramFilter, updateDiagramFilter } = useStorage(); | ||||
|     const [filter, setFilter] = useState<DiagramFilter>({}); | ||||
|     const [loading, setLoading] = useState<boolean>(true); | ||||
|  | ||||
|     const allSchemasIds = useMemo(() => { | ||||
|         return schemas.map((schema) => schema.id); | ||||
|     }, [schemas]); | ||||
|  | ||||
|     const allTables: FilterTableInfo[] = useMemo(() => { | ||||
|         return tables.map( | ||||
|             (table) => | ||||
|                 ({ | ||||
|                     id: table.id, | ||||
|                     schemaId: table.schema | ||||
|                         ? schemaNameToSchemaId(table.schema) | ||||
|                         : defaultSchemas[databaseType], | ||||
|                     schema: table.schema ?? defaultSchemas[databaseType], | ||||
|                     areaId: table.parentAreaId ?? undefined, | ||||
|                 }) satisfies FilterTableInfo | ||||
|         ); | ||||
|     }, [tables, databaseType]); | ||||
|  | ||||
|     const diagramIdOfLoadedFilter = useRef<string | null>(null); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (diagramId && diagramId === diagramIdOfLoadedFilter.current) { | ||||
|             updateDiagramFilter(diagramId, filter); | ||||
|         } | ||||
|     }, [diagramId, filter, updateDiagramFilter]); | ||||
|  | ||||
|     // Reset filter when diagram changes | ||||
|     useEffect(() => { | ||||
|         if (diagramIdOfLoadedFilter.current === diagramId) { | ||||
|             // If the diagramId hasn't changed, do not reset the filter | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         setLoading(true); | ||||
|  | ||||
|         const loadFilterFromStorage = async (diagramId: string) => { | ||||
|             if (diagramId) { | ||||
|                 const storedFilter = await getDiagramFilter(diagramId); | ||||
|  | ||||
|                 let filterToSet = storedFilter; | ||||
|  | ||||
|                 if (!filterToSet) { | ||||
|                     // If no filter is stored, set default based on database type | ||||
|                     filterToSet = | ||||
|                         schemas.length > 1 | ||||
|                             ? { schemaIds: [schemas[0].id] } | ||||
|                             : {}; | ||||
|                 } | ||||
|  | ||||
|                 setFilter(filterToSet); | ||||
|             } | ||||
|  | ||||
|             setLoading(false); | ||||
|         }; | ||||
|  | ||||
|         setFilter({}); | ||||
|  | ||||
|         if (diagramId) { | ||||
|             loadFilterFromStorage(diagramId); | ||||
|             diagramIdOfLoadedFilter.current = diagramId; | ||||
|         } | ||||
|     }, [diagramId, getDiagramFilter, schemas]); | ||||
|  | ||||
|     const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         schemaIds: undefined, | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         tableIds: undefined, | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         tableIds: [], | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     // Reset filter | ||||
|     const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => { | ||||
|         setFilter({}); | ||||
|     }, []); | ||||
|  | ||||
|     const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] = | ||||
|         useCallback( | ||||
|             (schemaId: string) => { | ||||
|                 setFilter((prev) => { | ||||
|                     const currentSchemaIds = prev.schemaIds; | ||||
|  | ||||
|                     // Check if schema is currently visible | ||||
|                     const isSchemaVisible = !allTables.some( | ||||
|                         (table) => | ||||
|                             table.schemaId === schemaId && | ||||
|                             filterTable({ | ||||
|                                 table: { | ||||
|                                     id: table.id, | ||||
|                                     schema: table.schema, | ||||
|                                 }, | ||||
|                                 filter: prev, | ||||
|                                 options: { | ||||
|                                     defaultSchema: defaultSchemas[databaseType], | ||||
|                                 }, | ||||
|                             }) === false | ||||
|                     ); | ||||
|  | ||||
|                     let newSchemaIds: string[] | undefined; | ||||
|                     let newTableIds: string[] | undefined = prev.tableIds; | ||||
|  | ||||
|                     if (isSchemaVisible) { | ||||
|                         // Schema is visible, make it not visible | ||||
|                         if (!currentSchemaIds) { | ||||
|                             // All schemas are visible, create filter with all except this one | ||||
|                             newSchemaIds = allSchemasIds.filter( | ||||
|                                 (id) => id !== schemaId | ||||
|                             ); | ||||
|                         } else { | ||||
|                             // Remove this schema from the filter | ||||
|                             newSchemaIds = currentSchemaIds.filter( | ||||
|                                 (id) => id !== schemaId | ||||
|                             ); | ||||
|                         } | ||||
|  | ||||
|                         // Remove tables from this schema from tableIds if present | ||||
|                         if (prev.tableIds) { | ||||
|                             const schemaTableIds = allTables | ||||
|                                 .filter((table) => table.schemaId === schemaId) | ||||
|                                 .map((table) => table.id); | ||||
|                             newTableIds = prev.tableIds.filter( | ||||
|                                 (id) => !schemaTableIds.includes(id) | ||||
|                             ); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Schema is not visible, make it visible | ||||
|                         newSchemaIds = [ | ||||
|                             ...new Set([...(currentSchemaIds || []), schemaId]), | ||||
|                         ]; | ||||
|  | ||||
|                         // Add tables from this schema to tableIds if tableIds is defined | ||||
|                         if (prev.tableIds) { | ||||
|                             const schemaTableIds = allTables | ||||
|                                 .filter((table) => table.schemaId === schemaId) | ||||
|                                 .map((table) => table.id); | ||||
|                             newTableIds = [ | ||||
|                                 ...new Set([ | ||||
|                                     ...prev.tableIds, | ||||
|                                     ...schemaTableIds, | ||||
|                                 ]), | ||||
|                             ]; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // Use reduceFilter to optimize and handle edge cases | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             schemaIds: newSchemaIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allSchemasIds, allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const toggleTableFilterForNoSchema = useCallback( | ||||
|         (tableId: string) => { | ||||
|             setFilter((prev) => { | ||||
|                 const currentTableIds = prev.tableIds; | ||||
|  | ||||
|                 // Check if table is currently visible | ||||
|                 const isTableVisible = filterTable({ | ||||
|                     table: { id: tableId, schema: undefined }, | ||||
|                     filter: prev, | ||||
|                     options: { defaultSchema: undefined }, | ||||
|                 }); | ||||
|  | ||||
|                 let newTableIds: string[] | undefined; | ||||
|  | ||||
|                 if (isTableVisible) { | ||||
|                     // Table is visible, make it not visible | ||||
|                     if (!currentTableIds) { | ||||
|                         // All tables are visible, create filter with all except this one | ||||
|                         newTableIds = allTables | ||||
|                             .filter((t) => t.id !== tableId) | ||||
|                             .map((t) => t.id); | ||||
|                     } else { | ||||
|                         // Remove this table from the filter | ||||
|                         newTableIds = currentTableIds.filter( | ||||
|                             (id) => id !== tableId | ||||
|                         ); | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Table is not visible, make it visible | ||||
|                     newTableIds = [ | ||||
|                         ...new Set([...(currentTableIds || []), tableId]), | ||||
|                     ]; | ||||
|                 } | ||||
|  | ||||
|                 // Use reduceFilter to optimize and handle edge cases | ||||
|                 return reduceFilter( | ||||
|                     { | ||||
|                         schemaIds: undefined, | ||||
|                         tableIds: newTableIds, | ||||
|                     }, | ||||
|                     allTables satisfies FilterTableInfo[], | ||||
|                     { | ||||
|                         databaseWithSchemas: | ||||
|                             databasesWithSchemas.includes(databaseType), | ||||
|                     } | ||||
|                 ); | ||||
|             }); | ||||
|         }, | ||||
|         [allTables, databaseType] | ||||
|     ); | ||||
|  | ||||
|     const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] = | ||||
|         useCallback( | ||||
|             (tableId: string) => { | ||||
|                 if (!databasesWithSchemas.includes(databaseType)) { | ||||
|                     // No schemas, toggle table filter without schema context | ||||
|                     toggleTableFilterForNoSchema(tableId); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 setFilter((prev) => { | ||||
|                     // Find the table in the tables list | ||||
|                     const tableInfo = allTables.find((t) => t.id === tableId); | ||||
|  | ||||
|                     if (!tableInfo) { | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // Check if table is currently visible using filterTable | ||||
|                     const isTableVisible = filterTable({ | ||||
|                         table: { | ||||
|                             id: tableInfo.id, | ||||
|                             schema: tableInfo.schema, | ||||
|                         }, | ||||
|                         filter: prev, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
|                     let newSchemaIds = prev.schemaIds; | ||||
|                     let newTableIds = prev.tableIds; | ||||
|  | ||||
|                     if (isTableVisible) { | ||||
|                         // Table is visible, make it not visible | ||||
|  | ||||
|                         // If the table is visible due to its schema being in schemaIds | ||||
|                         if ( | ||||
|                             tableInfo?.schemaId && | ||||
|                             prev.schemaIds?.includes(tableInfo.schemaId) | ||||
|                         ) { | ||||
|                             // Remove the schema from schemaIds and add all other tables from that schema to tableIds | ||||
|                             newSchemaIds = prev.schemaIds.filter( | ||||
|                                 (id) => id !== tableInfo.schemaId | ||||
|                             ); | ||||
|  | ||||
|                             // Get all other tables from this schema (except the one being toggled) | ||||
|                             const otherTablesFromSchema = allTables | ||||
|                                 .filter( | ||||
|                                     (t) => | ||||
|                                         t.schemaId === tableInfo.schemaId && | ||||
|                                         t.id !== tableId | ||||
|                                 ) | ||||
|                                 .map((t) => t.id); | ||||
|  | ||||
|                             // Add these tables to tableIds | ||||
|                             newTableIds = [ | ||||
|                                 ...(prev.tableIds || []), | ||||
|                                 ...otherTablesFromSchema, | ||||
|                             ]; | ||||
|                         } else if (prev.tableIds?.includes(tableId)) { | ||||
|                             // Table is visible because it's in tableIds, remove it | ||||
|                             newTableIds = prev.tableIds.filter( | ||||
|                                 (id) => id !== tableId | ||||
|                             ); | ||||
|                         } else if (!prev.tableIds && !prev.schemaIds) { | ||||
|                             // No filters = all visible, create filter with all tables except this one | ||||
|                             newTableIds = allTables | ||||
|                                 .filter((t) => t.id !== tableId) | ||||
|                                 .map((t) => t.id); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Table is not visible, make it visible by adding to tableIds | ||||
|                         newTableIds = [...(prev.tableIds || []), tableId]; | ||||
|                     } | ||||
|  | ||||
|                     // Use reduceFilter to optimize and handle edge cases | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             schemaIds: newSchemaIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType, toggleTableFilterForNoSchema] | ||||
|         ); | ||||
|  | ||||
|     const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] = | ||||
|         useCallback( | ||||
|             (schemaId: string) => { | ||||
|                 setFilter((prev) => { | ||||
|                     const currentSchemaIds = prev.schemaIds; | ||||
|                     if (!currentSchemaIds) { | ||||
|                         // No schemas are filtered | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // If schema is already filtered, do nothing | ||||
|                     if (currentSchemaIds.includes(schemaId)) { | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // Add schema to the filter | ||||
|                     const newSchemaIds = [...currentSchemaIds, schemaId]; | ||||
|  | ||||
|                     if (newSchemaIds.length === allSchemasIds.length) { | ||||
|                         // All schemas are now filtered, set to undefined | ||||
|                         return { | ||||
|                             ...prev, | ||||
|                             schemaIds: undefined, | ||||
|                         } satisfies DiagramFilter; | ||||
|                     } | ||||
|                     return { | ||||
|                         ...prev, | ||||
|                         schemaIds: newSchemaIds, | ||||
|                     } satisfies DiagramFilter; | ||||
|                 }); | ||||
|             }, | ||||
|             [allSchemasIds.length] | ||||
|         ); | ||||
|  | ||||
|     const hasActiveFilter: boolean = useMemo(() => { | ||||
|         return !!filter.schemaIds || !!filter.tableIds; | ||||
|     }, [filter]); | ||||
|  | ||||
|     const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] = | ||||
|         useMemo(() => { | ||||
|             if (!hasActiveFilter) { | ||||
|                 return schemas; | ||||
|             } | ||||
|  | ||||
|             const displayedSchemaIds = new Set<string>(); | ||||
|             for (const table of allTables) { | ||||
|                 if ( | ||||
|                     filterTable({ | ||||
|                         table: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ) { | ||||
|                     if (table.schemaId) { | ||||
|                         displayedSchemaIds.add(table.schemaId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return schemas.filter((schema) => | ||||
|                 displayedSchemaIds.has(schema.id) | ||||
|             ); | ||||
|         }, [hasActiveFilter, schemas, allTables, filter, databaseType]); | ||||
|  | ||||
|     const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] = | ||||
|         useCallback( | ||||
|             ({ tableIds, filterCallback }) => { | ||||
|                 setFilter((prev) => { | ||||
|                     let tableIdsToAdd: string[]; | ||||
|  | ||||
|                     if (tableIds) { | ||||
|                         // If tableIds are provided, use them directly | ||||
|                         tableIdsToAdd = tableIds; | ||||
|                     } else if (filterCallback) { | ||||
|                         // If filterCallback is provided, filter tables based on it | ||||
|                         tableIdsToAdd = allTables | ||||
|                             .filter(filterCallback) | ||||
|                             .map((table) => table.id); | ||||
|                     } else { | ||||
|                         // If neither is provided, do nothing | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     const filterByTableIds = spreadFilterTables( | ||||
|                         prev, | ||||
|                         allTables satisfies FilterTableInfo[] | ||||
|                     ); | ||||
|  | ||||
|                     const currentTableIds = filterByTableIds.tableIds || []; | ||||
|                     const newTableIds = [ | ||||
|                         ...new Set([...currentTableIds, ...tableIdsToAdd]), | ||||
|                     ]; | ||||
|  | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             ...filterByTableIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] = | ||||
|         useCallback( | ||||
|             ({ tableIds, filterCallback }) => { | ||||
|                 setFilter((prev) => { | ||||
|                     let tableIdsToRemovoe: string[]; | ||||
|  | ||||
|                     if (tableIds) { | ||||
|                         // If tableIds are provided, use them directly | ||||
|                         tableIdsToRemovoe = tableIds; | ||||
|                     } else if (filterCallback) { | ||||
|                         // If filterCallback is provided, filter tables based on it | ||||
|                         tableIdsToRemovoe = allTables | ||||
|                             .filter(filterCallback) | ||||
|                             .map((table) => table.id); | ||||
|                     } else { | ||||
|                         // If neither is provided, do nothing | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     const filterByTableIds = spreadFilterTables( | ||||
|                         prev, | ||||
|                         allTables satisfies FilterTableInfo[] | ||||
|                     ); | ||||
|  | ||||
|                     const currentTableIds = filterByTableIds.tableIds || []; | ||||
|                     const newTableIds = currentTableIds.filter( | ||||
|                         (id) => !tableIdsToRemovoe.includes(id) | ||||
|                     ); | ||||
|  | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             ...filterByTableIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const eventConsumer = useCallback( | ||||
|         (event: ChartDBEvent) => { | ||||
|             if (!hasActiveFilter) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (event.action === 'add_tables') { | ||||
|                 addTablesToFilter({ | ||||
|                     tableIds: event.data.tables.map((table) => table.id), | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         [hasActiveFilter, addTablesToFilter] | ||||
|     ); | ||||
|  | ||||
|     events.useSubscription(eventConsumer); | ||||
|  | ||||
|     const value: DiagramFilterContext = { | ||||
|         loading, | ||||
|         filter, | ||||
|         clearSchemaIdsFilter: clearSchemaIds, | ||||
|         setTableIdsFilterEmpty: setTableIdsEmpty, | ||||
|         clearTableIdsFilter: clearTableIds, | ||||
|         resetFilter, | ||||
|         toggleSchemaFilter, | ||||
|         toggleTableFilter, | ||||
|         addSchemaToFilter, | ||||
|         hasActiveFilter, | ||||
|         schemasDisplayed, | ||||
|         addTablesToFilter, | ||||
|         removeTablesFromFilter, | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <diagramFilterContext.Provider value={value}> | ||||
|             {children} | ||||
|         </diagramFilterContext.Provider> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										4
									
								
								src/context/diagram-filter-context/use-diagram-filter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/context/diagram-filter-context/use-diagram-filter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { diagramFilterContext } from './diagram-filter-context'; | ||||
|  | ||||
| export const useDiagramFilter = () => useContext(diagramFilterContext); | ||||
| @@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils'; | ||||
| import { createContext } from 'react'; | ||||
|  | ||||
| export type SidebarSection = | ||||
|     | 'dbml' | ||||
|     | 'tables' | ||||
|     | 'relationships' | ||||
|     | 'dependencies' | ||||
|     | 'refs' | ||||
|     | 'areas' | ||||
|     | 'customTypes'; | ||||
|  | ||||
| @@ -13,14 +13,16 @@ export interface LayoutContext { | ||||
|     openTableFromSidebar: (tableId: string) => void; | ||||
|     closeAllTablesInSidebar: () => void; | ||||
|  | ||||
|     openedRelationshipInSidebar: string | undefined; | ||||
|     openRelationshipFromSidebar: (relationshipId: string) => void; | ||||
|     closeAllRelationshipsInSidebar: () => void; | ||||
|  | ||||
|     openedDependencyInSidebar: string | undefined; | ||||
|     openDependencyFromSidebar: (dependencyId: string) => void; | ||||
|     closeAllDependenciesInSidebar: () => void; | ||||
|  | ||||
|     openedRefInSidebar: string | undefined; | ||||
|     openRefFromSidebar: (refId: string) => void; | ||||
|     closeAllRefsInSidebar: () => void; | ||||
|  | ||||
|     openedAreaInSidebar: string | undefined; | ||||
|     openAreaFromSidebar: (areaId: string) => void; | ||||
|     closeAllAreasInSidebar: () => void; | ||||
| @@ -36,24 +38,22 @@ export interface LayoutContext { | ||||
|     hideSidePanel: () => void; | ||||
|     showSidePanel: () => void; | ||||
|     toggleSidePanel: () => void; | ||||
|  | ||||
|     isSelectSchemaOpen: boolean; | ||||
|     openSelectSchema: () => void; | ||||
|     closeSelectSchema: () => void; | ||||
| } | ||||
|  | ||||
| export const layoutContext = createContext<LayoutContext>({ | ||||
|     openedTableInSidebar: undefined, | ||||
|     selectedSidebarSection: 'tables', | ||||
|  | ||||
|     openedRelationshipInSidebar: undefined, | ||||
|     openRelationshipFromSidebar: emptyFn, | ||||
|     closeAllRelationshipsInSidebar: emptyFn, | ||||
|  | ||||
|     openedDependencyInSidebar: undefined, | ||||
|     openDependencyFromSidebar: emptyFn, | ||||
|     closeAllDependenciesInSidebar: emptyFn, | ||||
|  | ||||
|     openedRefInSidebar: undefined, | ||||
|     openRefFromSidebar: emptyFn, | ||||
|     closeAllRefsInSidebar: emptyFn, | ||||
|  | ||||
|     openedAreaInSidebar: undefined, | ||||
|     openAreaFromSidebar: emptyFn, | ||||
|     closeAllAreasInSidebar: emptyFn, | ||||
| @@ -70,8 +70,4 @@ export const layoutContext = createContext<LayoutContext>({ | ||||
|     hideSidePanel: emptyFn, | ||||
|     showSidePanel: emptyFn, | ||||
|     toggleSidePanel: emptyFn, | ||||
|  | ||||
|     isSelectSchemaOpen: false, | ||||
|     openSelectSchema: emptyFn, | ||||
|     closeSelectSchema: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -10,10 +10,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
|     const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] = | ||||
|         React.useState<string | undefined>(); | ||||
|     const [openedDependencyInSidebar, setOpenedDependencyInSidebar] = | ||||
|         React.useState<string | undefined>(); | ||||
|     const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
|     const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
| @@ -23,17 +22,18 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         React.useState<SidebarSection>('tables'); | ||||
|     const [isSidePanelShowed, setIsSidePanelShowed] = | ||||
|         React.useState<boolean>(isDesktop); | ||||
|     const [isSelectSchemaOpen, setIsSelectSchemaOpen] = | ||||
|         React.useState<boolean>(false); | ||||
|  | ||||
|     const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] = | ||||
|         () => setOpenedTableInSidebar(''); | ||||
|  | ||||
|     const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] = | ||||
|         () => setOpenedRelationshipInSidebar(''); | ||||
|         () => setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] = | ||||
|         () => setOpenedDependencyInSidebar(''); | ||||
|         () => setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () => | ||||
|         setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] = | ||||
|         () => setOpenedAreaInSidebar(''); | ||||
| @@ -62,17 +62,23 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] = | ||||
|         (relationshipId) => { | ||||
|             showSidePanel(); | ||||
|             setSelectedSidebarSection('relationships'); | ||||
|             setOpenedRelationshipInSidebar(relationshipId); | ||||
|             setSelectedSidebarSection('refs'); | ||||
|             setOpenedRefInSidebar(relationshipId); | ||||
|         }; | ||||
|  | ||||
|     const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] = | ||||
|         (dependencyId) => { | ||||
|             showSidePanel(); | ||||
|             setSelectedSidebarSection('dependencies'); | ||||
|             setOpenedDependencyInSidebar(dependencyId); | ||||
|             setSelectedSidebarSection('refs'); | ||||
|             setOpenedRefInSidebar(dependencyId); | ||||
|         }; | ||||
|  | ||||
|     const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => { | ||||
|         showSidePanel(); | ||||
|         setSelectedSidebarSection('refs'); | ||||
|         setOpenedRefInSidebar(refId); | ||||
|     }; | ||||
|  | ||||
|     const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = ( | ||||
|         areaId | ||||
|     ) => { | ||||
| @@ -88,11 +94,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             setOpenedTableInSidebar(customTypeId); | ||||
|         }; | ||||
|  | ||||
|     const openSelectSchema: LayoutContext['openSelectSchema'] = () => | ||||
|         setIsSelectSchemaOpen(true); | ||||
|  | ||||
|     const closeSelectSchema: LayoutContext['closeSelectSchema'] = () => | ||||
|         setIsSelectSchemaOpen(false); | ||||
|     return ( | ||||
|         <layoutContext.Provider | ||||
|             value={{ | ||||
| @@ -100,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 selectedSidebarSection, | ||||
|                 openTableFromSidebar, | ||||
|                 selectSidebarSection: setSelectedSidebarSection, | ||||
|                 openedRelationshipInSidebar, | ||||
|                 openRelationshipFromSidebar, | ||||
|                 closeAllTablesInSidebar, | ||||
|                 closeAllRelationshipsInSidebar, | ||||
| @@ -108,12 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 hideSidePanel, | ||||
|                 showSidePanel, | ||||
|                 toggleSidePanel, | ||||
|                 isSelectSchemaOpen, | ||||
|                 openSelectSchema, | ||||
|                 closeSelectSchema, | ||||
|                 openedDependencyInSidebar, | ||||
|                 openDependencyFromSidebar, | ||||
|                 closeAllDependenciesInSidebar, | ||||
|                 openedRefInSidebar, | ||||
|                 openRefFromSidebar, | ||||
|                 closeAllRefsInSidebar, | ||||
|                 openedAreaInSidebar, | ||||
|                 openAreaFromSidebar, | ||||
|                 closeAllAreasInSidebar, | ||||
|   | ||||
| @@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context'; | ||||
|  | ||||
| export type ScrollAction = 'pan' | 'zoom'; | ||||
|  | ||||
| export type SchemasFilter = Record<string, string[]>; | ||||
|  | ||||
| export interface LocalConfigContext { | ||||
|     theme: Theme; | ||||
|     setTheme: (theme: Theme) => void; | ||||
| @@ -13,8 +11,8 @@ export interface LocalConfigContext { | ||||
|     scrollAction: ScrollAction; | ||||
|     setScrollAction: (action: ScrollAction) => void; | ||||
|  | ||||
|     schemasFilter: SchemasFilter; | ||||
|     setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>; | ||||
|     showDBViews: boolean; | ||||
|     setShowDBViews: (showViews: boolean) => void; | ||||
|  | ||||
|     showCardinality: boolean; | ||||
|     setShowCardinality: (showCardinality: boolean) => void; | ||||
| @@ -22,20 +20,12 @@ export interface LocalConfigContext { | ||||
|     showFieldAttributes: boolean; | ||||
|     setShowFieldAttributes: (showFieldAttributes: boolean) => void; | ||||
|  | ||||
|     hideMultiSchemaNotification: boolean; | ||||
|     setHideMultiSchemaNotification: ( | ||||
|         hideMultiSchemaNotification: boolean | ||||
|     ) => void; | ||||
|  | ||||
|     githubRepoOpened: boolean; | ||||
|     setGithubRepoOpened: (githubRepoOpened: boolean) => void; | ||||
|  | ||||
|     starUsDialogLastOpen: number; | ||||
|     setStarUsDialogLastOpen: (lastOpen: number) => void; | ||||
|  | ||||
|     showDependenciesOnCanvas: boolean; | ||||
|     setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void; | ||||
|  | ||||
|     showMiniMapOnCanvas: boolean; | ||||
|     setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void; | ||||
| } | ||||
| @@ -47,8 +37,8 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
|     scrollAction: 'pan', | ||||
|     setScrollAction: emptyFn, | ||||
|  | ||||
|     schemasFilter: {}, | ||||
|     setSchemasFilter: emptyFn, | ||||
|     showDBViews: false, | ||||
|     setShowDBViews: emptyFn, | ||||
|  | ||||
|     showCardinality: true, | ||||
|     setShowCardinality: emptyFn, | ||||
| @@ -56,18 +46,12 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
|     showFieldAttributes: true, | ||||
|     setShowFieldAttributes: emptyFn, | ||||
|  | ||||
|     hideMultiSchemaNotification: false, | ||||
|     setHideMultiSchemaNotification: emptyFn, | ||||
|  | ||||
|     githubRepoOpened: false, | ||||
|     setGithubRepoOpened: emptyFn, | ||||
|  | ||||
|     starUsDialogLastOpen: 0, | ||||
|     setStarUsDialogLastOpen: emptyFn, | ||||
|  | ||||
|     showDependenciesOnCanvas: false, | ||||
|     setShowDependenciesOnCanvas: emptyFn, | ||||
|  | ||||
|     showMiniMapOnCanvas: false, | ||||
|     setShowMiniMapOnCanvas: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -1,18 +1,16 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import type { SchemasFilter, ScrollAction } from './local-config-context'; | ||||
| import type { ScrollAction } from './local-config-context'; | ||||
| import { LocalConfigContext } from './local-config-context'; | ||||
| import type { Theme } from '../theme-context/theme-context'; | ||||
|  | ||||
| const themeKey = 'theme'; | ||||
| const scrollActionKey = 'scroll_action'; | ||||
| const schemasFilterKey = 'schemas_filter'; | ||||
| const showCardinalityKey = 'show_cardinality'; | ||||
| const showFieldAttributesKey = 'show_field_attributes'; | ||||
| const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification'; | ||||
| const githubRepoOpenedKey = 'github_repo_opened'; | ||||
| const starUsDialogLastOpenKey = 'star_us_dialog_last_open'; | ||||
| const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas'; | ||||
| const showMiniMapOnCanvasKey = 'show_minimap_on_canvas'; | ||||
| const showDBViewsKey = 'show_db_views'; | ||||
|  | ||||
| export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -25,10 +23,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         (localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan' | ||||
|     ); | ||||
|  | ||||
|     const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>( | ||||
|         JSON.parse( | ||||
|             localStorage.getItem(schemasFilterKey) || '{}' | ||||
|         ) as SchemasFilter | ||||
|     const [showDBViews, setShowDBViews] = React.useState<boolean>( | ||||
|         (localStorage.getItem(showDBViewsKey) || 'false') === 'true' | ||||
|     ); | ||||
|  | ||||
|     const [showCardinality, setShowCardinality] = React.useState<boolean>( | ||||
| @@ -40,12 +36,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             (localStorage.getItem(showFieldAttributesKey) || 'true') === 'true' | ||||
|         ); | ||||
|  | ||||
|     const [hideMultiSchemaNotification, setHideMultiSchemaNotification] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(hideMultiSchemaNotificationKey) || | ||||
|                 'false') === 'true' | ||||
|         ); | ||||
|  | ||||
|     const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>( | ||||
|         (localStorage.getItem(githubRepoOpenedKey) || 'false') === 'true' | ||||
|     ); | ||||
| @@ -55,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0') | ||||
|         ); | ||||
|  | ||||
|     const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') === | ||||
|                 'true' | ||||
|         ); | ||||
|  | ||||
|     const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true' | ||||
| @@ -77,13 +61,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString()); | ||||
|     }, [githubRepoOpened]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             hideMultiSchemaNotificationKey, | ||||
|             hideMultiSchemaNotification.toString() | ||||
|         ); | ||||
|     }, [hideMultiSchemaNotification]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(themeKey, theme); | ||||
|     }, [theme]); | ||||
| @@ -93,20 +70,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     }, [scrollAction]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter)); | ||||
|     }, [schemasFilter]); | ||||
|         localStorage.setItem(showDBViewsKey, showDBViews.toString()); | ||||
|     }, [showDBViews]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(showCardinalityKey, showCardinality.toString()); | ||||
|     }, [showCardinality]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             showDependenciesOnCanvasKey, | ||||
|             showDependenciesOnCanvas.toString() | ||||
|         ); | ||||
|     }, [showDependenciesOnCanvas]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             showMiniMapOnCanvasKey, | ||||
| @@ -121,20 +91,16 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 setTheme, | ||||
|                 scrollAction, | ||||
|                 setScrollAction, | ||||
|                 schemasFilter, | ||||
|                 setSchemasFilter, | ||||
|                 showDBViews, | ||||
|                 setShowDBViews, | ||||
|                 showCardinality, | ||||
|                 setShowCardinality, | ||||
|                 showFieldAttributes, | ||||
|                 setShowFieldAttributes, | ||||
|                 hideMultiSchemaNotification, | ||||
|                 setHideMultiSchemaNotification, | ||||
|                 setGithubRepoOpened, | ||||
|                 githubRepoOpened, | ||||
|                 starUsDialogLastOpen, | ||||
|                 setStarUsDialogLastOpen, | ||||
|                 showDependenciesOnCanvas, | ||||
|                 setShowDependenciesOnCanvas, | ||||
|                 showMiniMapOnCanvas, | ||||
|                 setShowMiniMapOnCanvas, | ||||
|             }} | ||||
|   | ||||
| @@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config'; | ||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||
| import type { Area } from '@/lib/domain/area'; | ||||
| import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||
| import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
|  | ||||
| export interface StorageContext { | ||||
|     // Config operations | ||||
|     getConfig: () => Promise<ChartDBConfig | undefined>; | ||||
|     updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>; | ||||
|  | ||||
|     // Diagram filter operations | ||||
|     getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>; | ||||
|     updateDiagramFilter: ( | ||||
|         diagramId: string, | ||||
|         filter: DiagramFilter | ||||
|     ) => Promise<void>; | ||||
|     deleteDiagramFilter: (diagramId: string) => Promise<void>; | ||||
|  | ||||
|     // Diagram operations | ||||
|     addDiagram: (params: { diagram: Diagram }) => Promise<void>; | ||||
|     listDiagrams: (options?: { | ||||
| @@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = { | ||||
|     getConfig: emptyFn, | ||||
|     updateConfig: emptyFn, | ||||
|  | ||||
|     getDiagramFilter: emptyFn, | ||||
|     updateDiagramFilter: emptyFn, | ||||
|     deleteDiagramFilter: emptyFn, | ||||
|  | ||||
|     addDiagram: emptyFn, | ||||
|     listDiagrams: emptyFn, | ||||
|     getDiagram: emptyFn, | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import type { ChartDBConfig } from '@/lib/domain/config'; | ||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||
| import type { Area } from '@/lib/domain/area'; | ||||
| import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||
| import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
|  | ||||
| export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -44,6 +45,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 ChartDBConfig & { id: number }, | ||||
|                 'id' // primary key "id" (for the typings only) | ||||
|             >; | ||||
|             diagram_filters: EntityTable< | ||||
|                 DiagramFilter & { diagramId: string }, | ||||
|                 'diagramId' // primary key "id" (for the typings only) | ||||
|             >; | ||||
|         }; | ||||
|  | ||||
|         // Schema declaration: | ||||
| @@ -190,6 +195,27 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             config: '++id, defaultDiagramId', | ||||
|         }); | ||||
|  | ||||
|         dexieDB | ||||
|             .version(12) | ||||
|             .stores({ | ||||
|                 diagrams: | ||||
|                     '++id, name, databaseType, databaseEdition, createdAt, updatedAt', | ||||
|                 db_tables: | ||||
|                     '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order', | ||||
|                 db_relationships: | ||||
|                     '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt', | ||||
|                 db_dependencies: | ||||
|                     '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt', | ||||
|                 areas: '++id, diagramId, name, x, y, width, height, color', | ||||
|                 db_custom_types: | ||||
|                     '++id, diagramId, schema, type, kind, values, fields', | ||||
|                 config: '++id, defaultDiagramId', | ||||
|                 diagram_filters: 'diagramId, tableIds, schemasIds', | ||||
|             }) | ||||
|             .upgrade((tx) => { | ||||
|                 tx.table('config').clear(); | ||||
|             }); | ||||
|  | ||||
|         dexieDB.on('ready', async () => { | ||||
|             const config = await dexieDB.config.get(1); | ||||
|  | ||||
| @@ -217,6 +243,34 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         [db] | ||||
|     ); | ||||
|  | ||||
|     const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback( | ||||
|         async (diagramId: string): Promise<DiagramFilter | undefined> => { | ||||
|             const filter = await db.diagram_filters.get({ diagramId }); | ||||
|  | ||||
|             return filter; | ||||
|         }, | ||||
|         [db] | ||||
|     ); | ||||
|  | ||||
|     const updateDiagramFilter: StorageContext['updateDiagramFilter'] = | ||||
|         useCallback( | ||||
|             async (diagramId, filter): Promise<void> => { | ||||
|                 await db.diagram_filters.put({ | ||||
|                     diagramId, | ||||
|                     ...filter, | ||||
|                 }); | ||||
|             }, | ||||
|             [db] | ||||
|         ); | ||||
|  | ||||
|     const deleteDiagramFilter: StorageContext['deleteDiagramFilter'] = | ||||
|         useCallback( | ||||
|             async (diagramId: string): Promise<void> => { | ||||
|                 await db.diagram_filters.where({ diagramId }).delete(); | ||||
|             }, | ||||
|             [db] | ||||
|         ); | ||||
|  | ||||
|     const addTable: StorageContext['addTable'] = useCallback( | ||||
|         async ({ diagramId, table }) => { | ||||
|             await db.db_tables.add({ | ||||
| @@ -756,6 +810,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 deleteCustomType, | ||||
|                 listCustomTypes, | ||||
|                 deleteDiagramCustomTypes, | ||||
|                 getDiagramFilter, | ||||
|                 updateDiagramFilter, | ||||
|                 deleteDiagramFilter, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { Dialog, DialogContent } from '@/components/dialog/dialog'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { loadFromDatabaseMetadata } from '@/lib/domain/diagram'; | ||||
| import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useConfig } from '@/hooks/use-config'; | ||||
| import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | ||||
|   | ||||
| @@ -69,6 +69,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({ | ||||
|                         type="button" | ||||
|                         variant="outline" | ||||
|                         onClick={createNewDiagram} | ||||
|                         disabled={databaseType === DatabaseType.GENERIC} | ||||
|                     > | ||||
|                         {t('new_diagram_dialog.empty_diagram')} | ||||
|                     </Button> | ||||
|   | ||||
| @@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC< | ||||
|                     closeCreateRelationshipDialog(); | ||||
|                 } | ||||
|             }} | ||||
|             modal={false} | ||||
|         > | ||||
|             <DialogContent className="flex flex-col overflow-y-auto" showClose> | ||||
|             <DialogContent | ||||
|                 className="flex flex-col overflow-y-auto" | ||||
|                 showClose | ||||
|                 forceOverlay | ||||
|                 onInteractOutside={(e) => e.preventDefault()} | ||||
|             > | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle> | ||||
|                         {t('create_relationship_dialog.title')} | ||||
|   | ||||
| @@ -17,15 +17,21 @@ import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { | ||||
|     exportBaseSQL, | ||||
|     exportSQL, | ||||
| } from '@/lib/data/export-metadata/export-sql-script'; | ||||
| } from '@/lib/data/sql-export/export-sql-script'; | ||||
| import { databaseTypeToLabelMap } from '@/lib/databases'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table'; | ||||
| import { Annoyed, Sparkles } from 'lucide-react'; | ||||
| import React, { useCallback, useEffect, useRef } from 'react'; | ||||
| import { Trans, useTranslation } from 'react-i18next'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter'; | ||||
| import { | ||||
|     filterDependency, | ||||
|     filterRelationship, | ||||
|     filterTable, | ||||
| } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
|  | ||||
| export interface ExportSQLDialogProps extends BaseDialogProps { | ||||
|     targetDatabaseType: DatabaseType; | ||||
| @@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|     targetDatabaseType, | ||||
| }) => { | ||||
|     const { closeExportSQLDialog } = useDialog(); | ||||
|     const { currentDiagram, filteredSchemas } = useChartDB(); | ||||
|     const { currentDiagram } = useChartDB(); | ||||
|     const { filter } = useDiagramFilter(); | ||||
|     const { t } = useTranslation(); | ||||
|     const [script, setScript] = React.useState<string>(); | ||||
|     const [error, setError] = React.useState<boolean>(false); | ||||
| @@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|         const filteredDiagram: Diagram = { | ||||
|             ...currentDiagram, | ||||
|             tables: currentDiagram.tables?.filter((table) => | ||||
|                 shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|                 filterTable({ | ||||
|                     table: { | ||||
|                         id: table.id, | ||||
|                         schema: table.schema, | ||||
|                     }, | ||||
|                     filter, | ||||
|                     options: { | ||||
|                         defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                     }, | ||||
|                 }) | ||||
|             ), | ||||
|             relationships: currentDiagram.relationships?.filter((rel) => { | ||||
|                 const sourceTable = currentDiagram.tables?.find( | ||||
| @@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 return ( | ||||
|                     sourceTable && | ||||
|                     targetTable && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         sourceTable, | ||||
|                         filteredSchemas | ||||
|                     ) && | ||||
|                     shouldShowTablesBySchemaFilter(targetTable, filteredSchemas) | ||||
|                     filterRelationship({ | ||||
|                         tableA: { | ||||
|                             id: sourceTable.id, | ||||
|                             schema: sourceTable.schema, | ||||
|                         }, | ||||
|                         tableB: { | ||||
|                             id: targetTable.id, | ||||
|                             schema: targetTable.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ); | ||||
|             }), | ||||
|             dependencies: currentDiagram.dependencies?.filter((dep) => { | ||||
| @@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 return ( | ||||
|                     table && | ||||
|                     dependentTable && | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         dependentTable, | ||||
|                         filteredSchemas | ||||
|                     ) | ||||
|                     filterDependency({ | ||||
|                         tableA: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         tableB: { | ||||
|                             id: dependentTable.id, | ||||
|                             schema: dependentTable.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ); | ||||
|             }), | ||||
|         }; | ||||
| @@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 signal: abortControllerRef.current?.signal, | ||||
|             }); | ||||
|         } | ||||
|     }, [targetDatabaseType, currentDiagram, filteredSchemas]); | ||||
|     }, [targetDatabaseType, currentDiagram, filter]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||
| import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | ||||
| import { loadDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { loadFromDatabaseMetadata } from '@/lib/domain/diagram'; | ||||
| import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| import { Trans, useTranslation } from 'react-i18next'; | ||||
|   | ||||
| @@ -132,7 +132,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|                 const preprocessedContent = preprocessDBML(content); | ||||
|                 const sanitizedContent = sanitizeDBML(preprocessedContent); | ||||
|                 const parser = new Parser(); | ||||
|                 parser.parse(sanitizedContent, 'dbml'); | ||||
|                 parser.parse(sanitizedContent, 'dbmlv2'); | ||||
|             } catch (e) { | ||||
|                 const parsedError = parseDBMLError(e); | ||||
|                 if (parsedError) { | ||||
|   | ||||
| @@ -0,0 +1,98 @@ | ||||
| import React, { useCallback } from 'react'; | ||||
| import { | ||||
|     DropdownMenu, | ||||
|     DropdownMenuContent, | ||||
|     DropdownMenuItem, | ||||
|     DropdownMenuSeparator, | ||||
|     DropdownMenuTrigger, | ||||
| } from '@/components/dropdown-menu/dropdown-menu'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import type { Diagram } from '@/lib/domain'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { cloneDiagram } from '@/lib/clone'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
|  | ||||
| interface DiagramRowActionsMenuProps { | ||||
|     diagram: Diagram; | ||||
|     onOpen: () => void; | ||||
|     refetch: () => void; | ||||
|     numberOfDiagrams: number; | ||||
| } | ||||
|  | ||||
| export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({ | ||||
|     diagram, | ||||
|     onOpen, | ||||
|     refetch, | ||||
|     numberOfDiagrams, | ||||
| }) => { | ||||
|     const { diagramId } = useChartDB(); | ||||
|     const { deleteDiagram, addDiagram } = useStorage(); | ||||
|     const { t } = useTranslation(); | ||||
|  | ||||
|     const onDelete = useCallback(async () => { | ||||
|         deleteDiagram(diagram.id); | ||||
|         refetch(); | ||||
|  | ||||
|         if (diagram.id === diagramId || numberOfDiagrams <= 1) { | ||||
|             window.location.href = '/'; | ||||
|         } | ||||
|     }, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]); | ||||
|  | ||||
|     const onDuplicate = useCallback(async () => { | ||||
|         const duplicatedDiagram = cloneDiagram(diagram); | ||||
|  | ||||
|         const diagramToAdd = duplicatedDiagram.diagram; | ||||
|  | ||||
|         if (!diagramToAdd) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         diagramToAdd.name = `${diagram.name} (Copy)`; | ||||
|  | ||||
|         addDiagram({ diagram: diagramToAdd }); | ||||
|         refetch(); | ||||
|     }, [addDiagram, refetch, diagram]); | ||||
|  | ||||
|     return ( | ||||
|         <DropdownMenu> | ||||
|             <DropdownMenuTrigger asChild> | ||||
|                 <Button | ||||
|                     variant="ghost" | ||||
|                     size="icon" | ||||
|                     className="size-8 p-0" | ||||
|                     onClick={(e) => e.stopPropagation()} | ||||
|                 > | ||||
|                     <Ellipsis className="size-4" /> | ||||
|                 </Button> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent align="end"> | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onOpen} | ||||
|                     className="flex justify-between gap-4" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.open')} | ||||
|                     <SquareArrowOutUpRight className="size-3.5" /> | ||||
|                 </DropdownMenuItem> | ||||
|  | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onDuplicate} | ||||
|                     className="flex justify-between gap-4" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.duplicate')} | ||||
|                     <Layers2 className="size-3.5" /> | ||||
|                 </DropdownMenuItem> | ||||
|  | ||||
|                 <DropdownMenuSeparator /> | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onDelete} | ||||
|                     className="flex justify-between gap-4 text-red-700" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.delete')} | ||||
|                     <Trash2 className="size-3.5 text-red-700" /> | ||||
|                 </DropdownMenuItem> | ||||
|             </DropdownMenuContent> | ||||
|         </DropdownMenu> | ||||
|     ); | ||||
| }; | ||||
| @@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useDebounce } from '@/hooks/use-debounce'; | ||||
| import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu'; | ||||
|  | ||||
| export interface OpenDiagramDialogProps extends BaseDialogProps { | ||||
|     canClose?: boolean; | ||||
| @@ -46,21 +47,22 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|         string | undefined | ||||
|     >(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setSelectedDiagramId(undefined); | ||||
|     }, [dialog.open]); | ||||
|     const fetchDiagrams = useCallback(async () => { | ||||
|         const diagrams = await listDiagrams({ includeTables: true }); | ||||
|         setDiagrams( | ||||
|             diagrams.sort( | ||||
|                 (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() | ||||
|             ) | ||||
|         ); | ||||
|     }, [listDiagrams]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const fetchDiagrams = async () => { | ||||
|             const diagrams = await listDiagrams({ includeTables: true }); | ||||
|             setDiagrams( | ||||
|                 diagrams.sort( | ||||
|                     (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() | ||||
|                 ) | ||||
|             ); | ||||
|         }; | ||||
|         if (!dialog.open) { | ||||
|             return; | ||||
|         } | ||||
|         setSelectedDiagramId(undefined); | ||||
|         fetchDiagrams(); | ||||
|     }, [listDiagrams, setDiagrams, dialog.open]); | ||||
|     }, [dialog.open, fetchDiagrams]); | ||||
|  | ||||
|     const openDiagram = useCallback( | ||||
|         (diagramId: string) => { | ||||
| @@ -166,6 +168,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                             'open_diagram_dialog.table_columns.tables_count' | ||||
|                                         )} | ||||
|                                     </TableHead> | ||||
|                                     <TableHead /> | ||||
|                                 </TableRow> | ||||
|                             </TableHeader> | ||||
|                             <TableBody> | ||||
| @@ -221,6 +224,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                         <TableCell className="text-center"> | ||||
|                                             {diagram.tables?.length} | ||||
|                                         </TableCell> | ||||
|                                         <TableCell className="items-center p-0 pr-1 text-right"> | ||||
|                                             <DiagramRowActionsMenu | ||||
|                                                 diagram={diagram} | ||||
|                                                 onOpen={() => { | ||||
|                                                     openDiagram(diagram.id); | ||||
|                                                     closeOpenDiagramDialog(); | ||||
|                                                 }} | ||||
|                                                 numberOfDiagrams={ | ||||
|                                                     diagrams.length | ||||
|                                                 } | ||||
|                                                 refetch={fetchDiagrams} | ||||
|                                             /> | ||||
|                                         </TableCell> | ||||
|                                     </TableRow> | ||||
|                                 ))} | ||||
|                             </TableBody> | ||||
|   | ||||
| @@ -44,7 +44,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|     allowSchemaCreation = false, | ||||
| }) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const { databaseType, filteredSchemas, filterSchemas } = useChartDB(); | ||||
|     const { databaseType } = useChartDB(); | ||||
|     const [selectedSchemaId, setSelectedSchemaId] = useState<string>( | ||||
|         table?.schema | ||||
|             ? schemaNameToSchemaId(table.schema) | ||||
| @@ -93,7 +93,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|     const { closeTableSchemaDialog } = useDialog(); | ||||
|  | ||||
|     const handleConfirm = useCallback(() => { | ||||
|         let createdSchemaId: string; | ||||
|         if (isCreatingNew && newSchemaName.trim()) { | ||||
|             const newSchema: DBSchema = { | ||||
|                 id: schemaNameToSchemaId(newSchemaName.trim()), | ||||
| @@ -101,30 +100,14 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|                 tableCount: 0, | ||||
|             }; | ||||
|  | ||||
|             createdSchemaId = newSchema.id; | ||||
|  | ||||
|             onConfirm({ schema: newSchema }); | ||||
|         } else { | ||||
|             const schema = schemas.find((s) => s.id === selectedSchemaId); | ||||
|             if (!schema) return; | ||||
|  | ||||
|             createdSchemaId = schema.id; | ||||
|             onConfirm({ schema }); | ||||
|         } | ||||
|  | ||||
|         filterSchemas([ | ||||
|             ...(filteredSchemas ?? schemas.map((s) => s.id)), | ||||
|             createdSchemaId, | ||||
|         ]); | ||||
|     }, [ | ||||
|         onConfirm, | ||||
|         selectedSchemaId, | ||||
|         schemas, | ||||
|         isCreatingNew, | ||||
|         newSchemaName, | ||||
|         filteredSchemas, | ||||
|         filterSchemas, | ||||
|     ]); | ||||
|     }, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]); | ||||
|  | ||||
|     const schemaOptions: SelectBoxOption[] = useMemo( | ||||
|         () => | ||||
|   | ||||
							
								
								
									
										142
									
								
								src/hooks/use-focus-on.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/hooks/use-focus-on.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| import { useCallback } from 'react'; | ||||
| import { useReactFlow } from '@xyflow/react'; | ||||
| import { useLayout } from '@/hooks/use-layout'; | ||||
| import { useBreakpoint } from '@/hooks/use-breakpoint'; | ||||
|  | ||||
| interface FocusOptions { | ||||
|     select?: boolean; | ||||
| } | ||||
|  | ||||
| export const useFocusOn = () => { | ||||
|     const { fitView, setNodes, setEdges } = useReactFlow(); | ||||
|     const { hideSidePanel } = useLayout(); | ||||
|     const { isMd: isDesktop } = useBreakpoint('md'); | ||||
|  | ||||
|     const focusOnArea = useCallback( | ||||
|         (areaId: string, options: FocusOptions = {}) => { | ||||
|             const { select = true } = options; | ||||
|  | ||||
|             if (select) { | ||||
|                 setNodes((nodes) => | ||||
|                     nodes.map((node) => | ||||
|                         node.id === areaId | ||||
|                             ? { | ||||
|                                   ...node, | ||||
|                                   selected: true, | ||||
|                               } | ||||
|                             : { | ||||
|                                   ...node, | ||||
|                                   selected: false, | ||||
|                               } | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             fitView({ | ||||
|                 duration: 500, | ||||
|                 maxZoom: 1, | ||||
|                 minZoom: 1, | ||||
|                 nodes: [ | ||||
|                     { | ||||
|                         id: areaId, | ||||
|                     }, | ||||
|                 ], | ||||
|             }); | ||||
|  | ||||
|             if (!isDesktop) { | ||||
|                 hideSidePanel(); | ||||
|             } | ||||
|         }, | ||||
|         [fitView, setNodes, hideSidePanel, isDesktop] | ||||
|     ); | ||||
|  | ||||
|     const focusOnTable = useCallback( | ||||
|         (tableId: string, options: FocusOptions = {}) => { | ||||
|             const { select = true } = options; | ||||
|  | ||||
|             if (select) { | ||||
|                 setNodes((nodes) => | ||||
|                     nodes.map((node) => | ||||
|                         node.id === tableId | ||||
|                             ? { | ||||
|                                   ...node, | ||||
|                                   selected: true, | ||||
|                               } | ||||
|                             : { | ||||
|                                   ...node, | ||||
|                                   selected: false, | ||||
|                               } | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             fitView({ | ||||
|                 duration: 500, | ||||
|                 maxZoom: 1, | ||||
|                 minZoom: 1, | ||||
|                 nodes: [ | ||||
|                     { | ||||
|                         id: tableId, | ||||
|                     }, | ||||
|                 ], | ||||
|             }); | ||||
|  | ||||
|             if (!isDesktop) { | ||||
|                 hideSidePanel(); | ||||
|             } | ||||
|         }, | ||||
|         [fitView, setNodes, hideSidePanel, isDesktop] | ||||
|     ); | ||||
|  | ||||
|     const focusOnRelationship = useCallback( | ||||
|         ( | ||||
|             relationshipId: string, | ||||
|             sourceTableId: string, | ||||
|             targetTableId: string, | ||||
|             options: FocusOptions = {} | ||||
|         ) => { | ||||
|             const { select = true } = options; | ||||
|  | ||||
|             if (select) { | ||||
|                 setEdges((edges) => | ||||
|                     edges.map((edge) => | ||||
|                         edge.id === relationshipId | ||||
|                             ? { | ||||
|                                   ...edge, | ||||
|                                   selected: true, | ||||
|                               } | ||||
|                             : { | ||||
|                                   ...edge, | ||||
|                                   selected: false, | ||||
|                               } | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             fitView({ | ||||
|                 duration: 500, | ||||
|                 maxZoom: 1, | ||||
|                 minZoom: 1, | ||||
|                 nodes: [ | ||||
|                     { | ||||
|                         id: sourceTableId, | ||||
|                     }, | ||||
|                     { | ||||
|                         id: targetTableId, | ||||
|                     }, | ||||
|                 ], | ||||
|             }); | ||||
|  | ||||
|             if (!isDesktop) { | ||||
|                 hideSidePanel(); | ||||
|             } | ||||
|         }, | ||||
|         [fitView, setEdges, hideSidePanel, isDesktop] | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|         focusOnArea, | ||||
|         focusOnTable, | ||||
|         focusOnRelationship, | ||||
|     }; | ||||
| }; | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ar: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'جديد', | ||||
|             browse: 'تصفح', | ||||
|             tables: 'الجداول', | ||||
|             refs: 'المراجع', | ||||
|             areas: 'المناطق', | ||||
|             dependencies: 'التبعيات', | ||||
|             custom_types: 'الأنواع المخصصة', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ملف', | ||||
|                 new: 'جديد', | ||||
|                 open: 'فتح', | ||||
|             actions: { | ||||
|                 actions: 'الإجراءات', | ||||
|                 new: 'جديد...', | ||||
|                 browse: 'تصفح...', | ||||
|                 save: 'حفظ', | ||||
|                 import: 'استيراد قاعدة بيانات', | ||||
|                 export_sql: 'SQL تصدير', | ||||
|                 export_as: 'تصدير كـ', | ||||
|                 delete_diagram: 'حذف الرسم البياني', | ||||
|                 exit: 'خروج', | ||||
|                 delete_diagram: 'حذف', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'تحرير', | ||||
| @@ -29,6 +37,7 @@ export const ar: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'إخفاء خصائص الحقل', | ||||
|                 show_field_attributes: 'إظهار خصائص الحقل', | ||||
|                 zoom_on_scroll: 'تكبير/تصغير عند التمرير', | ||||
|                 show_views: 'عروض قاعدة البيانات', | ||||
|                 theme: 'المظهر', | ||||
|                 show_dependencies: 'إظهار الاعتمادات', | ||||
|                 hide_dependencies: 'إخفاء الاعتمادات', | ||||
| @@ -65,22 +74,13 @@ export const ar: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'إعادة ترتيب الرسم البياني', | ||||
|             title: 'ترتيب تلقائي للرسم البياني', | ||||
|             description: | ||||
|                 'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟', | ||||
|             reorder: 'إعادة ترتيب', | ||||
|             reorder: 'ترتيب تلقائي', | ||||
|             cancel: 'إلغاء', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'مخططات متعددة', | ||||
|             description: | ||||
|                 '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'لا شيء', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'فشل النسخ', | ||||
| @@ -115,14 +115,11 @@ export const ar: LanguageTranslation = { | ||||
|         copied: '!تم النسخ', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: ':المخطط', | ||||
|             filter_by_schema: 'تصفية حسب المخطط', | ||||
|             search_schema: '...بحث في المخطط', | ||||
|             no_schemas_found: '.لم يتم العثور على مخططات', | ||||
|             view_all_options: '...عرض جميع الخيارات', | ||||
|             tables_section: { | ||||
|                 tables: 'الجداول', | ||||
|                 add_table: 'إضافة جدول', | ||||
|                 add_view: 'إضافة عرض', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 // TODO: Translate | ||||
| @@ -148,6 +145,7 @@ export const ar: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'خصائص الحقل', | ||||
|                         unique: 'فريد', | ||||
|                         auto_increment: 'زيادة تلقائية', | ||||
|                         comments: 'تعليقات', | ||||
|                         no_comments: 'لا يوجد تعليقات', | ||||
|                         delete_field: 'حذف الحقل', | ||||
| @@ -162,6 +160,7 @@ export const ar: LanguageTranslation = { | ||||
|                         title: 'خصائص الفهرس', | ||||
|                         name: 'الإسم', | ||||
|                         unique: 'فريد', | ||||
|                         index_type: 'نوع الفهرس', | ||||
|                         delete_index: 'حذف الفهرس', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -178,12 +177,15 @@ export const ar: LanguageTranslation = { | ||||
|                     description: 'أنشئ جدولاً للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'العلاقات', | ||||
|             refs_section: { | ||||
|                 refs: 'المراجع', | ||||
|                 filter: 'تصفية', | ||||
|                 add_relationship: 'إضافة علاقة', | ||||
|                 collapse: 'طي الكل', | ||||
|                 add_relationship: 'إضافة علاقة', | ||||
|                 relationships: 'العلاقات', | ||||
|                 dependencies: 'الاعتمادات', | ||||
|                 relationship: { | ||||
|                     relationship: 'العلاقة', | ||||
|                     primary: 'الجدول الأساسي', | ||||
|                     foreign: 'الجدول المرتبط', | ||||
|                     cardinality: 'الكاردينالية', | ||||
| @@ -193,16 +195,8 @@ export const ar: LanguageTranslation = { | ||||
|                         delete_relationship: 'حذف', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد علاقات', | ||||
|                     description: 'إنشئ علاقة لربط الجداول', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'الاعتمادات', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 dependency: { | ||||
|                     dependency: 'الاعتماد', | ||||
|                     table: 'الجدول', | ||||
|                     dependent_table: 'عرض الاعتمادات', | ||||
|                     delete_dependency: 'حذف', | ||||
| @@ -212,8 +206,8 @@ export const ar: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد اعتمادات', | ||||
|                     description: 'إنشاء اعتماد للبدء', | ||||
|                     title: 'لا توجد علاقات', | ||||
|                     description: 'إنشاء علاقة للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -254,6 +248,7 @@ export const ar: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'لم يتم تحديد قيم التعداد', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -276,7 +271,7 @@ export const ar: LanguageTranslation = { | ||||
|             show_all: 'عرض الكل', | ||||
|             undo: 'تراجع', | ||||
|             redo: 'إعادة', | ||||
|             reorder_diagram: 'إعادة ترتيب الرسم البياني', | ||||
|             reorder_diagram: 'ترتيب تلقائي للرسم البياني', | ||||
|             highlight_overlapping_tables: 'تمييز الجداول المتداخلة', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
| @@ -319,7 +314,7 @@ export const ar: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'فتح مخطط', | ||||
|             title: 'فتح قاعدة بيانات', | ||||
|             description: 'اختر مخططًا لفتحه من القائمة ادناه', | ||||
|             table_columns: { | ||||
|                 name: 'الإسم', | ||||
| @@ -329,6 +324,12 @@ export const ar: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'إلغاء', | ||||
|             open: 'فتح', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'فتح', | ||||
|                 duplicate: 'تكرار', | ||||
|                 delete: 'حذف', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -474,6 +475,7 @@ export const ar: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'جدول جديد', | ||||
|             new_view: 'عرض جديد', | ||||
|             new_relationship: 'علاقة جديدة', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -495,6 +497,8 @@ export const ar: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'اللغة', | ||||
|         }, | ||||
|         on: 'تشغيل', | ||||
|         off: 'إيقاف', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const bn: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'নতুন', | ||||
|             browse: 'ব্রাউজ', | ||||
|             tables: 'টেবিল', | ||||
|             refs: 'রেফস', | ||||
|             areas: 'এলাকা', | ||||
|             dependencies: 'নির্ভরতা', | ||||
|             custom_types: 'কাস্টম টাইপ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ফাইল', | ||||
|                 new: 'নতুন', | ||||
|                 open: 'খুলুন', | ||||
|             actions: { | ||||
|                 actions: 'কার্য', | ||||
|                 new: 'নতুন...', | ||||
|                 browse: 'ব্রাউজ করুন...', | ||||
|                 save: 'সংরক্ষণ করুন', | ||||
|                 import: 'ডাটাবেস আমদানি করুন', | ||||
|                 export_sql: 'SQL রপ্তানি করুন', | ||||
|                 export_as: 'রূপে রপ্তানি করুন', | ||||
|                 delete_diagram: 'ডায়াগ্রাম মুছুন', | ||||
|                 exit: 'প্রস্থান করুন', | ||||
|                 delete_diagram: 'মুছুন', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'সম্পাদনা', | ||||
| @@ -29,6 +37,7 @@ export const bn: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান', | ||||
|                 show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান', | ||||
|                 zoom_on_scroll: 'স্ক্রলে জুম করুন', | ||||
|                 show_views: 'ডাটাবেস ভিউ', | ||||
|                 theme: 'থিম', | ||||
|                 show_dependencies: 'নির্ভরতাগুলি দেখান', | ||||
|                 hide_dependencies: 'নির্ভরতাগুলি লুকান', | ||||
| @@ -66,22 +75,13 @@ export const bn: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', | ||||
|             title: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান', | ||||
|             description: | ||||
|                 'এই কাজটি ডায়াগ্রামের সমস্ত টেবিল পুনর্বিন্যাস করবে। আপনি কি চালিয়ে যেতে চান?', | ||||
|             reorder: 'পুনর্বিন্যাস করুন', | ||||
|             reorder: 'স্বয়ংক্রিয় সাজান', | ||||
|             cancel: 'বাতিল করুন', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'বহু স্কিমা', | ||||
|             description: | ||||
|                 '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'কিছুই না', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'কপি ব্যর্থ হয়েছে', | ||||
| @@ -116,14 +116,11 @@ export const bn: LanguageTranslation = { | ||||
|         copied: 'অনুলিপি সম্পন্ন!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'স্কিমা:', | ||||
|             filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন', | ||||
|             search_schema: 'স্কিমা খুঁজুন...', | ||||
|             no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।', | ||||
|             view_all_options: 'সমস্ত বিকল্প দেখুন...', | ||||
|             tables_section: { | ||||
|                 tables: 'টেবিল', | ||||
|                 add_table: 'টেবিল যোগ করুন', | ||||
|                 add_view: 'ভিউ যোগ করুন', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 // TODO: Translate | ||||
| @@ -149,6 +146,7 @@ export const bn: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ফিল্ড কর্ম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি', | ||||
|                         comments: 'মন্তব্য', | ||||
|                         no_comments: 'কোনো মন্তব্য নেই', | ||||
|                         delete_field: 'ফিল্ড মুছুন', | ||||
| @@ -164,6 +162,7 @@ export const bn: LanguageTranslation = { | ||||
|                         title: 'ইনডেক্স কর্ম', | ||||
|                         name: 'নাম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         index_type: 'ইনডেক্স ধরন', | ||||
|                         delete_index: 'ইনডেক্স মুছুন', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -180,14 +179,17 @@ export const bn: LanguageTranslation = { | ||||
|                     description: 'শুরু করতে একটি টেবিল তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'সম্পর্ক', | ||||
|             refs_section: { | ||||
|                 refs: 'রেফস', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 add_relationship: 'সম্পর্ক যোগ করুন', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 add_relationship: 'সম্পর্ক যোগ করুন', | ||||
|                 relationships: 'সম্পর্ক', | ||||
|                 dependencies: 'নির্ভরতাগুলি', | ||||
|                 relationship: { | ||||
|                     relationship: 'সম্পর্ক', | ||||
|                     primary: 'প্রাথমিক টেবিল', | ||||
|                     foreign: 'বিদেশি টেবিল', | ||||
|                     foreign: 'রেফারেন্স করা টেবিল', | ||||
|                     cardinality: 'কার্ডিনালিটি', | ||||
|                     delete_relationship: 'মুছুন', | ||||
|                     relationship_actions: { | ||||
| @@ -195,27 +197,19 @@ export const bn: LanguageTranslation = { | ||||
|                         delete_relationship: 'মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো সম্পর্ক নেই', | ||||
|                     description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'নির্ভরতাগুলি', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'ভাঁজ করুন', | ||||
|                 dependency: { | ||||
|                     dependency: 'নির্ভরতা', | ||||
|                     table: 'টেবিল', | ||||
|                     dependent_table: 'নির্ভরশীল টেবিল', | ||||
|                     delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                     dependent_table: 'নির্ভরশীল ভিউ', | ||||
|                     delete_dependency: 'মুছুন', | ||||
|                     dependency_actions: { | ||||
|                         title: 'কর্ম', | ||||
|                         delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                         delete_dependency: 'মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো নির্ভরতাগুলি নেই', | ||||
|                     description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', | ||||
|                     title: 'কোনো সম্পর্ক নেই', | ||||
|                     description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -255,6 +249,7 @@ export const bn: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'কোন enum মান সংজ্ঞায়িত নেই', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -277,7 +272,7 @@ export const bn: LanguageTranslation = { | ||||
|             show_all: 'সব দেখান', | ||||
|             undo: 'পূর্বাবস্থায় ফিরুন', | ||||
|             redo: 'পুনরায় করুন', | ||||
|             reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', | ||||
|             reorder_diagram: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান', | ||||
|             highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন', | ||||
|  | ||||
|             // TODO: Translate | ||||
| @@ -321,7 +316,7 @@ export const bn: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'চিত্র খুলুন', | ||||
|             title: 'ডেটাবেস খুলুন', | ||||
|             description: 'নিচের তালিকা থেকে একটি চিত্র নির্বাচন করুন।', | ||||
|             table_columns: { | ||||
|                 name: 'নাম', | ||||
| @@ -331,6 +326,12 @@ export const bn: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'বাতিল করুন', | ||||
|             open: 'খুলুন', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'খুলুন', | ||||
|                 duplicate: 'ডুপ্লিকেট', | ||||
|                 delete: 'মুছুন', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -479,6 +480,7 @@ export const bn: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'নতুন টেবিল', | ||||
|             new_view: 'নতুন ভিউ', | ||||
|             new_relationship: 'নতুন সম্পর্ক', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -500,6 +502,9 @@ export const bn: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'ভাষা পরিবর্তন করুন', | ||||
|         }, | ||||
|  | ||||
|         on: 'চালু', | ||||
|         off: 'বন্ধ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const de: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Neu', | ||||
|             browse: 'Durchsuchen', | ||||
|             tables: 'Tabellen', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Bereiche', | ||||
|             dependencies: 'Abhängigkeiten', | ||||
|             custom_types: 'Benutzerdefinierte Typen', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Datei', | ||||
|                 new: 'Neu', | ||||
|                 open: 'Öffnen', | ||||
|             actions: { | ||||
|                 actions: 'Aktionen', | ||||
|                 new: 'Neu...', | ||||
|                 browse: 'Durchsuchen...', | ||||
|                 save: 'Speichern', | ||||
|                 import: 'Datenbank importieren', | ||||
|                 export_sql: 'SQL exportieren', | ||||
|                 export_as: 'Exportieren als', | ||||
|                 delete_diagram: 'Diagramm löschen', | ||||
|                 exit: 'Beenden', | ||||
|                 delete_diagram: 'Löschen', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Bearbeiten', | ||||
| @@ -29,6 +37,7 @@ export const de: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'Feldattribute ausblenden', | ||||
|                 show_field_attributes: 'Feldattribute anzeigen', | ||||
|                 zoom_on_scroll: 'Zoom beim Scrollen', | ||||
|                 show_views: 'Datenbankansichten', | ||||
|                 theme: 'Stil', | ||||
|                 show_dependencies: 'Abhängigkeiten anzeigen', | ||||
|                 hide_dependencies: 'Abhängigkeiten ausblenden', | ||||
| @@ -66,22 +75,13 @@ export const de: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Diagramm neu anordnen', | ||||
|             title: 'Diagramm automatisch anordnen', | ||||
|             description: | ||||
|                 'Diese Aktion wird alle Tabellen im Diagramm neu anordnen. Möchten Sie fortfahren?', | ||||
|             reorder: 'Neu anordnen', | ||||
|             reorder: 'Automatisch anordnen', | ||||
|             cancel: 'Abbrechen', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Mehrere Schemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'Keine', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopieren fehlgeschlagen', | ||||
| @@ -117,14 +117,11 @@ export const de: LanguageTranslation = { | ||||
|         copied: 'Kopiert!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: 'Nach Schema filtern', | ||||
|             search_schema: 'Schema suchen...', | ||||
|             no_schemas_found: 'Keine Schemas gefunden.', | ||||
|             view_all_options: 'Alle Optionen anzeigen...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabellen', | ||||
|                 add_table: 'Tabelle hinzufügen', | ||||
|                 add_view: 'Ansicht hinzufügen', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 // TODO: Translate | ||||
| @@ -150,6 +147,7 @@ export const de: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Feldattribute', | ||||
|                         unique: 'Eindeutig', | ||||
|                         auto_increment: 'Automatisch hochzählen', | ||||
|                         comments: 'Kommentare', | ||||
|                         no_comments: 'Keine Kommentare', | ||||
|                         delete_field: 'Feld löschen', | ||||
| @@ -165,6 +163,7 @@ export const de: LanguageTranslation = { | ||||
|                         title: 'Indexattribute', | ||||
|                         name: 'Name', | ||||
|                         unique: 'Eindeutig', | ||||
|                         index_type: 'Indextyp', | ||||
|                         delete_index: 'Index löschen', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -181,32 +180,26 @@ export const de: LanguageTranslation = { | ||||
|                     description: 'Erstellen Sie eine Tabelle, um zu beginnen', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Beziehungen', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filter', | ||||
|                 add_relationship: 'Beziehung hinzufügen', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 add_relationship: 'Beziehung hinzufügen', | ||||
|                 relationships: 'Beziehungen', | ||||
|                 dependencies: 'Abhängigkeiten', | ||||
|                 relationship: { | ||||
|                     relationship: 'Beziehung', | ||||
|                     primary: 'Primäre Tabelle', | ||||
|                     foreign: 'Referenzierte Tabelle', | ||||
|                     cardinality: 'Kardinalität', | ||||
|                     delete_relationship: 'Beziehung löschen', | ||||
|                     delete_relationship: 'Löschen', | ||||
|                     relationship_actions: { | ||||
|                         title: 'Aktionen', | ||||
|                         delete_relationship: 'Beziehung löschen', | ||||
|                         delete_relationship: 'Löschen', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Keine Beziehungen', | ||||
|                     description: | ||||
|                         'Erstellen Sie eine Beziehung, um Tabellen zu verbinden', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Abhängigkeiten', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 dependency: { | ||||
|                     dependency: 'Abhängigkeit', | ||||
|                     table: 'Tabelle', | ||||
|                     dependent_table: 'Abhängige Ansicht', | ||||
|                     delete_dependency: 'Löschen', | ||||
| @@ -216,8 +209,8 @@ export const de: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Keine Abhängigkeiten', | ||||
|                     description: 'Erstellen Sie eine Ansicht, um zu beginnen', | ||||
|                     title: 'Keine Beziehungen', | ||||
|                     description: 'Erstellen Sie eine Beziehung, um zu beginnen', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -257,6 +250,7 @@ export const de: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Keine Enum-Werte definiert', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -279,7 +273,7 @@ export const de: LanguageTranslation = { | ||||
|             show_all: 'Alle anzeigen', | ||||
|             undo: 'Rückgängig', | ||||
|             redo: 'Wiederholen', | ||||
|             reorder_diagram: 'Diagramm neu anordnen', | ||||
|             reorder_diagram: 'Diagramm automatisch anordnen', | ||||
|  | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
| @@ -325,7 +319,7 @@ export const de: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Diagramm öffnen', | ||||
|             title: 'Datenbank öffnen', | ||||
|             description: 'Wählen Sie ein Diagramm aus der Liste unten aus.', | ||||
|             table_columns: { | ||||
|                 name: 'Name', | ||||
| @@ -335,6 +329,12 @@ export const de: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Abbrechen', | ||||
|             open: 'Öffnen', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Öffnen', | ||||
|                 duplicate: 'Duplizieren', | ||||
|                 delete: 'Löschen', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -483,6 +483,7 @@ export const de: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Neue Tabelle', | ||||
|             new_view: 'Neue Ansicht', | ||||
|             new_relationship: 'Neue Beziehung', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -505,6 +506,9 @@ export const de: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Sprache', | ||||
|         }, | ||||
|  | ||||
|         on: 'Ein', | ||||
|         off: 'Aus', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata } from '../types'; | ||||
|  | ||||
| export const en = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'New', | ||||
|             browse: 'Browse', | ||||
|             tables: 'Tables', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Areas', | ||||
|             dependencies: 'Dependencies', | ||||
|             custom_types: 'Custom Types', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'File', | ||||
|                 new: 'New', | ||||
|                 open: 'Open', | ||||
|             actions: { | ||||
|                 actions: 'Actions', | ||||
|                 new: 'New...', | ||||
|                 browse: 'Browse...', | ||||
|                 save: 'Save', | ||||
|                 import: 'Import', | ||||
|                 export_sql: 'Export SQL', | ||||
|                 export_as: 'Export as', | ||||
|                 delete_diagram: 'Delete Diagram', | ||||
|                 exit: 'Exit', | ||||
|                 delete_diagram: 'Delete', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Edit', | ||||
| @@ -29,6 +37,7 @@ export const en = { | ||||
|                 hide_field_attributes: 'Hide Field Attributes', | ||||
|                 show_field_attributes: 'Show Field Attributes', | ||||
|                 zoom_on_scroll: 'Zoom on Scroll', | ||||
|                 show_views: 'Database Views', | ||||
|                 theme: 'Theme', | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
|                 hide_dependencies: 'Hide Dependencies', | ||||
| @@ -64,21 +73,13 @@ export const en = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Reorder Diagram', | ||||
|             title: 'Auto Arrange Diagram', | ||||
|             description: | ||||
|                 'This action will rearrange all tables in the diagram. Do you want to continue?', | ||||
|             reorder: 'Reorder', | ||||
|             reorder: 'Auto Arrange', | ||||
|             cancel: 'Cancel', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Multiple Schemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.', | ||||
|             show_me: 'Show me', | ||||
|             none: 'none', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Copy failed', | ||||
| @@ -113,14 +114,11 @@ export const en = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: 'Filter by schema', | ||||
|             search_schema: 'Search schema...', | ||||
|             no_schemas_found: 'No schemas found.', | ||||
|             view_all_options: 'View all Options...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tables', | ||||
|                 add_table: 'Add Table', | ||||
|                 add_view: 'Add View', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 clear: 'Clear Filter', | ||||
| @@ -144,6 +142,7 @@ export const en = { | ||||
|                     field_actions: { | ||||
|                         title: 'Field Attributes', | ||||
|                         unique: 'Unique', | ||||
|                         auto_increment: 'Auto Increment', | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Precision', | ||||
|                         scale: 'Scale', | ||||
| @@ -157,6 +156,7 @@ export const en = { | ||||
|                         title: 'Index Attributes', | ||||
|                         name: 'Name', | ||||
|                         unique: 'Unique', | ||||
|                         index_type: 'Index Type', | ||||
|                         delete_index: 'Delete Index', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,12 +173,15 @@ export const en = { | ||||
|                     description: 'Create a table to get started', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relationships', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filter', | ||||
|                 add_relationship: 'Add Relationship', | ||||
|                 collapse: 'Collapse All', | ||||
|                 add_relationship: 'Add Relationship', | ||||
|                 relationships: 'Relationships', | ||||
|                 dependencies: 'Dependencies', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relationship', | ||||
|                     primary: 'Primary Table', | ||||
|                     foreign: 'Referenced Table', | ||||
|                     cardinality: 'Cardinality', | ||||
| @@ -188,16 +191,8 @@ export const en = { | ||||
|                         delete_relationship: 'Delete', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No relationships', | ||||
|                     description: 'Create a relationship to connect tables', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencies', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependency', | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Dependent View', | ||||
|                     delete_dependency: 'Delete', | ||||
| @@ -207,8 +202,8 @@ export const en = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No dependencies', | ||||
|                     description: 'Create a view to get started', | ||||
|                     title: 'No relationships', | ||||
|                     description: 'Create a relationship to get started', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -247,6 +242,7 @@ export const en = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'No enum values defined', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -269,7 +265,7 @@ export const en = { | ||||
|             show_all: 'Show All', | ||||
|             undo: 'Undo', | ||||
|             redo: 'Redo', | ||||
|             reorder_diagram: 'Reorder Diagram', | ||||
|             reorder_diagram: 'Auto Arrange Diagram', | ||||
|             highlight_overlapping_tables: 'Highlight Overlapping Tables', | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -311,7 +307,7 @@ export const en = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Open Diagram', | ||||
|             title: 'Open Database', | ||||
|             description: 'Select a diagram to open from the list below.', | ||||
|             table_columns: { | ||||
|                 name: 'Name', | ||||
| @@ -321,6 +317,12 @@ export const en = { | ||||
|             }, | ||||
|             cancel: 'Cancel', | ||||
|             open: 'Open', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Open', | ||||
|                 duplicate: 'Duplicate', | ||||
|                 delete: 'Delete', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -468,6 +470,7 @@ export const en = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'New Table', | ||||
|             new_view: 'New View', | ||||
|             new_relationship: 'New Relationship', | ||||
|             new_area: 'New Area', | ||||
|         }, | ||||
| @@ -488,6 +491,9 @@ export const en = { | ||||
|         language_select: { | ||||
|             change_language: 'Language', | ||||
|         }, | ||||
|  | ||||
|         on: 'On', | ||||
|         off: 'Off', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const es: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Nuevo', | ||||
|             browse: 'Examinar', | ||||
|             tables: 'Tablas', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Áreas', | ||||
|             dependencies: 'Dependencias', | ||||
|             custom_types: 'Tipos Personalizados', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Archivo', | ||||
|                 new: 'Nuevo', | ||||
|                 open: 'Abrir', | ||||
|             actions: { | ||||
|                 actions: 'Acciones', | ||||
|                 new: 'Nuevo...', | ||||
|                 browse: 'Examinar...', | ||||
|                 save: 'Guardar', | ||||
|                 import: 'Importar Base de Datos', | ||||
|                 export_sql: 'Exportar SQL', | ||||
|                 export_as: 'Exportar como', | ||||
|                 delete_diagram: 'Eliminar Diagrama', | ||||
|                 exit: 'Salir', | ||||
|                 delete_diagram: 'Eliminar', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Editar', | ||||
| @@ -29,6 +37,7 @@ export const es: LanguageTranslation = { | ||||
|                 show_sidebar: 'Mostrar Barra Lateral', | ||||
|                 hide_sidebar: 'Ocultar Barra Lateral', | ||||
|                 zoom_on_scroll: 'Zoom al Desplazarse', | ||||
|                 show_views: 'Vistas de Base de Datos', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Mostrar dependencias', | ||||
|                 hide_dependencies: 'Ocultar dependencias', | ||||
| @@ -65,10 +74,10 @@ export const es: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Reordenar Diagrama', | ||||
|             title: 'Organizar Diagrama Automáticamente', | ||||
|             description: | ||||
|                 'Esta acción reorganizará todas las tablas en el diagrama. ¿Deseas continuar?', | ||||
|             reorder: 'Reordenar', | ||||
|             reorder: 'Organizar Automáticamente', | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
| @@ -106,14 +115,11 @@ export const es: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Esquema:', | ||||
|             filter_by_schema: 'Filtrar por esquema', | ||||
|             search_schema: 'Buscar esquema...', | ||||
|             no_schemas_found: 'No se encontraron esquemas.', | ||||
|             view_all_options: 'Ver todas las opciones...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablas', | ||||
|                 add_table: 'Agregar Tabla', | ||||
|                 add_view: 'Agregar Vista', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todo', | ||||
|                 // TODO: Translate | ||||
| @@ -139,6 +145,7 @@ export const es: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atributos del Campo', | ||||
|                         unique: 'Único', | ||||
|                         auto_increment: 'Autoincremento', | ||||
|                         comments: 'Comentarios', | ||||
|                         no_comments: 'Sin comentarios', | ||||
|                         delete_field: 'Eliminar Campo', | ||||
| @@ -154,6 +161,7 @@ export const es: LanguageTranslation = { | ||||
|                         title: 'Atributos del Índice', | ||||
|                         name: 'Nombre', | ||||
|                         unique: 'Único', | ||||
|                         index_type: 'Tipo de Índice', | ||||
|                         delete_index: 'Eliminar Índice', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -170,14 +178,17 @@ export const es: LanguageTranslation = { | ||||
|                     description: 'Crea una tabla para comenzar', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relaciones', | ||||
|                 add_relationship: 'Agregar Relación', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todo', | ||||
|                 add_relationship: 'Agregar Relación', | ||||
|                 relationships: 'Relaciones', | ||||
|                 dependencies: 'Dependencias', | ||||
|                 relationship: { | ||||
|                     primary: 'Primaria', | ||||
|                     foreign: 'Foránea', | ||||
|                     relationship: 'Relación', | ||||
|                     primary: 'Tabla Primaria', | ||||
|                     foreign: 'Tabla Referenciada', | ||||
|                     cardinality: 'Cardinalidad', | ||||
|                     delete_relationship: 'Eliminar', | ||||
|                     relationship_actions: { | ||||
| @@ -185,18 +196,10 @@ export const es: LanguageTranslation = { | ||||
|                         delete_relationship: 'Eliminar', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No hay relaciones', | ||||
|                     description: 'Crea una relación para conectar tablas', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencias', | ||||
|                 filter: 'Filtro', | ||||
|                 collapse: 'Colapsar todo', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependencia', | ||||
|                     table: 'Tabla', | ||||
|                     dependent_table: 'Vista dependiente', | ||||
|                     dependent_table: 'Vista Dependiente', | ||||
|                     delete_dependency: 'Eliminar', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Acciones', | ||||
| @@ -204,8 +207,8 @@ export const es: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sin dependencias', | ||||
|                     description: 'Crea una vista para comenzar', | ||||
|                     title: 'Sin relaciones', | ||||
|                     description: 'Crea una relación para comenzar', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -245,6 +248,7 @@ export const es: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'No hay valores de enum definidos', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -267,7 +271,7 @@ export const es: LanguageTranslation = { | ||||
|             show_all: 'Mostrar Todo', | ||||
|             undo: 'Deshacer', | ||||
|             redo: 'Rehacer', | ||||
|             reorder_diagram: 'Reordenar Diagrama', | ||||
|             reorder_diagram: 'Organizar Diagrama Automáticamente', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -312,7 +316,7 @@ export const es: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Abrir Diagrama', | ||||
|             title: 'Abrir Base de Datos', | ||||
|             description: | ||||
|                 'Selecciona un diagrama para abrir de la lista a continuación.', | ||||
|             table_columns: { | ||||
| @@ -323,6 +327,12 @@ export const es: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Cancelar', | ||||
|             open: 'Abrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Abrir', | ||||
|                 duplicate: 'Duplicar', | ||||
|                 delete: 'Eliminar', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -424,14 +434,6 @@ export const es: LanguageTranslation = { | ||||
|             confirm: '¡Claro!', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Múltiples Esquemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'nada', | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         export_diagram_dialog: { | ||||
|             title: 'Export Diagram', | ||||
| @@ -480,6 +482,7 @@ export const es: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nueva Tabla', | ||||
|             new_view: 'Nueva Vista', | ||||
|             new_relationship: 'Nueva Relación', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -502,6 +505,9 @@ export const es: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Idioma', | ||||
|         }, | ||||
|  | ||||
|         on: 'Encendido', | ||||
|         off: 'Apagado', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const fr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Nouveau', | ||||
|             browse: 'Parcourir', | ||||
|             tables: 'Tables', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Zones', | ||||
|             dependencies: 'Dépendances', | ||||
|             custom_types: 'Types Personnalisés', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Fichier', | ||||
|                 new: 'Nouveau', | ||||
|                 open: 'Ouvrir', | ||||
|             actions: { | ||||
|                 actions: 'Actions', | ||||
|                 new: 'Nouveau...', | ||||
|                 browse: 'Parcourir...', | ||||
|                 save: 'Enregistrer', | ||||
|                 import: 'Importer Base de Données', | ||||
|                 export_sql: 'Exporter SQL', | ||||
|                 export_as: 'Exporter en tant que', | ||||
|                 delete_diagram: 'Supprimer le Diagramme', | ||||
|                 exit: 'Quitter', | ||||
|                 delete_diagram: 'Supprimer', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Édition', | ||||
| @@ -29,6 +37,7 @@ export const fr: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'Masquer les Attributs de Champ', | ||||
|                 show_field_attributes: 'Afficher les Attributs de Champ', | ||||
|                 zoom_on_scroll: 'Zoom sur le Défilement', | ||||
|                 show_views: 'Vues de Base de Données', | ||||
|                 theme: 'Thème', | ||||
|                 show_dependencies: 'Afficher les Dépendances', | ||||
|                 hide_dependencies: 'Masquer les Dépendances', | ||||
| @@ -64,10 +73,10 @@ export const fr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Réorganiser le Diagramme', | ||||
|             title: 'Organiser Automatiquement le Diagramme', | ||||
|             description: | ||||
|                 'Cette action réorganisera toutes les tables dans le diagramme. Voulez-vous continuer ?', | ||||
|             reorder: 'Réorganiser', | ||||
|             reorder: 'Organiser Automatiquement', | ||||
|             cancel: 'Annuler', | ||||
|         }, | ||||
|  | ||||
| @@ -105,14 +114,11 @@ export const fr: LanguageTranslation = { | ||||
|         copied: 'Copié !', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schéma:', | ||||
|             filter_by_schema: 'Filtrer par schéma', | ||||
|             search_schema: 'Rechercher un schéma...', | ||||
|             no_schemas_found: 'Aucun schéma trouvé.', | ||||
|             view_all_options: 'Voir toutes les Options...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tables', | ||||
|                 add_table: 'Ajouter une Table', | ||||
|                 add_view: 'Ajouter une Vue', | ||||
|                 filter: 'Filtrer', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 clear: 'Effacer le Filtre', | ||||
| @@ -137,6 +143,7 @@ export const fr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Attributs du Champ', | ||||
|                         unique: 'Unique', | ||||
|                         auto_increment: 'Auto-incrément', | ||||
|                         comments: 'Commentaires', | ||||
|                         no_comments: 'Pas de commentaires', | ||||
|                         delete_field: 'Supprimer le Champ', | ||||
| @@ -152,6 +159,7 @@ export const fr: LanguageTranslation = { | ||||
|                         title: "Attributs de l'Index", | ||||
|                         name: 'Nom', | ||||
|                         unique: 'Unique', | ||||
|                         index_type: "Type d'index", | ||||
|                         delete_index: "Supprimer l'Index", | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -168,12 +176,15 @@ export const fr: LanguageTranslation = { | ||||
|                     description: 'Créez une table pour commencer', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relations', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrer', | ||||
|                 add_relationship: 'Ajouter une Relation', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 add_relationship: 'Ajouter une Relation', | ||||
|                 relationships: 'Relations', | ||||
|                 dependencies: 'Dépendances', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relation', | ||||
|                     primary: 'Table Principale', | ||||
|                     foreign: 'Table Référencée', | ||||
|                     cardinality: 'Cardinalité', | ||||
| @@ -183,16 +194,8 @@ export const fr: LanguageTranslation = { | ||||
|                         delete_relationship: 'Supprimer', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Aucune relation', | ||||
|                     description: 'Créez une relation pour connecter les tables', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dépendances', | ||||
|                 filter: 'Filtrer', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dépendance', | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Vue Dépendante', | ||||
|                     delete_dependency: 'Supprimer', | ||||
| @@ -202,8 +205,8 @@ export const fr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Aucune dépendance', | ||||
|                     description: 'Créez une vue pour commencer', | ||||
|                     title: 'Aucune relation', | ||||
|                     description: 'Créez une relation pour commencer', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -243,6 +246,7 @@ export const fr: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: "Aucune valeur d'énumération définie", | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -265,7 +269,7 @@ export const fr: LanguageTranslation = { | ||||
|             show_all: 'Afficher Tout', | ||||
|             undo: 'Annuler', | ||||
|             redo: 'Rétablir', | ||||
|             reorder_diagram: 'Réorganiser le Diagramme', | ||||
|             reorder_diagram: 'Organiser Automatiquement le Diagramme', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -309,7 +313,7 @@ export const fr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Ouvrir Diagramme', | ||||
|             title: 'Ouvrir Base de Données', | ||||
|             description: | ||||
|                 'Sélectionnez un diagramme à ouvrir dans la liste ci-dessous.', | ||||
|             table_columns: { | ||||
| @@ -320,6 +324,12 @@ export const fr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Annuler', | ||||
|             open: 'Ouvrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Ouvrir', | ||||
|                 duplicate: 'Dupliquer', | ||||
|                 delete: 'Supprimer', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -357,15 +367,6 @@ export const fr: LanguageTranslation = { | ||||
|             transparent_description: 'Remove background color from image.', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Schémas Multiples', | ||||
|             description: | ||||
|                 '{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'Aucun', | ||||
|         }, | ||||
|  | ||||
|         new_table_schema_dialog: { | ||||
|             title: 'Sélectionner un Schéma', | ||||
|             description: | ||||
| @@ -477,6 +478,7 @@ export const fr: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nouvelle Table', | ||||
|             new_view: 'Nouvelle Vue', | ||||
|             new_relationship: 'Nouvelle Relation', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -499,6 +501,9 @@ export const fr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Langue', | ||||
|         }, | ||||
|  | ||||
|         on: 'Activé', | ||||
|         off: 'Désactivé', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const gu: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'નવું', | ||||
|             browse: 'બ્રાઉજ', | ||||
|             tables: 'ટેબલો', | ||||
|             refs: 'રેફ્સ', | ||||
|             areas: 'ક્ષેત્રો', | ||||
|             dependencies: 'નિર્ભરતાઓ', | ||||
|             custom_types: 'કસ્ટમ ટાઇપ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ફાઇલ', | ||||
|                 new: 'નવું', | ||||
|                 open: 'ખોલો', | ||||
|             actions: { | ||||
|                 actions: 'ક્રિયાઓ', | ||||
|                 new: 'નવું...', | ||||
|                 browse: 'બ્રાઉજ કરો...', | ||||
|                 save: 'સાચવો', | ||||
|                 import: 'ડેટાબેસ આયાત કરો', | ||||
|                 export_sql: 'SQL નિકાસ કરો', | ||||
|                 export_as: 'રૂપે નિકાસ કરો', | ||||
|                 delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', | ||||
|                 exit: 'બહાર જાઓ', | ||||
|                 delete_diagram: 'કાઢી નાખો', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'ફેરફાર', | ||||
| @@ -29,6 +37,7 @@ export const gu: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો', | ||||
|                 show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો', | ||||
|                 zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો', | ||||
|                 show_views: 'ડેટાબેઝ વ્યૂઝ', | ||||
|                 theme: 'થિમ', | ||||
|                 show_dependencies: 'નિર્ભરતાઓ બતાવો', | ||||
|                 hide_dependencies: 'નિર્ભરતાઓ છુપાવો', | ||||
| @@ -66,22 +75,13 @@ export const gu: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'ડાયાગ્રામ ફરી વ્યવસ્થિત કરો', | ||||
|             title: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો', | ||||
|             description: | ||||
|                 'આ ક્રિયા ડાયાગ્રામમાં બધી ટેબલ્સને ફરીથી વ્યવસ્થિત કરશે. શું તમે ચાલુ રાખવા માંગો છો?', | ||||
|             reorder: 'ફરી વ્યવસ્થિત કરો', | ||||
|             reorder: 'ઑટોમેટિક ગોઠવો', | ||||
|             cancel: 'રદ કરો', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'કઈંક વધારે સ્કીમા', | ||||
|             description: | ||||
|                 '{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'કઈ નહીં', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'નકલ નિષ્ફળ', | ||||
| @@ -116,14 +116,11 @@ export const gu: LanguageTranslation = { | ||||
|         copied: 'નકલ થયું!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'સ્કીમા:', | ||||
|             filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો', | ||||
|             search_schema: 'સ્કીમા શોધો...', | ||||
|             no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.', | ||||
|             view_all_options: 'બધા વિકલ્પો જુઓ...', | ||||
|             tables_section: { | ||||
|                 tables: 'ટેબલ્સ', | ||||
|                 add_table: 'ટેબલ ઉમેરો', | ||||
|                 add_view: 'વ્યૂ ઉમેરો', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 collapse: 'બધાને સકુચિત કરો', | ||||
|                 // TODO: Translate | ||||
| @@ -150,6 +147,7 @@ export const gu: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ફીલ્ડ લક્ષણો', | ||||
|                         unique: 'અદ્વિતીય', | ||||
|                         auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ', | ||||
|                         comments: 'ટિપ્પણીઓ', | ||||
|                         no_comments: 'કોઈ ટિપ્પણીઓ નથી', | ||||
|                         delete_field: 'ફીલ્ડ કાઢી નાખો', | ||||
| @@ -165,6 +163,7 @@ export const gu: LanguageTranslation = { | ||||
|                         title: 'ઇન્ડેક્સ લક્ષણો', | ||||
|                         name: 'નામ', | ||||
|                         unique: 'અદ્વિતીય', | ||||
|                         index_type: 'ઇન્ડેક્સ પ્રકાર', | ||||
|                         delete_index: 'ઇન્ડેક્સ કાઢી નાખો', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -181,14 +180,17 @@ export const gu: LanguageTranslation = { | ||||
|                     description: 'શરૂ કરવા માટે એક ટેબલ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'સંબંધો', | ||||
|             refs_section: { | ||||
|                 refs: 'રેફ્સ', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 add_relationship: 'સંબંધ ઉમેરો', | ||||
|                 collapse: 'બધાને સકુચિત કરો', | ||||
|                 add_relationship: 'સંબંધ ઉમેરો', | ||||
|                 relationships: 'સંબંધો', | ||||
|                 dependencies: 'નિર્ભરતાઓ', | ||||
|                 relationship: { | ||||
|                     relationship: 'સંબંધ', | ||||
|                     primary: 'પ્રાથમિક ટેબલ', | ||||
|                     foreign: 'સંદર્ભ ટેબલ', | ||||
|                     foreign: 'સંદર્ભિત ટેબલ', | ||||
|                     cardinality: 'કાર્ડિનાલિટી', | ||||
|                     delete_relationship: 'કાઢી નાખો', | ||||
|                     relationship_actions: { | ||||
| @@ -196,27 +198,19 @@ export const gu: LanguageTranslation = { | ||||
|                         delete_relationship: 'કાઢી નાખો', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'કોઈ સંબંધો નથી', | ||||
|                     description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'નિર્ભરતાઓ', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 collapse: 'સિકોડો', | ||||
|                 dependency: { | ||||
|                     dependency: 'નિર્ભરતા', | ||||
|                     table: 'ટેબલ', | ||||
|                     dependent_table: 'આધાર રાખેલું ટેબલ', | ||||
|                     delete_dependency: 'નિર્ભરતા કાઢી નાખો', | ||||
|                     dependent_table: 'નિર્ભરશીલ વ્યૂ', | ||||
|                     delete_dependency: 'કાઢી નાખો', | ||||
|                     dependency_actions: { | ||||
|                         title: 'ક્રિયાઓ', | ||||
|                         delete_dependency: 'નિર્ભરતા કાઢી નાખો', | ||||
|                         delete_dependency: 'કાઢી નાખો', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'કોઈ નિર્ભરતાઓ નથી', | ||||
|                     description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.', | ||||
|                     title: 'કોઈ સંબંધો નથી', | ||||
|                     description: 'શરૂ કરવા માટે એક સંબંધ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -256,6 +250,7 @@ export const gu: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'કોઈ enum મૂલ્યો વ્યાખ્યાયિત નથી', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -278,7 +273,7 @@ export const gu: LanguageTranslation = { | ||||
|             show_all: 'બધું બતાવો', | ||||
|             undo: 'અનડુ', | ||||
|             redo: 'રીડુ', | ||||
|             reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો', | ||||
|             reorder_diagram: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -321,7 +316,7 @@ export const gu: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'ડાયાગ્રામ ખોલો', | ||||
|             title: 'ડેટાબેસ ખોલો', | ||||
|             description: 'નીચેની યાદીમાંથી એક ડાયાગ્રામ પસંદ કરો.', | ||||
|             table_columns: { | ||||
|                 name: 'નામ', | ||||
| @@ -331,6 +326,12 @@ export const gu: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'રદ કરો', | ||||
|             open: 'ખોલો', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'ખોલો', | ||||
|                 duplicate: 'ડુપ્લિકેટ', | ||||
|                 delete: 'કાઢી નાખો', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -480,6 +481,7 @@ export const gu: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'નવું ટેબલ', | ||||
|             new_view: 'નવું વ્યૂ', | ||||
|             new_relationship: 'નવો સંબંધ', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -501,6 +503,9 @@ export const gu: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'ભાષા બદલો', | ||||
|         }, | ||||
|  | ||||
|         on: 'ચાલુ', | ||||
|         off: 'બંધ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const hi: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नया', | ||||
|             browse: 'ब्राउज़', | ||||
|             tables: 'टेबल', | ||||
|             refs: 'रेफ्स', | ||||
|             areas: 'क्षेत्र', | ||||
|             dependencies: 'निर्भरताएं', | ||||
|             custom_types: 'कस्टम टाइप', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फ़ाइल', | ||||
|                 new: 'नया', | ||||
|                 open: 'खोलें', | ||||
|             actions: { | ||||
|                 actions: 'कार्य', | ||||
|                 new: 'नया...', | ||||
|                 browse: 'ब्राउज़ करें...', | ||||
|                 save: 'सहेजें', | ||||
|                 import: 'डेटाबेस आयात करें', | ||||
|                 export_sql: 'SQL निर्यात करें', | ||||
|                 export_as: 'के रूप में निर्यात करें', | ||||
|                 delete_diagram: 'आरेख हटाएँ', | ||||
|                 exit: 'बाहर जाएँ', | ||||
|                 delete_diagram: 'हटाएँ', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'संपादित करें', | ||||
| @@ -29,6 +37,7 @@ export const hi: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ', | ||||
|                 show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ', | ||||
|                 zoom_on_scroll: 'स्क्रॉल पर ज़ूम', | ||||
|                 show_views: 'डेटाबेस व्यू', | ||||
|                 theme: 'थीम', | ||||
|                 show_dependencies: 'निर्भरता दिखाएँ', | ||||
|                 hide_dependencies: 'निर्भरता छिपाएँ', | ||||
| @@ -65,22 +74,13 @@ export const hi: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'आरेख पुनः व्यवस्थित करें', | ||||
|             title: 'आरेख स्वचालित व्यवस्थित करें', | ||||
|             description: | ||||
|                 'यह क्रिया आरेख में सभी तालिकाओं को पुनः व्यवस्थित कर देगी। क्या आप जारी रखना चाहते हैं?', | ||||
|             reorder: 'पुनः व्यवस्थित करें', | ||||
|             reorder: 'स्वचालित व्यवस्थित करें', | ||||
|             cancel: 'रद्द करें', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'एकाधिक स्कीमा', | ||||
|             description: | ||||
|                 '{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'कोई नहीं', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'कॉपी असफल', | ||||
| @@ -116,14 +116,11 @@ export const hi: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीमा:', | ||||
|             filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें', | ||||
|             search_schema: 'स्कीमा खोजें...', | ||||
|             no_schemas_found: 'कोई स्कीमा नहीं मिला।', | ||||
|             view_all_options: 'सभी विकल्प देखें...', | ||||
|             tables_section: { | ||||
|                 tables: 'तालिकाएँ', | ||||
|                 add_table: 'तालिका जोड़ें', | ||||
|                 add_view: 'व्यू जोड़ें', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 collapse: 'सभी को संक्षिप्त करें', | ||||
|                 // TODO: Translate | ||||
| @@ -149,6 +146,7 @@ export const hi: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'फ़ील्ड विशेषताएँ', | ||||
|                         unique: 'अद्वितीय', | ||||
|                         auto_increment: 'ऑटो इंक्रीमेंट', | ||||
|                         comments: 'टिप्पणियाँ', | ||||
|                         no_comments: 'कोई टिप्पणी नहीं', | ||||
|                         delete_field: 'फ़ील्ड हटाएँ', | ||||
| @@ -164,6 +162,7 @@ export const hi: LanguageTranslation = { | ||||
|                         title: 'सूचकांक विशेषताएँ', | ||||
|                         name: 'नाम', | ||||
|                         unique: 'अद्वितीय', | ||||
|                         index_type: 'इंडेक्स प्रकार', | ||||
|                         delete_index: 'सूचकांक हटाएँ', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -180,12 +179,15 @@ export const hi: LanguageTranslation = { | ||||
|                     description: 'शुरू करने के लिए एक तालिका बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'संबंध', | ||||
|             refs_section: { | ||||
|                 refs: 'रेफ्स', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 add_relationship: 'संबंध जोड़ें', | ||||
|                 collapse: 'सभी को संक्षिप्त करें', | ||||
|                 add_relationship: 'संबंध जोड़ें', | ||||
|                 relationships: 'संबंध', | ||||
|                 dependencies: 'निर्भरताएँ', | ||||
|                 relationship: { | ||||
|                     relationship: 'संबंध', | ||||
|                     primary: 'प्राथमिक तालिका', | ||||
|                     foreign: 'संदर्भित तालिका', | ||||
|                     cardinality: 'कार्डिनैलिटी', | ||||
| @@ -195,28 +197,19 @@ export const hi: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटाएँ', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोई संबंध नहीं', | ||||
|                     description: | ||||
|                         'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'निर्भरताएँ', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 collapse: 'सिकोड़ें', | ||||
|                 dependency: { | ||||
|                     dependency: 'निर्भरता', | ||||
|                     table: 'तालिका', | ||||
|                     dependent_table: 'आश्रित तालिका', | ||||
|                     delete_dependency: 'निर्भरता हटाएँ', | ||||
|                     dependent_table: 'आश्रित दृश्य', | ||||
|                     delete_dependency: 'हटाएँ', | ||||
|                     dependency_actions: { | ||||
|                         title: 'कार्रवाइयाँ', | ||||
|                         delete_dependency: 'निर्भरता हटाएँ', | ||||
|                         title: 'क्रियाएँ', | ||||
|                         delete_dependency: 'हटाएँ', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोई निर्भरता नहीं', | ||||
|                     description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।', | ||||
|                     title: 'कोई संबंध नहीं', | ||||
|                     description: 'शुरू करने के लिए एक संबंध बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -256,6 +249,7 @@ export const hi: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'कोई enum मान परिभाषित नहीं', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -278,7 +272,7 @@ export const hi: LanguageTranslation = { | ||||
|             show_all: 'सभी दिखाएँ', | ||||
|             undo: 'पूर्ववत करें', | ||||
|             redo: 'पुनः करें', | ||||
|             reorder_diagram: 'आरेख पुनः व्यवस्थित करें', | ||||
|             reorder_diagram: 'आरेख स्वचालित व्यवस्थित करें', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -324,7 +318,7 @@ export const hi: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'आरेख खोलें', | ||||
|             title: 'डेटाबेस खोलें', | ||||
|             description: 'नीचे दी गई सूची से एक आरेख चुनें।', | ||||
|             table_columns: { | ||||
|                 name: 'नाम', | ||||
| @@ -334,6 +328,12 @@ export const hi: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द करें', | ||||
|             open: 'खोलें', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'खोलें', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'हटाएं', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -483,6 +483,7 @@ export const hi: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नई तालिका', | ||||
|             new_view: 'नया व्यू', | ||||
|             new_relationship: 'नया संबंध', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -505,6 +506,9 @@ export const hi: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा बदलें', | ||||
|         }, | ||||
|  | ||||
|         on: 'चालू', | ||||
|         off: 'बंद', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const hr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Novi', | ||||
|             browse: 'Pregledaj', | ||||
|             tables: 'Tablice', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Područja', | ||||
|             dependencies: 'Ovisnosti', | ||||
|             custom_types: 'Prilagođeni Tipovi', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Datoteka', | ||||
|                 new: 'Nova', | ||||
|                 open: 'Otvori', | ||||
|             actions: { | ||||
|                 actions: 'Akcije', | ||||
|                 new: 'Novi...', | ||||
|                 browse: 'Pregledaj...', | ||||
|                 save: 'Spremi', | ||||
|                 import: 'Uvezi', | ||||
|                 export_sql: 'Izvezi SQL', | ||||
|                 export_as: 'Izvezi kao', | ||||
|                 delete_diagram: 'Izbriši dijagram', | ||||
|                 exit: 'Izađi', | ||||
|                 delete_diagram: 'Izbriši', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Uredi', | ||||
| @@ -29,6 +37,7 @@ export const hr: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'Sakrij atribute polja', | ||||
|                 show_field_attributes: 'Prikaži atribute polja', | ||||
|                 zoom_on_scroll: 'Zumiranje pri skrolanju', | ||||
|                 show_views: 'Pogledi Baze Podataka', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Prikaži ovisnosti', | ||||
|                 hide_dependencies: 'Sakrij ovisnosti', | ||||
| @@ -64,21 +73,13 @@ export const hr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Preuredi dijagram', | ||||
|             title: 'Automatski preuredi dijagram', | ||||
|             description: | ||||
|                 'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?', | ||||
|             reorder: 'Preuredi', | ||||
|             reorder: 'Automatski preuredi', | ||||
|             cancel: 'Odustani', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Više shema', | ||||
|             description: | ||||
|                 '{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.', | ||||
|             show_me: 'Prikaži mi', | ||||
|             none: 'nijedna', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopiranje neuspješno', | ||||
| @@ -113,14 +114,11 @@ export const hr: LanguageTranslation = { | ||||
|         copied: 'Kopirano!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Shema:', | ||||
|             filter_by_schema: 'Filtriraj po shemi', | ||||
|             search_schema: 'Pretraži shemu...', | ||||
|             no_schemas_found: 'Nema pronađenih shema.', | ||||
|             view_all_options: 'Prikaži sve opcije...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablice', | ||||
|                 add_table: 'Dodaj tablicu', | ||||
|                 add_view: 'Dodaj Pogled', | ||||
|                 filter: 'Filtriraj', | ||||
|                 collapse: 'Sažmi sve', | ||||
|                 clear: 'Očisti filter', | ||||
| @@ -145,6 +143,7 @@ export const hr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atributi polja', | ||||
|                         unique: 'Jedinstven', | ||||
|                         auto_increment: 'Automatsko povećavanje', | ||||
|                         character_length: 'Maksimalna dužina', | ||||
|                         precision: 'Preciznost', | ||||
|                         scale: 'Skala', | ||||
| @@ -158,6 +157,7 @@ export const hr: LanguageTranslation = { | ||||
|                         title: 'Atributi indeksa', | ||||
|                         name: 'Naziv', | ||||
|                         unique: 'Jedinstven', | ||||
|                         index_type: 'Vrsta indeksa', | ||||
|                         delete_index: 'Izbriši indeks', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -174,12 +174,15 @@ export const hr: LanguageTranslation = { | ||||
|                     description: 'Stvorite tablicu za početak', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Veze', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtriraj', | ||||
|                 add_relationship: 'Dodaj vezu', | ||||
|                 collapse: 'Sažmi sve', | ||||
|                 add_relationship: 'Dodaj vezu', | ||||
|                 relationships: 'Veze', | ||||
|                 dependencies: 'Ovisnosti', | ||||
|                 relationship: { | ||||
|                     relationship: 'Veza', | ||||
|                     primary: 'Primarna tablica', | ||||
|                     foreign: 'Referentna tablica', | ||||
|                     cardinality: 'Kardinalnost', | ||||
| @@ -189,16 +192,8 @@ export const hr: LanguageTranslation = { | ||||
|                         delete_relationship: 'Izbriši', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Nema veza', | ||||
|                     description: 'Stvorite vezu za povezivanje tablica', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Ovisnosti', | ||||
|                 filter: 'Filtriraj', | ||||
|                 collapse: 'Sažmi sve', | ||||
|                 dependency: { | ||||
|                     dependency: 'Ovisnost', | ||||
|                     table: 'Tablica', | ||||
|                     dependent_table: 'Ovisni pogled', | ||||
|                     delete_dependency: 'Izbriši', | ||||
| @@ -208,8 +203,8 @@ export const hr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Nema ovisnosti', | ||||
|                     description: 'Stvorite pogled za početak', | ||||
|                     title: 'Nema veze', | ||||
|                     description: 'Stvorite vezu za početak', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -250,6 +245,7 @@ export const hr: LanguageTranslation = { | ||||
|                     enum_values: 'Enum vrijednosti', | ||||
|                     composite_fields: 'Polja', | ||||
|                     no_fields: 'Nema definiranih polja', | ||||
|                     no_values: 'Nema definiranih enum vrijednosti', | ||||
|                     field_name_placeholder: 'Naziv polja', | ||||
|                     field_type_placeholder: 'Odaberi tip', | ||||
|                     add_field: 'Dodaj polje', | ||||
| @@ -273,7 +269,7 @@ export const hr: LanguageTranslation = { | ||||
|             show_all: 'Prikaži sve', | ||||
|             undo: 'Poništi', | ||||
|             redo: 'Ponovi', | ||||
|             reorder_diagram: 'Preuredi dijagram', | ||||
|             reorder_diagram: 'Automatski preuredi dijagram', | ||||
|             highlight_overlapping_tables: 'Istakni preklapajuće tablice', | ||||
|             clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -315,7 +311,7 @@ export const hr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Otvori dijagram', | ||||
|             title: 'Otvori bazu podataka', | ||||
|             description: 'Odaberite dijagram za otvaranje iz popisa ispod.', | ||||
|             table_columns: { | ||||
|                 name: 'Naziv', | ||||
| @@ -325,6 +321,12 @@ export const hr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Odustani', | ||||
|             open: 'Otvori', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Otvori', | ||||
|                 duplicate: 'Dupliciraj', | ||||
|                 delete: 'Obriši', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -473,6 +475,7 @@ export const hr: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nova tablica', | ||||
|             new_view: 'Novi Pogled', | ||||
|             new_relationship: 'Nova veza', | ||||
|             new_area: 'Novo područje', | ||||
|         }, | ||||
| @@ -493,6 +496,9 @@ export const hr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Jezik', | ||||
|         }, | ||||
|  | ||||
|         on: 'Uključeno', | ||||
|         off: 'Isključeno', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const id_ID: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Baru', | ||||
|             browse: 'Jelajahi', | ||||
|             tables: 'Tabel', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Area', | ||||
|             dependencies: 'Ketergantungan', | ||||
|             custom_types: 'Tipe Kustom', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Berkas', | ||||
|                 new: 'Buat Baru', | ||||
|                 open: 'Buka', | ||||
|             actions: { | ||||
|                 actions: 'Aksi', | ||||
|                 new: 'Baru...', | ||||
|                 browse: 'Jelajahi...', | ||||
|                 save: 'Simpan', | ||||
|                 import: 'Impor Database', | ||||
|                 export_sql: 'Ekspor SQL', | ||||
|                 export_as: 'Ekspor Sebagai', | ||||
|                 delete_diagram: 'Hapus Diagram', | ||||
|                 exit: 'Keluar', | ||||
|                 delete_diagram: 'Hapus', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Ubah', | ||||
| @@ -29,6 +37,7 @@ export const id_ID: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'Sembunyikan Atribut Kolom', | ||||
|                 show_field_attributes: 'Tampilkan Atribut Kolom', | ||||
|                 zoom_on_scroll: 'Perbesar saat Scroll', | ||||
|                 show_views: 'Tampilan Database', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Tampilkan Dependensi', | ||||
|                 hide_dependencies: 'Sembunyikan Dependensi', | ||||
| @@ -65,22 +74,13 @@ export const id_ID: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Atur Ulang Diagram', | ||||
|             title: 'Atur Otomatis Diagram', | ||||
|             description: | ||||
|                 'Tindakan ini akan mengatur ulang semua tabel di diagram. Apakah Anda ingin melanjutkan?', | ||||
|             reorder: 'Atur Ulang', | ||||
|             reorder: 'Atur Otomatis', | ||||
|             cancel: 'Batal', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Schema Lebih dari satu', | ||||
|             description: | ||||
|                 '{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'Tidak ada', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Gagal menyalin', | ||||
| @@ -115,14 +115,11 @@ export const id_ID: LanguageTranslation = { | ||||
|         copied: 'Tersalin!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Skema:', | ||||
|             filter_by_schema: 'Saring berdasarkan skema', | ||||
|             search_schema: 'Cari skema...', | ||||
|             no_schemas_found: 'Tidak ada skema yang ditemukan.', | ||||
|             view_all_options: 'Tampilkan Semua Pilihan...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabel', | ||||
|                 add_table: 'Tambah Tabel', | ||||
|                 add_view: 'Tambah Tampilan', | ||||
|                 filter: 'Saring', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 // TODO: Translate | ||||
| @@ -148,6 +145,7 @@ export const id_ID: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atribut Kolom', | ||||
|                         unique: 'Unik', | ||||
|                         auto_increment: 'Kenaikan Otomatis', | ||||
|                         comments: 'Komentar', | ||||
|                         no_comments: 'Tidak ada komentar', | ||||
|                         delete_field: 'Hapus Kolom', | ||||
| @@ -163,6 +161,7 @@ export const id_ID: LanguageTranslation = { | ||||
|                         title: 'Atribut Indeks', | ||||
|                         name: 'Nama', | ||||
|                         unique: 'Unik', | ||||
|                         index_type: 'Tipe Indeks', | ||||
|                         delete_index: 'Hapus Indeks', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -179,12 +178,15 @@ export const id_ID: LanguageTranslation = { | ||||
|                     description: 'Buat tabel untuk memulai', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Hubungan', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Saring', | ||||
|                 add_relationship: 'Tambah Hubungan', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 add_relationship: 'Tambah Hubungan', | ||||
|                 relationships: 'Hubungan', | ||||
|                 dependencies: 'Dependensi', | ||||
|                 relationship: { | ||||
|                     relationship: 'Hubungan', | ||||
|                     primary: 'Tabel Primer', | ||||
|                     foreign: 'Tabel Referensi', | ||||
|                     cardinality: 'Kardinalitas', | ||||
| @@ -194,16 +196,8 @@ export const id_ID: LanguageTranslation = { | ||||
|                         delete_relationship: 'Hapus', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Tidak ada hubungan', | ||||
|                     description: 'Buat hubungan untuk menghubungkan tabel', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependensi', | ||||
|                 filter: 'Saring', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependensi', | ||||
|                     table: 'Tabel', | ||||
|                     dependent_table: 'Tampilan Dependen', | ||||
|                     delete_dependency: 'Hapus', | ||||
| @@ -213,8 +207,8 @@ export const id_ID: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Tidak ada dependensi', | ||||
|                     description: 'Buat tampilan untuk memulai', | ||||
|                     title: 'Tidak ada hubungan', | ||||
|                     description: 'Buat hubungan untuk memulai', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -254,6 +248,7 @@ export const id_ID: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Tidak ada nilai enum yang ditentukan', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -276,7 +271,7 @@ export const id_ID: LanguageTranslation = { | ||||
|             show_all: 'Tampilkan Semua', | ||||
|             undo: 'Undo', | ||||
|             redo: 'Redo', | ||||
|             reorder_diagram: 'Atur Ulang Diagram', | ||||
|             reorder_diagram: 'Atur Otomatis Diagram', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -320,7 +315,7 @@ export const id_ID: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Buka Diagram', | ||||
|             title: 'Buka Database', | ||||
|             description: 'Pilih diagram untuk dibuka dari daftar di bawah.', | ||||
|             table_columns: { | ||||
|                 name: 'Name', | ||||
| @@ -330,6 +325,12 @@ export const id_ID: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Batal', | ||||
|             open: 'Buka', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Buka', | ||||
|                 duplicate: 'Duplikat', | ||||
|                 delete: 'Hapus', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -479,6 +480,7 @@ export const id_ID: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Tabel Baru', | ||||
|             new_view: 'Tampilan Baru', | ||||
|             new_relationship: 'Hubungan Baru', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -500,6 +502,9 @@ export const id_ID: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Bahasa', | ||||
|         }, | ||||
|  | ||||
|         on: 'Aktif', | ||||
|         off: 'Nonaktif', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ja: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新規', | ||||
|             browse: '参照', | ||||
|             tables: 'テーブル', | ||||
|             refs: '参照', | ||||
|             areas: 'エリア', | ||||
|             dependencies: '依存関係', | ||||
|             custom_types: 'カスタムタイプ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ファイル', | ||||
|                 new: '新規', | ||||
|                 open: '開く', | ||||
|             actions: { | ||||
|                 actions: 'アクション', | ||||
|                 new: '新規...', | ||||
|                 browse: '参照...', | ||||
|                 save: '保存', | ||||
|                 import: 'データベースをインポート', | ||||
|                 export_sql: 'SQLをエクスポート', | ||||
|                 export_as: '形式を指定してエクスポート', | ||||
|                 delete_diagram: 'ダイアグラムを削除', | ||||
|                 exit: '終了', | ||||
|                 delete_diagram: '削除', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '編集', | ||||
| @@ -29,6 +37,7 @@ export const ja: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'フィールド属性を非表示', | ||||
|                 show_field_attributes: 'フィールド属性を表示', | ||||
|                 zoom_on_scroll: 'スクロールでズーム', | ||||
|                 show_views: 'データベースビュー', | ||||
|                 theme: 'テーマ', | ||||
|                 // TODO: Translate | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
| @@ -67,22 +76,13 @@ export const ja: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'ダイアグラムを並べ替え', | ||||
|             title: 'ダイアグラムを自動配置', | ||||
|             description: | ||||
|                 'この操作によりダイアグラム内のすべてのテーブルが再配置されます。続行しますか?', | ||||
|             reorder: '並べ替え', | ||||
|             reorder: '自動配置', | ||||
|             cancel: 'キャンセル', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '複数のスキーマ', | ||||
|             description: | ||||
|                 'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'なし', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'コピー失敗', | ||||
| @@ -119,14 +119,11 @@ export const ja: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'スキーマ:', | ||||
|             filter_by_schema: 'スキーマでフィルタ', | ||||
|             search_schema: 'スキーマを検索...', | ||||
|             no_schemas_found: 'スキーマが見つかりません。', | ||||
|             view_all_options: 'すべてのオプションを表示...', | ||||
|             tables_section: { | ||||
|                 tables: 'テーブル', | ||||
|                 add_table: 'テーブルを追加', | ||||
|                 add_view: 'ビューを追加', | ||||
|                 filter: 'フィルタ', | ||||
|                 collapse: 'すべて折りたたむ', | ||||
|                 // TODO: Translate | ||||
| @@ -152,6 +149,7 @@ export const ja: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'フィールド属性', | ||||
|                         unique: 'ユニーク', | ||||
|                         auto_increment: 'オートインクリメント', | ||||
|                         comments: 'コメント', | ||||
|                         no_comments: 'コメントがありません', | ||||
|                         delete_field: 'フィールドを削除', | ||||
| @@ -167,6 +165,7 @@ export const ja: LanguageTranslation = { | ||||
|                         title: 'インデックス属性', | ||||
|                         name: '名前', | ||||
|                         unique: 'ユニーク', | ||||
|                         index_type: 'インデックスタイプ', | ||||
|                         delete_index: 'インデックスを削除', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -183,12 +182,15 @@ export const ja: LanguageTranslation = { | ||||
|                     description: 'テーブルを作成して開始してください', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'リレーションシップ', | ||||
|             refs_section: { | ||||
|                 refs: '参照', | ||||
|                 filter: 'フィルタ', | ||||
|                 add_relationship: 'リレーションシップを追加', | ||||
|                 collapse: 'すべて折りたたむ', | ||||
|                 add_relationship: 'リレーションシップを追加', | ||||
|                 relationships: 'リレーションシップ', | ||||
|                 dependencies: '依存関係', | ||||
|                 relationship: { | ||||
|                     relationship: 'リレーションシップ', | ||||
|                     primary: '主テーブル', | ||||
|                     foreign: '参照テーブル', | ||||
|                     cardinality: 'カーディナリティ', | ||||
| @@ -198,29 +200,20 @@ export const ja: LanguageTranslation = { | ||||
|                         delete_relationship: '削除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'リレーションシップがありません', | ||||
|                     description: | ||||
|                         'テーブルを接続するためにリレーションシップを作成してください', | ||||
|                 }, | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencies', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 dependency: { | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Dependent View', | ||||
|                     delete_dependency: 'Delete', | ||||
|                     dependency: '依存関係', | ||||
|                     table: 'テーブル', | ||||
|                     dependent_table: '依存ビュー', | ||||
|                     delete_dependency: '削除', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Actions', | ||||
|                         delete_dependency: 'Delete', | ||||
|                         title: '操作', | ||||
|                         delete_dependency: '削除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No dependencies', | ||||
|                     description: 'Create a view to get started', | ||||
|                     title: 'リレーションシップがありません', | ||||
|                     description: | ||||
|                         '開始するためにリレーションシップを作成してください', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -260,6 +253,7 @@ export const ja: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: '列挙値が定義されていません', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -282,7 +276,7 @@ export const ja: LanguageTranslation = { | ||||
|             show_all: 'すべて表示', | ||||
|             undo: '元に戻す', | ||||
|             redo: 'やり直し', | ||||
|             reorder_diagram: 'ダイアグラムを並べ替え', | ||||
|             reorder_diagram: 'ダイアグラムを自動配置', | ||||
|             // TODO: Translate | ||||
|             highlight_overlapping_tables: 'Highlight Overlapping Tables', | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
| @@ -326,7 +320,7 @@ export const ja: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'ダイアグラムを開く', | ||||
|             title: 'データベースを開く', | ||||
|             description: '以下のリストからダイアグラムを選択してください。', | ||||
|             table_columns: { | ||||
|                 name: '名前', | ||||
| @@ -336,6 +330,12 @@ export const ja: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'キャンセル', | ||||
|             open: '開く', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '開く', | ||||
|                 duplicate: '複製', | ||||
|                 delete: '削除', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -485,6 +485,7 @@ export const ja: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新しいテーブル', | ||||
|             new_view: '新しいビュー', | ||||
|             new_relationship: '新しいリレーションシップ', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -507,6 +508,9 @@ export const ja: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '言語', | ||||
|         }, | ||||
|  | ||||
|         on: 'オン', | ||||
|         off: 'オフ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ko_KR: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '새로 만들기', | ||||
|             browse: '찾아보기', | ||||
|             tables: '테이블', | ||||
|             refs: 'Refs', | ||||
|             areas: '영역', | ||||
|             dependencies: '종속성', | ||||
|             custom_types: '사용자 지정 타입', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '파일', | ||||
|                 new: '새 다이어그램', | ||||
|                 open: '열기', | ||||
|             actions: { | ||||
|                 actions: '작업', | ||||
|                 new: '새로 만들기...', | ||||
|                 browse: '찾아보기...', | ||||
|                 save: '저장', | ||||
|                 import: '데이터베이스 가져오기', | ||||
|                 export_sql: 'SQL로 저장', | ||||
|                 export_as: '다른 형식으로 저장', | ||||
|                 delete_diagram: '다이어그램 삭제', | ||||
|                 exit: '종료', | ||||
|                 delete_diagram: '삭제', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '편집', | ||||
| @@ -29,6 +37,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|                 hide_field_attributes: '필드 속성 숨기기', | ||||
|                 show_field_attributes: '필드 속성 보이기', | ||||
|                 zoom_on_scroll: '스크롤 시 확대', | ||||
|                 show_views: '데이터베이스 뷰', | ||||
|                 theme: '테마', | ||||
|                 show_dependencies: '종속성 보이기', | ||||
|                 hide_dependencies: '종속성 숨기기', | ||||
| @@ -65,22 +74,13 @@ export const ko_KR: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: '다이어그램 재정렬', | ||||
|             title: '다이어그램 자동 정렬', | ||||
|             description: | ||||
|                 '이 작업은 모든 다이어그램이 재정렬됩니다. 계속하시겠습니까?', | ||||
|             reorder: '재정렬', | ||||
|             reorder: '자동 정렬', | ||||
|             cancel: '취소', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '다중 스키마', | ||||
|             description: | ||||
|                 '현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: '없음', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '복사 실패', | ||||
| @@ -115,14 +115,11 @@ export const ko_KR: LanguageTranslation = { | ||||
|         copied: '복사됨!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: '스키마:', | ||||
|             filter_by_schema: '스키마로 필터링', | ||||
|             search_schema: '스키마 검색...', | ||||
|             no_schemas_found: '스키마를 찾을 수 없습니다.', | ||||
|             view_all_options: '전체 옵션 보기...', | ||||
|             tables_section: { | ||||
|                 tables: '테이블', | ||||
|                 add_table: '테이블 추가', | ||||
|                 add_view: '뷰 추가', | ||||
|                 filter: '필터', | ||||
|                 collapse: '모두 접기', | ||||
|                 // TODO: Translate | ||||
| @@ -148,6 +145,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '필드 속성', | ||||
|                         unique: '유니크 여부', | ||||
|                         auto_increment: '자동 증가', | ||||
|                         comments: '주석', | ||||
|                         no_comments: '주석 없음', | ||||
|                         delete_field: '필드 삭제', | ||||
| @@ -163,6 +161,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|                         title: '인덱스 속성', | ||||
|                         name: '인덱스 명', | ||||
|                         unique: '유니크 여부', | ||||
|                         index_type: '인덱스 타입', | ||||
|                         delete_index: '인덱스 삭제', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -179,12 +178,15 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     description: '테이블을 만들어 시작하세요.', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '연관 관계', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: '필터', | ||||
|                 add_relationship: '연관 관계 추가', | ||||
|                 collapse: '모두 접기', | ||||
|                 add_relationship: '연관 관계 추가', | ||||
|                 relationships: '연관 관계', | ||||
|                 dependencies: '종속성', | ||||
|                 relationship: { | ||||
|                     relationship: '연관 관계', | ||||
|                     primary: '주 테이블', | ||||
|                     foreign: '참조 테이블', | ||||
|                     cardinality: '카디널리티', | ||||
| @@ -194,16 +196,8 @@ export const ko_KR: LanguageTranslation = { | ||||
|                         delete_relationship: '연관 관계 삭제', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '연관 관계', | ||||
|                     description: '테이블 연결을 위해 연관 관계를 생성하세요', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '종속성', | ||||
|                 filter: '필터', | ||||
|                 collapse: '모두 접기', | ||||
|                 dependency: { | ||||
|                     dependency: '종속성', | ||||
|                     table: '테이블', | ||||
|                     dependent_table: '뷰 테이블', | ||||
|                     delete_dependency: '삭제', | ||||
| @@ -213,8 +207,8 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '뷰 테이블 없음', | ||||
|                     description: '뷰 테이블을 만들어 시작하세요.', | ||||
|                     title: '연관 관계 없음', | ||||
|                     description: '연관 관계를 만들어 시작하세요.', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -254,6 +248,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: '정의된 열거형 값이 없습니다', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -276,7 +271,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|             show_all: '전체 저장', | ||||
|             undo: '실행 취소', | ||||
|             redo: '다시 실행', | ||||
|             reorder_diagram: '다이어그램 재정렬', | ||||
|             reorder_diagram: '다이어그램 자동 정렬', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -320,7 +315,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: '다이어그램 열기', | ||||
|             title: '데이터베이스 열기', | ||||
|             description: '아래의 목록에서 다이어그램을 선택하세요.', | ||||
|             table_columns: { | ||||
|                 name: '이름', | ||||
| @@ -330,6 +325,12 @@ export const ko_KR: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '취소', | ||||
|             open: '열기', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '열기', | ||||
|                 duplicate: '복제', | ||||
|                 delete: '삭제', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -476,6 +477,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '새 테이블', | ||||
|             new_view: '새 뷰', | ||||
|             new_relationship: '새 연관관계', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -497,6 +499,9 @@ export const ko_KR: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '언어', | ||||
|         }, | ||||
|  | ||||
|         on: '켜기', | ||||
|         off: '끄기', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const mr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नवीन', | ||||
|             browse: 'ब्राउज', | ||||
|             tables: 'टेबल', | ||||
|             refs: 'Refs', | ||||
|             areas: 'क्षेत्रे', | ||||
|             dependencies: 'अवलंबने', | ||||
|             custom_types: 'कस्टम प्रकार', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फाइल', | ||||
|                 new: 'नवीन', | ||||
|                 open: 'उघडा', | ||||
|             actions: { | ||||
|                 actions: 'क्रिया', | ||||
|                 new: 'नवीन...', | ||||
|                 browse: 'ब्राउज करा...', | ||||
|                 save: 'जतन करा', | ||||
|                 import: 'डेटाबेस इम्पोर्ट करा', | ||||
|                 export_sql: 'SQL एक्स्पोर्ट करा', | ||||
|                 export_as: 'म्हणून एक्स्पोर्ट करा', | ||||
|                 delete_diagram: 'आरेख हटवा', | ||||
|                 exit: 'बाहेर पडा', | ||||
|                 delete_diagram: 'हटवा', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'संपादन करा', | ||||
| @@ -29,6 +37,7 @@ export const mr: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'फील्ड गुणधर्म लपवा', | ||||
|                 show_field_attributes: 'फील्ड गुणधर्म दाखवा', | ||||
|                 zoom_on_scroll: 'स्क्रोलवर झूम करा', | ||||
|                 show_views: 'डेटाबेस व्ह्यूज', | ||||
|                 theme: 'थीम', | ||||
|                 show_dependencies: 'डिपेंडेन्सि दाखवा', | ||||
|                 hide_dependencies: 'डिपेंडेन्सि लपवा', | ||||
| @@ -66,22 +75,13 @@ export const mr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'आरेख पुनःक्रमित करा', | ||||
|             title: 'आरेख स्वयंचलित व्यवस्थित करा', | ||||
|             description: | ||||
|                 'ही क्रिया आरेखातील सर्व टेबल्सची पुनर्रचना करेल. तुम्हाला पुढे जायचे आहे का?', | ||||
|             reorder: 'पुनःक्रमित करा', | ||||
|             reorder: 'स्वयंचलित व्यवस्थित करा', | ||||
|             cancel: 'रद्द करा', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'एकाधिक स्कीमा', | ||||
|             description: | ||||
|                 '{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'काहीही नाही', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'कॉपी अयशस्वी', | ||||
| @@ -118,14 +118,11 @@ export const mr: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीमा:', | ||||
|             filter_by_schema: 'स्कीमा द्वारे फिल्टर करा', | ||||
|             search_schema: 'स्कीमा शोधा...', | ||||
|             no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.', | ||||
|             view_all_options: 'सर्व पर्याय पहा...', | ||||
|             tables_section: { | ||||
|                 tables: 'टेबल्स', | ||||
|                 add_table: 'टेबल जोडा', | ||||
|                 add_view: 'व्ह्यू जोडा', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 // TODO: Translate | ||||
| @@ -151,6 +148,7 @@ export const mr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'फील्ड गुणधर्म', | ||||
|                         unique: 'युनिक', | ||||
|                         auto_increment: 'ऑटो इंक्रिमेंट', | ||||
|                         comments: 'टिप्पण्या', | ||||
|                         no_comments: 'कोणत्याही टिप्पणी नाहीत', | ||||
|                         delete_field: 'फील्ड हटवा', | ||||
| @@ -166,6 +164,7 @@ export const mr: LanguageTranslation = { | ||||
|                         title: 'इंडेक्स गुणधर्म', | ||||
|                         name: 'नाव', | ||||
|                         unique: 'युनिक', | ||||
|                         index_type: 'इंडेक्स प्रकार', | ||||
|                         delete_index: 'इंडेक्स हटवा', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -183,12 +182,15 @@ export const mr: LanguageTranslation = { | ||||
|                     description: 'सुरू करण्यासाठी एक टेबल तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'रिलेशनशिप', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'फिल्टर', | ||||
|                 add_relationship: 'रिलेशनशिप जोडा', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 add_relationship: 'रिलेशनशिप जोडा', | ||||
|                 relationships: 'रिलेशनशिप', | ||||
|                 dependencies: 'डिपेंडेन्सि', | ||||
|                 relationship: { | ||||
|                     relationship: 'रिलेशनशिप', | ||||
|                     primary: 'प्राथमिक टेबल', | ||||
|                     foreign: 'रेफरंस टेबल', | ||||
|                     cardinality: 'कार्डिनॅलिटी', | ||||
| @@ -198,17 +200,8 @@ export const mr: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटवा', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोणतेही रिलेशनशिप नाहीत', | ||||
|                     description: | ||||
|                         'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'डिपेंडेन्सि', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 dependency: { | ||||
|                     dependency: 'डिपेंडेन्सि', | ||||
|                     table: 'टेबल', | ||||
|                     dependent_table: 'डिपेंडेन्सि दृश्य', | ||||
|                     delete_dependency: 'हटवा', | ||||
| @@ -218,8 +211,8 @@ export const mr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोणत्याही डिपेंडेन्सि नाहीत', | ||||
|                     description: 'सुरू करण्यासाठी एक दृश्य तयार करा', | ||||
|                     title: 'कोणतेही रिलेशनशिप नाहीत', | ||||
|                     description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -259,6 +252,7 @@ export const mr: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'कोणतीही enum मूल्ये परिभाषित नाहीत', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -281,7 +275,7 @@ export const mr: LanguageTranslation = { | ||||
|             show_all: 'सर्व दाखवा', | ||||
|             undo: 'पूर्ववत करा', | ||||
|             redo: 'पुन्हा करा', | ||||
|             reorder_diagram: 'आरेख पुनःक्रमित करा', | ||||
|             reorder_diagram: 'आरेख स्वयंचलित व्यवस्थित करा', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -327,7 +321,7 @@ export const mr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'आरेख उघडा', | ||||
|             title: 'डेटाबेस उघडा', | ||||
|             description: 'खालील यादीतून उघडण्यासाठी एक आरेख निवडा.', | ||||
|             table_columns: { | ||||
|                 name: 'नाव', | ||||
| @@ -337,6 +331,12 @@ export const mr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द करा', | ||||
|             open: 'उघडा', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'उघडा', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'हटवा', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -489,6 +489,7 @@ export const mr: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नवीन टेबल', | ||||
|             new_view: 'नवीन व्ह्यू', | ||||
|             new_relationship: 'नवीन रिलेशनशिप', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -512,6 +513,9 @@ export const mr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा बदला', | ||||
|         }, | ||||
|  | ||||
|         on: 'चालू', | ||||
|         off: 'बंद', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ne: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नयाँ', | ||||
|             browse: 'ब्राउज', | ||||
|             tables: 'टेबलहरू', | ||||
|             refs: 'Refs', | ||||
|             areas: 'क्षेत्रहरू', | ||||
|             dependencies: 'निर्भरताहरू', | ||||
|             custom_types: 'कस्टम प्रकारहरू', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फाइल', | ||||
|                 new: 'नयाँ', | ||||
|                 open: 'खोल्नुहोस्', | ||||
|             actions: { | ||||
|                 actions: 'कार्यहरू', | ||||
|                 new: 'नयाँ...', | ||||
|                 browse: 'ब्राउज गर्नुहोस्...', | ||||
|                 save: 'सुरक्षित गर्नुहोस्', | ||||
|                 import: 'डाटाबेस आयात गर्नुहोस्', | ||||
|                 export_sql: 'SQL निर्यात गर्नुहोस्', | ||||
|                 export_as: 'निर्यात गर्नुहोस्', | ||||
|                 delete_diagram: 'डायाग्राम हटाउनुहोस्', | ||||
|                 exit: 'बाहिर निस्कनुहोस्', | ||||
|                 delete_diagram: 'हटाउनुहोस्', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'सम्पादन', | ||||
| @@ -29,6 +37,7 @@ export const ne: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्', | ||||
|                 show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्', | ||||
|                 zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्', | ||||
|                 show_views: 'डाटाबेस भ्यूहरू', | ||||
|                 theme: 'थिम', | ||||
|                 show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्', | ||||
|                 hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्', | ||||
| @@ -66,22 +75,13 @@ export const ne: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'डायाग्राम पुनः क्रमबद्ध गर्नुहोस्', | ||||
|             title: 'डायाग्राम स्वचालित मिलाउनुहोस्', | ||||
|             description: | ||||
|                 'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।', | ||||
|             reorder: 'पुनः क्रमबद्ध गर्नुहोस्', | ||||
|             reorder: 'स्वचालित मिलाउनुहोस्', | ||||
|             cancel: 'रद्द गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'विविध स्कीमहरू', | ||||
|             description: | ||||
|                 '{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'कुनै पनि छैन', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'प्रतिलिपि असफल', | ||||
| @@ -116,14 +116,11 @@ export const ne: LanguageTranslation = { | ||||
|         copied: 'प्रतिलिपि गरियो!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीम:', | ||||
|             filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्', | ||||
|             search_schema: 'स्कीम खोज्नुहोस्...', | ||||
|             no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्', | ||||
|             view_all_options: 'सबै विकल्पहरू हेर्नुहोस्', | ||||
|             tables_section: { | ||||
|                 tables: 'तालिकाहरू', | ||||
|                 add_table: 'तालिका थप्नुहोस्', | ||||
|                 add_view: 'भ्यू थप्नुहोस्', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 // TODO: Translate | ||||
| @@ -149,6 +146,7 @@ export const ne: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'क्षेत्र विशेषताहरू', | ||||
|                         unique: 'अनन्य', | ||||
|                         auto_increment: 'स्वचालित वृद्धि', | ||||
|                         comments: 'टिप्पणीहरू', | ||||
|                         no_comments: 'कुनै टिप्पणीहरू छैनन्', | ||||
|                         delete_field: 'क्षेत्र हटाउनुहोस्', | ||||
| @@ -164,6 +162,7 @@ export const ne: LanguageTranslation = { | ||||
|                         title: 'सूचक विशेषताहरू', | ||||
|                         name: 'नाम', | ||||
|                         unique: 'अनन्य', | ||||
|                         index_type: 'इन्डेक्स प्रकार', | ||||
|                         delete_index: 'सूचक हटाउनुहोस्', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -180,12 +179,15 @@ export const ne: LanguageTranslation = { | ||||
|                     description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'सम्बन्धहरू', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'फिल्टर', | ||||
|                 add_relationship: 'सम्बन्ध थप्नुहोस्', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 add_relationship: 'सम्बन्ध थप्नुहोस्', | ||||
|                 relationships: 'सम्बन्धहरू', | ||||
|                 dependencies: 'डिपेन्डेन्सीहरू', | ||||
|                 relationship: { | ||||
|                     relationship: 'सम्बन्ध', | ||||
|                     primary: 'मुख्य तालिका', | ||||
|                     foreign: 'परिचित तालिका', | ||||
|                     cardinality: 'कार्डिन्यालिटी', | ||||
| @@ -195,16 +197,8 @@ export const ne: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटाउनुहोस्', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कुनै सम्बन्धहरू छैनन्', | ||||
|                     description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'डिपेन्डेन्सीहरू', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 dependency: { | ||||
|                     dependency: 'डिपेन्डेन्सी', | ||||
|                     table: 'तालिका', | ||||
|                     dependent_table: 'विचलित तालिका', | ||||
|                     delete_dependency: 'हटाउनुहोस्', | ||||
| @@ -214,9 +208,8 @@ export const ne: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कुनै डिपेन्डेन्सीहरू छैनन्', | ||||
|                     description: | ||||
|                         'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्', | ||||
|                     title: 'कुनै सम्बन्धहरू छैनन्', | ||||
|                     description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -256,6 +249,7 @@ export const ne: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'कुनै enum मानहरू परिभाषित छैनन्', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -278,7 +272,7 @@ export const ne: LanguageTranslation = { | ||||
|             show_all: 'सबै देखाउनुहोस्', | ||||
|             undo: 'पूर्ववत', | ||||
|             redo: 'पुनः गर्नुहोस्', | ||||
|             reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्', | ||||
|             reorder_diagram: 'डायाग्राम स्वचालित मिलाउनुहोस्', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -323,7 +317,7 @@ export const ne: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'डायाग्राम खोल्नुहोस्', | ||||
|             title: 'डाटाबेस खोल्नुहोस्', | ||||
|             description: | ||||
|                 'तलको सूचीबाट खोल्नका लागि एक डायाग्राम चयन गर्नुहोस्।', | ||||
|             table_columns: { | ||||
| @@ -334,6 +328,12 @@ export const ne: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द गर्नुहोस्', | ||||
|             open: 'खोल्नुहोस्', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'खोल्नुहोस्', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'मेटाउनुहोस्', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -483,6 +483,7 @@ export const ne: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नयाँ तालिका', | ||||
|             new_view: 'नयाँ भ्यू', | ||||
|             new_relationship: 'नयाँ सम्बन्ध', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -504,6 +505,9 @@ export const ne: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा परिवर्तन गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         on: 'सक्रिय', | ||||
|         off: 'निष्क्रिय', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const pt_BR: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Novo', | ||||
|             browse: 'Navegar', | ||||
|             tables: 'Tabelas', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Áreas', | ||||
|             dependencies: 'Dependências', | ||||
|             custom_types: 'Tipos Personalizados', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Arquivo', | ||||
|                 new: 'Novo', | ||||
|                 open: 'Abrir', | ||||
|             actions: { | ||||
|                 actions: 'Ações', | ||||
|                 new: 'Novo...', | ||||
|                 browse: 'Navegar...', | ||||
|                 save: 'Salvar', | ||||
|                 import: 'Importar Banco de Dados', | ||||
|                 export_sql: 'Exportar SQL', | ||||
|                 export_as: 'Exportar como', | ||||
|                 delete_diagram: 'Excluir Diagrama', | ||||
|                 exit: 'Sair', | ||||
|                 delete_diagram: 'Excluir', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Editar', | ||||
| @@ -29,6 +37,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|                 hide_field_attributes: 'Ocultar Atributos de Campo', | ||||
|                 show_field_attributes: 'Mostrar Atributos de Campo', | ||||
|                 zoom_on_scroll: 'Zoom ao Rolar', | ||||
|                 show_views: 'Visualizações do Banco de Dados', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Mostrar Dependências', | ||||
|                 hide_dependencies: 'Ocultar Dependências', | ||||
| @@ -66,22 +75,13 @@ export const pt_BR: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Reordenar Diagrama', | ||||
|             title: 'Organizar Diagrama Automaticamente', | ||||
|             description: | ||||
|                 'Esta ação reorganizará todas as tabelas no diagrama. Deseja continuar?', | ||||
|             reorder: 'Reordenar', | ||||
|             reorder: 'Organizar Automaticamente', | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Múltiplos Esquemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'nenhum', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Falha na cópia', | ||||
| @@ -116,14 +116,11 @@ export const pt_BR: LanguageTranslation = { | ||||
|         copied: 'Copiado!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Esquema:', | ||||
|             filter_by_schema: 'Filtrar por esquema', | ||||
|             search_schema: 'Buscar esquema...', | ||||
|             no_schemas_found: 'Nenhum esquema encontrado.', | ||||
|             view_all_options: 'Ver todas as Opções...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabelas', | ||||
|                 add_table: 'Adicionar Tabela', | ||||
|                 add_view: 'Adicionar Visualização', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 // TODO: Translate | ||||
| @@ -149,6 +146,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atributos do Campo', | ||||
|                         unique: 'Único', | ||||
|                         auto_increment: 'Incremento Automático', | ||||
|                         comments: 'Comentários', | ||||
|                         no_comments: 'Sem comentários', | ||||
|                         delete_field: 'Excluir Campo', | ||||
| @@ -164,6 +162,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|                         title: 'Atributos do Índice', | ||||
|                         name: 'Nome', | ||||
|                         unique: 'Único', | ||||
|                         index_type: 'Tipo de Índice', | ||||
|                         delete_index: 'Excluir Índice', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -180,12 +179,15 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     description: 'Crie uma tabela para começar', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relacionamentos', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrar', | ||||
|                 add_relationship: 'Adicionar Relacionamento', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 add_relationship: 'Adicionar Relacionamento', | ||||
|                 relationships: 'Relacionamentos', | ||||
|                 dependencies: 'Dependências', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relacionamento', | ||||
|                     primary: 'Tabela Primária', | ||||
|                     foreign: 'Tabela Referenciada', | ||||
|                     cardinality: 'Cardinalidade', | ||||
| @@ -195,16 +197,8 @@ export const pt_BR: LanguageTranslation = { | ||||
|                         delete_relationship: 'Excluir', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sem relacionamentos', | ||||
|                     description: 'Crie um relacionamento para conectar tabelas', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependências', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependência', | ||||
|                     table: 'Tabela', | ||||
|                     dependent_table: 'Visualização Dependente', | ||||
|                     delete_dependency: 'Excluir', | ||||
| @@ -214,8 +208,8 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sem dependências', | ||||
|                     description: 'Crie uma visualização para começar', | ||||
|                     title: 'Sem relacionamentos', | ||||
|                     description: 'Crie um relacionamento para começar', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -255,6 +249,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Nenhum valor de enum definido', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -277,7 +272,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|             show_all: 'Mostrar Tudo', | ||||
|             undo: 'Desfazer', | ||||
|             redo: 'Refazer', | ||||
|             reorder_diagram: 'Reordenar Diagrama', | ||||
|             reorder_diagram: 'Organizar Diagrama Automaticamente', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -322,7 +317,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Abrir Diagrama', | ||||
|             title: 'Abrir Banco de Dados', | ||||
|             description: 'Selecione um diagrama para abrir da lista abaixo.', | ||||
|             table_columns: { | ||||
|                 name: 'Nome', | ||||
| @@ -332,6 +327,12 @@ export const pt_BR: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Cancelar', | ||||
|             open: 'Abrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Abrir', | ||||
|                 duplicate: 'Duplicar', | ||||
|                 delete: 'Excluir', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -481,6 +482,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nova Tabela', | ||||
|             new_view: 'Nova Visualização', | ||||
|             new_relationship: 'Novo Relacionamento', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -503,6 +505,9 @@ export const pt_BR: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Idioma', | ||||
|         }, | ||||
|  | ||||
|         on: 'Ligado', | ||||
|         off: 'Desligado', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ru: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Новая', | ||||
|             browse: 'Обзор', | ||||
|             tables: 'Таблицы', | ||||
|             refs: 'Ссылки', | ||||
|             areas: 'Области', | ||||
|             dependencies: 'Зависимости', | ||||
|             custom_types: 'Пользовательские типы', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Файл', | ||||
|                 new: 'Создать', | ||||
|                 open: 'Открыть', | ||||
|             actions: { | ||||
|                 actions: 'Действия', | ||||
|                 new: 'Новая...', | ||||
|                 browse: 'Обзор...', | ||||
|                 save: 'Сохранить', | ||||
|                 import: 'Импортировать базу данных', | ||||
|                 export_sql: 'Экспорт SQL', | ||||
|                 export_as: 'Экспортировать как', | ||||
|                 delete_diagram: 'Удалить диаграмму', | ||||
|                 exit: 'Выход', | ||||
|                 delete_diagram: 'Удалить', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Изменение', | ||||
| @@ -29,6 +37,7 @@ export const ru: LanguageTranslation = { | ||||
|                 show_field_attributes: 'Показать атрибуты поля', | ||||
|                 hide_field_attributes: 'Скрыть атрибуты поля', | ||||
|                 zoom_on_scroll: 'Увеличение при прокрутке', | ||||
|                 show_views: 'Представления базы данных', | ||||
|                 theme: 'Тема', | ||||
|                 show_dependencies: 'Показать зависимости', | ||||
|                 hide_dependencies: 'Скрыть зависимости', | ||||
| @@ -64,22 +73,13 @@ export const ru: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Переупорядочить диаграмму', | ||||
|             title: 'Автоматическая расстановка диаграммы', | ||||
|             description: | ||||
|                 'Это действие переставит все таблицы на диаграмме. Хотите продолжить?', | ||||
|             reorder: 'Изменить порядок', | ||||
|             reorder: 'Автоматическая расстановка', | ||||
|             cancel: 'Отменить', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Множественные схемы', | ||||
|             description: | ||||
|                 '{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'никто', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Ошибка копирования', | ||||
| @@ -113,14 +113,11 @@ export const ru: LanguageTranslation = { | ||||
|         show_less: 'Показать меньше', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Схема:', | ||||
|             filter_by_schema: 'Фильтр по схеме', | ||||
|             search_schema: 'Схема поиска...', | ||||
|             no_schemas_found: 'Схемы не найдены.', | ||||
|             view_all_options: 'Просмотреть все варианты...', | ||||
|             tables_section: { | ||||
|                 tables: 'Таблицы', | ||||
|                 add_table: 'Добавить таблицу', | ||||
|                 add_view: 'Добавить представление', | ||||
|                 filter: 'Фильтр', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 clear: 'Очистить фильтр', | ||||
| @@ -146,6 +143,7 @@ export const ru: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Атрибуты поля', | ||||
|                         unique: 'Уникальный', | ||||
|                         auto_increment: 'Автоинкремент', | ||||
|                         comments: 'Комментарии', | ||||
|                         no_comments: 'Нет комментария', | ||||
|                         delete_field: 'Удалить поле', | ||||
| @@ -160,6 +158,7 @@ export const ru: LanguageTranslation = { | ||||
|                         title: 'Атрибуты индекса', | ||||
|                         name: 'Имя', | ||||
|                         unique: 'Уникальный', | ||||
|                         index_type: 'Тип индекса', | ||||
|                         delete_index: 'Удалить индекс', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -176,12 +175,15 @@ export const ru: LanguageTranslation = { | ||||
|                     description: 'Создайте таблицу, чтобы начать', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Отношения', | ||||
|             refs_section: { | ||||
|                 refs: 'Ссылки', | ||||
|                 filter: 'Фильтр', | ||||
|                 add_relationship: 'Добавить отношение', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 add_relationship: 'Добавить отношение', | ||||
|                 relationships: 'Отношения', | ||||
|                 dependencies: 'Зависимости', | ||||
|                 relationship: { | ||||
|                     relationship: 'Отношение', | ||||
|                     primary: 'Основная таблица', | ||||
|                     foreign: 'Справочная таблица', | ||||
|                     cardinality: 'Тип множественной связи', | ||||
| @@ -191,18 +193,10 @@ export const ru: LanguageTranslation = { | ||||
|                         delete_relationship: 'Удалить', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Нет отношений', | ||||
|                     description: 'Создайте связь для соединения таблиц', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Зависимости', | ||||
|                 filter: 'Фильтр', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 dependency: { | ||||
|                     table: 'Стол', | ||||
|                     dependent_table: 'Зависимый вид', | ||||
|                     dependency: 'Зависимость', | ||||
|                     table: 'Таблица', | ||||
|                     dependent_table: 'Зависимое представление', | ||||
|                     delete_dependency: 'Удалить', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Действия', | ||||
| @@ -210,8 +204,8 @@ export const ru: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Нет зависимостей', | ||||
|                     description: 'Создайте представление, чтобы начать', | ||||
|                     title: 'Нет отношений', | ||||
|                     description: 'Создайте отношение, чтобы начать', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -252,6 +246,7 @@ export const ru: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Значения перечисления не определены', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -274,7 +269,7 @@ export const ru: LanguageTranslation = { | ||||
|             show_all: 'Показать все', | ||||
|             undo: 'Отменить', | ||||
|             redo: 'Вернуть', | ||||
|             reorder_diagram: 'Переупорядочить диаграмму', | ||||
|             reorder_diagram: 'Автоматическая расстановка диаграммы', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -318,7 +313,7 @@ export const ru: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Открыть диаграмму', | ||||
|             title: 'Открыть базу данных', | ||||
|             description: | ||||
|                 'Выберите диаграмму, которую нужно открыть, из списка ниже.', | ||||
|             table_columns: { | ||||
| @@ -329,6 +324,12 @@ export const ru: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Отмена', | ||||
|             open: 'Открыть', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Открыть', | ||||
|                 duplicate: 'Дублировать', | ||||
|                 delete: 'Удалить', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -477,6 +478,7 @@ export const ru: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Создать таблицу', | ||||
|             new_view: 'Новое представление', | ||||
|             new_relationship: 'Создать отношение', | ||||
|             new_area: 'Новая область', | ||||
|         }, | ||||
| @@ -498,6 +500,9 @@ export const ru: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Сменить язык', | ||||
|         }, | ||||
|  | ||||
|         on: 'Вкл', | ||||
|         off: 'Выкл', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const te: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'కొత్తది', | ||||
|             browse: 'బ్రాఉజ్', | ||||
|             tables: 'టేబల్లు', | ||||
|             refs: 'సంబంధాలు', | ||||
|             areas: 'ప్రదేశాలు', | ||||
|             dependencies: 'ఆధారతలు', | ||||
|             custom_types: 'కస్టమ్ టైప్స్', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ఫైల్', | ||||
|                 new: 'కొత్తది', | ||||
|                 open: 'తెరవు', | ||||
|             actions: { | ||||
|                 actions: 'చర్యలు', | ||||
|                 new: 'కొత్తది...', | ||||
|                 browse: 'బ్రాఉజ్ చేయండి...', | ||||
|                 save: 'సేవ్', | ||||
|                 import: 'డేటాబేస్ను దిగుమతి చేసుకోండి', | ||||
|                 export_sql: 'SQL ఎగుమతి', | ||||
|                 export_as: 'వగా ఎగుమతి చేయండి', | ||||
|                 delete_diagram: 'చిత్రాన్ని తొలగించండి', | ||||
|                 exit: 'నిష్క్రమించు', | ||||
|                 delete_diagram: 'తొలగించండి', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'సవరించు', | ||||
| @@ -29,6 +37,7 @@ export const te: LanguageTranslation = { | ||||
|                 show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు', | ||||
|                 hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి', | ||||
|                 zoom_on_scroll: 'స్క్రోల్పై జూమ్', | ||||
|                 show_views: 'డేటాబేస్ వ్యూలు', | ||||
|                 theme: 'థీమ్', | ||||
|                 show_dependencies: 'ఆధారాలు చూపించండి', | ||||
|                 hide_dependencies: 'ఆధారాలను దాచండి', | ||||
| @@ -66,22 +75,13 @@ export const te: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'చిత్రాన్ని పునఃసరిచేయండి', | ||||
|             title: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి', | ||||
|             description: | ||||
|                 'ఈ చర్య చిత్రంలోని అన్ని పట్టికలను పునఃస్థాపిస్తుంది. మీరు కొనసాగించాలనుకుంటున్నారా?', | ||||
|             reorder: 'పునఃసరిచేయండి', | ||||
|             reorder: 'స్వయంచాలకంగా అమర్చండి', | ||||
|             cancel: 'రద్దు', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'బహుళ స్కీమాలు', | ||||
|             description: | ||||
|                 '{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'ఎదరికాదు', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'కాపీ విఫలమైంది', | ||||
| @@ -116,14 +116,11 @@ export const te: LanguageTranslation = { | ||||
|         copied: 'కాపీ చేయబడింది!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'స్కీమా:', | ||||
|             filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి', | ||||
|             search_schema: 'స్కీమా కోసం శోధించండి...', | ||||
|             no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.', | ||||
|             view_all_options: 'అన్ని ఎంపికలను చూడండి...', | ||||
|             tables_section: { | ||||
|                 tables: 'పట్టికలు', | ||||
|                 add_table: 'పట్టికను జోడించు', | ||||
|                 add_view: 'వ్యూ జోడించండి', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 // TODO: Translate | ||||
| @@ -149,6 +146,7 @@ export const te: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ఫీల్డ్ గుణాలు', | ||||
|                         unique: 'అద్వితీయ', | ||||
|                         auto_increment: 'ఆటో ఇంక్రిమెంట్', | ||||
|                         comments: 'వ్యాఖ్యలు', | ||||
|                         no_comments: 'వ్యాఖ్యలు లేవు', | ||||
|                         delete_field: 'ఫీల్డ్ తొలగించు', | ||||
| @@ -164,6 +162,7 @@ export const te: LanguageTranslation = { | ||||
|                         title: 'ఇండెక్స్ గుణాలు', | ||||
|                         name: 'పేరు', | ||||
|                         unique: 'అద్వితీయ', | ||||
|                         index_type: 'ఇండెక్స్ రకం', | ||||
|                         delete_index: 'ఇండెక్స్ తొలగించు', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -181,12 +180,15 @@ export const te: LanguageTranslation = { | ||||
|                     description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'సంబంధాలు', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 add_relationship: 'సంబంధం జోడించు', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 add_relationship: 'సంబంధం జోడించు', | ||||
|                 relationships: 'సంబంధాలు', | ||||
|                 dependencies: 'ఆధారాలు', | ||||
|                 relationship: { | ||||
|                     relationship: 'సంబంధం', | ||||
|                     primary: 'ప్రాథమిక పట్టిక', | ||||
|                     foreign: 'సూచించబడిన పట్టిక', | ||||
|                     cardinality: 'కార్డినాలిటీ', | ||||
| @@ -196,16 +198,8 @@ export const te: LanguageTranslation = { | ||||
|                         delete_relationship: 'సంబంధం తొలగించు', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'సంబంధాలు లేవు', | ||||
|                     description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'ఆధారాలు', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 dependency: { | ||||
|                     dependency: 'ఆధారం', | ||||
|                     table: 'పట్టిక', | ||||
|                     dependent_table: 'ఆధారిత వీక్షణ', | ||||
|                     delete_dependency: 'ఆధారాన్ని తొలగించు', | ||||
| @@ -215,8 +209,8 @@ export const te: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'ఆధారాలు లేవు', | ||||
|                     description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి', | ||||
|                     title: 'సంబంధాలు లేవు', | ||||
|                     description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -256,6 +250,7 @@ export const te: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'ఏ enum విలువలు నిర్వచించబడలేదు', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -278,7 +273,7 @@ export const te: LanguageTranslation = { | ||||
|             show_all: 'అన్ని చూపించు', | ||||
|             undo: 'తిరిగి చేయు', | ||||
|             redo: 'మరలా చేయు', | ||||
|             reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి', | ||||
|             reorder_diagram: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -323,7 +318,7 @@ export const te: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'చిత్రం తెరవండి', | ||||
|             title: 'డేటాబేస్ తెరవండి', | ||||
|             description: 'కింద ఉన్న జాబితా నుండి చిత్రాన్ని ఎంచుకోండి.', | ||||
|             table_columns: { | ||||
|                 name: 'పేరు', | ||||
| @@ -333,6 +328,12 @@ export const te: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'రద్దు', | ||||
|             open: 'తెరవు', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'తెరవు', | ||||
|                 duplicate: 'నకలు', | ||||
|                 delete: 'తొలగించు', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -485,6 +486,7 @@ export const te: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'కొత్త పట్టిక', | ||||
|             new_view: 'కొత్త వ్యూ', | ||||
|             new_relationship: 'కొత్త సంబంధం', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -508,6 +510,9 @@ export const te: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'భాష మార్చు', | ||||
|         }, | ||||
|  | ||||
|         on: 'ఆన్', | ||||
|         off: 'ఆఫ్', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const tr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Yeni', | ||||
|             browse: 'Gözat', | ||||
|             tables: 'Tablolar', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Alanlar', | ||||
|             dependencies: 'Bağımlılıklar', | ||||
|             custom_types: 'Özel Tipler', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Dosya', | ||||
|                 new: 'Yeni', | ||||
|                 open: 'Aç', | ||||
|             actions: { | ||||
|                 actions: 'Eylemler', | ||||
|                 new: 'Yeni...', | ||||
|                 browse: 'Gözat...', | ||||
|                 save: 'Kaydet', | ||||
|                 import: 'Veritabanı İçe Aktar', | ||||
|                 export_sql: 'SQL Olarak Dışa Aktar', | ||||
|                 export_as: 'Olarak Dışa Aktar', | ||||
|                 delete_diagram: 'Diyagramı Sil', | ||||
|                 exit: 'Çıkış', | ||||
|                 delete_diagram: 'Sil', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Düzenle', | ||||
| @@ -29,6 +37,7 @@ export const tr: LanguageTranslation = { | ||||
|                 show_field_attributes: 'Alan Özelliklerini Göster', | ||||
|                 hide_field_attributes: 'Alan Özelliklerini Gizle', | ||||
|                 zoom_on_scroll: 'Kaydırarak Yakınlaştır', | ||||
|                 show_views: 'Veritabanı Görünümleri', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Bağımlılıkları Göster', | ||||
|                 hide_dependencies: 'Bağımlılıkları Gizle', | ||||
| @@ -66,22 +75,13 @@ export const tr: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Diyagramı Yeniden Sırala', | ||||
|             title: 'Diyagramı Otomatik Düzenle', | ||||
|             description: | ||||
|                 'Bu işlem tüm tabloları yeniden düzenleyecektir. Devam etmek istiyor musunuz?', | ||||
|             reorder: 'Yeniden Sırala', | ||||
|             reorder: 'Otomatik Düzenle', | ||||
|             cancel: 'İptal', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Birden Fazla Şema', | ||||
|             description: | ||||
|                 'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'yok', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopyalama başarısız', | ||||
| @@ -115,14 +115,11 @@ export const tr: LanguageTranslation = { | ||||
|         copy_to_clipboard: 'Panoya Kopyala', | ||||
|         copied: 'Kopyalandı!', | ||||
|         side_panel: { | ||||
|             schema: 'Şema:', | ||||
|             filter_by_schema: 'Şemaya Göre Filtrele', | ||||
|             search_schema: 'Şema ara...', | ||||
|             no_schemas_found: 'Şema bulunamadı.', | ||||
|             view_all_options: 'Tüm Seçenekleri Gör...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablolar', | ||||
|                 add_table: 'Tablo Ekle', | ||||
|                 add_view: 'Görünüm Ekle', | ||||
|                 filter: 'Filtrele', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 // TODO: Translate | ||||
| @@ -148,6 +145,7 @@ export const tr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Alan Özellikleri', | ||||
|                         unique: 'Tekil', | ||||
|                         auto_increment: 'Otomatik Artış', | ||||
|                         comments: 'Yorumlar', | ||||
|                         no_comments: 'Yorum yok', | ||||
|                         delete_field: 'Alanı Sil', | ||||
| @@ -163,6 +161,7 @@ export const tr: LanguageTranslation = { | ||||
|                         title: 'İndeks Özellikleri', | ||||
|                         name: 'Ad', | ||||
|                         unique: 'Tekil', | ||||
|                         index_type: 'İndeks Türü', | ||||
|                         delete_index: 'İndeksi Sil', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -180,12 +179,15 @@ export const tr: LanguageTranslation = { | ||||
|                     description: 'Başlamak için bir tablo oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'İlişkiler', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrele', | ||||
|                 add_relationship: 'İlişki Ekle', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 add_relationship: 'İlişki Ekle', | ||||
|                 relationships: 'İlişkiler', | ||||
|                 dependencies: 'Bağımlılıklar', | ||||
|                 relationship: { | ||||
|                     relationship: 'İlişki', | ||||
|                     primary: 'Birincil Tablo', | ||||
|                     foreign: 'Referans Tablo', | ||||
|                     cardinality: 'Kardinalite', | ||||
| @@ -195,16 +197,8 @@ export const tr: LanguageTranslation = { | ||||
|                         delete_relationship: 'Sil', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'İlişki yok', | ||||
|                     description: 'Tabloları bağlamak için bir ilişki oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Bağımlılıklar', | ||||
|                 filter: 'Filtrele', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 dependency: { | ||||
|                     dependency: 'Bağımlılık', | ||||
|                     table: 'Tablo', | ||||
|                     dependent_table: 'Bağımlı Görünüm', | ||||
|                     delete_dependency: 'Sil', | ||||
| @@ -214,8 +208,8 @@ export const tr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Bağımlılık yok', | ||||
|                     description: 'Başlamak için bir görünüm oluşturun', | ||||
|                     title: 'İlişki yok', | ||||
|                     description: 'Başlamak için bir ilişki oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -255,6 +249,7 @@ export const tr: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Tanımlanmış enum değeri yok', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -276,7 +271,7 @@ export const tr: LanguageTranslation = { | ||||
|             show_all: 'Hepsini Gör', | ||||
|             undo: 'Geri Al', | ||||
|             redo: 'Yinele', | ||||
|             reorder_diagram: 'Diyagramı Yeniden Sırala', | ||||
|             reorder_diagram: 'Diyagramı Otomatik Düzenle', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -318,7 +313,7 @@ export const tr: LanguageTranslation = { | ||||
|             import: 'İçe Aktar', | ||||
|         }, | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Diyagramı Aç', | ||||
|             title: 'Veritabanı Aç', | ||||
|             description: 'Aşağıdaki listeden açmak için bir diyagram seçin.', | ||||
|             table_columns: { | ||||
|                 name: 'Ad', | ||||
| @@ -328,6 +323,12 @@ export const tr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'İptal', | ||||
|             open: 'Aç', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Aç', | ||||
|                 duplicate: 'Kopyala', | ||||
|                 delete: 'Sil', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -470,6 +471,7 @@ export const tr: LanguageTranslation = { | ||||
|         }, | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Yeni Tablo', | ||||
|             new_view: 'Yeni Görünüm', | ||||
|             new_relationship: 'Yeni İlişki', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -492,6 +494,9 @@ export const tr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Dil', | ||||
|         }, | ||||
|  | ||||
|         on: 'Açık', | ||||
|         off: 'Kapalı', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const uk: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Нова', | ||||
|             browse: 'Огляд', | ||||
|             tables: 'Таблиці', | ||||
|             refs: 'Зв’язки', | ||||
|             areas: 'Області', | ||||
|             dependencies: 'Залежності', | ||||
|             custom_types: 'Користувацькі типи', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Файл', | ||||
|                 new: 'Новий', | ||||
|                 open: 'Відкрити', | ||||
|             actions: { | ||||
|                 actions: 'Дії', | ||||
|                 new: 'Нова...', | ||||
|                 browse: 'Огляд...', | ||||
|                 save: 'Зберегти', | ||||
|                 import: 'Імпорт бази даних', | ||||
|                 export_sql: 'Експорт SQL', | ||||
|                 export_as: 'Експортувати як', | ||||
|                 delete_diagram: 'Видалити діаграму', | ||||
|                 exit: 'Вийти', | ||||
|                 delete_diagram: 'Видалити', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Редагувати', | ||||
| @@ -29,6 +37,7 @@ export const uk: LanguageTranslation = { | ||||
|                 show_field_attributes: 'Показати атрибути полів', | ||||
|                 hide_field_attributes: 'Приховати атрибути полів', | ||||
|                 zoom_on_scroll: 'Масштабувати прокручуванням', | ||||
|                 show_views: 'Представлення бази даних', | ||||
|                 theme: 'Тема', | ||||
|                 show_dependencies: 'Показати залежності', | ||||
|                 hide_dependencies: 'Приховати залежності', | ||||
| @@ -64,22 +73,13 @@ export const uk: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Перевпорядкувати діаграму', | ||||
|             title: 'Автоматичне розміщення діаграми', | ||||
|             description: | ||||
|                 'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?', | ||||
|             reorder: 'Перевпорядкувати', | ||||
|             reorder: 'Автоматичне розміщення', | ||||
|             cancel: 'Скасувати', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Кілька схем', | ||||
|             description: | ||||
|                 '{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'немає', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Помилка копіювання', | ||||
| @@ -114,14 +114,11 @@ export const uk: LanguageTranslation = { | ||||
|         copied: 'Скопійовано!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Схема:', | ||||
|             filter_by_schema: 'Фільтрувати за схемою', | ||||
|             search_schema: 'Пошук схеми…', | ||||
|             no_schemas_found: 'Схеми не знайдено.', | ||||
|             view_all_options: 'Переглянути всі параметри…', | ||||
|             tables_section: { | ||||
|                 tables: 'Таблиці', | ||||
|                 add_table: 'Додати таблицю', | ||||
|                 add_view: 'Додати представлення', | ||||
|                 filter: 'Фільтр', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 // TODO: Translate | ||||
| @@ -147,6 +144,7 @@ export const uk: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Атрибути полів', | ||||
|                         unique: 'Унікальне', | ||||
|                         auto_increment: 'Автоінкремент', | ||||
|                         comments: 'Коментарі', | ||||
|                         no_comments: 'Немає коментарів', | ||||
|                         delete_field: 'Видалити поле', | ||||
| @@ -162,6 +160,7 @@ export const uk: LanguageTranslation = { | ||||
|                         title: 'Атрибути індексу', | ||||
|                         name: 'Назва індекса', | ||||
|                         unique: 'Унікальний', | ||||
|                         index_type: 'Тип індексу', | ||||
|                         delete_index: 'Видалити індекс', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -178,12 +177,15 @@ export const uk: LanguageTranslation = { | ||||
|                     description: 'Щоб почати, створіть таблицю', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Звʼязки', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Фільтр', | ||||
|                 add_relationship: 'Додати звʼязок', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 add_relationship: 'Додати звʼязок', | ||||
|                 relationships: 'Звʼязки', | ||||
|                 dependencies: 'Залежності', | ||||
|                 relationship: { | ||||
|                     relationship: 'Звʼязок', | ||||
|                     primary: 'Первинна таблиця', | ||||
|                     foreign: 'Посилання на таблицю', | ||||
|                     cardinality: 'Звʼязок', | ||||
| @@ -193,16 +195,8 @@ export const uk: LanguageTranslation = { | ||||
|                         delete_relationship: 'Видалити', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Звʼязків немає', | ||||
|                     description: 'Створіть звʼязок для зʼєднання таблиць', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Залежності', | ||||
|                 filter: 'Фільтр', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 dependency: { | ||||
|                     dependency: 'Залежність', | ||||
|                     table: 'Таблиця', | ||||
|                     dependent_table: 'Залежне подання', | ||||
|                     delete_dependency: 'Видалити', | ||||
| @@ -212,8 +206,8 @@ export const uk: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Жодних залежностей', | ||||
|                     description: 'Створіть подання, щоб почати', | ||||
|                     title: 'Жодних зв’язків', | ||||
|                     description: 'Створіть зв’язок, щоб почати', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -253,6 +247,7 @@ export const uk: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Значення переліку не визначені', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -275,7 +270,7 @@ export const uk: LanguageTranslation = { | ||||
|             show_all: 'Показати все', | ||||
|             undo: 'Скасувати', | ||||
|             redo: 'Повторити', | ||||
|             reorder_diagram: 'Перевпорядкувати діаграму', | ||||
|             reorder_diagram: 'Автоматичне розміщення діаграми', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -319,7 +314,7 @@ export const uk: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Відкрити діаграму', | ||||
|             title: 'Відкрити базу даних', | ||||
|             description: | ||||
|                 'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.', | ||||
|             table_columns: { | ||||
| @@ -330,6 +325,12 @@ export const uk: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Скасувати', | ||||
|             open: 'Відкрити', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Відкрити', | ||||
|                 duplicate: 'Дублювати', | ||||
|                 delete: 'Видалити', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -476,6 +477,7 @@ export const uk: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Нова таблиця', | ||||
|             new_view: 'Нове представлення', | ||||
|             new_relationship: 'Новий звʼязок', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -497,6 +499,9 @@ export const uk: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Мова', | ||||
|         }, | ||||
|  | ||||
|         on: 'Увімк', | ||||
|         off: 'Вимк', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const vi: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Mới', | ||||
|             browse: 'Duyệt', | ||||
|             tables: 'Bảng', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Khu vực', | ||||
|             dependencies: 'Phụ thuộc', | ||||
|             custom_types: 'Kiểu tùy chỉnh', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Tệp', | ||||
|                 new: 'Tạo mới', | ||||
|                 open: 'Mở', | ||||
|             actions: { | ||||
|                 actions: 'Hành động', | ||||
|                 new: 'Mới...', | ||||
|                 browse: 'Duyệt...', | ||||
|                 save: 'Lưu', | ||||
|                 import: 'Nhập cơ sở dữ liệu', | ||||
|                 export_sql: 'Xuất SQL', | ||||
|                 export_as: 'Xuất thành', | ||||
|                 delete_diagram: 'Xóa sơ đồ', | ||||
|                 exit: 'Thoát', | ||||
|                 delete_diagram: 'Xóa', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Sửa', | ||||
| @@ -29,6 +37,7 @@ export const vi: LanguageTranslation = { | ||||
|                 show_field_attributes: 'Hiển thị thuộc tính trường', | ||||
|                 hide_field_attributes: 'Ẩn thuộc tính trường', | ||||
|                 zoom_on_scroll: 'Thu phóng khi cuộn', | ||||
|                 show_views: 'Chế độ xem Cơ sở dữ liệu', | ||||
|                 theme: 'Chủ đề', | ||||
|                 show_dependencies: 'Hiển thị các phụ thuộc', | ||||
|                 hide_dependencies: 'Ẩn các phụ thuộc', | ||||
| @@ -65,22 +74,13 @@ export const vi: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Sắp xếp lại sơ đồ', | ||||
|             title: 'Tự động sắp xếp sơ đồ', | ||||
|             description: | ||||
|                 'Hành động này sẽ sắp xếp lại tất cả các bảng trong sơ đồ. Bạn có muốn tiếp tục không?', | ||||
|             reorder: 'Sắp xếp', | ||||
|             reorder: 'Tự động sắp xếp', | ||||
|             cancel: 'Hủy', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Có nhiều lược đồ', | ||||
|             description: | ||||
|                 'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: 'không có', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Sao chép thất bại', | ||||
| @@ -115,14 +115,11 @@ export const vi: LanguageTranslation = { | ||||
|         copied: 'Đã sao chép!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Lược đồ:', | ||||
|             filter_by_schema: 'Lọc bởi lược đồ', | ||||
|             search_schema: 'Tìm kiếm lược đồ...', | ||||
|             no_schemas_found: 'Không tìm thấy lược đồ.', | ||||
|             view_all_options: 'Xem tất cả tùy chọn...', | ||||
|             tables_section: { | ||||
|                 tables: 'Bảng', | ||||
|                 add_table: 'Thêm bảng', | ||||
|                 add_view: 'Thêm Chế độ xem', | ||||
|                 filter: 'Lọc', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 // TODO: Translate | ||||
| @@ -148,6 +145,7 @@ export const vi: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Thuộc tính trường', | ||||
|                         unique: 'Giá trị duy nhất', | ||||
|                         auto_increment: 'Tự động tăng', | ||||
|                         comments: 'Bình luận', | ||||
|                         no_comments: 'Không có bình luận', | ||||
|                         delete_field: 'Xóa trường', | ||||
| @@ -163,6 +161,7 @@ export const vi: LanguageTranslation = { | ||||
|                         title: 'Thuộc tính chỉ mục', | ||||
|                         name: 'Tên', | ||||
|                         unique: 'Giá trị duy nhất', | ||||
|                         index_type: 'Loại chỉ mục', | ||||
|                         delete_index: 'Xóa chỉ mục', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -179,12 +178,15 @@ export const vi: LanguageTranslation = { | ||||
|                     description: 'Tạo một bảng để bắt đầu', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Quan hệ', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Lọc', | ||||
|                 add_relationship: 'Thêm quan hệ', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 add_relationship: 'Thêm quan hệ', | ||||
|                 relationships: 'Quan hệ', | ||||
|                 dependencies: 'Phụ thuộc', | ||||
|                 relationship: { | ||||
|                     relationship: 'Quan hệ', | ||||
|                     primary: 'Bảng khóa chính', | ||||
|                     foreign: 'Bảng khóa ngoại', | ||||
|                     cardinality: 'Quan hệ', | ||||
| @@ -194,16 +196,8 @@ export const vi: LanguageTranslation = { | ||||
|                         delete_relationship: 'Xóa', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Không có quan hệ', | ||||
|                     description: 'Tạo quan hệ để kết nối các bảng', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Phụ thuộc', | ||||
|                 filter: 'Lọc', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 dependency: { | ||||
|                     dependency: 'Phụ thuộc', | ||||
|                     table: 'Bảng', | ||||
|                     dependent_table: 'Bảng xem phụ thuộc', | ||||
|                     delete_dependency: 'Xóa', | ||||
| @@ -213,8 +207,8 @@ export const vi: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Không có phụ thuộc', | ||||
|                     description: 'Tạo bảng xem phụ thuộc để bắt đầu', | ||||
|                     title: 'Không có quan hệ', | ||||
|                     description: 'Tạo một quan hệ để bắt đầu', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -254,6 +248,7 @@ export const vi: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: 'Không có giá trị enum được định nghĩa', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -276,7 +271,7 @@ export const vi: LanguageTranslation = { | ||||
|             show_all: 'Hiển thị tất cả', | ||||
|             undo: 'Hoàn tác', | ||||
|             redo: 'Làm lại', | ||||
|             reorder_diagram: 'Sắp xếp lại sơ đồ', | ||||
|             reorder_diagram: 'Tự động sắp xếp sơ đồ', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -320,7 +315,7 @@ export const vi: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Mở sơ đồ', | ||||
|             title: 'Mở cơ sở dữ liệu', | ||||
|             description: 'Chọn sơ đồ để mở từ danh sách bên dưới.', | ||||
|             table_columns: { | ||||
|                 name: 'Tên', | ||||
| @@ -330,6 +325,12 @@ export const vi: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Hủy', | ||||
|             open: 'Mở', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Mở', | ||||
|                 duplicate: 'Nhân bản', | ||||
|                 delete: 'Xóa', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -477,6 +478,7 @@ export const vi: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Tạo bảng mới', | ||||
|             new_view: 'Chế độ xem Mới', | ||||
|             new_relationship: 'Tạo quan hệ mới', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -498,6 +500,9 @@ export const vi: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Ngôn ngữ', | ||||
|         }, | ||||
|  | ||||
|         on: 'Bật', | ||||
|         off: 'Tắt', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const zh_CN: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新建', | ||||
|             browse: '浏览', | ||||
|             tables: '表', | ||||
|             refs: '引用', | ||||
|             areas: '区域', | ||||
|             dependencies: '依赖关系', | ||||
|             custom_types: '自定义类型', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '文件', | ||||
|                 new: '新建', | ||||
|                 open: '打开', | ||||
|             actions: { | ||||
|                 actions: '操作', | ||||
|                 new: '新建...', | ||||
|                 browse: '浏览...', | ||||
|                 save: '保存', | ||||
|                 import: '导入数据库', | ||||
|                 export_sql: '导出 SQL 语句', | ||||
|                 export_as: '导出为', | ||||
|                 delete_diagram: '删除关系图', | ||||
|                 exit: '退出', | ||||
|                 delete_diagram: '删除', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '编辑', | ||||
| @@ -29,6 +37,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|                 show_field_attributes: '展示字段属性', | ||||
|                 hide_field_attributes: '隐藏字段属性', | ||||
|                 zoom_on_scroll: '滚动缩放', | ||||
|                 show_views: '数据库视图', | ||||
|                 theme: '主题', | ||||
|                 show_dependencies: '展示依赖', | ||||
|                 hide_dependencies: '隐藏依赖', | ||||
| @@ -63,21 +72,12 @@ export const zh_CN: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: '重新排列关系图', | ||||
|             title: '自动排列关系图', | ||||
|             description: '此操作将重新排列关系图中的所有表。是否要继续?', | ||||
|             reorder: '重新排列', | ||||
|             reorder: '自动排列', | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '多个模式', | ||||
|             description: | ||||
|                 '此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: '无', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '复制失败', | ||||
| @@ -112,14 +112,11 @@ export const zh_CN: LanguageTranslation = { | ||||
|         copied: '复制了!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: '模式:', | ||||
|             filter_by_schema: '按模式筛选', | ||||
|             search_schema: '搜索模式...', | ||||
|             no_schemas_found: '未找到模式。', | ||||
|             view_all_options: '查看所有选项...', | ||||
|             tables_section: { | ||||
|                 tables: '表', | ||||
|                 add_table: '添加表', | ||||
|                 add_view: '添加视图', | ||||
|                 filter: '筛选', | ||||
|                 collapse: '全部折叠', | ||||
|                 // TODO: Translate | ||||
| @@ -145,6 +142,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '字段属性', | ||||
|                         unique: '唯一', | ||||
|                         auto_increment: '自动递增', | ||||
|                         comments: '注释', | ||||
|                         no_comments: '空', | ||||
|                         delete_field: '删除字段', | ||||
| @@ -160,6 +158,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|                         title: '索引属性', | ||||
|                         name: '名称', | ||||
|                         unique: '唯一', | ||||
|                         index_type: '索引类型', | ||||
|                         delete_index: '删除索引', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -176,12 +175,15 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     description: '新建表以开始', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '关系', | ||||
|             refs_section: { | ||||
|                 refs: '引用', | ||||
|                 filter: '筛选', | ||||
|                 add_relationship: '添加关系', | ||||
|                 collapse: '全部折叠', | ||||
|                 add_relationship: '添加关系', | ||||
|                 relationships: '关系', | ||||
|                 dependencies: '依赖关系', | ||||
|                 relationship: { | ||||
|                     relationship: '关系', | ||||
|                     primary: '主表', | ||||
|                     foreign: '被引用表', | ||||
|                     cardinality: '基数', | ||||
| @@ -191,16 +193,8 @@ export const zh_CN: LanguageTranslation = { | ||||
|                         delete_relationship: '删除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '无关系', | ||||
|                     description: '创建关系以连接表', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '依赖关系', | ||||
|                 filter: '筛选', | ||||
|                 collapse: '全部折叠', | ||||
|                 dependency: { | ||||
|                     dependency: '依赖', | ||||
|                     table: '表', | ||||
|                     dependent_table: '依赖视图', | ||||
|                     delete_dependency: '删除', | ||||
| @@ -210,8 +204,8 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '无依赖', | ||||
|                     description: '创建视图以开始', | ||||
|                     title: '无关系', | ||||
|                     description: '创建关系以开始', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,6 +245,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: '没有定义枚举值', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -273,7 +268,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|             show_all: '展示全部', | ||||
|             undo: '撤销', | ||||
|             redo: '重做', | ||||
|             reorder_diagram: '重新排列关系图', | ||||
|             reorder_diagram: '自动排列关系图', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -317,7 +312,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: '打开关系图', | ||||
|             title: '打开数据库', | ||||
|             description: '从下面的列表中选择一个图表打开。', | ||||
|             table_columns: { | ||||
|                 name: '名称', | ||||
| @@ -327,6 +322,12 @@ export const zh_CN: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '取消', | ||||
|             open: '打开', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '打开', | ||||
|                 duplicate: '复制', | ||||
|                 delete: '删除', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -472,6 +473,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新建表', | ||||
|             new_view: '新建视图', | ||||
|             new_relationship: '新建关系', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -493,6 +495,9 @@ export const zh_CN: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '语言', | ||||
|         }, | ||||
|  | ||||
|         on: '开启', | ||||
|         off: '关闭', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const zh_TW: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新建', | ||||
|             browse: '瀏覽', | ||||
|             tables: '表格', | ||||
|             refs: 'Refs', | ||||
|             areas: '區域', | ||||
|             dependencies: '相依性', | ||||
|             custom_types: '自定義類型', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '檔案', | ||||
|                 new: '新增', | ||||
|                 open: '開啟', | ||||
|             actions: { | ||||
|                 actions: '操作', | ||||
|                 new: '新增...', | ||||
|                 browse: '瀏覽...', | ||||
|                 save: '儲存', | ||||
|                 import: '匯入資料庫', | ||||
|                 export_sql: '匯出 SQL', | ||||
|                 export_as: '匯出為特定格式', | ||||
|                 delete_diagram: '刪除圖表', | ||||
|                 exit: '退出', | ||||
|                 delete_diagram: '刪除', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '編輯', | ||||
| @@ -29,6 +37,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|                 hide_field_attributes: '隱藏欄位屬性', | ||||
|                 show_field_attributes: '顯示欄位屬性', | ||||
|                 zoom_on_scroll: '滾動縮放', | ||||
|                 show_views: '資料庫檢視', | ||||
|                 theme: '主題', | ||||
|                 show_dependencies: '顯示相依性', | ||||
|                 hide_dependencies: '隱藏相依性', | ||||
| @@ -63,21 +72,12 @@ export const zh_TW: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: '重新排列圖表', | ||||
|             title: '自動排列圖表', | ||||
|             description: '此操作將重新排列圖表中的所有表格。是否繼續?', | ||||
|             reorder: '重新排列', | ||||
|             reorder: '自動排列', | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '多重 Schema', | ||||
|             description: | ||||
|                 '此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。', | ||||
|             // TODO: Translate | ||||
|             show_me: 'Show me', | ||||
|             none: '無', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '複製失敗', | ||||
| @@ -112,14 +112,11 @@ export const zh_TW: LanguageTranslation = { | ||||
|         copied: '已複製!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: '依 Schema 篩選', | ||||
|             search_schema: '搜尋 Schema...', | ||||
|             no_schemas_found: '未找到 Schema。', | ||||
|             view_all_options: '顯示所有選項...', | ||||
|             tables_section: { | ||||
|                 tables: '表格', | ||||
|                 add_table: '新增表格', | ||||
|                 add_view: '新增檢視', | ||||
|                 filter: '篩選', | ||||
|                 collapse: '全部摺疊', | ||||
|                 // TODO: Translate | ||||
| @@ -145,6 +142,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '欄位屬性', | ||||
|                         unique: '唯一', | ||||
|                         auto_increment: '自動遞增', | ||||
|                         comments: '註解', | ||||
|                         no_comments: '無註解', | ||||
|                         delete_field: '刪除欄位', | ||||
| @@ -160,6 +158,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|                         title: '索引屬性', | ||||
|                         name: '名稱', | ||||
|                         unique: '唯一', | ||||
|                         index_type: '索引類型', | ||||
|                         delete_index: '刪除索引', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -176,12 +175,15 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     description: '請新增表格以開始', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '關聯', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: '篩選', | ||||
|                 add_relationship: '新增關聯', | ||||
|                 collapse: '全部摺疊', | ||||
|                 add_relationship: '新增關聯', | ||||
|                 relationships: '關聯', | ||||
|                 dependencies: '相依性', | ||||
|                 relationship: { | ||||
|                     relationship: '關聯', | ||||
|                     primary: '主表格', | ||||
|                     foreign: '參照表格', | ||||
|                     cardinality: '基數', | ||||
| @@ -191,16 +193,8 @@ export const zh_TW: LanguageTranslation = { | ||||
|                         delete_relationship: '刪除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '尚無關聯', | ||||
|                     description: '請新增關聯以連接表格', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '相依性', | ||||
|                 filter: '篩選', | ||||
|                 collapse: '全部摺疊', | ||||
|                 dependency: { | ||||
|                     dependency: '相依性', | ||||
|                     table: '表格', | ||||
|                     dependent_table: '相依檢視', | ||||
|                     delete_dependency: '刪除', | ||||
| @@ -210,8 +204,8 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '尚無相依性', | ||||
|                     description: '請建立檢視以開始', | ||||
|                     title: '尚無關聯', | ||||
|                     description: '請建立關聯以開始', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,6 +245,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     enum_values: 'Enum Values', | ||||
|                     composite_fields: 'Fields', | ||||
|                     no_fields: 'No fields defined', | ||||
|                     no_values: '沒有定義列舉值', | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
| @@ -273,7 +268,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|             show_all: '顯示全部', | ||||
|             undo: '復原', | ||||
|             redo: '重做', | ||||
|             reorder_diagram: '重新排列圖表', | ||||
|             reorder_diagram: '自動排列圖表', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
| @@ -316,7 +311,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: '開啟圖表', | ||||
|             title: '開啟資料庫', | ||||
|             description: '請從以下列表中選擇一個圖表。', | ||||
|             table_columns: { | ||||
|                 name: '名稱', | ||||
| @@ -326,6 +321,12 @@ export const zh_TW: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '取消', | ||||
|             open: '開啟', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '開啟', | ||||
|                 duplicate: '複製', | ||||
|                 delete: '刪除', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -472,6 +473,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新建表格', | ||||
|             new_view: '新檢視', | ||||
|             new_relationship: '新建關聯', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -493,6 +495,9 @@ export const zh_TW: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '變更語言', | ||||
|         }, | ||||
|  | ||||
|         on: '開啟', | ||||
|         off: '關閉', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -19,3 +19,5 @@ export const randomColor = () => { | ||||
|  | ||||
| export const viewColor = '#b0b0b0'; | ||||
| export const materializedViewColor = '#7d7d7d'; | ||||
| export const defaultTableColor = '#8eb7ff'; | ||||
| export const defaultAreaColor = '#b067e9'; | ||||
|   | ||||
| @@ -146,3 +146,22 @@ export const findDataTypeDataById = ( | ||||
|  | ||||
|     return dataTypesOptions.find((dataType) => dataType.id === id); | ||||
| }; | ||||
|  | ||||
| export const supportsAutoIncrementDataType = ( | ||||
|     dataTypeName: string | ||||
| ): boolean => { | ||||
|     return [ | ||||
|         'integer', | ||||
|         'int', | ||||
|         'bigint', | ||||
|         'smallint', | ||||
|         'tinyint', | ||||
|         'mediumint', | ||||
|         'serial', | ||||
|         'bigserial', | ||||
|         'smallserial', | ||||
|         'number', | ||||
|         'numeric', | ||||
|         'decimal', | ||||
|     ].includes(dataTypeName.toLocaleLowerCase()); | ||||
| }; | ||||
|   | ||||
| @@ -50,5 +50,8 @@ export const sqliteDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'bigint', id: 'bigint' }, | ||||
|     { name: 'bool', id: 'bool' }, | ||||
|     { name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility | ||||
|     { name: 'time', id: 'time' }, | ||||
|     { name: 'date', id: 'date' }, // Added for smartquery compatibility | ||||
|     { name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility | ||||
| ] as const; | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/lib/data/import-metadata/import/custom-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/lib/data/import-metadata/import/custom-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import type { DBCustomType, DBCustomTypeKind } from '@/lib/domain'; | ||||
| import { schemaNameToDomainSchemaName } from '@/lib/domain'; | ||||
| import type { DBCustomTypeInfo } from '../metadata-types/custom-type-info'; | ||||
| import { generateId } from '@/lib/utils'; | ||||
|  | ||||
| export const createCustomTypesFromMetadata = ({ | ||||
|     customTypes, | ||||
| }: { | ||||
|     customTypes: DBCustomTypeInfo[]; | ||||
| }): DBCustomType[] => { | ||||
|     return customTypes.map((customType) => { | ||||
|         return { | ||||
|             id: generateId(), | ||||
|             schema: schemaNameToDomainSchemaName(customType.schema), | ||||
|             name: customType.type, | ||||
|             kind: customType.kind as DBCustomTypeKind, | ||||
|             values: customType.values, | ||||
|             fields: customType.fields, | ||||
|         }; | ||||
|     }); | ||||
| }; | ||||
							
								
								
									
										351
									
								
								src/lib/data/import-metadata/import/dependencies.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								src/lib/data/import-metadata/import/dependencies.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,351 @@ | ||||
| import { generateId } from '@/lib/utils'; | ||||
| import type { AST } from 'node-sql-parser'; | ||||
| import type { DBDependency, DBTable } from '@/lib/domain'; | ||||
| import { DatabaseType, schemaNameToDomainSchemaName } from '@/lib/domain'; | ||||
| import type { ViewInfo } from '../metadata-types/view-info'; | ||||
| import { decodeViewDefinition } from './tables'; | ||||
|  | ||||
| const astDatabaseTypes: Record<DatabaseType, string> = { | ||||
|     [DatabaseType.POSTGRESQL]: 'postgresql', | ||||
|     [DatabaseType.MYSQL]: 'postgresql', | ||||
|     [DatabaseType.MARIADB]: 'postgresql', | ||||
|     [DatabaseType.GENERIC]: 'postgresql', | ||||
|     [DatabaseType.SQLITE]: 'postgresql', | ||||
|     [DatabaseType.SQL_SERVER]: 'postgresql', | ||||
|     [DatabaseType.CLICKHOUSE]: 'postgresql', | ||||
|     [DatabaseType.COCKROACHDB]: 'postgresql', | ||||
|     [DatabaseType.ORACLE]: 'postgresql', | ||||
| }; | ||||
|  | ||||
| export const createDependenciesFromMetadata = async ({ | ||||
|     views, | ||||
|     tables, | ||||
|     databaseType, | ||||
| }: { | ||||
|     views: ViewInfo[]; | ||||
|     tables: DBTable[]; | ||||
|     databaseType: DatabaseType; | ||||
| }): Promise<DBDependency[]> => { | ||||
|     if (!views || views.length === 0) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     const { Parser } = await import('node-sql-parser'); | ||||
|     const parser = new Parser(); | ||||
|  | ||||
|     const dependencies = views | ||||
|         .flatMap((view) => { | ||||
|             const viewSchema = schemaNameToDomainSchemaName(view.schema); | ||||
|             const viewTable = tables.find( | ||||
|                 (table) => | ||||
|                     table.name === view.view_name && viewSchema === table.schema | ||||
|             ); | ||||
|  | ||||
|             if (!viewTable) { | ||||
|                 console.warn( | ||||
|                     `Source table for view ${view.view_name} not found (schema: ${viewSchema})` | ||||
|                 ); | ||||
|                 return []; // Skip this view and proceed to the next | ||||
|             } | ||||
|  | ||||
|             if (view.view_definition) { | ||||
|                 try { | ||||
|                     const decodedViewDefinition = decodeViewDefinition( | ||||
|                         databaseType, | ||||
|                         view.view_definition | ||||
|                     ); | ||||
|  | ||||
|                     let modifiedViewDefinition = ''; | ||||
|                     if ( | ||||
|                         databaseType === DatabaseType.MYSQL || | ||||
|                         databaseType === DatabaseType.MARIADB | ||||
|                     ) { | ||||
|                         modifiedViewDefinition = preprocessViewDefinitionMySQL( | ||||
|                             decodedViewDefinition | ||||
|                         ); | ||||
|                     } else if (databaseType === DatabaseType.SQL_SERVER) { | ||||
|                         modifiedViewDefinition = | ||||
|                             preprocessViewDefinitionSQLServer( | ||||
|                                 decodedViewDefinition | ||||
|                             ); | ||||
|                     } else { | ||||
|                         modifiedViewDefinition = preprocessViewDefinition( | ||||
|                             decodedViewDefinition | ||||
|                         ); | ||||
|                     } | ||||
|  | ||||
|                     // Parse using the appropriate dialect | ||||
|                     const ast = parser.astify(modifiedViewDefinition, { | ||||
|                         database: astDatabaseTypes[databaseType], | ||||
|                         type: 'select', // Parsing a SELECT statement | ||||
|                     }); | ||||
|  | ||||
|                     let relatedTables = extractTablesFromAST(ast); | ||||
|  | ||||
|                     // Filter out duplicate tables without schema | ||||
|                     relatedTables = filterDuplicateTables(relatedTables); | ||||
|  | ||||
|                     return relatedTables.map((relTable) => { | ||||
|                         const relSchema = relTable.schema || view.schema; // Use view's schema if relSchema is undefined | ||||
|                         const relTableName = relTable.tableName; | ||||
|  | ||||
|                         const table = tables.find( | ||||
|                             (table) => | ||||
|                                 table.name === relTableName && | ||||
|                                 (table.schema || '') === relSchema | ||||
|                         ); | ||||
|  | ||||
|                         if (table) { | ||||
|                             const dependency: DBDependency = { | ||||
|                                 id: generateId(), | ||||
|                                 schema: view.schema, | ||||
|                                 tableId: table.id, // related table | ||||
|                                 dependentSchema: table.schema, | ||||
|                                 dependentTableId: viewTable.id, // dependent view | ||||
|                                 createdAt: Date.now(), | ||||
|                             }; | ||||
|  | ||||
|                             return dependency; | ||||
|                         } else { | ||||
|                             console.warn( | ||||
|                                 `Dependent table ${relSchema}.${relTableName} not found for view ${view.schema}.${view.view_name}` | ||||
|                             ); | ||||
|                             return null; | ||||
|                         } | ||||
|                     }); | ||||
|                 } catch (error) { | ||||
|                     console.error( | ||||
|                         `Error parsing view ${view.schema}.${view.view_name}:`, | ||||
|                         error | ||||
|                     ); | ||||
|                     return []; | ||||
|                 } | ||||
|             } else { | ||||
|                 console.warn( | ||||
|                     `View definition missing for ${view.schema}.${view.view_name}` | ||||
|                 ); | ||||
|                 return []; | ||||
|             } | ||||
|         }) | ||||
|         .filter((dependency) => dependency !== null); | ||||
|  | ||||
|     return dependencies; | ||||
| }; | ||||
|  | ||||
| // Add this new function to filter out duplicate tables | ||||
| function filterDuplicateTables( | ||||
|     tables: { schema?: string; tableName: string }[] | ||||
| ): { schema?: string; tableName: string }[] { | ||||
|     const tableMap = new Map<string, { schema?: string; tableName: string }>(); | ||||
|  | ||||
|     for (const table of tables) { | ||||
|         const key = table.tableName; | ||||
|         const existingTable = tableMap.get(key); | ||||
|  | ||||
|         if (!existingTable || (table.schema && !existingTable.schema)) { | ||||
|             tableMap.set(key, table); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return Array.from(tableMap.values()); | ||||
| } | ||||
|  | ||||
| // Preprocess the view_definition to remove schema from CREATE VIEW | ||||
| function preprocessViewDefinition(viewDefinition: string): string { | ||||
|     if (!viewDefinition) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // Remove leading and trailing whitespace | ||||
|     viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim(); | ||||
|  | ||||
|     // Replace escaped double quotes with regular ones | ||||
|     viewDefinition = viewDefinition.replace(/\\"/g, '"'); | ||||
|  | ||||
|     // Replace 'CREATE MATERIALIZED VIEW' with 'CREATE VIEW' | ||||
|     viewDefinition = viewDefinition.replace( | ||||
|         /CREATE\s+MATERIALIZED\s+VIEW/i, | ||||
|         'CREATE VIEW' | ||||
|     ); | ||||
|  | ||||
|     // Regular expression to match 'CREATE VIEW [schema.]view_name [ (column definitions) ] AS' | ||||
|     // This regex captures the view name and skips any content between the view name and 'AS' | ||||
|     const regex = | ||||
|         /CREATE\s+VIEW\s+(?:(?:`[^`]+`|"[^"]+"|\w+)\.)?(?:`([^`]+)`|"([^"]+)"|(\w+))[\s\S]*?\bAS\b\s+/i; | ||||
|  | ||||
|     const match = viewDefinition.match(regex); | ||||
|     let modifiedDefinition: string; | ||||
|  | ||||
|     if (match) { | ||||
|         const viewName = match[1] || match[2] || match[3]; | ||||
|         // Extract the SQL after the 'AS' keyword | ||||
|         const restOfDefinition = viewDefinition.substring( | ||||
|             match.index! + match[0].length | ||||
|         ); | ||||
|  | ||||
|         // Replace double-quoted identifiers with unquoted ones | ||||
|         let modifiedSQL = restOfDefinition.replace(/"(\w+)"/g, '$1'); | ||||
|  | ||||
|         // Replace '::' type casts with 'CAST' expressions | ||||
|         modifiedSQL = modifiedSQL.replace( | ||||
|             /\(([^()]+)\)::(\w+)/g, | ||||
|             'CAST($1 AS $2)' | ||||
|         ); | ||||
|  | ||||
|         // Remove ClickHouse-specific syntax that may still be present | ||||
|         // For example, remove SETTINGS clauses inside the SELECT statement | ||||
|         modifiedSQL = modifiedSQL.replace(/\bSETTINGS\b[\s\S]*$/i, ''); | ||||
|  | ||||
|         modifiedDefinition = `CREATE VIEW ${viewName} AS ${modifiedSQL}`; | ||||
|     } else { | ||||
|         console.warn('Could not preprocess view definition:', viewDefinition); | ||||
|         modifiedDefinition = viewDefinition; | ||||
|     } | ||||
|  | ||||
|     return modifiedDefinition; | ||||
| } | ||||
|  | ||||
| // Preprocess the view_definition for SQL Server | ||||
| function preprocessViewDefinitionSQLServer(viewDefinition: string): string { | ||||
|     if (!viewDefinition) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // Remove BOM if present | ||||
|     viewDefinition = viewDefinition.replace(/^\uFEFF/, ''); | ||||
|  | ||||
|     // Normalize whitespace | ||||
|     viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim(); | ||||
|  | ||||
|     // Remove square brackets and replace with double quotes | ||||
|     viewDefinition = viewDefinition.replace(/\[([^\]]+)\]/g, '"$1"'); | ||||
|  | ||||
|     // Remove database names from fully qualified identifiers | ||||
|     viewDefinition = viewDefinition.replace( | ||||
|         /"([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"/g, | ||||
|         '"$2"."$3"' | ||||
|     ); | ||||
|  | ||||
|     // Replace SQL Server functions with PostgreSQL equivalents | ||||
|     viewDefinition = viewDefinition.replace(/\bGETDATE\(\)/gi, 'NOW()'); | ||||
|     viewDefinition = viewDefinition.replace(/\bISNULL\(/gi, 'COALESCE('); | ||||
|  | ||||
|     // Replace 'TOP N' with 'LIMIT N' at the end of the query | ||||
|     const topMatch = viewDefinition.match(/SELECT\s+TOP\s+(\d+)/i); | ||||
|     if (topMatch) { | ||||
|         const topN = topMatch[1]; | ||||
|         viewDefinition = viewDefinition.replace( | ||||
|             /SELECT\s+TOP\s+\d+/i, | ||||
|             'SELECT' | ||||
|         ); | ||||
|         viewDefinition = viewDefinition.replace(/;+\s*$/, ''); // Remove semicolons at the end | ||||
|         viewDefinition += ` LIMIT ${topN}`; | ||||
|     } | ||||
|  | ||||
|     viewDefinition = viewDefinition.replace(/\n/g, ''); // Remove newlines | ||||
|  | ||||
|     // Adjust CREATE VIEW syntax | ||||
|     const regex = | ||||
|         /CREATE\s+VIEW\s+(?:"?([^".\s]+)"?\.)?"?([^".\s]+)"?\s+AS\s+/i; | ||||
|     const match = viewDefinition.match(regex); | ||||
|     let modifiedDefinition: string; | ||||
|  | ||||
|     if (match) { | ||||
|         const viewName = match[2]; | ||||
|         const modifiedSQL = viewDefinition.substring( | ||||
|             match.index! + match[0].length | ||||
|         ); | ||||
|  | ||||
|         // Remove semicolons at the end | ||||
|         const finalSQL = modifiedSQL.replace(/;+\s*$/, ''); | ||||
|  | ||||
|         modifiedDefinition = `CREATE VIEW "${viewName}" AS ${finalSQL}`; | ||||
|     } else { | ||||
|         console.warn('Could not preprocess view definition:', viewDefinition); | ||||
|         modifiedDefinition = viewDefinition; | ||||
|     } | ||||
|  | ||||
|     return modifiedDefinition; | ||||
| } | ||||
|  | ||||
| // Preprocess the view_definition to remove schema from CREATE VIEW | ||||
| function preprocessViewDefinitionMySQL(viewDefinition: string): string { | ||||
|     if (!viewDefinition) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // Remove any trailing semicolons | ||||
|     viewDefinition = viewDefinition.replace(/;\s*$/, ''); | ||||
|  | ||||
|     // Remove backticks from identifiers | ||||
|     viewDefinition = viewDefinition.replace(/`/g, ''); | ||||
|  | ||||
|     // Remove unnecessary parentheses around joins and ON clauses | ||||
|     viewDefinition = removeRedundantParentheses(viewDefinition); | ||||
|  | ||||
|     return viewDefinition; | ||||
| } | ||||
|  | ||||
| function removeRedundantParentheses(sql: string): string { | ||||
|     // Regular expressions to match unnecessary parentheses | ||||
|     const patterns = [ | ||||
|         /\(\s*(JOIN\s+[^()]+?)\s*\)/gi, | ||||
|         /\(\s*(ON\s+[^()]+?)\s*\)/gi, | ||||
|         // Additional patterns if necessary | ||||
|     ]; | ||||
|  | ||||
|     let prevSql; | ||||
|     do { | ||||
|         prevSql = sql; | ||||
|         patterns.forEach((pattern) => { | ||||
|             sql = sql.replace(pattern, '$1'); | ||||
|         }); | ||||
|     } while (sql !== prevSql); | ||||
|  | ||||
|     return sql; | ||||
| } | ||||
|  | ||||
| function extractTablesFromAST( | ||||
|     ast: AST | AST[] | ||||
| ): { schema?: string; tableName: string }[] { | ||||
|     const tablesMap = new Map<string, { schema: string; tableName: string }>(); | ||||
|     const visitedNodes = new Set(); | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
|     function traverse(node: any) { | ||||
|         if (!node || visitedNodes.has(node)) return; | ||||
|         visitedNodes.add(node); | ||||
|  | ||||
|         if (Array.isArray(node)) { | ||||
|             node.forEach(traverse); | ||||
|         } else if (typeof node === 'object') { | ||||
|             // Check if node represents a table | ||||
|             if ( | ||||
|                 Object.hasOwnProperty.call(node, 'table') && | ||||
|                 typeof node.table === 'string' | ||||
|             ) { | ||||
|                 let schema = node.db || node.schema; | ||||
|                 const tableName = node.table; | ||||
|                 if (tableName) { | ||||
|                     // Assign default schema if undefined | ||||
|                     schema = schemaNameToDomainSchemaName(schema) || ''; | ||||
|                     const key = `${schema}.${tableName}`; | ||||
|                     if (!tablesMap.has(key)) { | ||||
|                         tablesMap.set(key, { schema, tableName }); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Recursively traverse all properties | ||||
|             for (const key in node) { | ||||
|                 if (Object.hasOwnProperty.call(node, key)) { | ||||
|                     traverse(node[key]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     traverse(ast); | ||||
|  | ||||
|     return Array.from(tablesMap.values()); | ||||
| } | ||||
							
								
								
									
										64
									
								
								src/lib/data/import-metadata/import/fields.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/lib/data/import-metadata/import/fields.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import type { DBField } from '@/lib/domain'; | ||||
| import type { ColumnInfo } from '../metadata-types/column-info'; | ||||
| import type { AggregatedIndexInfo } from '../metadata-types/index-info'; | ||||
| import type { PrimaryKeyInfo } from '../metadata-types/primary-key-info'; | ||||
| import type { TableInfo } from '../metadata-types/table-info'; | ||||
| import { generateId } from '@/lib/utils'; | ||||
|  | ||||
| export const createFieldsFromMetadata = ({ | ||||
|     tableColumns, | ||||
|     tablePrimaryKeys, | ||||
|     aggregatedIndexes, | ||||
| }: { | ||||
|     tableColumns: ColumnInfo[]; | ||||
|     tableSchema?: string; | ||||
|     tableInfo: TableInfo; | ||||
|     tablePrimaryKeys: PrimaryKeyInfo[]; | ||||
|     aggregatedIndexes: AggregatedIndexInfo[]; | ||||
| }) => { | ||||
|     const uniqueColumns = tableColumns.reduce((acc, col) => { | ||||
|         if (!acc.has(col.name)) { | ||||
|             acc.set(col.name, col); | ||||
|         } | ||||
|         return acc; | ||||
|     }, new Map<string, ColumnInfo>()); | ||||
|  | ||||
|     const sortedColumns = Array.from(uniqueColumns.values()).sort( | ||||
|         (a, b) => a.ordinal_position - b.ordinal_position | ||||
|     ); | ||||
|  | ||||
|     const tablePrimaryKeysColumns = tablePrimaryKeys.map((pk) => | ||||
|         pk.column.trim() | ||||
|     ); | ||||
|  | ||||
|     return sortedColumns.map( | ||||
|         (col: ColumnInfo): DBField => ({ | ||||
|             id: generateId(), | ||||
|             name: col.name, | ||||
|             type: { | ||||
|                 id: col.type.split(' ').join('_').toLowerCase(), | ||||
|                 name: col.type.toLowerCase(), | ||||
|             }, | ||||
|             primaryKey: tablePrimaryKeysColumns.includes(col.name), | ||||
|             unique: Object.values(aggregatedIndexes).some( | ||||
|                 (idx) => | ||||
|                     idx.unique && | ||||
|                     idx.columns.length === 1 && | ||||
|                     idx.columns[0].name === col.name | ||||
|             ), | ||||
|             nullable: Boolean(col.nullable), | ||||
|             ...(col.character_maximum_length && | ||||
|             col.character_maximum_length !== 'null' | ||||
|                 ? { characterMaximumLength: col.character_maximum_length } | ||||
|                 : {}), | ||||
|             ...(col.precision?.precision | ||||
|                 ? { precision: col.precision.precision } | ||||
|                 : {}), | ||||
|             ...(col.precision?.scale ? { scale: col.precision.scale } : {}), | ||||
|             ...(col.default ? { default: col.default } : {}), | ||||
|             ...(col.collation ? { collation: col.collation } : {}), | ||||
|             createdAt: Date.now(), | ||||
|             comments: col.comment ? col.comment : undefined, | ||||
|         }) | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										82
									
								
								src/lib/data/import-metadata/import/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/lib/data/import-metadata/import/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import type { DatabaseEdition, Diagram } from '@/lib/domain'; | ||||
| import { adjustTablePositions, DatabaseType } from '@/lib/domain'; | ||||
| import { generateDiagramId } from '@/lib/utils'; | ||||
| import type { DatabaseMetadata } from '../metadata-types/database-metadata'; | ||||
| import { createCustomTypesFromMetadata } from './custom-types'; | ||||
| import { createRelationshipsFromMetadata } from './relationships'; | ||||
| import { createTablesFromMetadata } from './tables'; | ||||
| import { createDependenciesFromMetadata } from './dependencies'; | ||||
|  | ||||
| export const loadFromDatabaseMetadata = async ({ | ||||
|     databaseType, | ||||
|     databaseMetadata, | ||||
|     diagramNumber, | ||||
|     databaseEdition, | ||||
| }: { | ||||
|     databaseType: DatabaseType; | ||||
|     databaseMetadata: DatabaseMetadata; | ||||
|     diagramNumber?: number; | ||||
|     databaseEdition?: DatabaseEdition; | ||||
| }): Promise<Diagram> => { | ||||
|     const { | ||||
|         fk_info: foreignKeys, | ||||
|         views: views, | ||||
|         custom_types: customTypes, | ||||
|     } = databaseMetadata; | ||||
|  | ||||
|     const tables = createTablesFromMetadata({ | ||||
|         databaseMetadata, | ||||
|         databaseType, | ||||
|     }); | ||||
|  | ||||
|     const relationships = createRelationshipsFromMetadata({ | ||||
|         foreignKeys, | ||||
|         tables, | ||||
|     }); | ||||
|  | ||||
|     const dependencies = await createDependenciesFromMetadata({ | ||||
|         views, | ||||
|         tables, | ||||
|         databaseType, | ||||
|     }); | ||||
|  | ||||
|     const dbCustomTypes = customTypes | ||||
|         ? createCustomTypesFromMetadata({ | ||||
|               customTypes, | ||||
|           }) | ||||
|         : []; | ||||
|  | ||||
|     const adjustedTables = adjustTablePositions({ | ||||
|         tables, | ||||
|         relationships, | ||||
|         mode: 'perSchema', | ||||
|     }); | ||||
|  | ||||
|     const sortedTables = adjustedTables.sort((a, b) => { | ||||
|         if (a.isView === b.isView) { | ||||
|             // Both are either tables or views, so sort alphabetically by name | ||||
|             return a.name.localeCompare(b.name); | ||||
|         } | ||||
|         // If one is a view and the other is not, put tables first | ||||
|         return a.isView ? 1 : -1; | ||||
|     }); | ||||
|  | ||||
|     const diagram: Diagram = { | ||||
|         id: generateDiagramId(), | ||||
|         name: databaseMetadata.database_name | ||||
|             ? `${databaseMetadata.database_name}-db` | ||||
|             : diagramNumber | ||||
|               ? `Diagram ${diagramNumber}` | ||||
|               : 'New Diagram', | ||||
|         databaseType: databaseType ?? DatabaseType.GENERIC, | ||||
|         databaseEdition, | ||||
|         tables: sortedTables, | ||||
|         relationships, | ||||
|         dependencies, | ||||
|         customTypes: dbCustomTypes, | ||||
|         createdAt: new Date(), | ||||
|         updatedAt: new Date(), | ||||
|     }; | ||||
|  | ||||
|     return diagram; | ||||
| }; | ||||
							
								
								
									
										24
									
								
								src/lib/data/import-metadata/import/indexes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/lib/data/import-metadata/import/indexes.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import type { DBField, DBIndex, IndexType } from '@/lib/domain'; | ||||
| import type { AggregatedIndexInfo } from '../metadata-types/index-info'; | ||||
| import { generateId } from '@/lib/utils'; | ||||
|  | ||||
| export const createIndexesFromMetadata = ({ | ||||
|     aggregatedIndexes, | ||||
|     fields, | ||||
| }: { | ||||
|     aggregatedIndexes: AggregatedIndexInfo[]; | ||||
|     fields: DBField[]; | ||||
| }): DBIndex[] => | ||||
|     aggregatedIndexes.map( | ||||
|         (idx): DBIndex => ({ | ||||
|             id: generateId(), | ||||
|             name: idx.name, | ||||
|             unique: Boolean(idx.unique), | ||||
|             fieldIds: idx.columns | ||||
|                 .sort((a, b) => a.position - b.position) | ||||
|                 .map((c) => fields.find((f) => f.name === c.name)?.id) | ||||
|                 .filter((id): id is string => id !== undefined), | ||||
|             createdAt: Date.now(), | ||||
|             type: idx.index_type?.toLowerCase() as IndexType, | ||||
|         }) | ||||
|     ); | ||||
							
								
								
									
										85
									
								
								src/lib/data/import-metadata/import/relationships.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/lib/data/import-metadata/import/relationships.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import type { | ||||
|     Cardinality, | ||||
|     DBField, | ||||
|     DBRelationship, | ||||
|     DBTable, | ||||
| } from '@/lib/domain'; | ||||
| import { schemaNameToDomainSchemaName } from '@/lib/domain'; | ||||
| import type { ForeignKeyInfo } from '../metadata-types/foreign-key-info'; | ||||
| import { generateId } from '@/lib/utils'; | ||||
|  | ||||
| const determineCardinality = ( | ||||
|     field: DBField, | ||||
|     isTablePKComplex: boolean | ||||
| ): Cardinality => { | ||||
|     return field.unique || (field.primaryKey && !isTablePKComplex) | ||||
|         ? 'one' | ||||
|         : 'many'; | ||||
| }; | ||||
|  | ||||
| export const createRelationshipsFromMetadata = ({ | ||||
|     foreignKeys, | ||||
|     tables, | ||||
| }: { | ||||
|     foreignKeys: ForeignKeyInfo[]; | ||||
|     tables: DBTable[]; | ||||
| }): DBRelationship[] => { | ||||
|     return foreignKeys | ||||
|         .map((fk: ForeignKeyInfo): DBRelationship | null => { | ||||
|             const schema = schemaNameToDomainSchemaName(fk.schema); | ||||
|             const sourceTable = tables.find( | ||||
|                 (table) => table.name === fk.table && table.schema === schema | ||||
|             ); | ||||
|  | ||||
|             const targetSchema = schemaNameToDomainSchemaName( | ||||
|                 fk.reference_schema | ||||
|             ); | ||||
|  | ||||
|             const targetTable = tables.find( | ||||
|                 (table) => | ||||
|                     table.name === fk.reference_table && | ||||
|                     table.schema === targetSchema | ||||
|             ); | ||||
|             const sourceField = sourceTable?.fields.find( | ||||
|                 (field) => field.name === fk.column | ||||
|             ); | ||||
|             const targetField = targetTable?.fields.find( | ||||
|                 (field) => field.name === fk.reference_column | ||||
|             ); | ||||
|  | ||||
|             const isSourceTablePKComplex = | ||||
|                 (sourceTable?.fields.filter((field) => field.primaryKey) ?? []) | ||||
|                     .length > 1; | ||||
|             const isTargetTablePKComplex = | ||||
|                 (targetTable?.fields.filter((field) => field.primaryKey) ?? []) | ||||
|                     .length > 1; | ||||
|  | ||||
|             if (sourceTable && targetTable && sourceField && targetField) { | ||||
|                 const sourceCardinality = determineCardinality( | ||||
|                     sourceField, | ||||
|                     isSourceTablePKComplex | ||||
|                 ); | ||||
|                 const targetCardinality = determineCardinality( | ||||
|                     targetField, | ||||
|                     isTargetTablePKComplex | ||||
|                 ); | ||||
|  | ||||
|                 return { | ||||
|                     id: generateId(), | ||||
|                     name: fk.foreign_key_name, | ||||
|                     sourceSchema: schema, | ||||
|                     targetSchema: targetSchema, | ||||
|                     sourceTableId: sourceTable.id, | ||||
|                     targetTableId: targetTable.id, | ||||
|                     sourceFieldId: sourceField.id, | ||||
|                     targetFieldId: targetField.id, | ||||
|                     sourceCardinality, | ||||
|                     targetCardinality, | ||||
|                     createdAt: Date.now(), | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }) | ||||
|         .filter((rel) => rel !== null) as DBRelationship[]; | ||||
| }; | ||||
							
								
								
									
										228
									
								
								src/lib/data/import-metadata/import/tables.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/lib/data/import-metadata/import/tables.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | ||||
| import type { DBIndex, DBTable } from '@/lib/domain'; | ||||
| import { | ||||
|     DatabaseType, | ||||
|     generateTableKey, | ||||
|     schemaNameToDomainSchemaName, | ||||
| } from '@/lib/domain'; | ||||
| import type { DatabaseMetadata } from '../metadata-types/database-metadata'; | ||||
| import type { TableInfo } from '../metadata-types/table-info'; | ||||
| import { createAggregatedIndexes } from '../metadata-types/index-info'; | ||||
| import { | ||||
|     decodeBase64ToUtf16LE, | ||||
|     decodeBase64ToUtf8, | ||||
|     generateId, | ||||
| } from '@/lib/utils'; | ||||
| import { | ||||
|     defaultTableColor, | ||||
|     materializedViewColor, | ||||
|     viewColor, | ||||
| } from '@/lib/colors'; | ||||
| import { createFieldsFromMetadata } from './fields'; | ||||
| import { createIndexesFromMetadata } from './indexes'; | ||||
|  | ||||
| export const decodeViewDefinition = ( | ||||
|     databaseType: DatabaseType, | ||||
|     viewDefinition?: string | ||||
| ): string => { | ||||
|     if (!viewDefinition) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     let decodedViewDefinition: string; | ||||
|     if (databaseType === DatabaseType.SQL_SERVER) { | ||||
|         decodedViewDefinition = decodeBase64ToUtf16LE(viewDefinition); | ||||
|     } else { | ||||
|         decodedViewDefinition = decodeBase64ToUtf8(viewDefinition); | ||||
|     } | ||||
|  | ||||
|     return decodedViewDefinition; | ||||
| }; | ||||
|  | ||||
| export const createTablesFromMetadata = ({ | ||||
|     databaseMetadata, | ||||
|     databaseType, | ||||
| }: { | ||||
|     databaseMetadata: DatabaseMetadata; | ||||
|     databaseType: DatabaseType; | ||||
| }): DBTable[] => { | ||||
|     const { | ||||
|         tables: tableInfos, | ||||
|         pk_info: primaryKeys, | ||||
|         columns, | ||||
|         indexes, | ||||
|         views: views, | ||||
|     } = databaseMetadata; | ||||
|  | ||||
|     // Pre-compute view names for faster lookup if there are views | ||||
|     const viewNamesSet = new Set<string>(); | ||||
|     const materializedViewNamesSet = new Set<string>(); | ||||
|  | ||||
|     if (views && views.length > 0) { | ||||
|         views.forEach((view) => { | ||||
|             const key = generateTableKey({ | ||||
|                 schemaName: view.schema, | ||||
|                 tableName: view.view_name, | ||||
|             }); | ||||
|             viewNamesSet.add(key); | ||||
|  | ||||
|             if ( | ||||
|                 view.view_definition && | ||||
|                 decodeViewDefinition(databaseType, view.view_definition) | ||||
|                     .toLowerCase() | ||||
|                     .includes('materialized') | ||||
|             ) { | ||||
|                 materializedViewNamesSet.add(key); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Pre-compute lookup maps for better performance | ||||
|     const columnsByTable = new Map<string, (typeof columns)[0][]>(); | ||||
|     const indexesByTable = new Map<string, (typeof indexes)[0][]>(); | ||||
|     const primaryKeysByTable = new Map<string, (typeof primaryKeys)[0][]>(); | ||||
|  | ||||
|     // Group columns by table | ||||
|     columns.forEach((col) => { | ||||
|         const key = generateTableKey({ | ||||
|             schemaName: col.schema, | ||||
|             tableName: col.table, | ||||
|         }); | ||||
|         if (!columnsByTable.has(key)) { | ||||
|             columnsByTable.set(key, []); | ||||
|         } | ||||
|         columnsByTable.get(key)!.push(col); | ||||
|     }); | ||||
|  | ||||
|     // Group indexes by table | ||||
|     indexes.forEach((idx) => { | ||||
|         const key = generateTableKey({ | ||||
|             schemaName: idx.schema, | ||||
|             tableName: idx.table, | ||||
|         }); | ||||
|         if (!indexesByTable.has(key)) { | ||||
|             indexesByTable.set(key, []); | ||||
|         } | ||||
|         indexesByTable.get(key)!.push(idx); | ||||
|     }); | ||||
|  | ||||
|     // Group primary keys by table | ||||
|     primaryKeys.forEach((pk) => { | ||||
|         const key = generateTableKey({ | ||||
|             schemaName: pk.schema, | ||||
|             tableName: pk.table, | ||||
|         }); | ||||
|         if (!primaryKeysByTable.has(key)) { | ||||
|             primaryKeysByTable.set(key, []); | ||||
|         } | ||||
|         primaryKeysByTable.get(key)!.push(pk); | ||||
|     }); | ||||
|  | ||||
|     const result = tableInfos.map((tableInfo: TableInfo) => { | ||||
|         const tableSchema = schemaNameToDomainSchemaName(tableInfo.schema); | ||||
|         const tableKey = generateTableKey({ | ||||
|             schemaName: tableInfo.schema, | ||||
|             tableName: tableInfo.table, | ||||
|         }); | ||||
|  | ||||
|         // Use pre-computed lookups instead of filtering entire arrays | ||||
|         const tableIndexes = indexesByTable.get(tableKey) || []; | ||||
|         const tablePrimaryKeys = primaryKeysByTable.get(tableKey) || []; | ||||
|         const tableColumns = columnsByTable.get(tableKey) || []; | ||||
|  | ||||
|         // Aggregate indexes with multiple columns | ||||
|         const aggregatedIndexes = createAggregatedIndexes({ | ||||
|             tableInfo, | ||||
|             tableSchema, | ||||
|             tableIndexes, | ||||
|         }); | ||||
|  | ||||
|         const fields = createFieldsFromMetadata({ | ||||
|             aggregatedIndexes, | ||||
|             tableColumns, | ||||
|             tablePrimaryKeys, | ||||
|             tableInfo, | ||||
|             tableSchema, | ||||
|         }); | ||||
|  | ||||
|         // Check for composite primary key and find matching index name | ||||
|         const primaryKeyFields = fields.filter((f) => f.primaryKey); | ||||
|         let pkMatchingIndexName: string | undefined; | ||||
|         let pkIndex: DBIndex | undefined; | ||||
|  | ||||
|         if (primaryKeyFields.length >= 1) { | ||||
|             // We have a composite primary key, look for an index that matches all PK columns | ||||
|             const pkFieldNames = primaryKeyFields.map((f) => f.name).sort(); | ||||
|  | ||||
|             // Find an index that matches the primary key columns exactly | ||||
|             const matchingIndex = aggregatedIndexes.find((index) => { | ||||
|                 const indexColumnNames = index.columns | ||||
|                     .map((c) => c.name) | ||||
|                     .sort(); | ||||
|                 return ( | ||||
|                     indexColumnNames.length === pkFieldNames.length && | ||||
|                     indexColumnNames.every((col, i) => col === pkFieldNames[i]) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             if (matchingIndex) { | ||||
|                 pkMatchingIndexName = matchingIndex.name; | ||||
|                 // Create a special PK index | ||||
|                 pkIndex = { | ||||
|                     id: generateId(), | ||||
|                     name: matchingIndex.name, | ||||
|                     unique: true, | ||||
|                     fieldIds: primaryKeyFields.map((f) => f.id), | ||||
|                     createdAt: Date.now(), | ||||
|                     isPrimaryKey: true, | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Filter out the index that matches the composite PK (to avoid duplication) | ||||
|         const filteredAggregatedIndexes = pkMatchingIndexName | ||||
|             ? aggregatedIndexes.filter( | ||||
|                   (idx) => idx.name !== pkMatchingIndexName | ||||
|               ) | ||||
|             : aggregatedIndexes; | ||||
|  | ||||
|         const dbIndexes = createIndexesFromMetadata({ | ||||
|             aggregatedIndexes: filteredAggregatedIndexes, | ||||
|             fields, | ||||
|         }); | ||||
|  | ||||
|         // Add the PK index if it exists | ||||
|         if (pkIndex) { | ||||
|             dbIndexes.push(pkIndex); | ||||
|         } | ||||
|  | ||||
|         // Determine if the current table is a view by checking against pre-computed sets | ||||
|         const viewKey = generateTableKey({ | ||||
|             schemaName: tableSchema, | ||||
|             tableName: tableInfo.table, | ||||
|         }); | ||||
|         const isView = viewNamesSet.has(viewKey); | ||||
|         const isMaterializedView = materializedViewNamesSet.has(viewKey); | ||||
|  | ||||
|         // Initial random positions; these will be adjusted later | ||||
|         return { | ||||
|             id: generateId(), | ||||
|             name: tableInfo.table, | ||||
|             schema: tableSchema, | ||||
|             x: Math.random() * 1000, // Placeholder X | ||||
|             y: Math.random() * 800, // Placeholder Y | ||||
|             fields, | ||||
|             indexes: dbIndexes, | ||||
|             color: isMaterializedView | ||||
|                 ? materializedViewColor | ||||
|                 : isView | ||||
|                   ? viewColor | ||||
|                   : defaultTableColor, | ||||
|             isView: isView, | ||||
|             isMaterializedView: isMaterializedView, | ||||
|             createdAt: Date.now(), | ||||
|             comments: tableInfo.comment ? tableInfo.comment : undefined, | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     return result; | ||||
| }; | ||||
| @@ -1,20 +1,10 @@ | ||||
| import { describe, it, expect, vi } from 'vitest'; | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { exportBaseSQL } from '../export-sql-script'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
| 
 | ||||
| // Mock the dbml/core importer
 | ||||
| vi.mock('@dbml/core', () => ({ | ||||
|     importer: { | ||||
|         import: vi.fn((sql: string) => { | ||||
|             // Return a simplified DBML for testing
 | ||||
|             return sql; | ||||
|         }), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| describe('DBML Export - SQL Generation Tests', () => { | ||||
|     // Helper to generate test IDs and timestamps
 | ||||
|     let idCounter = 0; | ||||
| @@ -116,7 +106,7 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should contain composite primary key syntax
 | ||||
|             expect(sql).toContain('PRIMARY KEY (spell_id, component_id)'); | ||||
|             expect(sql).toContain('PRIMARY KEY ("spell_id", "component_id")'); | ||||
|             // Should NOT contain individual PRIMARY KEY constraints
 | ||||
|             expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); | ||||
|             expect(sql).not.toMatch( | ||||
| @@ -124,6 +114,96 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             ); | ||||
|         }); | ||||
| 
 | ||||
|         it('should not create duplicate index for composite primary key', () => { | ||||
|             const tableId = testId(); | ||||
|             const field1Id = testId(); | ||||
|             const field2Id = testId(); | ||||
|             const field3Id = testId(); | ||||
| 
 | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Landlord System', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: tableId, | ||||
|                         name: 'users_master_table', | ||||
|                         schema: 'landlord', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: field1Id, | ||||
|                                 name: 'master_user_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: field2Id, | ||||
|                                 name: 'tenant_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: field3Id, | ||||
|                                 name: 'tenant_user_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'enabled', | ||||
|                                 type: { id: 'boolean', name: 'boolean' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [ | ||||
|                             { | ||||
|                                 id: testId(), | ||||
|                                 name: 'idx_users_master_table_master_user_id_tenant_id_tenant_user_id', | ||||
|                                 unique: false, | ||||
|                                 fieldIds: [field1Id, field2Id, field3Id], | ||||
|                                 createdAt: testTime, | ||||
|                             }, | ||||
|                             { | ||||
|                                 id: testId(), | ||||
|                                 name: 'index_1', | ||||
|                                 unique: true, | ||||
|                                 fieldIds: [field2Id, field3Id], | ||||
|                                 createdAt: testTime, | ||||
|                             }, | ||||
|                         ], | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
| 
 | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
| 
 | ||||
|             // Should contain composite primary key constraint
 | ||||
|             expect(sql).toContain( | ||||
|                 'PRIMARY KEY ("master_user_id", "tenant_id", "tenant_user_id")' | ||||
|             ); | ||||
| 
 | ||||
|             // Should NOT contain the duplicate index for the primary key fields
 | ||||
|             expect(sql).not.toContain( | ||||
|                 'CREATE INDEX idx_users_master_table_master_user_id_tenant_id_tenant_user_id' | ||||
|             ); | ||||
| 
 | ||||
|             // Should still contain the unique index on subset of fields
 | ||||
|             expect(sql).toContain('CREATE UNIQUE INDEX index_1'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should handle single primary keys inline', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
| @@ -165,7 +245,7 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should contain inline PRIMARY KEY
 | ||||
|             expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); | ||||
|             expect(sql).toMatch(/"id"\s+uuid\s+NOT NULL\s+PRIMARY KEY/); | ||||
|             // Should NOT contain separate PRIMARY KEY constraint
 | ||||
|             expect(sql).not.toContain('PRIMARY KEY (id)'); | ||||
|         }); | ||||
| @@ -226,8 +306,8 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             expect(sql).not.toContain('DEFAULT has default'); | ||||
|             expect(sql).not.toContain('DEFAULT DEFAULT has default'); | ||||
|             // The fields should still be in the table
 | ||||
|             expect(sql).toContain('is_active boolean'); | ||||
|             expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
 | ||||
|             expect(sql).toContain('"is_active" boolean'); | ||||
|             expect(sql).toContain('"stock_count" integer NOT NULL'); // integer gets simplified to int
 | ||||
|         }); | ||||
| 
 | ||||
|         it('should handle valid default values correctly', () => { | ||||
| @@ -349,8 +429,8 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should convert NOW to NOW() and ('now') to now()
 | ||||
|             expect(sql).toContain('created_at timestamp DEFAULT NOW'); | ||||
|             expect(sql).toContain('updated_at timestamp DEFAULT now()'); | ||||
|             expect(sql).toContain('"created_at" timestamp DEFAULT NOW'); | ||||
|             expect(sql).toContain('"updated_at" timestamp DEFAULT now()'); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -405,9 +485,9 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should handle char with explicit length
 | ||||
|             expect(sql).toContain('element_code char(2)'); | ||||
|             expect(sql).toContain('"element_code" char(2)'); | ||||
|             // Should add default length for char without length
 | ||||
|             expect(sql).toContain('status char(1)'); | ||||
|             expect(sql).toContain('"status" char(1)'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should not have spaces between char and parentheses', () => { | ||||
| @@ -516,7 +596,7 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should create a valid table without primary key
 | ||||
|             expect(sql).toContain('CREATE TABLE experiment_logs'); | ||||
|             expect(sql).toContain('CREATE TABLE "experiment_logs"'); | ||||
|             expect(sql).not.toContain('PRIMARY KEY'); | ||||
|         }); | ||||
| 
 | ||||
| @@ -631,11 +711,11 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should create both tables
 | ||||
|             expect(sql).toContain('CREATE TABLE guilds'); | ||||
|             expect(sql).toContain('CREATE TABLE guild_members'); | ||||
|             expect(sql).toContain('CREATE TABLE "guilds"'); | ||||
|             expect(sql).toContain('CREATE TABLE "guild_members"'); | ||||
|             // Should create foreign key
 | ||||
|             expect(sql).toContain( | ||||
|                 'ALTER TABLE guild_members ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES guilds (id)' | ||||
|                 'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY ("guild_id") REFERENCES "guilds" ("id");' | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
| @@ -709,12 +789,9 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
| 
 | ||||
|             // Should create schemas
 | ||||
|             expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation'); | ||||
|             expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic'); | ||||
|             // Should use schema-qualified table names
 | ||||
|             expect(sql).toContain('CREATE TABLE transportation.portals'); | ||||
|             expect(sql).toContain('CREATE TABLE magic.spells'); | ||||
|             expect(sql).toContain('CREATE TABLE "transportation"."portals"'); | ||||
|             expect(sql).toContain('CREATE TABLE "magic"."spells"'); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -761,7 +838,7 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should still create table structure
 | ||||
|             expect(sql).toContain('CREATE TABLE empty_table'); | ||||
|             expect(sql).toContain('CREATE TABLE "empty_table"'); | ||||
|             expect(sql).toContain('(\n\n)'); | ||||
|         }); | ||||
| 
 | ||||
| @@ -862,9 +939,9 @@ describe('DBML Export - SQL Generation Tests', () => { | ||||
|             }); | ||||
| 
 | ||||
|             // Should include precision and scale
 | ||||
|             expect(sql).toContain('amount numeric(15, 2)'); | ||||
|             expect(sql).toContain('"amount" numeric(15, 2)'); | ||||
|             // Should include precision only when scale is not provided
 | ||||
|             expect(sql).toContain('interest_rate numeric(5)'); | ||||
|             expect(sql).toContain('"interest_rate" numeric(5)'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -156,11 +156,11 @@ export function exportMSSQL({ | ||||
|                         const notNull = field.nullable ? '' : ' NOT NULL'; | ||||
| 
 | ||||
|                         // Check if identity column
 | ||||
|                         const identity = field.default | ||||
|                             ?.toLowerCase() | ||||
|                             .includes('identity') | ||||
|                             ? ' IDENTITY(1,1)' | ||||
|                             : ''; | ||||
|                         const identity = | ||||
|                             field.increment || | ||||
|                             field.default?.toLowerCase().includes('identity') | ||||
|                                 ? ' IDENTITY(1,1)' | ||||
|                                 : ''; | ||||
| 
 | ||||
|                         const unique = | ||||
|                             !field.primaryKey && field.unique ? ' UNIQUE' : ''; | ||||
| @@ -168,6 +168,7 @@ export function exportMSSQL({ | ||||
|                         // Handle default value using SQL Server specific parser
 | ||||
|                         const defaultValue = | ||||
|                             field.default && | ||||
|                             !field.increment && | ||||
|                             !field.default.toLowerCase().includes('identity') | ||||
|                                 ? ` DEFAULT ${parseMSSQLDefault(field)}` | ||||
|                                 : ''; | ||||
| @@ -177,7 +178,15 @@ export function exportMSSQL({ | ||||
|                     }) | ||||
|                     .join(',\n')}${ | ||||
|                     table.fields.filter((f) => f.primaryKey).length > 0 | ||||
|                         ? `,\n    PRIMARY KEY (${table.fields | ||||
|                         ? `,\n    ${(() => { | ||||
|                               // Find PK index to get the constraint name
 | ||||
|                               const pkIndex = table.indexes.find( | ||||
|                                   (idx) => idx.isPrimaryKey | ||||
|                               ); | ||||
|                               return pkIndex?.name | ||||
|                                   ? `CONSTRAINT [${pkIndex.name}] ` | ||||
|                                   : ''; | ||||
|                           })()}PRIMARY KEY (${table.fields | ||||
|                               .filter((f) => f.primaryKey) | ||||
|                               .map((f) => `[${f.name}]`) | ||||
|                               .join(', ')})` | ||||
| @@ -274,14 +274,15 @@ export function exportMySQL({ | ||||
|                         // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
 | ||||
|                         let autoIncrement = ''; | ||||
|                         if ( | ||||
|                             field.primaryKey && | ||||
|                             (field.default | ||||
|                                 ?.toLowerCase() | ||||
|                                 .includes('identity') || | ||||
|                                 field.default | ||||
|                             field.increment || | ||||
|                             (field.primaryKey && | ||||
|                                 (field.default | ||||
|                                     ?.toLowerCase() | ||||
|                                     .includes('autoincrement') || | ||||
|                                 field.default?.includes('nextval')) | ||||
|                                     .includes('identity') || | ||||
|                                     field.default | ||||
|                                         ?.toLowerCase() | ||||
|                                         .includes('autoincrement') || | ||||
|                                     field.default?.includes('nextval'))) | ||||
|                         ) { | ||||
|                             autoIncrement = ' AUTO_INCREMENT'; | ||||
|                         } | ||||
| @@ -290,9 +291,10 @@ export function exportMySQL({ | ||||
|                         const unique = | ||||
|                             !field.primaryKey && field.unique ? ' UNIQUE' : ''; | ||||
| 
 | ||||
|                         // Handle default value
 | ||||
|                         // Handle default value - skip if auto increment
 | ||||
|                         const defaultValue = | ||||
|                             field.default && | ||||
|                             !field.increment && | ||||
|                             !field.default.toLowerCase().includes('identity') && | ||||
|                             !field.default | ||||
|                                 .toLowerCase() | ||||
| @@ -311,7 +313,15 @@ export function exportMySQL({ | ||||
|                     .join(',\n')}${ | ||||
|                     // Add PRIMARY KEY as table constraint
 | ||||
|                     primaryKeyFields.length > 0 | ||||
|                         ? `,\n    PRIMARY KEY (${primaryKeyFields | ||||
|                         ? `,\n    ${(() => { | ||||
|                               // Find PK index to get the constraint name
 | ||||
|                               const pkIndex = table.indexes.find( | ||||
|                                   (idx) => idx.isPrimaryKey | ||||
|                               ); | ||||
|                               return pkIndex?.name | ||||
|                                   ? `CONSTRAINT \`${pkIndex.name}\` ` | ||||
|                                   : ''; | ||||
|                           })()}PRIMARY KEY (${primaryKeyFields | ||||
|                               .map((f) => `\`${f.name}\``) | ||||
|                               .join(', ')})` | ||||
|                         : '' | ||||
| @@ -325,7 +325,15 @@ export function exportPostgreSQL({ | ||||
|                     }) | ||||
|                     .join(',\n')}${ | ||||
|                     primaryKeyFields.length > 0 | ||||
|                         ? `,\n    PRIMARY KEY (${primaryKeyFields | ||||
|                         ? `,\n    ${(() => { | ||||
|                               // Find PK index to get the constraint name
 | ||||
|                               const pkIndex = table.indexes.find( | ||||
|                                   (idx) => idx.isPrimaryKey | ||||
|                               ); | ||||
|                               return pkIndex?.name | ||||
|                                   ? `CONSTRAINT "${pkIndex.name}" ` | ||||
|                                   : ''; | ||||
|                           })()}PRIMARY KEY (${primaryKeyFields | ||||
|                               .map((f) => `"${f.name}"`) | ||||
|                               .join(', ')})` | ||||
|                         : '' | ||||
| @@ -405,7 +413,7 @@ export function exportPostgreSQL({ | ||||
|                                     .filter(Boolean); | ||||
| 
 | ||||
|                                 return indexFieldNames.length > 0 | ||||
|                                     ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});` | ||||
|                                     ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});` | ||||
|                                     : ''; | ||||
|                             }) | ||||
|                             .filter(Boolean); | ||||
| @@ -67,8 +67,9 @@ function parseSQLiteDefault(field: DBField): string { | ||||
|     return `'${defaultValue.replace(/'/g, "''")}'`; | ||||
| } | ||||
| 
 | ||||
| // Map problematic types to SQLite compatible types
 | ||||
| // Preserve original types for SQLite export (only map when necessary)
 | ||||
| function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string { | ||||
|     const originalType = typeName; | ||||
|     typeName = typeName.toLowerCase(); | ||||
| 
 | ||||
|     // Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
 | ||||
| @@ -76,59 +77,62 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string { | ||||
|         return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
 | ||||
|     } | ||||
| 
 | ||||
|     // Map common types to SQLite's simplified type system
 | ||||
|     // Preserve original type names that SQLite accepts
 | ||||
|     switch (typeName) { | ||||
|         // Keep these types as-is
 | ||||
|         case 'integer': | ||||
|         case 'text': | ||||
|         case 'real': | ||||
|         case 'blob': | ||||
|         case 'numeric': | ||||
|         case 'decimal': | ||||
|         case 'boolean': | ||||
|         case 'date': | ||||
|         case 'datetime': | ||||
|         case 'timestamp': | ||||
|         case 'float': | ||||
|         case 'double': | ||||
|         case 'varchar': | ||||
|         case 'char': | ||||
|         case 'int': | ||||
|         case 'smallint': | ||||
|         case 'tinyint': | ||||
|         case 'mediumint': | ||||
|         case 'bigint': | ||||
|             return 'INTEGER'; | ||||
|         case 'json': | ||||
|             return typeName.toUpperCase(); | ||||
| 
 | ||||
|         case 'decimal': | ||||
|         case 'numeric': | ||||
|         case 'float': | ||||
|         case 'double': | ||||
|         case 'real': | ||||
|             return 'REAL'; | ||||
| 
 | ||||
|         case 'char': | ||||
|         // Only map types that SQLite truly doesn't recognize
 | ||||
|         case 'nchar': | ||||
|         case 'varchar': | ||||
|         case 'nvarchar': | ||||
|         case 'text': | ||||
|         case 'ntext': | ||||
|         case 'character varying': | ||||
|         case 'character': | ||||
|             return 'TEXT'; | ||||
| 
 | ||||
|         case 'date': | ||||
|         case 'datetime': | ||||
|         case 'timestamp': | ||||
|         case 'datetime2': | ||||
|             return 'TEXT'; // SQLite doesn't have dedicated date types
 | ||||
|             return 'DATETIME'; | ||||
| 
 | ||||
|         case 'blob': | ||||
|         case 'binary': | ||||
|         case 'varbinary': | ||||
|         case 'image': | ||||
|             return 'BLOB'; | ||||
| 
 | ||||
|         case 'bit': | ||||
|         case 'boolean': | ||||
|             return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
 | ||||
|             return 'BOOLEAN'; | ||||
| 
 | ||||
|         case 'user-defined': | ||||
|         case 'json': | ||||
|         case 'jsonb': | ||||
|             return 'TEXT'; // Store as JSON text
 | ||||
|             return 'TEXT'; | ||||
| 
 | ||||
|         case 'array': | ||||
|             return 'TEXT'; // Store as serialized array text
 | ||||
|             return 'TEXT'; | ||||
| 
 | ||||
|         case 'geometry': | ||||
|         case 'geography': | ||||
|             return 'BLOB'; // Store spatial data as BLOB in SQLite
 | ||||
|             return 'BLOB'; | ||||
| 
 | ||||
|         case 'mediumint': | ||||
|             return 'INTEGER'; | ||||
|     } | ||||
| 
 | ||||
|     // If type has array notation (ends with []), treat as TEXT
 | ||||
| @@ -136,8 +140,8 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string { | ||||
|         return 'TEXT'; | ||||
|     } | ||||
| 
 | ||||
|     // For any other types, default to TEXT
 | ||||
|     return typeName; | ||||
|     // For any other types, preserve the original
 | ||||
|     return originalType.toUpperCase(); | ||||
| } | ||||
| 
 | ||||
| export function exportSQLite({ | ||||
| @@ -157,6 +161,11 @@ export function exportSQLite({ | ||||
|     // Start SQL script - SQLite doesn't use schemas, so we skip schema creation
 | ||||
|     let sqlScript = '-- SQLite database export\n'; | ||||
| 
 | ||||
|     // Add PRAGMA foreign_keys = ON if there are relationships
 | ||||
|     if (relationships && relationships.length > 0) { | ||||
|         sqlScript += 'PRAGMA foreign_keys = ON;\n\n'; | ||||
|     } | ||||
| 
 | ||||
|     // Begin transaction for faster import
 | ||||
|     sqlScript += 'BEGIN TRANSACTION;\n'; | ||||
| 
 | ||||
| @@ -205,6 +214,86 @@ export function exportSQLite({ | ||||
|                         'integer' || | ||||
|                         primaryKeyFields[0].type.name.toLowerCase() === 'int'); | ||||
| 
 | ||||
|                 // Collect foreign key constraints for this table
 | ||||
|                 const tableForeignKeys: string[] = []; | ||||
|                 relationships.forEach((r: DBRelationship) => { | ||||
|                     const sourceTable = tables.find( | ||||
|                         (t) => t.id === r.sourceTableId | ||||
|                     ); | ||||
|                     const targetTable = tables.find( | ||||
|                         (t) => t.id === r.targetTableId | ||||
|                     ); | ||||
| 
 | ||||
|                     if ( | ||||
|                         !sourceTable || | ||||
|                         !targetTable || | ||||
|                         sourceTable.isView || | ||||
|                         targetTable.isView || | ||||
|                         sqliteSystemTables.includes( | ||||
|                             sourceTable.name.toLowerCase() | ||||
|                         ) || | ||||
|                         sqliteSystemTables.includes( | ||||
|                             targetTable.name.toLowerCase() | ||||
|                         ) | ||||
|                     ) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     const sourceField = sourceTable.fields.find( | ||||
|                         (f) => f.id === r.sourceFieldId | ||||
|                     ); | ||||
|                     const targetField = targetTable.fields.find( | ||||
|                         (f) => f.id === r.targetFieldId | ||||
|                     ); | ||||
| 
 | ||||
|                     if (!sourceField || !targetField) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Determine which table should have the foreign key based on cardinality
 | ||||
|                     let fkTable, fkField, refTable, refField; | ||||
| 
 | ||||
|                     if ( | ||||
|                         r.sourceCardinality === 'one' && | ||||
|                         r.targetCardinality === 'many' | ||||
|                     ) { | ||||
|                         // FK goes on target table
 | ||||
|                         fkTable = targetTable; | ||||
|                         fkField = targetField; | ||||
|                         refTable = sourceTable; | ||||
|                         refField = sourceField; | ||||
|                     } else if ( | ||||
|                         r.sourceCardinality === 'many' && | ||||
|                         r.targetCardinality === 'one' | ||||
|                     ) { | ||||
|                         // FK goes on source table
 | ||||
|                         fkTable = sourceTable; | ||||
|                         fkField = sourceField; | ||||
|                         refTable = targetTable; | ||||
|                         refField = targetField; | ||||
|                     } else if ( | ||||
|                         r.sourceCardinality === 'one' && | ||||
|                         r.targetCardinality === 'one' | ||||
|                     ) { | ||||
|                         // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | ||||
|                         // We'll keep the current behavior for 1:1
 | ||||
|                         fkTable = sourceTable; | ||||
|                         fkField = sourceField; | ||||
|                         refTable = targetTable; | ||||
|                         refField = targetField; | ||||
|                     } else { | ||||
|                         // Many-to-many relationships need a junction table, skip for now
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // If this foreign key belongs to the current table, add it
 | ||||
|                     if (fkTable.id === table.id) { | ||||
|                         tableForeignKeys.push( | ||||
|                             `    FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}")` | ||||
|                         ); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 return `${schemaComment}${ | ||||
|                     table.comments ? formatTableComment(table.comments) : '' | ||||
|                 }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields | ||||
| @@ -212,14 +301,40 @@ export function exportSQLite({ | ||||
|                         const fieldName = `"${field.name}"`; | ||||
| 
 | ||||
|                         // Handle type name - map to SQLite compatible types
 | ||||
|                         const typeName = mapSQLiteType( | ||||
|                         const baseTypeName = mapSQLiteType( | ||||
|                             field.type.name, | ||||
|                             field.primaryKey | ||||
|                         ); | ||||
| 
 | ||||
|                         // SQLite ignores length specifiers, so we don't add them
 | ||||
|                         // We'll keep this simple without size info
 | ||||
|                         const typeWithoutSize = typeName; | ||||
|                         // Add size/precision/scale parameters if applicable
 | ||||
|                         let typeWithParams = baseTypeName; | ||||
| 
 | ||||
|                         // Add character maximum length for VARCHAR, CHAR, etc.
 | ||||
|                         if ( | ||||
|                             field.characterMaximumLength && | ||||
|                             ['VARCHAR', 'CHAR', 'TEXT'].includes( | ||||
|                                 baseTypeName.toUpperCase() | ||||
|                             ) | ||||
|                         ) { | ||||
|                             typeWithParams = `${baseTypeName}(${field.characterMaximumLength})`; | ||||
|                         } | ||||
|                         // Add precision and scale for DECIMAL, NUMERIC, etc.
 | ||||
|                         else if ( | ||||
|                             field.precision && | ||||
|                             [ | ||||
|                                 'DECIMAL', | ||||
|                                 'NUMERIC', | ||||
|                                 'REAL', | ||||
|                                 'FLOAT', | ||||
|                                 'DOUBLE', | ||||
|                             ].includes(baseTypeName.toUpperCase()) | ||||
|                         ) { | ||||
|                             if (field.scale) { | ||||
|                                 typeWithParams = `${baseTypeName}(${field.precision}, ${field.scale})`; | ||||
|                             } else { | ||||
|                                 typeWithParams = `${baseTypeName}(${field.precision})`; | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         const notNull = field.nullable ? '' : ' NOT NULL'; | ||||
| 
 | ||||
| @@ -228,9 +343,10 @@ export function exportSQLite({ | ||||
|                         if ( | ||||
|                             field.primaryKey && | ||||
|                             singleIntegerPrimaryKey && | ||||
|                             (field.default | ||||
|                                 ?.toLowerCase() | ||||
|                                 .includes('identity') || | ||||
|                             (field.increment || | ||||
|                                 field.default | ||||
|                                     ?.toLowerCase() | ||||
|                                     .includes('identity') || | ||||
|                                 field.default | ||||
|                                     ?.toLowerCase() | ||||
|                                     .includes('autoincrement') || | ||||
| @@ -247,6 +363,7 @@ export function exportSQLite({ | ||||
|                         let defaultValue = ''; | ||||
|                         if ( | ||||
|                             field.default && | ||||
|                             !field.increment && | ||||
|                             !field.default.toLowerCase().includes('identity') && | ||||
|                             !field.default | ||||
|                                 .toLowerCase() | ||||
| @@ -267,7 +384,7 @@ export function exportSQLite({ | ||||
|                                 ? ' PRIMARY KEY' + autoIncrement | ||||
|                                 : ''; | ||||
| 
 | ||||
|                         return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`; | ||||
|                         return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithParams}${primaryKey}${notNull}${unique}${defaultValue}`; | ||||
|                     }) | ||||
|                     .join(',\n')}${ | ||||
|                     // Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
 | ||||
| @@ -276,6 +393,11 @@ export function exportSQLite({ | ||||
|                               .map((f) => `"${f.name}"`) | ||||
|                               .join(', ')})` | ||||
|                         : '' | ||||
|                 }${ | ||||
|                     // Add foreign key constraints
 | ||||
|                     tableForeignKeys.length > 0 | ||||
|                         ? ',\n' + tableForeignKeys.join(',\n') | ||||
|                         : '' | ||||
|                 }\n);\n${ | ||||
|                     // Add indexes - SQLite doesn't support indexes in CREATE TABLE
 | ||||
|                     (() => { | ||||
| @@ -333,82 +455,8 @@ export function exportSQLite({ | ||||
|             .filter(Boolean) // Remove empty strings (views)
 | ||||
|             .join('\n'); | ||||
|     } | ||||
|     // Generate table constraints and triggers for foreign keys
 | ||||
|     // SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
 | ||||
|     // But we'll also provide individual ALTER TABLE statements as comments for reference
 | ||||
| 
 | ||||
|     if (relationships.length > 0) { | ||||
|         sqlScript += '\n-- Foreign key constraints\n'; | ||||
|         sqlScript += | ||||
|             '-- Note: SQLite requires foreign_keys pragma to be enabled:\n'; | ||||
|         sqlScript += '-- PRAGMA foreign_keys = ON;\n'; | ||||
| 
 | ||||
|         relationships.forEach((r: DBRelationship) => { | ||||
|             const sourceTable = tables.find((t) => t.id === r.sourceTableId); | ||||
|             const targetTable = tables.find((t) => t.id === r.targetTableId); | ||||
| 
 | ||||
|             if ( | ||||
|                 !sourceTable || | ||||
|                 !targetTable || | ||||
|                 sourceTable.isView || | ||||
|                 targetTable.isView || | ||||
|                 sqliteSystemTables.includes(sourceTable.name.toLowerCase()) || | ||||
|                 sqliteSystemTables.includes(targetTable.name.toLowerCase()) | ||||
|             ) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const sourceField = sourceTable.fields.find( | ||||
|                 (f) => f.id === r.sourceFieldId | ||||
|             ); | ||||
|             const targetField = targetTable.fields.find( | ||||
|                 (f) => f.id === r.targetFieldId | ||||
|             ); | ||||
| 
 | ||||
|             if (!sourceField || !targetField) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Determine which table should have the foreign key based on cardinality
 | ||||
|             let fkTable, fkField, refTable, refField; | ||||
| 
 | ||||
|             if ( | ||||
|                 r.sourceCardinality === 'one' && | ||||
|                 r.targetCardinality === 'many' | ||||
|             ) { | ||||
|                 // FK goes on target table
 | ||||
|                 fkTable = targetTable; | ||||
|                 fkField = targetField; | ||||
|                 refTable = sourceTable; | ||||
|                 refField = sourceField; | ||||
|             } else if ( | ||||
|                 r.sourceCardinality === 'many' && | ||||
|                 r.targetCardinality === 'one' | ||||
|             ) { | ||||
|                 // FK goes on source table
 | ||||
|                 fkTable = sourceTable; | ||||
|                 fkField = sourceField; | ||||
|                 refTable = targetTable; | ||||
|                 refField = targetField; | ||||
|             } else if ( | ||||
|                 r.sourceCardinality === 'one' && | ||||
|                 r.targetCardinality === 'one' | ||||
|             ) { | ||||
|                 // For 1:1, FK can go on either side, but typically goes on the table that references the other
 | ||||
|                 // We'll keep the current behavior for 1:1
 | ||||
|                 fkTable = sourceTable; | ||||
|                 fkField = sourceField; | ||||
|                 refTable = targetTable; | ||||
|                 refField = targetField; | ||||
|             } else { | ||||
|                 // Many-to-many relationships need a junction table, skip for now
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Create commented out version of what would be ALTER TABLE statement
 | ||||
|             sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`; | ||||
|         }); | ||||
|     } | ||||
|     // Foreign keys are now included inline in CREATE TABLE statements
 | ||||
|     // No need for separate ALTER TABLE statements in SQLite
 | ||||
| 
 | ||||
|     // Commit transaction
 | ||||
|     sqlScript += '\nCOMMIT;\n'; | ||||
| @@ -1,6 +1,9 @@ | ||||
| import type { Diagram } from '../../domain/diagram'; | ||||
| import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { | ||||
|     DatabaseType, | ||||
|     databaseTypesWithCommentSupport, | ||||
| } from '@/lib/domain/database-type'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import type { DataType } from '../data-types/data-types'; | ||||
| import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache'; | ||||
| @@ -8,6 +11,7 @@ import { exportMSSQL } from './export-per-type/mssql'; | ||||
| import { exportPostgreSQL } from './export-per-type/postgresql'; | ||||
| import { exportSQLite } from './export-per-type/sqlite'; | ||||
| import { exportMySQL } from './export-per-type/mysql'; | ||||
| import { escapeSQLComment } from './export-per-type/common'; | ||||
| 
 | ||||
| // Function to simplify verbose data type names
 | ||||
| const simplifyDataType = (typeName: string): string => { | ||||
| @@ -16,6 +20,61 @@ const simplifyDataType = (typeName: string): string => { | ||||
|     return typeMap[typeName.toLowerCase()] || typeName; | ||||
| }; | ||||
| 
 | ||||
| // Helper function to properly quote table/schema names with special characters
 | ||||
| const getQuotedTableName = ( | ||||
|     table: DBTable, | ||||
|     isDBMLFlow: boolean = false | ||||
| ): string => { | ||||
|     // Check if a name is already quoted
 | ||||
|     const isAlreadyQuoted = (name: string) => { | ||||
|         return ( | ||||
|             (name.startsWith('"') && name.endsWith('"')) || | ||||
|             (name.startsWith('`') && name.endsWith('`')) || | ||||
|             (name.startsWith('[') && name.endsWith(']')) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     // Only add quotes if needed and not already quoted
 | ||||
|     const quoteIfNeeded = (name: string) => { | ||||
|         if (isAlreadyQuoted(name)) { | ||||
|             return name; | ||||
|         } | ||||
|         const needsQuoting = /[^a-zA-Z0-9_]/.test(name) || isDBMLFlow; | ||||
|         return needsQuoting ? `"${name}"` : name; | ||||
|     }; | ||||
| 
 | ||||
|     if (table.schema) { | ||||
|         const quotedSchema = quoteIfNeeded(table.schema); | ||||
|         const quotedTable = quoteIfNeeded(table.name); | ||||
|         return `${quotedSchema}.${quotedTable}`; | ||||
|     } else { | ||||
|         return quoteIfNeeded(table.name); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const getQuotedFieldName = ( | ||||
|     fieldName: string, | ||||
|     isDBMLFlow: boolean = false | ||||
| ): string => { | ||||
|     // Check if a name is already quoted
 | ||||
|     const isAlreadyQuoted = (name: string) => { | ||||
|         return ( | ||||
|             (name.startsWith('"') && name.endsWith('"')) || | ||||
|             (name.startsWith('`') && name.endsWith('`')) || | ||||
|             (name.startsWith('[') && name.endsWith(']')) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     if (isAlreadyQuoted(fieldName)) { | ||||
|         return fieldName; | ||||
|     } | ||||
| 
 | ||||
|     // For DBML flow, always quote field names
 | ||||
|     // Otherwise, only quote if it contains special characters
 | ||||
|     const needsQuoting = /[^a-zA-Z0-9_]/.test(fieldName) || isDBMLFlow; | ||||
|     return needsQuoting ? `"${fieldName}"` : fieldName; | ||||
| }; | ||||
| 
 | ||||
| export const exportBaseSQL = ({ | ||||
|     diagram, | ||||
|     targetDatabaseType, | ||||
| @@ -59,18 +118,21 @@ export const exportBaseSQL = ({ | ||||
|     let sqlScript = ''; | ||||
| 
 | ||||
|     // First create the CREATE SCHEMA statements for all the found schemas based on tables
 | ||||
|     const schemas = new Set<string>(); | ||||
|     tables.forEach((table) => { | ||||
|         if (table.schema) { | ||||
|             schemas.add(table.schema); | ||||
|         } | ||||
|     }); | ||||
|     // Skip schema creation for DBML flow as DBML doesn't support CREATE SCHEMA syntax
 | ||||
|     if (!isDBMLFlow) { | ||||
|         const schemas = new Set<string>(); | ||||
|         tables.forEach((table) => { | ||||
|             if (table.schema) { | ||||
|                 schemas.add(table.schema); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     // Add CREATE SCHEMA statements if any schemas exist
 | ||||
|     schemas.forEach((schema) => { | ||||
|         sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`; | ||||
|     }); | ||||
|     if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
 | ||||
|         // Add CREATE SCHEMA statements if any schemas exist
 | ||||
|         schemas.forEach((schema) => { | ||||
|             sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`; | ||||
|         }); | ||||
|         if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
 | ||||
|     } | ||||
| 
 | ||||
|     // Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
 | ||||
|     if (diagram.customTypes && diagram.customTypes.length > 0) { | ||||
| @@ -162,9 +224,7 @@ export const exportBaseSQL = ({ | ||||
| 
 | ||||
|     // Loop through each non-view table to generate the SQL statements
 | ||||
|     nonViewTables.forEach((table) => { | ||||
|         const tableName = table.schema | ||||
|             ? `${table.schema}.${table.name}` | ||||
|             : table.name; | ||||
|         const tableName = getQuotedTableName(table, isDBMLFlow); | ||||
|         sqlScript += `CREATE TABLE ${tableName} (\n`; | ||||
| 
 | ||||
|         // Check for composite primary keys
 | ||||
| @@ -233,7 +293,8 @@ export const exportBaseSQL = ({ | ||||
|                 typeName = 'char'; | ||||
|             } | ||||
| 
 | ||||
|             sqlScript += `  ${field.name} ${typeName}`; | ||||
|             const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow); | ||||
|             sqlScript += `  ${quotedFieldName} ${typeName}`; | ||||
| 
 | ||||
|             // Add size for character types
 | ||||
|             if ( | ||||
| @@ -270,8 +331,13 @@ export const exportBaseSQL = ({ | ||||
|                 sqlScript += ` UNIQUE`; | ||||
|             } | ||||
| 
 | ||||
|             // Handle AUTO INCREMENT - add as a comment for AI to process
 | ||||
|             if (field.increment) { | ||||
|                 sqlScript += ` /* AUTO_INCREMENT */`; | ||||
|             } | ||||
| 
 | ||||
|             // Handle DEFAULT value
 | ||||
|             if (field.default) { | ||||
|             if (field.default && !field.increment) { | ||||
|                 // Temp remove default user-define value when it have it
 | ||||
|                 let fieldDefault = field.default; | ||||
| 
 | ||||
| @@ -304,45 +370,87 @@ export const exportBaseSQL = ({ | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Handle PRIMARY KEY constraint - only add inline if not composite
 | ||||
|             if (field.primaryKey && !hasCompositePrimaryKey) { | ||||
|             // Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
 | ||||
|             const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey); | ||||
|             if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) { | ||||
|                 sqlScript += ' PRIMARY KEY'; | ||||
|             } | ||||
| 
 | ||||
|             // Add a comma after each field except the last one (or before composite primary key)
 | ||||
|             if (index < table.fields.length - 1 || hasCompositePrimaryKey) { | ||||
|             // Add a comma after each field except the last one (or before PK constraint)
 | ||||
|             const needsPKConstraint = | ||||
|                 hasCompositePrimaryKey || | ||||
|                 (primaryKeyFields.length === 1 && pkIndex?.name); | ||||
|             if (index < table.fields.length - 1 || needsPKConstraint) { | ||||
|                 sqlScript += ',\n'; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Add composite primary key constraint if needed
 | ||||
|         if (hasCompositePrimaryKey) { | ||||
|             const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', '); | ||||
|             sqlScript += `\n  PRIMARY KEY (${pkFieldNames})`; | ||||
|         // Add primary key constraint if needed (for composite PKs or single PK with custom name)
 | ||||
|         const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey); | ||||
|         if ( | ||||
|             hasCompositePrimaryKey || | ||||
|             (primaryKeyFields.length === 1 && pkIndex?.name) | ||||
|         ) { | ||||
|             const pkFieldNames = primaryKeyFields | ||||
|                 .map((f) => getQuotedFieldName(f.name, isDBMLFlow)) | ||||
|                 .join(', '); | ||||
|             if (pkIndex?.name) { | ||||
|                 sqlScript += `\n  CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`; | ||||
|             } else { | ||||
|                 sqlScript += `\n  PRIMARY KEY (${pkFieldNames})`; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         sqlScript += '\n);\n'; | ||||
| 
 | ||||
|         // Add table comment
 | ||||
|         if (table.comments) { | ||||
|             sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`; | ||||
|         // Add table comment (only for databases that support COMMENT ON syntax)
 | ||||
|         const supportsCommentOn = | ||||
|             databaseTypesWithCommentSupport.includes(targetDatabaseType); | ||||
| 
 | ||||
|         if (table.comments && supportsCommentOn) { | ||||
|             sqlScript += `COMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';\n`; | ||||
|         } | ||||
| 
 | ||||
|         table.fields.forEach((field) => { | ||||
|             // Add column comment
 | ||||
|             if (field.comments) { | ||||
|                 sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`; | ||||
|             // Add column comment (only for databases that support COMMENT ON syntax)
 | ||||
|             if (field.comments && supportsCommentOn) { | ||||
|                 const quotedFieldName = getQuotedFieldName( | ||||
|                     field.name, | ||||
|                     isDBMLFlow | ||||
|                 ); | ||||
|                 sqlScript += `COMMENT ON COLUMN ${tableName}.${quotedFieldName} IS '${escapeSQLComment(field.comments)}';\n`; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Generate SQL for indexes
 | ||||
|         table.indexes.forEach((index) => { | ||||
|             const fieldNames = index.fieldIds | ||||
|                 .map( | ||||
|                     (fieldId) => | ||||
|                         table.fields.find((field) => field.id === fieldId)?.name | ||||
|             // Skip the primary key index (it's already handled as a constraint)
 | ||||
|             if (index.isPrimaryKey) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Get the fields for this index
 | ||||
|             const indexFields = index.fieldIds | ||||
|                 .map((fieldId) => table.fields.find((f) => f.id === fieldId)) | ||||
|                 .filter( | ||||
|                     (field): field is NonNullable<typeof field> => | ||||
|                         field !== undefined | ||||
|                 ); | ||||
| 
 | ||||
|             // Skip if this index exactly matches the primary key fields
 | ||||
|             // This prevents creating redundant indexes for composite primary keys
 | ||||
|             if ( | ||||
|                 primaryKeyFields.length > 0 && | ||||
|                 primaryKeyFields.length === indexFields.length && | ||||
|                 primaryKeyFields.every((pk) => | ||||
|                     indexFields.some((field) => field.id === pk.id) | ||||
|                 ) | ||||
|                 .filter(Boolean) | ||||
|             ) { | ||||
|                 return; // Skip this index as it's redundant with the primary key
 | ||||
|             } | ||||
| 
 | ||||
|             const fieldNames = indexFields | ||||
|                 .map((field) => getQuotedFieldName(field.name, isDBMLFlow)) | ||||
|                 .join(', '); | ||||
| 
 | ||||
|             if (fieldNames) { | ||||
| @@ -420,13 +528,18 @@ export const exportBaseSQL = ({ | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const fkTableName = fkTable.schema | ||||
|                 ? `${fkTable.schema}.${fkTable.name}` | ||||
|                 : fkTable.name; | ||||
|             const refTableName = refTable.schema | ||||
|                 ? `${refTable.schema}.${refTable.name}` | ||||
|                 : refTable.name; | ||||
|             sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`; | ||||
|             const fkTableName = getQuotedTableName(fkTable, isDBMLFlow); | ||||
|             const refTableName = getQuotedTableName(refTable, isDBMLFlow); | ||||
|             const quotedFkFieldName = getQuotedFieldName( | ||||
|                 fkField.name, | ||||
|                 isDBMLFlow | ||||
|             ); | ||||
|             const quotedRefFieldName = getQuotedFieldName( | ||||
|                 refField.name, | ||||
|                 isDBMLFlow | ||||
|             ); | ||||
| 
 | ||||
|             sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\n`; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| @@ -6,7 +6,7 @@ import type { DBField } from '@/lib/domain/db-field'; | ||||
| import type { DBIndex } from '@/lib/domain/db-index'; | ||||
| import type { DataType } from '@/lib/data/data-types/data-types'; | ||||
| import { genericDataTypes } from '@/lib/data/data-types/generic-data-types'; | ||||
| import { randomColor } from '@/lib/colors'; | ||||
| import { defaultTableColor } from '@/lib/colors'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||
| import { DBCustomTypeKind } from '@/lib/domain/db-custom-type'; | ||||
| @@ -86,7 +86,7 @@ export interface SQLBinaryExpr extends SQLASTNode { | ||||
|  | ||||
| export interface SQLFunctionNode extends SQLASTNode { | ||||
|     type: 'function'; | ||||
|     name: string; | ||||
|     name: string | { name: Array<{ value: string }> }; | ||||
|     args?: { | ||||
|         value: SQLASTArg[]; | ||||
|     }; | ||||
| @@ -108,6 +108,31 @@ export interface SQLStringLiteral extends SQLASTNode { | ||||
|     value: string; | ||||
| } | ||||
|  | ||||
| export interface SQLDefaultNode extends SQLASTNode { | ||||
|     type: 'default'; | ||||
|     value: SQLASTNode; | ||||
| } | ||||
|  | ||||
| export interface SQLCastNode extends SQLASTNode { | ||||
|     type: 'cast'; | ||||
|     expr: SQLASTNode; | ||||
|     target: Array<{ dataType: string }>; | ||||
| } | ||||
|  | ||||
| export interface SQLBooleanNode extends SQLASTNode { | ||||
|     type: 'bool'; | ||||
|     value: boolean; | ||||
| } | ||||
|  | ||||
| export interface SQLNullNode extends SQLASTNode { | ||||
|     type: 'null'; | ||||
| } | ||||
|  | ||||
| export interface SQLNumberNode extends SQLASTNode { | ||||
|     type: 'number'; | ||||
|     value: number; | ||||
| } | ||||
|  | ||||
| export type SQLASTArg = | ||||
|     | SQLColumnRef | ||||
|     | SQLStringLiteral | ||||
| @@ -146,6 +171,22 @@ export function buildSQLFromAST( | ||||
| ): string { | ||||
|     if (!ast) return ''; | ||||
|  | ||||
|     // Handle default value wrapper | ||||
|     if (ast.type === 'default' && 'value' in ast) { | ||||
|         const defaultNode = ast as SQLDefaultNode; | ||||
|         return buildSQLFromAST(defaultNode.value, dbType); | ||||
|     } | ||||
|  | ||||
|     // Handle PostgreSQL cast expressions (e.g., 'value'::type) | ||||
|     if (ast.type === 'cast' && 'expr' in ast && 'target' in ast) { | ||||
|         const castNode = ast as SQLCastNode; | ||||
|         const expr = buildSQLFromAST(castNode.expr, dbType); | ||||
|         if (castNode.target.length > 0 && castNode.target[0].dataType) { | ||||
|             return `${expr}::${castNode.target[0].dataType.toLowerCase()}`; | ||||
|         } | ||||
|         return expr; | ||||
|     } | ||||
|  | ||||
|     if (ast.type === 'binary_expr') { | ||||
|         const expr = ast as SQLBinaryExpr; | ||||
|         const leftSQL = buildSQLFromAST(expr.left, dbType); | ||||
| @@ -155,7 +196,59 @@ export function buildSQLFromAST( | ||||
|  | ||||
|     if (ast.type === 'function') { | ||||
|         const func = ast as SQLFunctionNode; | ||||
|         let expr = func.name; | ||||
|         let funcName = ''; | ||||
|  | ||||
|         // Handle nested function name structure | ||||
|         if (typeof func.name === 'object' && func.name && 'name' in func.name) { | ||||
|             const nameObj = func.name as { name: Array<{ value: string }> }; | ||||
|             if (nameObj.name.length > 0) { | ||||
|                 funcName = nameObj.name[0].value || ''; | ||||
|             } | ||||
|         } else if (typeof func.name === 'string') { | ||||
|             funcName = func.name; | ||||
|         } | ||||
|  | ||||
|         if (!funcName) return ''; | ||||
|  | ||||
|         // Normalize PostgreSQL function names to uppercase for consistency | ||||
|         if (dbType === DatabaseType.POSTGRESQL) { | ||||
|             const pgFunctions = [ | ||||
|                 'now', | ||||
|                 'current_timestamp', | ||||
|                 'current_date', | ||||
|                 'current_time', | ||||
|                 'gen_random_uuid', | ||||
|                 'random', | ||||
|                 'nextval', | ||||
|                 'currval', | ||||
|             ]; | ||||
|             if (pgFunctions.includes(funcName.toLowerCase())) { | ||||
|                 funcName = funcName.toUpperCase(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Some PostgreSQL functions don't have parentheses (like CURRENT_TIMESTAMP) | ||||
|         if (funcName === 'CURRENT_TIMESTAMP' && !func.args) { | ||||
|             return funcName; | ||||
|         } | ||||
|  | ||||
|         // Handle SQL Server function defaults that were preprocessed as strings | ||||
|         // The preprocessor converts NEWID() to 'newid', GETDATE() to 'getdate', etc. | ||||
|         if (dbType === DatabaseType.SQL_SERVER) { | ||||
|             const sqlServerFunctions: Record<string, string> = { | ||||
|                 newid: 'NEWID()', | ||||
|                 newsequentialid: 'NEWSEQUENTIALID()', | ||||
|                 getdate: 'GETDATE()', | ||||
|                 sysdatetime: 'SYSDATETIME()', | ||||
|             }; | ||||
|  | ||||
|             const lowerFuncName = funcName.toLowerCase(); | ||||
|             if (sqlServerFunctions[lowerFuncName]) { | ||||
|                 return sqlServerFunctions[lowerFuncName]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let expr = funcName; | ||||
|         if (func.args) { | ||||
|             expr += | ||||
|                 '(' + | ||||
| @@ -175,12 +268,31 @@ export function buildSQLFromAST( | ||||
|                     }) | ||||
|                     .join(', ') + | ||||
|                 ')'; | ||||
|         } else { | ||||
|             expr += '()'; | ||||
|         } | ||||
|         return expr; | ||||
|     } else if (ast.type === 'column_ref') { | ||||
|         return quoteIdentifier((ast as SQLColumnRef).column, dbType); | ||||
|     } else if (ast.type === 'expr_list') { | ||||
|         return (ast as SQLExprList).value.map((v) => v.value).join(' AND '); | ||||
|     } else if (ast.type === 'single_quote_string') { | ||||
|         // String literal with single quotes | ||||
|         const strNode = ast as SQLStringLiteral; | ||||
|         return `'${strNode.value}'`; | ||||
|     } else if (ast.type === 'double_quote_string') { | ||||
|         // String literal with double quotes | ||||
|         const strNode = ast as SQLStringLiteral; | ||||
|         return `"${strNode.value}"`; | ||||
|     } else if (ast.type === 'bool') { | ||||
|         // Boolean value | ||||
|         const boolNode = ast as SQLBooleanNode; | ||||
|         return boolNode.value ? 'TRUE' : 'FALSE'; | ||||
|     } else if (ast.type === 'null') { | ||||
|         return 'NULL'; | ||||
|     } else if (ast.type === 'number') { | ||||
|         const numNode = ast as SQLNumberNode; | ||||
|         return String(numNode.value); | ||||
|     } else { | ||||
|         const valueNode = ast as { type: string; value: string | number }; | ||||
|         return typeof valueNode.value === 'string' | ||||
| @@ -727,10 +839,10 @@ export function convertToChartDBDiagram( | ||||
|             indexes, | ||||
|             x: col * tableSpacing, | ||||
|             y: row * tableSpacing, | ||||
|             color: randomColor(), | ||||
|             color: defaultTableColor, | ||||
|             isView: false, | ||||
|             createdAt: Date.now(), | ||||
|         }; | ||||
|         } satisfies DBTable; | ||||
|     }); | ||||
|  | ||||
|     // Process relationships | ||||
| @@ -779,19 +891,13 @@ export function convertToChartDBDiagram( | ||||
|         } | ||||
|  | ||||
|         const sourceField = sourceTable.fields.find( | ||||
|             (f) => f.name === rel.sourceColumn | ||||
|             (f) => f.name.toLowerCase() === rel.sourceColumn.toLowerCase() | ||||
|         ); | ||||
|         const targetField = targetTable.fields.find( | ||||
|             (f) => f.name === rel.targetColumn | ||||
|             (f) => f.name.toLowerCase() === rel.targetColumn.toLowerCase() | ||||
|         ); | ||||
|  | ||||
|         if (!sourceField || !targetField) { | ||||
|             console.log('Relationship refers to non-existent field:', { | ||||
|                 sourceTable: rel.sourceTable, | ||||
|                 sourceField: rel.sourceColumn, | ||||
|                 targetTable: rel.targetTable, | ||||
|                 targetField: rel.targetColumn, | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,228 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromMySQL } from '../mysql'; | ||||
|  | ||||
| describe('MySQL Default Value Import', () => { | ||||
|     describe('String Default Values', () => { | ||||
|         it('should parse simple string defaults with single quotes', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE tavern_patrons ( | ||||
|                     patron_id INT NOT NULL, | ||||
|                     membership_status VARCHAR(50) DEFAULT 'regular', | ||||
|                     PRIMARY KEY (patron_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const statusColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'membership_status' | ||||
|             ); | ||||
|             expect(statusColumn?.default).toBe("'regular'"); | ||||
|         }); | ||||
|  | ||||
|         it('should parse string defaults with escaped quotes', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE wizard_spellbooks ( | ||||
|                     spellbook_id INT NOT NULL, | ||||
|                     incantation VARCHAR(255) DEFAULT 'Dragon\\'s flame', | ||||
|                     spell_metadata TEXT DEFAULT '{"type": "fire"}', | ||||
|                     PRIMARY KEY (spellbook_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const incantationColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'incantation' | ||||
|             ); | ||||
|             expect(incantationColumn?.default).toBeTruthy(); | ||||
|             const metadataColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'spell_metadata' | ||||
|             ); | ||||
|             expect(metadataColumn?.default).toBeTruthy(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Numeric Default Values', () => { | ||||
|         it('should parse integer defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE dungeon_levels ( | ||||
|                     level_id INT NOT NULL, | ||||
|                     monster_count INT DEFAULT 0, | ||||
|                     max_treasure INT DEFAULT 1000, | ||||
|                     PRIMARY KEY (level_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const monsterColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'monster_count' | ||||
|             ); | ||||
|             expect(monsterColumn?.default).toBe('0'); | ||||
|             const treasureColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'max_treasure' | ||||
|             ); | ||||
|             expect(treasureColumn?.default).toBe('1000'); | ||||
|         }); | ||||
|  | ||||
|         it('should parse decimal defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE merchant_inventory ( | ||||
|                     item_id INT NOT NULL, | ||||
|                     base_price DECIMAL(10, 2) DEFAULT 99.99, | ||||
|                     loyalty_discount FLOAT DEFAULT 0.15, | ||||
|                     PRIMARY KEY (item_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const priceColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'base_price' | ||||
|             ); | ||||
|             expect(priceColumn?.default).toBe('99.99'); | ||||
|             const discountColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'loyalty_discount' | ||||
|             ); | ||||
|             expect(discountColumn?.default).toBe('0.15'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Boolean Default Values', () => { | ||||
|         it('should parse boolean defaults in MySQL (using TINYINT)', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE character_status ( | ||||
|                     character_id INT NOT NULL, | ||||
|                     is_alive TINYINT(1) DEFAULT 1, | ||||
|                     is_cursed TINYINT(1) DEFAULT 0, | ||||
|                     has_magic BOOLEAN DEFAULT TRUE, | ||||
|                     PRIMARY KEY (character_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const aliveColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_alive' | ||||
|             ); | ||||
|             expect(aliveColumn?.default).toBe('1'); | ||||
|             const cursedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_cursed' | ||||
|             ); | ||||
|             expect(cursedColumn?.default).toBe('0'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('NULL Default Values', () => { | ||||
|         it('should parse NULL defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE companion_animals ( | ||||
|                     companion_id INT NOT NULL, | ||||
|                     special_trait VARCHAR(255) DEFAULT NULL, | ||||
|                     PRIMARY KEY (companion_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const traitColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'special_trait' | ||||
|             ); | ||||
|             expect(traitColumn?.default).toBe('NULL'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Function Default Values', () => { | ||||
|         it('should parse function defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE quest_entries ( | ||||
|                     entry_id INT NOT NULL AUTO_INCREMENT, | ||||
|                     quest_accepted TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|                     last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||||
|                     quest_uuid VARCHAR(36) DEFAULT (UUID()), | ||||
|                     PRIMARY KEY (entry_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const acceptedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'quest_accepted' | ||||
|             ); | ||||
|             expect(acceptedColumn?.default).toBe('CURRENT_TIMESTAMP'); | ||||
|             const updatedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'last_updated' | ||||
|             ); | ||||
|             expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('AUTO_INCREMENT', () => { | ||||
|         it('should handle AUTO_INCREMENT columns correctly', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE hero_registry ( | ||||
|                     hero_id INT NOT NULL AUTO_INCREMENT, | ||||
|                     hero_name VARCHAR(100), | ||||
|                     PRIMARY KEY (hero_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromMySQL(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const idColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'hero_id' | ||||
|             ); | ||||
|             expect(idColumn?.increment).toBe(true); | ||||
|             // AUTO_INCREMENT columns typically don't have a default value | ||||
|             expect(idColumn?.default).toBeUndefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Complex Real-World Example', () => { | ||||
|         it('should handle complex table with multiple default types', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE adventurer_profiles ( | ||||
|                     adventurer_id BIGINT NOT NULL AUTO_INCREMENT, | ||||
|                     character_name VARCHAR(50) NOT NULL, | ||||
|                     guild_email VARCHAR(255) NOT NULL, | ||||
|                     rank VARCHAR(20) DEFAULT 'novice', | ||||
|                     is_guild_verified TINYINT(1) DEFAULT 0, | ||||
|                     gold_coins INT DEFAULT 100, | ||||
|                     account_balance DECIMAL(10, 2) DEFAULT 0.00, | ||||
|                     joined_realm TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|                     last_quest TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||||
|                     inventory_data JSON DEFAULT NULL, | ||||
|                     PRIMARY KEY (adventurer_id), | ||||
|                     UNIQUE KEY uk_guild_email (guild_email), | ||||
|                     INDEX idx_rank (rank) | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromMySQL(sql); | ||||
|             const table = result.tables[0]; | ||||
|             expect(table).toBeDefined(); | ||||
|  | ||||
|             // Check various default values | ||||
|             const rankColumn = table.columns.find((c) => c.name === 'rank'); | ||||
|             expect(rankColumn?.default).toBe("'novice'"); | ||||
|  | ||||
|             const verifiedColumn = table.columns.find( | ||||
|                 (c) => c.name === 'is_guild_verified' | ||||
|             ); | ||||
|             expect(verifiedColumn?.default).toBe('0'); | ||||
|  | ||||
|             const goldColumn = table.columns.find( | ||||
|                 (c) => c.name === 'gold_coins' | ||||
|             ); | ||||
|             expect(goldColumn?.default).toBe('100'); | ||||
|  | ||||
|             const balanceColumn = table.columns.find( | ||||
|                 (c) => c.name === 'account_balance' | ||||
|             ); | ||||
|             expect(balanceColumn?.default).toBe('0.00'); | ||||
|  | ||||
|             const joinedColumn = table.columns.find( | ||||
|                 (c) => c.name === 'joined_realm' | ||||
|             ); | ||||
|             expect(joinedColumn?.default).toBe('CURRENT_TIMESTAMP'); | ||||
|  | ||||
|             const inventoryColumn = table.columns.find( | ||||
|                 (c) => c.name === 'inventory_data' | ||||
|             ); | ||||
|             expect(inventoryColumn?.default).toBe('NULL'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -101,12 +101,28 @@ function extractColumnsFromCreateTable(statement: string): SQLColumn[] { | ||||
|             const typeMatch = definition.match(/^([^\s(]+)(?:\(([^)]+)\))?/); | ||||
|             const dataType = typeMatch ? typeMatch[1] : ''; | ||||
|  | ||||
|             // Extract default value | ||||
|             let defaultValue: string | undefined; | ||||
|             const defaultMatch = definition.match( | ||||
|                 /DEFAULT\s+('[^']*'|"[^"]*"|NULL|CURRENT_TIMESTAMP|\S+)/i | ||||
|             ); | ||||
|             if (defaultMatch) { | ||||
|                 defaultValue = defaultMatch[1]; | ||||
|             } | ||||
|  | ||||
|             // Check for AUTO_INCREMENT | ||||
|             const increment = definition | ||||
|                 .toUpperCase() | ||||
|                 .includes('AUTO_INCREMENT'); | ||||
|  | ||||
|             columns.push({ | ||||
|                 name: columnName, | ||||
|                 type: dataType, | ||||
|                 nullable, | ||||
|                 primaryKey, | ||||
|                 unique: definition.toUpperCase().includes('UNIQUE'), | ||||
|                 default: defaultValue, | ||||
|                 increment, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| @@ -721,7 +737,28 @@ export async function fromMySQL(sqlContent: string): Promise<SQLParserResult> { | ||||
|                         parseError | ||||
|                     ); | ||||
|  | ||||
|                     // Error handling without logging | ||||
|                     // Try fallback parser when main parser fails | ||||
|                     const tableMatch = trimmedStmt.match( | ||||
|                         /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^`\s(]+)`?\s*\(/i | ||||
|                     ); | ||||
|                     if (tableMatch) { | ||||
|                         const tableName = tableMatch[1]; | ||||
|                         const tableId = generateId(); | ||||
|                         tableMap[tableName] = tableId; | ||||
|  | ||||
|                         const extractedColumns = | ||||
|                             extractColumnsFromCreateTable(trimmedStmt); | ||||
|                         if (extractedColumns.length > 0) { | ||||
|                             tables.push({ | ||||
|                                 id: tableId, | ||||
|                                 name: tableName, | ||||
|                                 schema: undefined, | ||||
|                                 columns: extractedColumns, | ||||
|                                 indexes: [], | ||||
|                                 order: tables.length, | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,215 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromPostgres } from '../postgresql'; | ||||
|  | ||||
| describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { | ||||
|     it('should handle ALTER TABLE ADD COLUMN statements', async () => { | ||||
|         const sql = ` | ||||
|             CREATE SCHEMA IF NOT EXISTS "public"; | ||||
|  | ||||
|             CREATE TABLE "public"."location" ( | ||||
|                 "id" bigint NOT NULL, | ||||
|                 CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id") | ||||
|             ); | ||||
|  | ||||
|             -- Add new fields to existing location table | ||||
|             ALTER TABLE location ADD COLUMN country_id INT; | ||||
|             ALTER TABLE location ADD COLUMN state_id INT; | ||||
|             ALTER TABLE location ADD COLUMN location_type_id INT; | ||||
|             ALTER TABLE location ADD COLUMN city_id INT; | ||||
|             ALTER TABLE location ADD COLUMN street TEXT; | ||||
|             ALTER TABLE location ADD COLUMN block TEXT; | ||||
|             ALTER TABLE location ADD COLUMN building TEXT; | ||||
|             ALTER TABLE location ADD COLUMN floor TEXT; | ||||
|             ALTER TABLE location ADD COLUMN apartment TEXT; | ||||
|             ALTER TABLE location ADD COLUMN lat INT; | ||||
|             ALTER TABLE location ADD COLUMN long INT; | ||||
|             ALTER TABLE location ADD COLUMN elevation INT; | ||||
|             ALTER TABLE location ADD COLUMN erp_site_id INT; | ||||
|             ALTER TABLE location ADD COLUMN is_active TEXT; | ||||
|             ALTER TABLE location ADD COLUMN remarks TEXT; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const locationTable = result.tables[0]; | ||||
|  | ||||
|         expect(locationTable.name).toBe('location'); | ||||
|         expect(locationTable.schema).toBe('public'); | ||||
|  | ||||
|         // Should have the original id column plus all the added columns | ||||
|         expect(locationTable.columns).toHaveLength(16); | ||||
|  | ||||
|         // Check that the id column is present | ||||
|         const idColumn = locationTable.columns.find((col) => col.name === 'id'); | ||||
|         expect(idColumn).toBeDefined(); | ||||
|         expect(idColumn?.type).toBe('BIGINT'); | ||||
|         expect(idColumn?.primaryKey).toBe(true); | ||||
|  | ||||
|         // Check some of the added columns | ||||
|         const countryIdColumn = locationTable.columns.find( | ||||
|             (col) => col.name === 'country_id' | ||||
|         ); | ||||
|         expect(countryIdColumn).toBeDefined(); | ||||
|         expect(countryIdColumn?.type).toBe('INTEGER'); | ||||
|  | ||||
|         const streetColumn = locationTable.columns.find( | ||||
|             (col) => col.name === 'street' | ||||
|         ); | ||||
|         expect(streetColumn).toBeDefined(); | ||||
|         expect(streetColumn?.type).toBe('TEXT'); | ||||
|  | ||||
|         const remarksColumn = locationTable.columns.find( | ||||
|             (col) => col.name === 'remarks' | ||||
|         ); | ||||
|         expect(remarksColumn).toBeDefined(); | ||||
|         expect(remarksColumn?.type).toBe('TEXT'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle ALTER TABLE ADD COLUMN with schema qualification', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE public.users ( | ||||
|                 id INTEGER PRIMARY KEY | ||||
|             ); | ||||
|  | ||||
|             ALTER TABLE public.users ADD COLUMN email VARCHAR(255); | ||||
|             ALTER TABLE public.users ADD COLUMN created_at TIMESTAMP; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const usersTable = result.tables[0]; | ||||
|  | ||||
|         expect(usersTable.columns).toHaveLength(3); | ||||
|  | ||||
|         const emailColumn = usersTable.columns.find( | ||||
|             (col) => col.name === 'email' | ||||
|         ); | ||||
|         expect(emailColumn).toBeDefined(); | ||||
|         expect(emailColumn?.type).toBe('VARCHAR(255)'); | ||||
|  | ||||
|         const createdAtColumn = usersTable.columns.find( | ||||
|             (col) => col.name === 'created_at' | ||||
|         ); | ||||
|         expect(createdAtColumn).toBeDefined(); | ||||
|         expect(createdAtColumn?.type).toBe('TIMESTAMP'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle ALTER TABLE ADD COLUMN with constraints', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE products ( | ||||
|                 id SERIAL PRIMARY KEY | ||||
|             ); | ||||
|  | ||||
|             ALTER TABLE products ADD COLUMN name VARCHAR(100) NOT NULL; | ||||
|             ALTER TABLE products ADD COLUMN sku VARCHAR(50) UNIQUE; | ||||
|             ALTER TABLE products ADD COLUMN price DECIMAL(10,2) DEFAULT 0.00; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const productsTable = result.tables[0]; | ||||
|  | ||||
|         expect(productsTable.columns).toHaveLength(4); | ||||
|  | ||||
|         const nameColumn = productsTable.columns.find( | ||||
|             (col) => col.name === 'name' | ||||
|         ); | ||||
|         expect(nameColumn).toBeDefined(); | ||||
|         expect(nameColumn?.nullable).toBe(false); | ||||
|  | ||||
|         const skuColumn = productsTable.columns.find( | ||||
|             (col) => col.name === 'sku' | ||||
|         ); | ||||
|         expect(skuColumn).toBeDefined(); | ||||
|         expect(skuColumn?.unique).toBe(true); | ||||
|  | ||||
|         const priceColumn = productsTable.columns.find( | ||||
|             (col) => col.name === 'price' | ||||
|         ); | ||||
|         expect(priceColumn).toBeDefined(); | ||||
|         expect(priceColumn?.default).toBe('0'); | ||||
|     }); | ||||
|  | ||||
|     it('should not add duplicate columns', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE items ( | ||||
|                 id INTEGER PRIMARY KEY, | ||||
|                 name VARCHAR(100) | ||||
|             ); | ||||
|  | ||||
|             ALTER TABLE items ADD COLUMN description TEXT; | ||||
|             ALTER TABLE items ADD COLUMN name VARCHAR(200); -- Should not be added as duplicate | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const itemsTable = result.tables[0]; | ||||
|  | ||||
|         // Should only have 3 columns: id, name (original), and description | ||||
|         expect(itemsTable.columns).toHaveLength(3); | ||||
|  | ||||
|         const nameColumns = itemsTable.columns.filter( | ||||
|             (col) => col.name === 'name' | ||||
|         ); | ||||
|         expect(nameColumns).toHaveLength(1); | ||||
|         expect(nameColumns[0].type).toBe('VARCHAR(100)'); // Should keep original type | ||||
|     }); | ||||
|  | ||||
|     it('should use default schema when not specified', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE test_table ( | ||||
|                 id INTEGER PRIMARY KEY | ||||
|             ); | ||||
|  | ||||
|             ALTER TABLE test_table ADD COLUMN value TEXT; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const testTable = result.tables[0]; | ||||
|  | ||||
|         expect(testTable.schema).toBe('public'); | ||||
|         expect(testTable.columns).toHaveLength(2); | ||||
|  | ||||
|         const valueColumn = testTable.columns.find( | ||||
|             (col) => col.name === 'value' | ||||
|         ); | ||||
|         expect(valueColumn).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it('should handle quoted identifiers in ALTER TABLE ADD COLUMN', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE "my-table" ( | ||||
|                 "id" INTEGER PRIMARY KEY | ||||
|             ); | ||||
|  | ||||
|             ALTER TABLE "my-table" ADD COLUMN "my-column" VARCHAR(50); | ||||
|             ALTER TABLE "my-table" ADD COLUMN "another-column" INTEGER; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const myTable = result.tables[0]; | ||||
|  | ||||
|         expect(myTable.name).toBe('my-table'); | ||||
|         expect(myTable.columns).toHaveLength(3); | ||||
|  | ||||
|         const myColumn = myTable.columns.find( | ||||
|             (col) => col.name === 'my-column' | ||||
|         ); | ||||
|         expect(myColumn).toBeDefined(); | ||||
|         expect(myColumn?.type).toBe('VARCHAR(50)'); | ||||
|  | ||||
|         const anotherColumn = myTable.columns.find( | ||||
|             (col) => col.name === 'another-column' | ||||
|         ); | ||||
|         expect(anotherColumn).toBeDefined(); | ||||
|         expect(anotherColumn?.type).toBe('INTEGER'); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,118 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromPostgres } from '../postgresql'; | ||||
|  | ||||
| describe('PostgreSQL ALTER TABLE ALTER COLUMN TYPE', () => { | ||||
|     it('should handle ALTER TABLE ALTER COLUMN TYPE statements', async () => { | ||||
|         const sql = ` | ||||
| CREATE SCHEMA IF NOT EXISTS "public"; | ||||
|  | ||||
| CREATE TABLE "public"."table_12" ( | ||||
|     "id" SERIAL, | ||||
|     "field1" varchar(200), | ||||
|     "field2" varchar(200), | ||||
|     "field3" varchar(200), | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254); | ||||
| ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254); | ||||
| ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254); | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const table = result.tables[0]; | ||||
|  | ||||
|         expect(table.name).toBe('table_12'); | ||||
|         expect(table.columns).toHaveLength(4); // id, field1, field2, field3 | ||||
|  | ||||
|         // Check that the columns have the updated type | ||||
|         const field1 = table.columns.find((col) => col.name === 'field1'); | ||||
|         expect(field1).toBeDefined(); | ||||
|         expect(field1?.type).toBe('VARCHAR(254)'); // Should be updated from 200 to 254 | ||||
|  | ||||
|         const field2 = table.columns.find((col) => col.name === 'field2'); | ||||
|         expect(field2).toBeDefined(); | ||||
|         expect(field2?.type).toBe('VARCHAR(254)'); | ||||
|  | ||||
|         const field3 = table.columns.find((col) => col.name === 'field3'); | ||||
|         expect(field3).toBeDefined(); | ||||
|         expect(field3?.type).toBe('VARCHAR(254)'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle various ALTER COLUMN TYPE scenarios', async () => { | ||||
|         const sql = ` | ||||
| CREATE TABLE test_table ( | ||||
|     id INTEGER PRIMARY KEY, | ||||
|     name VARCHAR(50), | ||||
|     age SMALLINT, | ||||
|     score NUMERIC(5,2) | ||||
| ); | ||||
|  | ||||
| -- Change varchar length | ||||
| ALTER TABLE test_table ALTER COLUMN name TYPE VARCHAR(100); | ||||
|  | ||||
| -- Change numeric type | ||||
| ALTER TABLE test_table ALTER COLUMN age TYPE INTEGER; | ||||
|  | ||||
| -- Change precision | ||||
| ALTER TABLE test_table ALTER COLUMN score TYPE NUMERIC(10,4); | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         const table = result.tables[0]; | ||||
|  | ||||
|         const nameCol = table.columns.find((col) => col.name === 'name'); | ||||
|         expect(nameCol?.type).toBe('VARCHAR(100)'); | ||||
|  | ||||
|         const ageCol = table.columns.find((col) => col.name === 'age'); | ||||
|         expect(ageCol?.type).toBe('INTEGER'); | ||||
|  | ||||
|         const scoreCol = table.columns.find((col) => col.name === 'score'); | ||||
|         expect(scoreCol?.type).toBe('NUMERIC(10,4)'); | ||||
|     }); | ||||
|  | ||||
|     it('should handle multiple type changes on the same column', async () => { | ||||
|         const sql = ` | ||||
| CREATE SCHEMA IF NOT EXISTS "public"; | ||||
|  | ||||
| CREATE TABLE "public"."table_12" ( | ||||
|     "id" SERIAL, | ||||
|     "field1" varchar(200), | ||||
|     "field2" varchar(200), | ||||
|     "field3" varchar(200), | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254); | ||||
| ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254); | ||||
| ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254); | ||||
| ALTER TABLE table_12 ALTER COLUMN field1 TYPE BIGINT; | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(1); | ||||
|         const table = result.tables[0]; | ||||
|  | ||||
|         expect(table.name).toBe('table_12'); | ||||
|         expect(table.schema).toBe('public'); | ||||
|         expect(table.columns).toHaveLength(4); | ||||
|  | ||||
|         // Check that field1 has the final type (BIGINT), not the intermediate VARCHAR(254) | ||||
|         const field1 = table.columns.find((col) => col.name === 'field1'); | ||||
|         expect(field1).toBeDefined(); | ||||
|         expect(field1?.type).toBe('BIGINT'); // Should be BIGINT, not VARCHAR(254) | ||||
|  | ||||
|         // Check that field2 and field3 still have VARCHAR(254) | ||||
|         const field2 = table.columns.find((col) => col.name === 'field2'); | ||||
|         expect(field2).toBeDefined(); | ||||
|         expect(field2?.type).toBe('VARCHAR(254)'); | ||||
|  | ||||
|         const field3 = table.columns.find((col) => col.name === 'field3'); | ||||
|         expect(field3).toBeDefined(); | ||||
|         expect(field3?.type).toBe('VARCHAR(254)'); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,117 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromPostgres } from '../postgresql'; | ||||
|  | ||||
| describe('PostgreSQL ALTER TABLE with Foreign Keys', () => { | ||||
|     it('should handle ALTER TABLE ADD COLUMN followed by ALTER TABLE ADD FOREIGN KEY', async () => { | ||||
|         const sql = ` | ||||
| CREATE SCHEMA IF NOT EXISTS "public"; | ||||
|  | ||||
| CREATE TABLE "public"."location" ( | ||||
|     "id" bigint NOT NULL, | ||||
|     CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- Add new fields to existing location table | ||||
| ALTER TABLE location ADD COLUMN country_id INT; | ||||
| ALTER TABLE location ADD COLUMN state_id INT; | ||||
| ALTER TABLE location ADD COLUMN location_type_id INT; | ||||
| ALTER TABLE location ADD COLUMN city_id INT; | ||||
| ALTER TABLE location ADD COLUMN street TEXT; | ||||
| ALTER TABLE location ADD COLUMN block TEXT; | ||||
| ALTER TABLE location ADD COLUMN building TEXT; | ||||
| ALTER TABLE location ADD COLUMN floor TEXT; | ||||
| ALTER TABLE location ADD COLUMN apartment TEXT; | ||||
| ALTER TABLE location ADD COLUMN lat INT; | ||||
| ALTER TABLE location ADD COLUMN long INT; | ||||
| ALTER TABLE location ADD COLUMN elevation INT; | ||||
| ALTER TABLE location ADD COLUMN erp_site_id INT; | ||||
| ALTER TABLE location ADD COLUMN is_active TEXT; | ||||
| ALTER TABLE location ADD COLUMN remarks TEXT; | ||||
|  | ||||
| -- Create lookup tables | ||||
| CREATE TABLE country ( | ||||
|     id SERIAL PRIMARY KEY, | ||||
|     name VARCHAR(100) NOT NULL, | ||||
|     code VARCHAR(3) UNIQUE | ||||
| ); | ||||
|  | ||||
| CREATE TABLE state ( | ||||
|     id SERIAL PRIMARY KEY, | ||||
|     name VARCHAR(100) NOT NULL, | ||||
|     country_id INT NOT NULL, | ||||
|     FOREIGN KEY (country_id) REFERENCES country(id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE location_type ( | ||||
|     id SERIAL PRIMARY KEY, | ||||
|     name VARCHAR(100) NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE city ( | ||||
|     id SERIAL PRIMARY KEY, | ||||
|     name VARCHAR(100) NOT NULL, | ||||
|     state_id INT NOT NULL, | ||||
|     FOREIGN KEY (state_id) REFERENCES state(id) | ||||
| ); | ||||
|  | ||||
| -- Add foreign key constraints from location to lookup tables | ||||
| ALTER TABLE location ADD CONSTRAINT fk_location_country  | ||||
|     FOREIGN KEY (country_id) REFERENCES country(id); | ||||
| ALTER TABLE location ADD CONSTRAINT fk_location_state  | ||||
|     FOREIGN KEY (state_id) REFERENCES state(id); | ||||
| ALTER TABLE location ADD CONSTRAINT fk_location_location_type  | ||||
|     FOREIGN KEY (location_type_id) REFERENCES location_type(id); | ||||
| ALTER TABLE location ADD CONSTRAINT fk_location_city  | ||||
|     FOREIGN KEY (city_id) REFERENCES city(id); | ||||
|         `; | ||||
|  | ||||
|         const result = await fromPostgres(sql); | ||||
|  | ||||
|         const locationTable = result.tables.find((t) => t.name === 'location'); | ||||
|  | ||||
|         // Check tables | ||||
|         expect(result.tables).toHaveLength(5); // location, country, state, location_type, city | ||||
|  | ||||
|         // Check location table has all columns | ||||
|         expect(locationTable).toBeDefined(); | ||||
|         expect(locationTable?.columns).toHaveLength(16); // id + 15 added columns | ||||
|  | ||||
|         // Check foreign key relationships | ||||
|         const locationRelationships = result.relationships.filter( | ||||
|             (r) => r.sourceTable === 'location' | ||||
|         ); | ||||
|  | ||||
|         // Should have 4 FKs from location to lookup tables + 2 from state/city | ||||
|         expect(result.relationships.length).toBeGreaterThanOrEqual(6); | ||||
|  | ||||
|         // Check specific foreign keys from location | ||||
|         expect( | ||||
|             locationRelationships.some( | ||||
|                 (r) => | ||||
|                     r.sourceColumn === 'country_id' && | ||||
|                     r.targetTable === 'country' | ||||
|             ) | ||||
|         ).toBe(true); | ||||
|  | ||||
|         expect( | ||||
|             locationRelationships.some( | ||||
|                 (r) => | ||||
|                     r.sourceColumn === 'state_id' && r.targetTable === 'state' | ||||
|             ) | ||||
|         ).toBe(true); | ||||
|  | ||||
|         expect( | ||||
|             locationRelationships.some( | ||||
|                 (r) => | ||||
|                     r.sourceColumn === 'location_type_id' && | ||||
|                     r.targetTable === 'location_type' | ||||
|             ) | ||||
|         ).toBe(true); | ||||
|  | ||||
|         expect( | ||||
|             locationRelationships.some( | ||||
|                 (r) => r.sourceColumn === 'city_id' && r.targetTable === 'city' | ||||
|             ) | ||||
|         ).toBe(true); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,395 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromPostgres } from '../postgresql'; | ||||
|  | ||||
| describe('PostgreSQL Default Value Import', () => { | ||||
|     describe('String Default Values', () => { | ||||
|         it('should parse simple string defaults with single quotes', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE heroes ( | ||||
|                     hero_id INTEGER NOT NULL, | ||||
|                     hero_status CHARACTER VARYING DEFAULT 'questing', | ||||
|                     PRIMARY KEY (hero_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const statusColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'hero_status' | ||||
|             ); | ||||
|             expect(statusColumn?.default).toBe("'questing'"); | ||||
|         }); | ||||
|  | ||||
|         it('should parse string defaults with special characters that need escaping', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE spell_scrolls ( | ||||
|                     scroll_id INTEGER NOT NULL, | ||||
|                     incantation CHARACTER VARYING DEFAULT 'Dragon''s breath', | ||||
|                     rune_inscription TEXT DEFAULT 'Ancient rune | ||||
| Sacred symbol', | ||||
|                     PRIMARY KEY (scroll_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const incantationColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'incantation' | ||||
|             ); | ||||
|             expect(incantationColumn?.default).toBe("'Dragon''s breath'"); | ||||
|         }); | ||||
|  | ||||
|         it('should parse elvish text default values', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE elven_greetings ( | ||||
|                     greeting_id INTEGER NOT NULL, | ||||
|                     elvish_welcome CHARACTER VARYING DEFAULT 'Mae govannen', | ||||
|                     PRIMARY KEY (greeting_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const greetingColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'elvish_welcome' | ||||
|             ); | ||||
|             expect(greetingColumn?.default).toBe("'Mae govannen'"); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Numeric Default Values', () => { | ||||
|         it('should parse integer defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE dragon_hoards ( | ||||
|                     hoard_id INTEGER NOT NULL, | ||||
|                     gold_pieces INTEGER DEFAULT 0, | ||||
|                     max_treasure_value INTEGER DEFAULT 10000, | ||||
|                     PRIMARY KEY (hoard_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const goldColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'gold_pieces' | ||||
|             ); | ||||
|             expect(goldColumn?.default).toBe('0'); | ||||
|             const treasureColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'max_treasure_value' | ||||
|             ); | ||||
|             expect(treasureColumn?.default).toBe('10000'); | ||||
|         }); | ||||
|  | ||||
|         it('should parse decimal defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE enchanted_items ( | ||||
|                     item_id INTEGER NOT NULL, | ||||
|                     market_price DECIMAL(10, 2) DEFAULT 99.99, | ||||
|                     magic_power_rating NUMERIC DEFAULT 0.85, | ||||
|                     PRIMARY KEY (item_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const priceColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'market_price' | ||||
|             ); | ||||
|             expect(priceColumn?.default).toBe('99.99'); | ||||
|             const powerColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'magic_power_rating' | ||||
|             ); | ||||
|             expect(powerColumn?.default).toBe('0.85'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Boolean Default Values', () => { | ||||
|         it('should parse boolean defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE magical_artifacts ( | ||||
|                     artifact_id INTEGER NOT NULL, | ||||
|                     is_cursed BOOLEAN DEFAULT TRUE, | ||||
|                     is_destroyed BOOLEAN DEFAULT FALSE, | ||||
|                     is_legendary BOOLEAN DEFAULT '1', | ||||
|                     is_identified BOOLEAN DEFAULT '0', | ||||
|                     PRIMARY KEY (artifact_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const cursedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_cursed' | ||||
|             ); | ||||
|             expect(cursedColumn?.default).toBe('TRUE'); | ||||
|             const destroyedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_destroyed' | ||||
|             ); | ||||
|             expect(destroyedColumn?.default).toBe('FALSE'); | ||||
|             const legendaryColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_legendary' | ||||
|             ); | ||||
|             expect(legendaryColumn?.default).toBe("'1'"); | ||||
|             const identifiedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_identified' | ||||
|             ); | ||||
|             expect(identifiedColumn?.default).toBe("'0'"); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('NULL Default Values', () => { | ||||
|         it('should parse NULL defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE wizard_familiars ( | ||||
|                     familiar_id INTEGER NOT NULL, | ||||
|                     special_ability CHARACTER VARYING DEFAULT NULL, | ||||
|                     PRIMARY KEY (familiar_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const abilityColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'special_ability' | ||||
|             ); | ||||
|             expect(abilityColumn?.default).toBe('NULL'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Function Default Values', () => { | ||||
|         it('should parse function defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE quest_logs ( | ||||
|                     quest_id UUID DEFAULT gen_random_uuid(), | ||||
|                     quest_started TIMESTAMP DEFAULT NOW(), | ||||
|                     last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|                     difficulty_roll INTEGER DEFAULT random() | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const questIdColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'quest_id' | ||||
|             ); | ||||
|             expect(questIdColumn?.default).toBe('GEN_RANDOM_UUID()'); | ||||
|             const startedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'quest_started' | ||||
|             ); | ||||
|             expect(startedColumn?.default).toBe('NOW()'); | ||||
|             const updatedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'last_updated' | ||||
|             ); | ||||
|             expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP'); | ||||
|             const difficultyColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'difficulty_roll' | ||||
|             ); | ||||
|             expect(difficultyColumn?.default).toBe('RANDOM()'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Complex Real-World Example', () => { | ||||
|         it('should handle a complex guild management table correctly', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "realm"( | ||||
|                     "realm_id" integer NOT NULL | ||||
|                 ); | ||||
|  | ||||
|                 CREATE TABLE "guild"( | ||||
|                     "guild_id" CHARACTER VARYING NOT NULL UNIQUE, | ||||
|                     PRIMARY KEY ("guild_id") | ||||
|                 ); | ||||
|  | ||||
|                 CREATE TABLE "guild_schedule"( | ||||
|                     "schedule_id" CHARACTER VARYING NOT NULL UNIQUE, | ||||
|                     PRIMARY KEY ("schedule_id") | ||||
|                 ); | ||||
|  | ||||
|                 CREATE TABLE "guild_quests"( | ||||
|                     "is_active" CHARACTER VARYING NOT NULL DEFAULT 'active', | ||||
|                     "quest_description" CHARACTER VARYING, | ||||
|                     "quest_type" CHARACTER VARYING, | ||||
|                     "quest_status" CHARACTER VARYING DEFAULT 'pending', | ||||
|                     "quest_id" CHARACTER VARYING NOT NULL UNIQUE, | ||||
|                     "reward_gold" CHARACTER VARYING, | ||||
|                     "quest_giver" CHARACTER VARYING, | ||||
|                     "party_size" CHARACTER VARYING, | ||||
|                     "difficulty_level" CHARACTER VARYING, | ||||
|                     "monster_type" CHARACTER VARYING, | ||||
|                     "dungeon_location" CHARACTER VARYING, | ||||
|                     "main_guild_ref" CHARACTER VARYING NOT NULL, | ||||
|                     "schedule_ref" CHARACTER VARYING, | ||||
|                     "last_attempt" CHARACTER VARYING, | ||||
|                     "max_attempts" INTEGER, | ||||
|                     "failed_attempts" INTEGER, | ||||
|                     "party_members" INTEGER, | ||||
|                     "loot_distributor" CHARACTER VARYING, | ||||
|                     "quest_validator" CHARACTER VARYING, | ||||
|                     "scout_report" CHARACTER VARYING, | ||||
|                     "completion_xp" INTEGER, | ||||
|                     "bonus_xp" INTEGER, | ||||
|                     "map_coordinates" CHARACTER VARYING, | ||||
|                     "quest_correlation" CHARACTER VARYING, | ||||
|                     "is_completed" BOOLEAN NOT NULL DEFAULT '0', | ||||
|                     "reward_items" CHARACTER VARYING, | ||||
|                     "quest_priority" INTEGER, | ||||
|                     "started_at" CHARACTER VARYING, | ||||
|                     "status" CHARACTER VARYING, | ||||
|                     "completed_at" CHARACTER VARYING, | ||||
|                     "party_level" INTEGER, | ||||
|                     "quest_master" CHARACTER VARYING, | ||||
|                     PRIMARY KEY ("quest_id"), | ||||
|                     FOREIGN KEY ("main_guild_ref") REFERENCES "guild"("guild_id"), | ||||
|                     FOREIGN KEY ("schedule_ref") REFERENCES "guild_schedule"("schedule_id") | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             // Find the guild_quests table | ||||
|             const questTable = result.tables.find( | ||||
|                 (t) => t.name === 'guild_quests' | ||||
|             ); | ||||
|             expect(questTable).toBeDefined(); | ||||
|  | ||||
|             // Check specific default values | ||||
|             const activeColumn = questTable?.columns.find( | ||||
|                 (c) => c.name === 'is_active' | ||||
|             ); | ||||
|             expect(activeColumn?.default).toBe("'active'"); | ||||
|  | ||||
|             const statusColumn = questTable?.columns.find( | ||||
|                 (c) => c.name === 'quest_status' | ||||
|             ); | ||||
|             expect(statusColumn?.default).toBe("'pending'"); | ||||
|  | ||||
|             const completedColumn = questTable?.columns.find( | ||||
|                 (c) => c.name === 'is_completed' | ||||
|             ); | ||||
|             expect(completedColumn?.default).toBe("'0'"); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('ALTER TABLE ADD COLUMN with defaults', () => { | ||||
|         it('should handle ALTER TABLE ADD COLUMN with default values', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE adventurers ( | ||||
|                     adventurer_id INTEGER NOT NULL, | ||||
|                     PRIMARY KEY (adventurer_id) | ||||
|                 ); | ||||
|                  | ||||
|                 ALTER TABLE adventurers ADD COLUMN class_type VARCHAR(50) DEFAULT 'warrior'; | ||||
|                 ALTER TABLE adventurers ADD COLUMN experience_points INTEGER DEFAULT 0; | ||||
|                 ALTER TABLE adventurers ADD COLUMN is_guild_member BOOLEAN DEFAULT TRUE; | ||||
|                 ALTER TABLE adventurers ADD COLUMN joined_at TIMESTAMP DEFAULT NOW(); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const classColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'class_type' | ||||
|             ); | ||||
|             expect(classColumn?.default).toBe("'warrior'"); | ||||
|  | ||||
|             const xpColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'experience_points' | ||||
|             ); | ||||
|             expect(xpColumn?.default).toBe('0'); | ||||
|  | ||||
|             const guildColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_guild_member' | ||||
|             ); | ||||
|             expect(guildColumn?.default).toBe('TRUE'); | ||||
|  | ||||
|             const joinedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'joined_at' | ||||
|             ); | ||||
|             expect(joinedColumn?.default).toBe('NOW()'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Edge Cases and Special Characters', () => { | ||||
|         it('should handle defaults with parentheses in strings', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE spell_formulas ( | ||||
|                     formula_id INTEGER NOT NULL, | ||||
|                     damage_calculation VARCHAR DEFAULT '(strength + magic) * 2', | ||||
|                     mana_cost TEXT DEFAULT 'cast(level * 10 - wisdom)', | ||||
|                     PRIMARY KEY (formula_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const damageColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'damage_calculation' | ||||
|             ); | ||||
|             expect(damageColumn?.default).toBe("'(strength + magic) * 2'"); | ||||
|             const manaColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'mana_cost' | ||||
|             ); | ||||
|             expect(manaColumn?.default).toBe("'cast(level * 10 - wisdom)'"); | ||||
|         }); | ||||
|  | ||||
|         it('should handle defaults with JSON strings', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE item_enchantments ( | ||||
|                     enchantment_id INTEGER NOT NULL, | ||||
|                     properties JSON DEFAULT '{"element": "fire"}', | ||||
|                     modifiers JSONB DEFAULT '[]', | ||||
|                     PRIMARY KEY (enchantment_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const propertiesColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'properties' | ||||
|             ); | ||||
|             expect(propertiesColumn?.default).toBe(`'{"element": "fire"}'`); | ||||
|             const modifiersColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'modifiers' | ||||
|             ); | ||||
|             expect(modifiersColumn?.default).toBe("'[]'"); | ||||
|         }); | ||||
|  | ||||
|         it('should handle casting in defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE ancient_runes ( | ||||
|                     rune_id INTEGER NOT NULL, | ||||
|                     rune_type VARCHAR DEFAULT 'healing'::text, | ||||
|                     PRIMARY KEY (rune_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const runeColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'rune_type' | ||||
|             ); | ||||
|             expect(runeColumn?.default).toBe("'healing'::text"); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Serial Types', () => { | ||||
|         it('should not set default for SERIAL types as they auto-increment', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE monster_spawns ( | ||||
|                     spawn_id SERIAL PRIMARY KEY, | ||||
|                     minion_id SMALLSERIAL, | ||||
|                     boss_id BIGSERIAL | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromPostgres(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const spawnColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'spawn_id' | ||||
|             ); | ||||
|             expect(spawnColumn?.default).toBeUndefined(); | ||||
|             expect(spawnColumn?.increment).toBe(true); | ||||
|  | ||||
|             const minionColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'minion_id' | ||||
|             ); | ||||
|             expect(minionColumn?.default).toBeUndefined(); | ||||
|             expect(minionColumn?.increment).toBe(true); | ||||
|  | ||||
|             const bossColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'boss_id' | ||||
|             ); | ||||
|             expect(bossColumn?.default).toBeUndefined(); | ||||
|             expect(bossColumn?.increment).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,350 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromPostgres } from '../postgresql'; | ||||
|  | ||||
| describe('PostgreSQL Import - Quoted Identifiers with Special Characters', () => { | ||||
|     describe('CREATE TABLE with quoted identifiers', () => { | ||||
|         it('should handle tables with quoted schema and table names', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "my-schema"."user-profiles" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     name text NOT NULL | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('my-schema'); | ||||
|             expect(table.name).toBe('user-profiles'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle tables with spaces in schema and table names', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "user schema"."profile table" ( | ||||
|                     "user id" integer PRIMARY KEY, | ||||
|                     "full name" varchar(255) | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('user schema'); | ||||
|             expect(table.name).toBe('profile table'); | ||||
|             expect(table.columns).toBeDefined(); | ||||
|             expect(table.columns.length).toBeGreaterThan(0); | ||||
|             // Note: Column names with spaces might be parsed differently | ||||
|         }); | ||||
|  | ||||
|         it('should handle mixed quoted and unquoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "special-schema".users ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|                 CREATE TABLE public."special-table" ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(2); | ||||
|  | ||||
|             expect(result.tables[0].schema).toBe('special-schema'); | ||||
|             expect(result.tables[0].name).toBe('users'); | ||||
|             expect(result.tables[1].schema).toBe('public'); | ||||
|             expect(result.tables[1].name).toBe('special-table'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle tables with dots in names', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "schema.with.dots"."table.with.dots" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     data text | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('schema.with.dots'); | ||||
|             expect(table.name).toBe('table.with.dots'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('FOREIGN KEY with quoted identifiers', () => { | ||||
|         it('should handle inline REFERENCES with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "auth-schema"."users" ( | ||||
|                     "user-id" serial PRIMARY KEY, | ||||
|                     email text UNIQUE | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "app-schema"."user-profiles" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     "user-id" integer REFERENCES "auth-schema"."users"("user-id"), | ||||
|                     bio text | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(2); | ||||
|             expect(result.relationships).toHaveLength(1); | ||||
|  | ||||
|             const relationship = result.relationships[0]; | ||||
|             expect(relationship.sourceTable).toBe('user-profiles'); | ||||
|             expect(relationship.targetTable).toBe('users'); | ||||
|             expect(relationship.sourceColumn).toBe('user-id'); | ||||
|             expect(relationship.targetColumn).toBe('user-id'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle FOREIGN KEY constraints with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "schema one"."table one" ( | ||||
|                     "id field" serial PRIMARY KEY, | ||||
|                     "data field" text | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "schema two"."table two" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     "ref id" integer, | ||||
|                     FOREIGN KEY ("ref id") REFERENCES "schema one"."table one"("id field") | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(2); | ||||
|             expect(result.relationships).toHaveLength(1); | ||||
|  | ||||
|             const relationship = result.relationships[0]; | ||||
|             expect(relationship.sourceTable).toBe('table two'); | ||||
|             expect(relationship.targetTable).toBe('table one'); | ||||
|             expect(relationship.sourceColumn).toBe('ref id'); | ||||
|             expect(relationship.targetColumn).toBe('id field'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle named constraints with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "auth"."users" ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "app"."profiles" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     user_id integer, | ||||
|                     CONSTRAINT "fk-user-profile" FOREIGN KEY (user_id) REFERENCES "auth"."users"(id) | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.relationships).toHaveLength(1); | ||||
|  | ||||
|             const relationship = result.relationships[0]; | ||||
|             // Note: Constraint names with special characters might be normalized | ||||
|             expect(relationship.name).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('should handle ALTER TABLE ADD CONSTRAINT with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "user-schema"."user-accounts" ( | ||||
|                     "account-id" serial PRIMARY KEY, | ||||
|                     username text | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "order-schema"."user-orders" ( | ||||
|                     "order-id" serial PRIMARY KEY, | ||||
|                     "account-id" integer | ||||
|                 ); | ||||
|                  | ||||
|                 ALTER TABLE "order-schema"."user-orders"  | ||||
|                 ADD CONSTRAINT "fk_orders_accounts"  | ||||
|                 FOREIGN KEY ("account-id")  | ||||
|                 REFERENCES "user-schema"."user-accounts"("account-id"); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(2); | ||||
|             expect(result.relationships).toHaveLength(1); | ||||
|  | ||||
|             const relationship = result.relationships[0]; | ||||
|             expect(relationship.name).toBe('fk_orders_accounts'); | ||||
|             expect(relationship.sourceTable).toBe('user-orders'); | ||||
|             expect(relationship.targetTable).toBe('user-accounts'); | ||||
|             expect(relationship.sourceColumn).toBe('account-id'); | ||||
|             expect(relationship.targetColumn).toBe('account-id'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle complex mixed quoting scenarios', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE auth.users ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "app-data"."user_profiles" ( | ||||
|                     profile_id serial PRIMARY KEY, | ||||
|                     "user-id" integer REFERENCES auth.users(id) | ||||
|                 ); | ||||
|                  | ||||
|                 CREATE TABLE "app-data".posts ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     profile_id integer | ||||
|                 ); | ||||
|                  | ||||
|                 ALTER TABLE "app-data".posts  | ||||
|                 ADD CONSTRAINT fk_posts_profiles  | ||||
|                 FOREIGN KEY (profile_id)  | ||||
|                 REFERENCES "app-data"."user_profiles"(profile_id); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(3); | ||||
|             expect(result.relationships).toHaveLength(2); | ||||
|  | ||||
|             // Verify the relationships were correctly identified | ||||
|             const profilesTable = result.tables.find( | ||||
|                 (t) => t.name === 'user_profiles' | ||||
|             ); | ||||
|             expect(profilesTable?.schema).toBe('app-data'); | ||||
|  | ||||
|             const postsTable = result.tables.find((t) => t.name === 'posts'); | ||||
|             expect(postsTable?.schema).toBe('app-data'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Edge cases and special scenarios', () => { | ||||
|         it('should handle Unicode characters in quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "схема"."таблица" ( | ||||
|                     "идентификатор" serial PRIMARY KEY, | ||||
|                     "данные" text | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('схема'); | ||||
|             expect(table.name).toBe('таблица'); | ||||
|             expect(table.columns).toBeDefined(); | ||||
|             expect(table.columns.length).toBeGreaterThan(0); | ||||
|         }); | ||||
|  | ||||
|         it('should handle parentheses in quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "schema(prod)"."users(archived)" ( | ||||
|                     id serial PRIMARY KEY, | ||||
|                     data text | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('schema(prod)'); | ||||
|             expect(table.name).toBe('users(archived)'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle forward slashes in quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "api/v1"."users/profiles" ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('api/v1'); | ||||
|             expect(table.name).toBe('users/profiles'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle IF NOT EXISTS with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE IF NOT EXISTS "test-schema"."test-table" ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('test-schema'); | ||||
|             expect(table.name).toBe('test-table'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle ONLY keyword with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE ONLY "parent-schema"."parent-table" ( | ||||
|                     id serial PRIMARY KEY | ||||
|                 ); | ||||
|                  | ||||
|                 ALTER TABLE ONLY "parent-schema"."parent-table" | ||||
|                 ADD CONSTRAINT "unique-constraint" UNIQUE (id); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             // ONLY keyword might trigger warnings | ||||
|             expect(result.warnings).toBeDefined(); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|  | ||||
|             const table = result.tables[0]; | ||||
|             expect(table.schema).toBe('parent-schema'); | ||||
|             expect(table.name).toBe('parent-table'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle self-referencing foreign keys with quoted identifiers', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE "org-schema"."departments" ( | ||||
|                     "dept-id" serial PRIMARY KEY, | ||||
|                     "parent-dept-id" integer REFERENCES "org-schema"."departments"("dept-id"), | ||||
|                     name text | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromPostgres(sql); | ||||
|  | ||||
|             expect(result.warnings || []).toHaveLength(0); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             expect(result.relationships).toHaveLength(1); | ||||
|  | ||||
|             const relationship = result.relationships[0]; | ||||
|             expect(relationship.sourceTable).toBe('departments'); | ||||
|             expect(relationship.targetTable).toBe('departments'); // Self-reference | ||||
|             expect(relationship.sourceColumn).toBe('parent-dept-id'); | ||||
|             expect(relationship.targetColumn).toBe('dept-id'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -91,7 +91,38 @@ export interface AlterTableExprItem { | ||||
|     action: string; | ||||
|     resource?: string; | ||||
|     type?: string; | ||||
|     keyword?: string; | ||||
|     constraint?: { constraint_type?: string }; | ||||
|     // Properties for ADD COLUMN | ||||
|     column?: | ||||
|         | { | ||||
|               column?: | ||||
|                   | { | ||||
|                         expr?: { | ||||
|                             value?: string; | ||||
|                         }; | ||||
|                     } | ||||
|                   | string; | ||||
|           } | ||||
|         | string | ||||
|         | ColumnReference; | ||||
|     definition?: { | ||||
|         dataType?: string; | ||||
|         length?: number; | ||||
|         precision?: number; | ||||
|         scale?: number; | ||||
|         suffix?: unknown[]; | ||||
|         nullable?: { type: string }; | ||||
|         unique?: string; | ||||
|         primary_key?: string; | ||||
|         constraint?: string; | ||||
|         default_val?: unknown; | ||||
|         auto_increment?: string; | ||||
|     }; | ||||
|     nullable?: { type: string; value?: string }; | ||||
|     unique?: string; | ||||
|     default_val?: unknown; | ||||
|     // Properties for constraints | ||||
|     create_definitions?: | ||||
|         | AlterTableConstraintDefinition | ||||
|         | { | ||||
| @@ -203,11 +234,6 @@ export function findTableWithSchemaSupport( | ||||
|     // If still not found with schema, try any match on the table name | ||||
|     if (!table) { | ||||
|         table = tables.find((t) => t.name === tableName); | ||||
|         if (table) { | ||||
|             console.log( | ||||
|                 `Found table ${tableName} without schema match, source schema: ${effectiveSchema}, table schema: ${table.schema}` | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return table; | ||||
| @@ -235,11 +261,7 @@ export function getTableIdWithSchemaSupport( | ||||
|     // If still not found with schema, try without schema | ||||
|     if (!tableId) { | ||||
|         tableId = tableMap[tableName]; | ||||
|         if (tableId) { | ||||
|             console.log( | ||||
|                 `Found table ID for ${tableName} without schema match, source schema: ${effectiveSchema}` | ||||
|             ); | ||||
|         } else { | ||||
|         if (!tableId) { | ||||
|             console.warn( | ||||
|                 `No table ID found for ${tableName} with schema ${effectiveSchema}` | ||||
|             ); | ||||
|   | ||||
| @@ -7,6 +7,8 @@ import type { | ||||
|     SQLForeignKey, | ||||
|     SQLEnumType, | ||||
| } from '../../common'; | ||||
| import { buildSQLFromAST } from '../../common'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { | ||||
|     TableReference, | ||||
|     ColumnReference, | ||||
| @@ -347,13 +349,20 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] { | ||||
|  | ||||
|         // Try to extract column definition | ||||
|         // Match: column_name TYPE[(params)][array] | ||||
|         // Updated regex to handle complex types like GEOGRAPHY(POINT, 4326) and custom types like subscription_status | ||||
|         const columnMatch = trimmedLine.match( | ||||
|             /^\s*["']?(\w+)["']?\s+([\w_]+(?:\([^)]+\))?(?:\[\])?)/i | ||||
|         ); | ||||
|         // First extract column name and everything after it | ||||
|         const columnMatch = trimmedLine.match(/^\s*["']?(\w+)["']?\s+(.+)/i); | ||||
|         if (columnMatch) { | ||||
|             const columnName = columnMatch[1]; | ||||
|             let columnType = columnMatch[2]; | ||||
|             const restOfLine = columnMatch[2]; | ||||
|  | ||||
|             // Now extract the type from the rest of the line | ||||
|             // Match type which could be multi-word (like CHARACTER VARYING) with optional params | ||||
|             const typeMatch = restOfLine.match( | ||||
|                 /^((?:CHARACTER\s+VARYING|DOUBLE\s+PRECISION|[\w]+)(?:\([^)]+\))?(?:\[\])?)/i | ||||
|             ); | ||||
|  | ||||
|             if (!typeMatch) continue; | ||||
|             let columnType = typeMatch[1].trim(); | ||||
|  | ||||
|             // Normalize PostGIS types | ||||
|             if (columnType.toUpperCase().startsWith('GEOGRAPHY')) { | ||||
| @@ -380,7 +389,65 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] { | ||||
|             const isPrimary = trimmedLine.match(/PRIMARY\s+KEY/i) !== null; | ||||
|             const isNotNull = trimmedLine.match(/NOT\s+NULL/i) !== null; | ||||
|             const isUnique = trimmedLine.match(/\bUNIQUE\b/i) !== null; | ||||
|             const hasDefault = trimmedLine.match(/DEFAULT\s+/i) !== null; | ||||
|  | ||||
|             // Extract default value | ||||
|             let defaultValue: string | undefined; | ||||
|             // Updated regex to handle casting with :: operator | ||||
|             const defaultMatch = trimmedLine.match( | ||||
|                 /DEFAULT\s+((?:'[^']*'|"[^"]*"|\S+)(?:::\w+)?)/i | ||||
|             ); | ||||
|             if (defaultMatch) { | ||||
|                 let defVal = defaultMatch[1].trim(); | ||||
|                 // Remove trailing comma if present | ||||
|                 defVal = defVal.replace(/,$/, '').trim(); | ||||
|                 // Handle string literals | ||||
|                 if (defVal.startsWith("'") && defVal.endsWith("'")) { | ||||
|                     // Keep the quotes for string literals | ||||
|                     defaultValue = defVal; | ||||
|                 } else if (defVal.match(/^\d+(\.\d+)?$/)) { | ||||
|                     // Numeric value | ||||
|                     defaultValue = defVal; | ||||
|                 } else if ( | ||||
|                     defVal.toUpperCase() === 'TRUE' || | ||||
|                     defVal.toUpperCase() === 'FALSE' | ||||
|                 ) { | ||||
|                     // Boolean value | ||||
|                     defaultValue = defVal.toUpperCase(); | ||||
|                 } else if (defVal.toUpperCase() === 'NULL') { | ||||
|                     // NULL value | ||||
|                     defaultValue = 'NULL'; | ||||
|                 } else if (defVal.includes('(') && defVal.includes(')')) { | ||||
|                     // Function call (like gen_random_uuid()) | ||||
|                     // Normalize PostgreSQL function names to uppercase | ||||
|                     const funcMatch = defVal.match(/^(\w+)\(/); | ||||
|                     if (funcMatch) { | ||||
|                         const funcName = funcMatch[1]; | ||||
|                         const pgFunctions = [ | ||||
|                             'now', | ||||
|                             'current_timestamp', | ||||
|                             'current_date', | ||||
|                             'current_time', | ||||
|                             'gen_random_uuid', | ||||
|                             'random', | ||||
|                             'nextval', | ||||
|                             'currval', | ||||
|                         ]; | ||||
|                         if (pgFunctions.includes(funcName.toLowerCase())) { | ||||
|                             defaultValue = defVal.replace( | ||||
|                                 funcName, | ||||
|                                 funcName.toUpperCase() | ||||
|                             ); | ||||
|                         } else { | ||||
|                             defaultValue = defVal; | ||||
|                         } | ||||
|                     } else { | ||||
|                         defaultValue = defVal; | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Other expressions | ||||
|                     defaultValue = defVal; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             columns.push({ | ||||
|                 name: columnName, | ||||
| @@ -388,7 +455,7 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] { | ||||
|                 nullable: !isNotNull && !isPrimary, | ||||
|                 primaryKey: isPrimary, | ||||
|                 unique: isUnique || isPrimary, | ||||
|                 default: hasDefault ? 'has default' : undefined, | ||||
|                 default: defaultValue, | ||||
|                 increment: | ||||
|                     isSerialType || | ||||
|                     trimmedLine.includes('gen_random_uuid()') || | ||||
| @@ -490,16 +557,21 @@ function extractForeignKeysFromCreateTable( | ||||
|  | ||||
|     const tableBody = tableBodyMatch[1]; | ||||
|  | ||||
|     // Pattern for inline REFERENCES - more flexible to handle various formats | ||||
|     // Pattern for inline REFERENCES - handles quoted and unquoted identifiers | ||||
|     const inlineRefPattern = | ||||
|         /["']?(\w+)["']?\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi; | ||||
|         /(?:"([^"]+)"|([^"\s,()]+))\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi; | ||||
|  | ||||
|     let match; | ||||
|     while ((match = inlineRefPattern.exec(tableBody)) !== null) { | ||||
|         const sourceColumn = match[1]; | ||||
|         const targetSchema = match[2] || 'public'; | ||||
|         const targetTable = match[3]; | ||||
|         const targetColumn = match[4]; | ||||
|         // Extract values from appropriate match groups | ||||
|         // Groups: 1=quoted source col, 2=unquoted source col, | ||||
|         //         3=quoted schema, 4=unquoted schema, | ||||
|         //         5=quoted target table, 6=unquoted target table, | ||||
|         //         7=quoted target col, 8=unquoted target col | ||||
|         const sourceColumn = match[1] || match[2]; | ||||
|         const targetSchema = match[3] || match[4] || 'public'; | ||||
|         const targetTable = match[5] || match[6]; | ||||
|         const targetColumn = match[7] || match[8]; | ||||
|  | ||||
|         const targetTableKey = `${targetSchema}.${targetTable}`; | ||||
|         const targetTableId = tableMap[targetTableKey]; | ||||
| @@ -521,15 +593,16 @@ function extractForeignKeysFromCreateTable( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Pattern for FOREIGN KEY constraints | ||||
|     // Pattern for FOREIGN KEY constraints - handles quoted and unquoted identifiers | ||||
|     const fkConstraintPattern = | ||||
|         /FOREIGN\s+KEY\s*\(\s*["']?(\w+)["']?\s*\)\s*REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi; | ||||
|         /FOREIGN\s+KEY\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)\s*REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi; | ||||
|  | ||||
|     while ((match = fkConstraintPattern.exec(tableBody)) !== null) { | ||||
|         const sourceColumn = match[1]; | ||||
|         const targetSchema = match[2] || 'public'; | ||||
|         const targetTable = match[3]; | ||||
|         const targetColumn = match[4]; | ||||
|         // Extract values from appropriate match groups | ||||
|         const sourceColumn = match[1] || match[2]; | ||||
|         const targetSchema = match[3] || match[4] || 'public'; | ||||
|         const targetTable = match[5] || match[6]; | ||||
|         const targetColumn = match[7] || match[8]; | ||||
|  | ||||
|         const targetTableKey = `${targetSchema}.${targetTable}`; | ||||
|         const targetTableId = tableMap[targetTableKey]; | ||||
| @@ -585,12 +658,16 @@ export async function fromPostgres( | ||||
|                     ? stmt.sql.substring(createTableIndex) | ||||
|                     : stmt.sql; | ||||
|  | ||||
|             // Updated regex to properly handle quoted identifiers with special characters | ||||
|             // Matches: schema.table, "schema"."table", "schema".table, schema."table" | ||||
|             const tableMatch = sqlFromCreate.match( | ||||
|                 /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i | ||||
|                 /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i | ||||
|             ); | ||||
|             if (tableMatch) { | ||||
|                 const schemaName = tableMatch[1] || 'public'; | ||||
|                 const tableName = tableMatch[2]; | ||||
|                 // Extract schema and table names from the appropriate match groups | ||||
|                 // Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table | ||||
|                 const schemaName = tableMatch[1] || tableMatch[2] || 'public'; | ||||
|                 const tableName = tableMatch[3] || tableMatch[4]; | ||||
|                 const tableKey = `${schemaName}.${tableName}`; | ||||
|                 tableMap[tableKey] = generateId(); | ||||
|             } | ||||
| @@ -938,12 +1015,16 @@ export async function fromPostgres( | ||||
|                     ? stmt.sql.substring(createTableIndex) | ||||
|                     : stmt.sql; | ||||
|  | ||||
|             // Updated regex to properly handle quoted identifiers with special characters | ||||
|             // Matches: schema.table, "schema"."table", "schema".table, schema."table" | ||||
|             const tableMatch = sqlFromCreate.match( | ||||
|                 /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i | ||||
|                 /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i | ||||
|             ); | ||||
|             if (tableMatch) { | ||||
|                 const schemaName = tableMatch[1] || 'public'; | ||||
|                 const tableName = tableMatch[2]; | ||||
|                 // Extract schema and table names from the appropriate match groups | ||||
|                 // Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table | ||||
|                 const schemaName = tableMatch[1] || tableMatch[2] || 'public'; | ||||
|                 const tableName = tableMatch[3] || tableMatch[4]; | ||||
|                 const tableKey = `${schemaName}.${tableName}`; | ||||
|                 const tableId = tableMap[tableKey]; | ||||
|  | ||||
| @@ -982,7 +1063,7 @@ export async function fromPostgres( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Fourth pass: process ALTER TABLE statements for foreign keys | ||||
|     // Fourth pass: process ALTER TABLE statements for foreign keys and ADD COLUMN | ||||
|     for (const stmt of statements) { | ||||
|         if (stmt.type === 'alter' && stmt.parsed) { | ||||
|             const alterTableStmt = stmt.parsed as AlterTableStatement; | ||||
| @@ -1012,13 +1093,440 @@ export async function fromPostgres( | ||||
|             ); | ||||
|             if (!table) continue; | ||||
|  | ||||
|             // Process foreign key constraints in ALTER TABLE | ||||
|             // Process ALTER TABLE expressions | ||||
|             if (alterTableStmt.expr && Array.isArray(alterTableStmt.expr)) { | ||||
|                 alterTableStmt.expr.forEach((expr: AlterTableExprItem) => { | ||||
|                     if (expr.action === 'add' && expr.create_definitions) { | ||||
|                     // Handle ALTER COLUMN TYPE | ||||
|                     if (expr.action === 'alter' && expr.resource === 'column') { | ||||
|                         // Extract column name | ||||
|                         let columnName: string | undefined; | ||||
|                         if ( | ||||
|                             typeof expr.column === 'object' && | ||||
|                             'column' in expr.column | ||||
|                         ) { | ||||
|                             const innerColumn = expr.column.column; | ||||
|                             if ( | ||||
|                                 typeof innerColumn === 'object' && | ||||
|                                 'expr' in innerColumn && | ||||
|                                 innerColumn.expr?.value | ||||
|                             ) { | ||||
|                                 columnName = innerColumn.expr.value; | ||||
|                             } else if (typeof innerColumn === 'string') { | ||||
|                                 columnName = innerColumn; | ||||
|                             } | ||||
|                         } else if (typeof expr.column === 'string') { | ||||
|                             columnName = expr.column; | ||||
|                         } | ||||
|  | ||||
|                         // Check if it's a TYPE change | ||||
|                         if ( | ||||
|                             columnName && | ||||
|                             expr.type === 'alter' && | ||||
|                             expr.definition?.dataType | ||||
|                         ) { | ||||
|                             // Find the column in the table and update its type | ||||
|                             const column = table.columns.find( | ||||
|                                 (col) => (col as SQLColumn).name === columnName | ||||
|                             ); | ||||
|                             if (column) { | ||||
|                                 const definition = expr.definition; | ||||
|                                 const rawDataType = String(definition.dataType); | ||||
|  | ||||
|                                 // console.log('ALTER TYPE expr:', JSON.stringify(expr, null, 2)); | ||||
|  | ||||
|                                 // Normalize the type | ||||
|                                 let normalizedType = | ||||
|                                     normalizePostgreSQLType(rawDataType); | ||||
|  | ||||
|                                 // Handle type parameters | ||||
|                                 if ( | ||||
|                                     definition.scale !== undefined && | ||||
|                                     definition.scale !== null | ||||
|                                 ) { | ||||
|                                     // For NUMERIC/DECIMAL with scale, length is actually precision | ||||
|                                     const precision = | ||||
|                                         definition.length || | ||||
|                                         definition.precision; | ||||
|                                     normalizedType = `${normalizedType}(${precision},${definition.scale})`; | ||||
|                                 } else if ( | ||||
|                                     definition.length !== undefined && | ||||
|                                     definition.length !== null | ||||
|                                 ) { | ||||
|                                     normalizedType = `${normalizedType}(${definition.length})`; | ||||
|                                 } else if (definition.precision !== undefined) { | ||||
|                                     normalizedType = `${normalizedType}(${definition.precision})`; | ||||
|                                 } else if ( | ||||
|                                     definition.suffix && | ||||
|                                     Array.isArray(definition.suffix) && | ||||
|                                     definition.suffix.length > 0 | ||||
|                                 ) { | ||||
|                                     const params = definition.suffix | ||||
|                                         .map((s: unknown) => { | ||||
|                                             if ( | ||||
|                                                 typeof s === 'object' && | ||||
|                                                 s !== null && | ||||
|                                                 'value' in s | ||||
|                                             ) { | ||||
|                                                 return String(s.value); | ||||
|                                             } | ||||
|                                             return String(s); | ||||
|                                         }) | ||||
|                                         .join(','); | ||||
|                                     normalizedType = `${normalizedType}(${params})`; | ||||
|                                 } | ||||
|  | ||||
|                                 // Update the column type | ||||
|                                 (column as SQLColumn).type = normalizedType; | ||||
|  | ||||
|                                 // Update typeArgs if applicable | ||||
|                                 if ( | ||||
|                                     definition.scale !== undefined && | ||||
|                                     definition.scale !== null | ||||
|                                 ) { | ||||
|                                     // For NUMERIC/DECIMAL with scale | ||||
|                                     const precision = | ||||
|                                         definition.length || | ||||
|                                         definition.precision; | ||||
|                                     (column as SQLColumn).typeArgs = { | ||||
|                                         precision: precision, | ||||
|                                         scale: definition.scale, | ||||
|                                     }; | ||||
|                                 } else if (definition.length) { | ||||
|                                     (column as SQLColumn).typeArgs = { | ||||
|                                         length: definition.length, | ||||
|                                     }; | ||||
|                                 } else if (definition.precision) { | ||||
|                                     (column as SQLColumn).typeArgs = { | ||||
|                                         precision: definition.precision, | ||||
|                                     }; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         // Handle ADD COLUMN | ||||
|                     } else if ( | ||||
|                         expr.action === 'add' && | ||||
|                         expr.resource === 'column' | ||||
|                     ) { | ||||
|                         // Handle ADD COLUMN directly from expr structure | ||||
|                         // Extract column name from the nested structure | ||||
|                         let columnName: string | undefined; | ||||
|                         if ( | ||||
|                             typeof expr.column === 'object' && | ||||
|                             'column' in expr.column | ||||
|                         ) { | ||||
|                             const innerColumn = expr.column.column; | ||||
|                             if ( | ||||
|                                 typeof innerColumn === 'object' && | ||||
|                                 'expr' in innerColumn && | ||||
|                                 innerColumn.expr?.value | ||||
|                             ) { | ||||
|                                 columnName = innerColumn.expr.value; | ||||
|                             } else if (typeof innerColumn === 'string') { | ||||
|                                 columnName = innerColumn; | ||||
|                             } | ||||
|                         } else if (typeof expr.column === 'string') { | ||||
|                             columnName = expr.column; | ||||
|                         } | ||||
|  | ||||
|                         if (columnName && typeof columnName === 'string') { | ||||
|                             const definition = expr.definition || {}; | ||||
|                             const rawDataType = String( | ||||
|                                 definition?.dataType || 'TEXT' | ||||
|                             ); | ||||
|                             // console.log('expr:', JSON.stringify(expr, null, 2)); | ||||
|  | ||||
|                             // Normalize the type | ||||
|                             let normalizedBaseType = | ||||
|                                 normalizePostgreSQLType(rawDataType); | ||||
|  | ||||
|                             // Check if it's a serial type | ||||
|                             const upperType = rawDataType.toUpperCase(); | ||||
|                             const isSerialType = [ | ||||
|                                 'SERIAL', | ||||
|                                 'SERIAL2', | ||||
|                                 'SERIAL4', | ||||
|                                 'SERIAL8', | ||||
|                                 'BIGSERIAL', | ||||
|                                 'SMALLSERIAL', | ||||
|                             ].includes(upperType.split('(')[0]); | ||||
|  | ||||
|                             if (isSerialType) { | ||||
|                                 const typeLength = definition?.length as | ||||
|                                     | number | ||||
|                                     | undefined; | ||||
|                                 if (upperType === 'SERIAL') { | ||||
|                                     if (typeLength === 2) { | ||||
|                                         normalizedBaseType = 'SMALLINT'; | ||||
|                                     } else if (typeLength === 8) { | ||||
|                                         normalizedBaseType = 'BIGINT'; | ||||
|                                     } else { | ||||
|                                         normalizedBaseType = 'INTEGER'; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             // Handle type parameters | ||||
|                             let finalDataType = normalizedBaseType; | ||||
|                             const isNormalizedIntegerType = | ||||
|                                 ['INTEGER', 'BIGINT', 'SMALLINT'].includes( | ||||
|                                     normalizedBaseType | ||||
|                                 ) && | ||||
|                                 (upperType === 'INT' || upperType === 'SERIAL'); | ||||
|  | ||||
|                             if (!isSerialType && !isNormalizedIntegerType) { | ||||
|                                 const precision = definition?.precision; | ||||
|                                 const scale = definition?.scale; | ||||
|                                 const length = definition?.length; | ||||
|                                 const suffix = | ||||
|                                     (definition?.suffix as unknown[]) || []; | ||||
|  | ||||
|                                 if (suffix.length > 0) { | ||||
|                                     const params = suffix | ||||
|                                         .map((s: unknown) => { | ||||
|                                             if ( | ||||
|                                                 typeof s === 'object' && | ||||
|                                                 s !== null && | ||||
|                                                 'value' in s | ||||
|                                             ) { | ||||
|                                                 return String( | ||||
|                                                     (s as { value: unknown }) | ||||
|                                                         .value | ||||
|                                                 ); | ||||
|                                             } | ||||
|                                             return String(s); | ||||
|                                         }) | ||||
|                                         .join(','); | ||||
|                                     finalDataType = `${normalizedBaseType}(${params})`; | ||||
|                                 } else if (precision !== undefined) { | ||||
|                                     if (scale !== undefined) { | ||||
|                                         finalDataType = `${normalizedBaseType}(${precision},${scale})`; | ||||
|                                     } else { | ||||
|                                         finalDataType = `${normalizedBaseType}(${precision})`; | ||||
|                                     } | ||||
|                                 } else if ( | ||||
|                                     length !== undefined && | ||||
|                                     length !== null | ||||
|                                 ) { | ||||
|                                     finalDataType = `${normalizedBaseType}(${length})`; | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             // Check for nullable constraint | ||||
|                             let nullable = true; | ||||
|                             if (isSerialType) { | ||||
|                                 nullable = false; | ||||
|                             } else if ( | ||||
|                                 expr.nullable && | ||||
|                                 expr.nullable.type === 'not null' | ||||
|                             ) { | ||||
|                                 nullable = false; | ||||
|                             } else if ( | ||||
|                                 definition?.nullable && | ||||
|                                 definition.nullable.type === 'not null' | ||||
|                             ) { | ||||
|                                 nullable = false; | ||||
|                             } | ||||
|  | ||||
|                             // Check for unique constraint | ||||
|                             const isUnique = | ||||
|                                 expr.unique === 'unique' || | ||||
|                                 definition?.unique === 'unique'; | ||||
|  | ||||
|                             // Check for default value | ||||
|                             let defaultValue: string | undefined; | ||||
|                             const defaultVal = | ||||
|                                 expr.default_val || definition?.default_val; | ||||
|                             if (defaultVal && !isSerialType) { | ||||
|                                 // Create a temporary columnDef to use the getDefaultValueString function | ||||
|                                 const tempColumnDef = { | ||||
|                                     default_val: defaultVal, | ||||
|                                 } as ColumnDefinition; | ||||
|                                 defaultValue = | ||||
|                                     getDefaultValueString(tempColumnDef); | ||||
|                             } | ||||
|  | ||||
|                             // Create the new column object | ||||
|                             const newColumn: SQLColumn = { | ||||
|                                 name: columnName, | ||||
|                                 type: finalDataType, | ||||
|                                 nullable: nullable, | ||||
|                                 primaryKey: | ||||
|                                     definition?.primary_key === 'primary key' || | ||||
|                                     definition?.constraint === 'primary key' || | ||||
|                                     isSerialType, | ||||
|                                 unique: isUnique, | ||||
|                                 default: defaultValue, | ||||
|                                 increment: | ||||
|                                     isSerialType || | ||||
|                                     definition?.auto_increment === | ||||
|                                         'auto_increment' || | ||||
|                                     (stmt.sql | ||||
|                                         .toUpperCase() | ||||
|                                         .includes('GENERATED') && | ||||
|                                         stmt.sql | ||||
|                                             .toUpperCase() | ||||
|                                             .includes('IDENTITY')), | ||||
|                             }; | ||||
|  | ||||
|                             // Add the column to the table if it doesn't already exist | ||||
|                             const tableColumns = table.columns as SQLColumn[]; | ||||
|                             if ( | ||||
|                                 !tableColumns.some( | ||||
|                                     (col) => col.name === columnName | ||||
|                                 ) | ||||
|                             ) { | ||||
|                                 tableColumns.push(newColumn); | ||||
|                             } | ||||
|                         } | ||||
|                     } else if ( | ||||
|                         expr.action === 'add' && | ||||
|                         expr.create_definitions | ||||
|                     ) { | ||||
|                         const createDefs = expr.create_definitions; | ||||
|  | ||||
|                         if ( | ||||
|                         // Check if it's adding a column (legacy structure) | ||||
|                         if (createDefs.resource === 'column') { | ||||
|                             const columnDef = | ||||
|                                 createDefs as unknown as ColumnDefinition; | ||||
|                             const columnName = extractColumnName( | ||||
|                                 columnDef.column | ||||
|                             ); | ||||
|  | ||||
|                             if (columnName) { | ||||
|                                 // Extract the column type and properties | ||||
|                                 const definition = | ||||
|                                     columnDef.definition as Record< | ||||
|                                         string, | ||||
|                                         unknown | ||||
|                                     >; | ||||
|                                 const rawDataType = String( | ||||
|                                     definition?.dataType || 'TEXT' | ||||
|                                 ); | ||||
|  | ||||
|                                 // Normalize the type | ||||
|                                 let normalizedBaseType = | ||||
|                                     normalizePostgreSQLType(rawDataType); | ||||
|  | ||||
|                                 // Check if it's a serial type | ||||
|                                 const upperType = rawDataType.toUpperCase(); | ||||
|                                 const isSerialType = [ | ||||
|                                     'SERIAL', | ||||
|                                     'SERIAL2', | ||||
|                                     'SERIAL4', | ||||
|                                     'SERIAL8', | ||||
|                                     'BIGSERIAL', | ||||
|                                     'SMALLSERIAL', | ||||
|                                 ].includes(upperType.split('(')[0]); | ||||
|  | ||||
|                                 if (isSerialType) { | ||||
|                                     const typeLength = definition?.length as | ||||
|                                         | number | ||||
|                                         | undefined; | ||||
|                                     if (upperType === 'SERIAL') { | ||||
|                                         if (typeLength === 2) { | ||||
|                                             normalizedBaseType = 'SMALLINT'; | ||||
|                                         } else if (typeLength === 8) { | ||||
|                                             normalizedBaseType = 'BIGINT'; | ||||
|                                         } else { | ||||
|                                             normalizedBaseType = 'INTEGER'; | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 // Handle type parameters | ||||
|                                 let finalDataType = normalizedBaseType; | ||||
|                                 const isNormalizedIntegerType = | ||||
|                                     ['INTEGER', 'BIGINT', 'SMALLINT'].includes( | ||||
|                                         normalizedBaseType | ||||
|                                     ) && | ||||
|                                     (upperType === 'INT' || | ||||
|                                         upperType === 'SERIAL'); | ||||
|  | ||||
|                                 if (!isSerialType && !isNormalizedIntegerType) { | ||||
|                                     const precision = | ||||
|                                         columnDef.definition?.precision; | ||||
|                                     const scale = columnDef.definition?.scale; | ||||
|                                     const length = columnDef.definition?.length; | ||||
|                                     const suffix = | ||||
|                                         (definition?.suffix as unknown[]) || []; | ||||
|  | ||||
|                                     if (suffix.length > 0) { | ||||
|                                         const params = suffix | ||||
|                                             .map((s: unknown) => { | ||||
|                                                 if ( | ||||
|                                                     typeof s === 'object' && | ||||
|                                                     s !== null && | ||||
|                                                     'value' in s | ||||
|                                                 ) { | ||||
|                                                     return String( | ||||
|                                                         ( | ||||
|                                                             s as { | ||||
|                                                                 value: unknown; | ||||
|                                                             } | ||||
|                                                         ).value | ||||
|                                                     ); | ||||
|                                                 } | ||||
|                                                 return String(s); | ||||
|                                             }) | ||||
|                                             .join(','); | ||||
|                                         finalDataType = `${normalizedBaseType}(${params})`; | ||||
|                                     } else if (precision !== undefined) { | ||||
|                                         if (scale !== undefined) { | ||||
|                                             finalDataType = `${normalizedBaseType}(${precision},${scale})`; | ||||
|                                         } else { | ||||
|                                             finalDataType = `${normalizedBaseType}(${precision})`; | ||||
|                                         } | ||||
|                                     } else if ( | ||||
|                                         length !== undefined && | ||||
|                                         length !== null | ||||
|                                     ) { | ||||
|                                         finalDataType = `${normalizedBaseType}(${length})`; | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 // Create the new column object | ||||
|                                 const newColumn: SQLColumn = { | ||||
|                                     name: columnName, | ||||
|                                     type: finalDataType, | ||||
|                                     nullable: isSerialType | ||||
|                                         ? false | ||||
|                                         : columnDef.nullable?.type !== | ||||
|                                           'not null', | ||||
|                                     primaryKey: | ||||
|                                         columnDef.primary_key === | ||||
|                                             'primary key' || | ||||
|                                         columnDef.definition?.constraint === | ||||
|                                             'primary key' || | ||||
|                                         isSerialType, | ||||
|                                     unique: columnDef.unique === 'unique', | ||||
|                                     typeArgs: getTypeArgs(columnDef.definition), | ||||
|                                     default: isSerialType | ||||
|                                         ? undefined | ||||
|                                         : getDefaultValueString(columnDef), | ||||
|                                     increment: | ||||
|                                         isSerialType || | ||||
|                                         columnDef.auto_increment === | ||||
|                                             'auto_increment' || | ||||
|                                         (stmt.sql | ||||
|                                             .toUpperCase() | ||||
|                                             .includes('GENERATED') && | ||||
|                                             stmt.sql | ||||
|                                                 .toUpperCase() | ||||
|                                                 .includes('IDENTITY')), | ||||
|                                 }; | ||||
|  | ||||
|                                 // Add the column to the table if it doesn't already exist | ||||
|                                 const tableColumns2 = | ||||
|                                     table.columns as SQLColumn[]; | ||||
|                                 if ( | ||||
|                                     !tableColumns2.some( | ||||
|                                         (col) => col.name === columnName | ||||
|                                     ) | ||||
|                                 ) { | ||||
|                                     tableColumns2.push(newColumn); | ||||
|                                 } | ||||
|                             } | ||||
|                         } else if ( | ||||
|                             createDefs.constraint_type === 'FOREIGN KEY' || | ||||
|                             createDefs.constraint_type === 'foreign key' | ||||
|                         ) { | ||||
| @@ -1129,19 +1637,188 @@ export async function fromPostgres( | ||||
|             } | ||||
|         } else if (stmt.type === 'alter' && !stmt.parsed) { | ||||
|             // Handle ALTER TABLE statements that failed to parse | ||||
|  | ||||
|             // First try to extract ALTER COLUMN TYPE statements | ||||
|             const alterTypeMatch = stmt.sql.match( | ||||
|                 /ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ALTER\s+COLUMN\s+(?:"([^"]+)"|([^"\s]+))\s+TYPE\s+([\w_]+(?:\([^)]*\))?(?:\[\])?)/i | ||||
|             ); | ||||
|  | ||||
|             if (alterTypeMatch) { | ||||
|                 const schemaName = | ||||
|                     alterTypeMatch[1] || alterTypeMatch[2] || 'public'; | ||||
|                 const tableName = alterTypeMatch[3] || alterTypeMatch[4]; | ||||
|                 const columnName = alterTypeMatch[5] || alterTypeMatch[6]; | ||||
|                 let columnType = alterTypeMatch[7]; | ||||
|  | ||||
|                 const table = findTableWithSchemaSupport( | ||||
|                     tables, | ||||
|                     tableName, | ||||
|                     schemaName | ||||
|                 ); | ||||
|                 if (table && columnName) { | ||||
|                     const column = (table.columns as SQLColumn[]).find( | ||||
|                         (col) => col.name === columnName | ||||
|                     ); | ||||
|                     if (column) { | ||||
|                         // Normalize and update the type | ||||
|                         columnType = normalizePostgreSQLType(columnType); | ||||
|                         column.type = columnType; | ||||
|  | ||||
|                         // Extract and update typeArgs if present | ||||
|                         const typeMatch = columnType.match( | ||||
|                             /^(\w+)(?:\(([^)]+)\))?$/ | ||||
|                         ); | ||||
|                         if (typeMatch && typeMatch[2]) { | ||||
|                             const params = typeMatch[2] | ||||
|                                 .split(',') | ||||
|                                 .map((p) => p.trim()); | ||||
|                             if (params.length === 1) { | ||||
|                                 column.typeArgs = { | ||||
|                                     length: parseInt(params[0]), | ||||
|                                 }; | ||||
|                             } else if (params.length === 2) { | ||||
|                                 column.typeArgs = { | ||||
|                                     precision: parseInt(params[0]), | ||||
|                                     scale: parseInt(params[1]), | ||||
|                                 }; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Then try to extract ADD COLUMN statements | ||||
|             const alterColumnMatch = stmt.sql.match( | ||||
|                 /ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ADD\s+COLUMN\s+(?:"([^"]+)"|([^"\s]+))\s+([\w_]+(?:\([^)]*\))?(?:\[\])?)/i | ||||
|             ); | ||||
|  | ||||
|             if (alterColumnMatch) { | ||||
|                 const schemaName = | ||||
|                     alterColumnMatch[1] || alterColumnMatch[2] || 'public'; | ||||
|                 const tableName = alterColumnMatch[3] || alterColumnMatch[4]; | ||||
|                 const columnName = alterColumnMatch[5] || alterColumnMatch[6]; | ||||
|                 let columnType = alterColumnMatch[7]; | ||||
|  | ||||
|                 const table = findTableWithSchemaSupport( | ||||
|                     tables, | ||||
|                     tableName, | ||||
|                     schemaName | ||||
|                 ); | ||||
|                 if (table && columnName) { | ||||
|                     const tableColumns = table.columns as SQLColumn[]; | ||||
|                     if (!tableColumns.some((col) => col.name === columnName)) { | ||||
|                         // Normalize the type | ||||
|                         columnType = normalizePostgreSQLType(columnType); | ||||
|  | ||||
|                         // Check for constraints in the statement | ||||
|                         const columnDefPart = stmt.sql.substring( | ||||
|                             stmt.sql.indexOf(columnName) | ||||
|                         ); | ||||
|                         const isPrimary = | ||||
|                             columnDefPart.match(/PRIMARY\s+KEY/i) !== null; | ||||
|                         const isNotNull = | ||||
|                             columnDefPart.match(/NOT\s+NULL/i) !== null; | ||||
|                         const isUnique = | ||||
|                             columnDefPart.match(/\bUNIQUE\b/i) !== null; | ||||
|                         // Extract default value | ||||
|                         let defaultValue: string | undefined; | ||||
|                         // Updated regex to handle casting with :: operator | ||||
|                         const defaultMatch = columnDefPart.match( | ||||
|                             /DEFAULT\s+((?:'[^']*'|"[^"]*"|\S+)(?:::\w+)?)/i | ||||
|                         ); | ||||
|                         if (defaultMatch) { | ||||
|                             let defVal = defaultMatch[1].trim(); | ||||
|                             // Remove trailing comma or semicolon if present | ||||
|                             defVal = defVal.replace(/[,;]$/, '').trim(); | ||||
|                             // Handle string literals | ||||
|                             if ( | ||||
|                                 defVal.startsWith("'") && | ||||
|                                 defVal.endsWith("'") | ||||
|                             ) { | ||||
|                                 // Keep the quotes for string literals | ||||
|                                 defaultValue = defVal; | ||||
|                             } else if (defVal.match(/^\d+(\.\d+)?$/)) { | ||||
|                                 // Numeric value | ||||
|                                 defaultValue = defVal; | ||||
|                             } else if ( | ||||
|                                 defVal.toUpperCase() === 'TRUE' || | ||||
|                                 defVal.toUpperCase() === 'FALSE' | ||||
|                             ) { | ||||
|                                 // Boolean value | ||||
|                                 defaultValue = defVal.toUpperCase(); | ||||
|                             } else if (defVal.toUpperCase() === 'NULL') { | ||||
|                                 // NULL value | ||||
|                                 defaultValue = 'NULL'; | ||||
|                             } else if ( | ||||
|                                 defVal.includes('(') && | ||||
|                                 defVal.includes(')') | ||||
|                             ) { | ||||
|                                 // Function call | ||||
|                                 // Normalize PostgreSQL function names to uppercase | ||||
|                                 const funcMatch = defVal.match(/^(\w+)\(/); | ||||
|                                 if (funcMatch) { | ||||
|                                     const funcName = funcMatch[1]; | ||||
|                                     const pgFunctions = [ | ||||
|                                         'now', | ||||
|                                         'current_timestamp', | ||||
|                                         'current_date', | ||||
|                                         'current_time', | ||||
|                                         'gen_random_uuid', | ||||
|                                         'random', | ||||
|                                         'nextval', | ||||
|                                         'currval', | ||||
|                                     ]; | ||||
|                                     if ( | ||||
|                                         pgFunctions.includes( | ||||
|                                             funcName.toLowerCase() | ||||
|                                         ) | ||||
|                                     ) { | ||||
|                                         defaultValue = defVal.replace( | ||||
|                                             funcName, | ||||
|                                             funcName.toUpperCase() | ||||
|                                         ); | ||||
|                                     } else { | ||||
|                                         defaultValue = defVal; | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     defaultValue = defVal; | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 // Other expressions | ||||
|                                 defaultValue = defVal; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         tableColumns.push({ | ||||
|                             name: columnName, | ||||
|                             type: columnType, | ||||
|                             nullable: !isNotNull && !isPrimary, | ||||
|                             primaryKey: isPrimary, | ||||
|                             unique: isUnique || isPrimary, | ||||
|                             default: defaultValue, | ||||
|                             increment: false, | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Extract foreign keys using regex as fallback | ||||
|             // Updated regex to handle quoted identifiers properly | ||||
|             const alterFKMatch = stmt.sql.match( | ||||
|                 /ALTER\s+TABLE\s+(?:ONLY\s+)?(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s+ADD\s+CONSTRAINT\s+["']?([^"'\s]+)["']?\s+FOREIGN\s+KEY\s*\(["']?([^"'\s)]+)["']?\)\s+REFERENCES\s+(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s*\(["']?([^"'\s)]+)["']?\)/i | ||||
|                 /ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ADD\s+CONSTRAINT\s+(?:"([^"]+)"|([^"\s]+))\s+FOREIGN\s+KEY\s*\((?:"([^"]+)"|([^"\s)]+))\)\s+REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\((?:"([^"]+)"|([^"\s)]+))\)/i | ||||
|             ); | ||||
|  | ||||
|             if (alterFKMatch) { | ||||
|                 const sourceSchema = alterFKMatch[1] || 'public'; | ||||
|                 const sourceTable = alterFKMatch[2]; | ||||
|                 const constraintName = alterFKMatch[3]; | ||||
|                 const sourceColumn = alterFKMatch[4]; | ||||
|                 const targetSchema = alterFKMatch[5] || 'public'; | ||||
|                 const targetTable = alterFKMatch[6]; | ||||
|                 const targetColumn = alterFKMatch[7]; | ||||
|                 // Extract values from appropriate match groups | ||||
|                 const sourceSchema = | ||||
|                     alterFKMatch[1] || alterFKMatch[2] || 'public'; | ||||
|                 const sourceTable = alterFKMatch[3] || alterFKMatch[4]; | ||||
|                 const constraintName = alterFKMatch[5] || alterFKMatch[6]; | ||||
|                 const sourceColumn = alterFKMatch[7] || alterFKMatch[8]; | ||||
|                 const targetSchema = | ||||
|                     alterFKMatch[9] || alterFKMatch[10] || 'public'; | ||||
|                 const targetTable = alterFKMatch[11] || alterFKMatch[12]; | ||||
|                 const targetColumn = alterFKMatch[13] || alterFKMatch[14]; | ||||
|  | ||||
|                 const sourceTableId = getTableIdWithSchemaSupport( | ||||
|                     tableMap, | ||||
| @@ -1275,58 +1952,10 @@ export async function fromPostgres( | ||||
| function getDefaultValueString( | ||||
|     columnDef: ColumnDefinition | ||||
| ): string | undefined { | ||||
|     let defVal = columnDef.default_val; | ||||
|  | ||||
|     if ( | ||||
|         defVal && | ||||
|         typeof defVal === 'object' && | ||||
|         defVal.type === 'default' && | ||||
|         'value' in defVal | ||||
|     ) { | ||||
|         defVal = defVal.value; | ||||
|     } | ||||
|     const defVal = columnDef.default_val; | ||||
|  | ||||
|     if (defVal === undefined || defVal === null) return undefined; | ||||
|  | ||||
|     let value: string | undefined; | ||||
|  | ||||
|     switch (typeof defVal) { | ||||
|         case 'string': | ||||
|             value = defVal; | ||||
|             break; | ||||
|         case 'number': | ||||
|             value = String(defVal); | ||||
|             break; | ||||
|         case 'boolean': | ||||
|             value = defVal ? 'TRUE' : 'FALSE'; | ||||
|             break; | ||||
|         case 'object': | ||||
|             if ('value' in defVal && typeof defVal.value === 'string') { | ||||
|                 value = defVal.value; | ||||
|             } else if ('raw' in defVal && typeof defVal.raw === 'string') { | ||||
|                 value = defVal.raw; | ||||
|             } else if (defVal.type === 'bool') { | ||||
|                 value = defVal.value ? 'TRUE' : 'FALSE'; | ||||
|             } else if (defVal.type === 'function' && defVal.name) { | ||||
|                 const fnName = defVal.name; | ||||
|                 if ( | ||||
|                     fnName && | ||||
|                     typeof fnName === 'object' && | ||||
|                     Array.isArray(fnName.name) && | ||||
|                     fnName.name.length > 0 && | ||||
|                     fnName.name[0].value | ||||
|                 ) { | ||||
|                     value = fnName.name[0].value.toUpperCase(); | ||||
|                 } else if (typeof fnName === 'string') { | ||||
|                     value = fnName.toUpperCase(); | ||||
|                 } else { | ||||
|                     value = 'UNKNOWN_FUNCTION'; | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|         default: | ||||
|             value = undefined; | ||||
|     } | ||||
|  | ||||
|     return value; | ||||
|     // Use buildSQLFromAST to reconstruct the default value | ||||
|     return buildSQLFromAST(defVal, DatabaseType.POSTGRESQL); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,252 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server Default Value Import', () => { | ||||
|     describe('String Default Values', () => { | ||||
|         it('should parse simple string defaults with single quotes', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE kingdom_citizens ( | ||||
|                     citizen_id INT NOT NULL, | ||||
|                     allegiance NVARCHAR(50) DEFAULT 'neutral', | ||||
|                     PRIMARY KEY (citizen_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const allegianceColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'allegiance' | ||||
|             ); | ||||
|             expect(allegianceColumn?.default).toBe("'neutral'"); | ||||
|         }); | ||||
|  | ||||
|         it('should parse string defaults with Unicode prefix', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE ancient_scrolls ( | ||||
|                     scroll_id INT NOT NULL, | ||||
|                     runic_inscription NVARCHAR(255) DEFAULT N'Ancient wisdom', | ||||
|                     prophecy NVARCHAR(MAX) DEFAULT N'The chosen one shall rise', | ||||
|                     PRIMARY KEY (scroll_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const runicColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'runic_inscription' | ||||
|             ); | ||||
|             expect(runicColumn?.default).toBe("N'Ancient wisdom'"); | ||||
|             const prophecyColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'prophecy' | ||||
|             ); | ||||
|             expect(prophecyColumn?.default).toBe( | ||||
|                 "N'The chosen one shall rise'" | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Numeric Default Values', () => { | ||||
|         it('should parse integer defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE castle_treasury ( | ||||
|                     treasury_id INT NOT NULL, | ||||
|                     gold_count INT DEFAULT 0, | ||||
|                     max_capacity BIGINT DEFAULT 100000, | ||||
|                     guard_posts SMALLINT DEFAULT 5, | ||||
|                     PRIMARY KEY (treasury_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const goldColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'gold_count' | ||||
|             ); | ||||
|             expect(goldColumn?.default).toBe('0'); | ||||
|             const capacityColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'max_capacity' | ||||
|             ); | ||||
|             expect(capacityColumn?.default).toBe('100000'); | ||||
|             const guardColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'guard_posts' | ||||
|             ); | ||||
|             expect(guardColumn?.default).toBe('5'); | ||||
|         }); | ||||
|  | ||||
|         it('should parse decimal defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE blacksmith_shop ( | ||||
|                     item_id INT NOT NULL, | ||||
|                     weapon_price DECIMAL(10, 2) DEFAULT 99.99, | ||||
|                     guild_discount FLOAT DEFAULT 0.15, | ||||
|                     enchantment_tax NUMERIC(5, 4) DEFAULT 0.0825, | ||||
|                     PRIMARY KEY (item_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const priceColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'weapon_price' | ||||
|             ); | ||||
|             expect(priceColumn?.default).toBe('99.99'); | ||||
|             const discountColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'guild_discount' | ||||
|             ); | ||||
|             expect(discountColumn?.default).toBe('0.15'); | ||||
|             const taxColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'enchantment_tax' | ||||
|             ); | ||||
|             expect(taxColumn?.default).toBe('0.0825'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Boolean Default Values', () => { | ||||
|         it('should parse BIT defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE magic_barriers ( | ||||
|                     barrier_id INT NOT NULL, | ||||
|                     is_active BIT DEFAULT 1, | ||||
|                     is_breached BIT DEFAULT 0, | ||||
|                     PRIMARY KEY (barrier_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const activeColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_active' | ||||
|             ); | ||||
|             expect(activeColumn?.default).toBe('1'); | ||||
|             const breachedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'is_breached' | ||||
|             ); | ||||
|             expect(breachedColumn?.default).toBe('0'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Date and Time Default Values', () => { | ||||
|         it('should parse date/time function defaults', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE battle_logs ( | ||||
|                     battle_id INT NOT NULL, | ||||
|                     battle_started DATETIME DEFAULT GETDATE(), | ||||
|                     last_action DATETIME2 DEFAULT SYSDATETIME(), | ||||
|                     battle_date DATE DEFAULT GETDATE(), | ||||
|                     PRIMARY KEY (battle_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const startedColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'battle_started' | ||||
|             ); | ||||
|             expect(startedColumn?.default).toBe('GETDATE()'); | ||||
|             const actionColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'last_action' | ||||
|             ); | ||||
|             expect(actionColumn?.default).toBe('SYSDATETIME()'); | ||||
|             const dateColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'battle_date' | ||||
|             ); | ||||
|             expect(dateColumn?.default).toBe('GETDATE()'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('IDENTITY columns', () => { | ||||
|         it('should handle IDENTITY columns correctly', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE legendary_weapons ( | ||||
|                     weapon_id INT IDENTITY(1,1) NOT NULL, | ||||
|                     legacy_id BIGINT IDENTITY(100,10) NOT NULL, | ||||
|                     weapon_name NVARCHAR(100), | ||||
|                     PRIMARY KEY (weapon_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const weaponColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'weapon_id' | ||||
|             ); | ||||
|             expect(weaponColumn?.increment).toBe(true); | ||||
|             const legacyColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'legacy_id' | ||||
|             ); | ||||
|             expect(legacyColumn?.increment).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Complex Real-World Example with Schema', () => { | ||||
|         it('should handle complex table with schema and multiple default types', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE [dbo].[QuestContracts] ( | ||||
|                     [ContractID] INT IDENTITY(1,1) NOT NULL, | ||||
|                     [AdventurerID] INT NOT NULL, | ||||
|                     [QuestDate] DATETIME DEFAULT GETDATE(), | ||||
|                     [QuestStatus] NVARCHAR(20) DEFAULT N'Available', | ||||
|                     [RewardAmount] DECIMAL(10, 2) DEFAULT 0.00, | ||||
|                     [IsCompleted] BIT DEFAULT 0, | ||||
|                     [CompletedDate] DATETIME NULL, | ||||
|                     [QuestNotes] NVARCHAR(MAX) DEFAULT NULL, | ||||
|                     [DifficultyLevel] INT DEFAULT 5, | ||||
|                     [QuestGuid] UNIQUEIDENTIFIER DEFAULT NEWID(), | ||||
|                     PRIMARY KEY ([ContractID]) | ||||
|                 ); | ||||
|             `; | ||||
|  | ||||
|             const result = await fromSQLServer(sql); | ||||
|             const table = result.tables[0]; | ||||
|             expect(table).toBeDefined(); | ||||
|             expect(table.schema).toBe('dbo'); | ||||
|  | ||||
|             // Check various default values | ||||
|             const questDateColumn = table.columns.find( | ||||
|                 (c) => c.name === 'QuestDate' | ||||
|             ); | ||||
|             expect(questDateColumn?.default).toBe('GETDATE()'); | ||||
|  | ||||
|             const statusColumn = table.columns.find( | ||||
|                 (c) => c.name === 'QuestStatus' | ||||
|             ); | ||||
|             expect(statusColumn?.default).toBe("N'Available'"); | ||||
|  | ||||
|             const rewardColumn = table.columns.find( | ||||
|                 (c) => c.name === 'RewardAmount' | ||||
|             ); | ||||
|             expect(rewardColumn?.default).toBe('0.00'); | ||||
|  | ||||
|             const completedColumn = table.columns.find( | ||||
|                 (c) => c.name === 'IsCompleted' | ||||
|             ); | ||||
|             expect(completedColumn?.default).toBe('0'); | ||||
|  | ||||
|             const difficultyColumn = table.columns.find( | ||||
|                 (c) => c.name === 'DifficultyLevel' | ||||
|             ); | ||||
|             expect(difficultyColumn?.default).toBe('5'); | ||||
|  | ||||
|             const guidColumn = table.columns.find( | ||||
|                 (c) => c.name === 'QuestGuid' | ||||
|             ); | ||||
|             expect(guidColumn?.default).toBe('NEWID()'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Expressions in defaults', () => { | ||||
|         it('should handle parentheses in default expressions', async () => { | ||||
|             const sql = ` | ||||
|                 CREATE TABLE spell_calculations ( | ||||
|                     calculation_id INT NOT NULL, | ||||
|                     base_damage INT DEFAULT (10 + 5), | ||||
|                     total_power DECIMAL(10,2) DEFAULT ((100.0 * 0.15) + 10), | ||||
|                     PRIMARY KEY (calculation_id) | ||||
|                 ); | ||||
|             `; | ||||
|             const result = await fromSQLServer(sql); | ||||
|             expect(result.tables).toHaveLength(1); | ||||
|             const damageColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'base_damage' | ||||
|             ); | ||||
|             expect(damageColumn?.default).toBe('(10 + 5)'); | ||||
|             const powerColumn = result.tables[0].columns.find( | ||||
|                 (c) => c.name === 'total_power' | ||||
|             ); | ||||
|             expect(powerColumn?.default).toBe('((100.0 * 0.15) + 10)'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,91 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server Complex Fantasy Case', () => { | ||||
|     it('should parse complex SQL with SpellDefinition and SpellComponent tables', async () => { | ||||
|         // Complex SQL with same structure as user's case but fantasy-themed | ||||
|         const sql = `CREATE TABLE [DBO].[SpellDefinition]( | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [HASVERBALCOMP] BOOLEAN,   | ||||
|   [INCANTATION] [VARCHAR](128),   | ||||
|   [INCANTATIONFIX] BOOLEAN,   | ||||
|   [ITSCOMPONENTREL]  [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),  | ||||
|   [SHOWVISUALS] BOOLEAN,    ) ON [PRIMARY] | ||||
|  | ||||
| CREATE TABLE [DBO].[SpellComponent]( | ||||
|   [ALIAS] CHAR (50),     | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [ISOPTIONAL] BOOLEAN,   | ||||
|   [ITSPARENTCOMP]  [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),  | ||||
|   [ITSSCHOOLMETA]  [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),  | ||||
|   [KEYATTR] CHAR (100),  ) ON [PRIMARY]`; | ||||
|  | ||||
|         console.log('Testing complex fantasy SQL...'); | ||||
|         console.log( | ||||
|             'Number of CREATE TABLE statements:', | ||||
|             (sql.match(/CREATE\s+TABLE/gi) || []).length | ||||
|         ); | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         console.log( | ||||
|             'Result tables:', | ||||
|             result.tables.map((t) => t.name) | ||||
|         ); | ||||
|         console.log('Result relationships:', result.relationships.length); | ||||
|  | ||||
|         // Debug: Show actual relationships | ||||
|         if (result.relationships.length === 0) { | ||||
|             console.log('WARNING: No relationships found!'); | ||||
|         } else { | ||||
|             console.log('Relationships found:'); | ||||
|             result.relationships.forEach((r) => { | ||||
|                 console.log( | ||||
|                     `  ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}` | ||||
|                 ); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Should create TWO tables | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|  | ||||
|         // Check first table | ||||
|         const spellDef = result.tables.find( | ||||
|             (t) => t.name === 'SpellDefinition' | ||||
|         ); | ||||
|         expect(spellDef).toBeDefined(); | ||||
|         expect(spellDef?.schema).toBe('DBO'); | ||||
|         expect(spellDef?.columns).toHaveLength(6); | ||||
|  | ||||
|         // Check second table | ||||
|         const spellComp = result.tables.find( | ||||
|             (t) => t.name === 'SpellComponent' | ||||
|         ); | ||||
|         expect(spellComp).toBeDefined(); | ||||
|         expect(spellComp?.schema).toBe('DBO'); | ||||
|         expect(spellComp?.columns).toHaveLength(6); | ||||
|  | ||||
|         // Check foreign key relationships (should have at least 2) | ||||
|         expect(result.relationships.length).toBeGreaterThanOrEqual(2); | ||||
|  | ||||
|         // Check FK from SpellDefinition to SpellComponent | ||||
|         const fkDefToComp = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'SpellDefinition' && | ||||
|                 r.targetTable === 'SpellComponent' && | ||||
|                 r.sourceColumn === 'itscomponentrel' | ||||
|         ); | ||||
|         expect(fkDefToComp).toBeDefined(); | ||||
|         expect(fkDefToComp?.targetColumn).toBe('SPELLID'); | ||||
|  | ||||
|         // Check self-referential FK in SpellComponent | ||||
|         const selfRefFK = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'SpellComponent' && | ||||
|                 r.targetTable === 'SpellComponent' && | ||||
|                 r.sourceColumn === 'itsparentcomp' | ||||
|         ); | ||||
|         expect(selfRefFK).toBeDefined(); | ||||
|         expect(selfRefFK?.targetColumn).toBe('SPELLID'); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,102 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { sqlImportToDiagram } from '../../../index'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
|  | ||||
| describe('SQL Server Full Import Flow', () => { | ||||
|     it('should create relationships when importing through the full flow', async () => { | ||||
|         const sql = `CREATE TABLE [DBO].[SpellDefinition]( | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [HASVERBALCOMP] BOOLEAN,   | ||||
|   [INCANTATION] [VARCHAR](128),   | ||||
|   [INCANTATIONFIX] BOOLEAN,   | ||||
|   [ITSCOMPONENTREL]  [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),  | ||||
|   [SHOWVISUALS] BOOLEAN,    ) ON [PRIMARY] | ||||
|  | ||||
| CREATE TABLE [DBO].[SpellComponent]( | ||||
|   [ALIAS] CHAR (50),     | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [ISOPTIONAL] BOOLEAN,   | ||||
|   [ITSPARENTCOMP]  [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),  | ||||
|   [ITSSCHOOLMETA]  [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),  | ||||
|   [KEYATTR] CHAR (100),  ) ON [PRIMARY]`; | ||||
|  | ||||
|         // Test the full import flow as the application uses it | ||||
|         const diagram = await sqlImportToDiagram({ | ||||
|             sqlContent: sql, | ||||
|             sourceDatabaseType: DatabaseType.SQL_SERVER, | ||||
|             targetDatabaseType: DatabaseType.SQL_SERVER, | ||||
|         }); | ||||
|  | ||||
|         // Verify tables | ||||
|         expect(diagram.tables).toHaveLength(2); | ||||
|         const tableNames = diagram.tables?.map((t) => t.name).sort(); | ||||
|         expect(tableNames).toEqual(['SpellComponent', 'SpellDefinition']); | ||||
|  | ||||
|         // Verify relationships are created in the diagram | ||||
|         expect(diagram.relationships).toBeDefined(); | ||||
|         expect(diagram.relationships?.length).toBeGreaterThanOrEqual(2); | ||||
|  | ||||
|         // Check specific relationships | ||||
|         const fk1 = diagram.relationships?.find( | ||||
|             (r) => | ||||
|                 r.sourceFieldId && | ||||
|                 r.targetFieldId && // Must have field IDs | ||||
|                 diagram.tables?.some( | ||||
|                     (t) => | ||||
|                         t.id === r.sourceTableId && t.name === 'SpellDefinition' | ||||
|                 ) | ||||
|         ); | ||||
|         expect(fk1).toBeDefined(); | ||||
|  | ||||
|         const fk2 = diagram.relationships?.find( | ||||
|             (r) => | ||||
|                 r.sourceFieldId && | ||||
|                 r.targetFieldId && // Must have field IDs | ||||
|                 diagram.tables?.some( | ||||
|                     (t) => | ||||
|                         t.id === r.sourceTableId && | ||||
|                         t.name === 'SpellComponent' && | ||||
|                         t.id === r.targetTableId // self-reference | ||||
|                 ) | ||||
|         ); | ||||
|         expect(fk2).toBeDefined(); | ||||
|  | ||||
|         console.log( | ||||
|             'Full flow test - Relationships created:', | ||||
|             diagram.relationships?.length | ||||
|         ); | ||||
|         diagram.relationships?.forEach((r) => { | ||||
|             const sourceTable = diagram.tables?.find( | ||||
|                 (t) => t.id === r.sourceTableId | ||||
|             ); | ||||
|             const targetTable = diagram.tables?.find( | ||||
|                 (t) => t.id === r.targetTableId | ||||
|             ); | ||||
|             const sourceField = sourceTable?.fields.find( | ||||
|                 (f) => f.id === r.sourceFieldId | ||||
|             ); | ||||
|             const targetField = targetTable?.fields.find( | ||||
|                 (f) => f.id === r.targetFieldId | ||||
|             ); | ||||
|             console.log( | ||||
|                 `  ${sourceTable?.name}.${sourceField?.name} -> ${targetTable?.name}.${targetField?.name}` | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should handle case-insensitive field matching', async () => { | ||||
|         const sql = `CREATE TABLE DragonLair ( | ||||
|   [LAIRID] INT PRIMARY KEY, | ||||
|   [parentLairId] INT, FOREIGN KEY (PARENTLAIRID) REFERENCES DragonLair(lairid) | ||||
| )`; | ||||
|  | ||||
|         const diagram = await sqlImportToDiagram({ | ||||
|             sqlContent: sql, | ||||
|             sourceDatabaseType: DatabaseType.SQL_SERVER, | ||||
|             targetDatabaseType: DatabaseType.SQL_SERVER, | ||||
|         }); | ||||
|  | ||||
|         // Should create the self-referential relationship despite case differences | ||||
|         expect(diagram.relationships?.length).toBe(1); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,573 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server Multi-Schema Database Tests', () => { | ||||
|     it('should parse a fantasy-themed multi-schema database with cross-schema relationships', async () => { | ||||
|         const sql = ` | ||||
| -- ============================================= | ||||
| -- Magical Realm Multi-Schema Database | ||||
| -- A comprehensive fantasy database with multiple schemas | ||||
| -- ============================================= | ||||
|  | ||||
| -- Create schemas | ||||
| CREATE SCHEMA [realm]; | ||||
| CREATE SCHEMA [academy]; | ||||
| CREATE SCHEMA [treasury]; | ||||
| CREATE SCHEMA [combat]; | ||||
| CREATE SCHEMA [marketplace]; | ||||
|  | ||||
| -- ============================================= | ||||
| -- REALM Schema - Core realm entities | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [realm].[kingdoms] ( | ||||
|     [kingdom_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [kingdom_name] NVARCHAR(100) NOT NULL UNIQUE, | ||||
|     [ruler_name] NVARCHAR(100) NOT NULL, | ||||
|     [founding_date] DATE NOT NULL, | ||||
|     [capital_city] NVARCHAR(100), | ||||
|     [population] BIGINT, | ||||
|     [treasury_gold] DECIMAL(18, 2) DEFAULT 10000.00 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [realm].[cities] ( | ||||
|     [city_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [city_name] NVARCHAR(100) NOT NULL, | ||||
|     [kingdom_id] BIGINT NOT NULL, | ||||
|     [population] INT, | ||||
|     [has_walls] BIT DEFAULT 0, | ||||
|     [has_academy] BIT DEFAULT 0, | ||||
|     [has_marketplace] BIT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [realm].[guilds] ( | ||||
|     [guild_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [guild_name] NVARCHAR(100) NOT NULL, | ||||
|     [guild_type] NVARCHAR(50) NOT NULL, -- 'Mages', 'Warriors', 'Thieves', 'Merchants' | ||||
|     [headquarters_city_id] BIGINT NOT NULL, | ||||
|     [founding_year] INT, | ||||
|     [member_count] INT DEFAULT 0, | ||||
|     [guild_master] NVARCHAR(100) | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- ACADEMY Schema - Educational institutions | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [academy].[schools] ( | ||||
|     [school_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [school_name] NVARCHAR(150) NOT NULL, | ||||
|     [city_id] BIGINT NOT NULL, | ||||
|     [specialization] NVARCHAR(100), -- 'Elemental Magic', 'Necromancy', 'Healing', 'Alchemy' | ||||
|     [founded_year] INT, | ||||
|     [tuition_gold] DECIMAL(10, 2), | ||||
|     [headmaster] NVARCHAR(100) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [academy].[students] ( | ||||
|     [student_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [first_name] NVARCHAR(50) NOT NULL, | ||||
|     [last_name] NVARCHAR(50) NOT NULL, | ||||
|     [school_id] BIGINT NOT NULL, | ||||
|     [enrollment_date] DATE NOT NULL, | ||||
|     [graduation_date] DATE NULL, | ||||
|     [major_discipline] NVARCHAR(100), | ||||
|     [home_kingdom_id] BIGINT NOT NULL, | ||||
|     [sponsor_guild_id] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [academy].[courses] ( | ||||
|     [course_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [course_name] NVARCHAR(200) NOT NULL, | ||||
|     [school_id] BIGINT NOT NULL, | ||||
|     [credit_hours] INT, | ||||
|     [difficulty_level] INT CHECK (difficulty_level BETWEEN 1 AND 10), | ||||
|     [prerequisites] NVARCHAR(MAX), | ||||
|     [professor_name] NVARCHAR(100) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [academy].[enrollments] ( | ||||
|     [enrollment_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [student_id] BIGINT NOT NULL, | ||||
|     [course_id] BIGINT NOT NULL, | ||||
|     [enrollment_date] DATE NOT NULL, | ||||
|     [grade] NVARCHAR(2), | ||||
|     [completed] BIT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- TREASURY Schema - Financial entities | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [treasury].[currencies] ( | ||||
|     [currency_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [currency_name] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [symbol] NVARCHAR(10), | ||||
|     [gold_exchange_rate] DECIMAL(10, 4) NOT NULL, | ||||
|     [issuing_kingdom_id] BIGINT NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [treasury].[banks] ( | ||||
|     [bank_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [bank_name] NVARCHAR(100) NOT NULL, | ||||
|     [headquarters_city_id] BIGINT NOT NULL, | ||||
|     [total_deposits] DECIMAL(18, 2) DEFAULT 0, | ||||
|     [vault_security_level] INT CHECK (vault_security_level BETWEEN 1 AND 10), | ||||
|     [founding_date] DATE | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [treasury].[accounts] ( | ||||
|     [account_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [account_number] NVARCHAR(20) NOT NULL UNIQUE, | ||||
|     [bank_id] BIGINT NOT NULL, | ||||
|     [owner_type] NVARCHAR(20) NOT NULL, -- 'Student', 'Guild', 'Kingdom', 'Merchant' | ||||
|     [owner_id] BIGINT NOT NULL, | ||||
|     [balance] DECIMAL(18, 2) DEFAULT 0, | ||||
|     [currency_id] BIGINT NOT NULL, | ||||
|     [opened_date] DATE NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [treasury].[transactions] ( | ||||
|     [transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [from_account_id] BIGINT NULL, | ||||
|     [to_account_id] BIGINT NULL, | ||||
|     [amount] DECIMAL(18, 2) NOT NULL, | ||||
|     [currency_id] BIGINT NOT NULL, | ||||
|     [transaction_date] DATETIME NOT NULL, | ||||
|     [description] NVARCHAR(500), | ||||
|     [transaction_type] NVARCHAR(50) -- 'Deposit', 'Withdrawal', 'Transfer', 'Payment' | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- COMBAT Schema - Battle and warrior entities | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [combat].[warriors] ( | ||||
|     [warrior_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [warrior_name] NVARCHAR(100) NOT NULL, | ||||
|     [class] NVARCHAR(50) NOT NULL, -- 'Knight', 'Archer', 'Mage', 'Barbarian' | ||||
|     [level] INT DEFAULT 1, | ||||
|     [experience_points] BIGINT DEFAULT 0, | ||||
|     [guild_id] BIGINT NULL, | ||||
|     [home_city_id] BIGINT NOT NULL, | ||||
|     [strength] INT, | ||||
|     [agility] INT, | ||||
|     [intelligence] INT, | ||||
|     [current_hp] INT, | ||||
|     [max_hp] INT | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [combat].[weapons] ( | ||||
|     [weapon_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [weapon_name] NVARCHAR(100) NOT NULL, | ||||
|     [weapon_type] NVARCHAR(50), -- 'Sword', 'Bow', 'Staff', 'Axe' | ||||
|     [damage] INT, | ||||
|     [durability] INT, | ||||
|     [enchantment_level] INT DEFAULT 0, | ||||
|     [market_value] DECIMAL(10, 2), | ||||
|     [owner_warrior_id] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [combat].[battles] ( | ||||
|     [battle_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [battle_name] NVARCHAR(200), | ||||
|     [battle_date] DATETIME NOT NULL, | ||||
|     [location_city_id] BIGINT NOT NULL, | ||||
|     [victor_warrior_id] BIGINT NULL, | ||||
|     [total_participants] INT, | ||||
|     [battle_type] NVARCHAR(50) -- 'Duel', 'Tournament', 'War', 'Training' | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [combat].[battle_participants] ( | ||||
|     [participant_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [battle_id] BIGINT NOT NULL, | ||||
|     [warrior_id] BIGINT NOT NULL, | ||||
|     [damage_dealt] INT DEFAULT 0, | ||||
|     [damage_received] INT DEFAULT 0, | ||||
|     [survived] BIT DEFAULT 1, | ||||
|     [rewards_earned] DECIMAL(10, 2) DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- MARKETPLACE Schema - Commerce entities | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [marketplace].[merchants] ( | ||||
|     [merchant_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [merchant_name] NVARCHAR(100) NOT NULL, | ||||
|     [shop_name] NVARCHAR(150), | ||||
|     [city_id] BIGINT NOT NULL, | ||||
|     [specialization] NVARCHAR(100), -- 'Weapons', 'Potions', 'Scrolls', 'Artifacts' | ||||
|     [reputation_score] INT DEFAULT 50, | ||||
|     [bank_account_id] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [marketplace].[items] ( | ||||
|     [item_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [item_name] NVARCHAR(150) NOT NULL, | ||||
|     [item_type] NVARCHAR(50), | ||||
|     [base_price] DECIMAL(10, 2), | ||||
|     [rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary' | ||||
|     [merchant_id] BIGINT NOT NULL, | ||||
|     [stock_quantity] INT DEFAULT 0, | ||||
|     [magical_properties] NVARCHAR(MAX) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [marketplace].[trade_routes] ( | ||||
|     [route_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [from_city_id] BIGINT NOT NULL, | ||||
|     [to_city_id] BIGINT NOT NULL, | ||||
|     [distance_leagues] INT, | ||||
|     [travel_days] INT, | ||||
|     [danger_level] INT CHECK (danger_level BETWEEN 1 AND 10), | ||||
|     [toll_cost] DECIMAL(10, 2), | ||||
|     [controlled_by_guild_id] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [marketplace].[transactions] ( | ||||
|     [transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [buyer_type] NVARCHAR(20), -- 'Warrior', 'Student', 'Merchant' | ||||
|     [buyer_id] BIGINT NOT NULL, | ||||
|     [merchant_id] BIGINT NOT NULL, | ||||
|     [item_id] BIGINT NOT NULL, | ||||
|     [quantity] INT NOT NULL, | ||||
|     [total_price] DECIMAL(10, 2) NOT NULL, | ||||
|     [transaction_date] DATETIME NOT NULL, | ||||
|     [payment_account_id] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Foreign Key Constraints - Cross-Schema Relationships | ||||
| -- ============================================= | ||||
|  | ||||
| -- Realm schema relationships | ||||
| ALTER TABLE [realm].[cities] ADD CONSTRAINT [FK_Cities_Kingdoms]  | ||||
|     FOREIGN KEY ([kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]); | ||||
|  | ||||
| ALTER TABLE [realm].[guilds] ADD CONSTRAINT [FK_Guilds_Cities]  | ||||
|     FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| -- Academy schema relationships (references realm schema) | ||||
| ALTER TABLE [academy].[schools] ADD CONSTRAINT [FK_Schools_Cities]  | ||||
|     FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Schools]  | ||||
|     FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Kingdoms]  | ||||
|     FOREIGN KEY ([home_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Guilds]  | ||||
|     FOREIGN KEY ([sponsor_guild_id]) REFERENCES [realm].[guilds]([guild_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[courses] ADD CONSTRAINT [FK_Courses_Schools]  | ||||
|     FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Students]  | ||||
|     FOREIGN KEY ([student_id]) REFERENCES [academy].[students]([student_id]); | ||||
|  | ||||
| ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Courses]  | ||||
|     FOREIGN KEY ([course_id]) REFERENCES [academy].[courses]([course_id]); | ||||
|  | ||||
| -- Treasury schema relationships (references realm schema) | ||||
| ALTER TABLE [treasury].[currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]  | ||||
|     FOREIGN KEY ([issuing_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[banks] ADD CONSTRAINT [FK_Banks_Cities]  | ||||
|     FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Banks]  | ||||
|     FOREIGN KEY ([bank_id]) REFERENCES [treasury].[banks]([bank_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Currencies]  | ||||
|     FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_FromAccount]  | ||||
|     FOREIGN KEY ([from_account_id]) REFERENCES [treasury].[accounts]([account_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_ToAccount]  | ||||
|     FOREIGN KEY ([to_account_id]) REFERENCES [treasury].[accounts]([account_id]); | ||||
|  | ||||
| ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_Currency]  | ||||
|     FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]); | ||||
|  | ||||
| -- Combat schema relationships (references realm and combat schemas) | ||||
| ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Guilds]  | ||||
|     FOREIGN KEY ([guild_id]) REFERENCES [realm].[guilds]([guild_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Cities]  | ||||
|     FOREIGN KEY ([home_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[weapons] ADD CONSTRAINT [FK_Weapons_Warriors]  | ||||
|     FOREIGN KEY ([owner_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_Cities]  | ||||
|     FOREIGN KEY ([location_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_VictorWarrior]  | ||||
|     FOREIGN KEY ([victor_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Battles]  | ||||
|     FOREIGN KEY ([battle_id]) REFERENCES [combat].[battles]([battle_id]); | ||||
|  | ||||
| ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Warriors]  | ||||
|     FOREIGN KEY ([warrior_id]) REFERENCES [combat].[warriors]([warrior_id]); | ||||
|  | ||||
| -- Marketplace schema relationships (references multiple schemas) | ||||
| ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_Cities]  | ||||
|     FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_BankAccounts]  | ||||
|     FOREIGN KEY ([bank_account_id]) REFERENCES [treasury].[accounts]([account_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[items] ADD CONSTRAINT [FK_Items_Merchants]  | ||||
|     FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_FromCity]  | ||||
|     FOREIGN KEY ([from_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_ToCity]  | ||||
|     FOREIGN KEY ([to_city_id]) REFERENCES [realm].[cities]([city_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_Guilds]  | ||||
|     FOREIGN KEY ([controlled_by_guild_id]) REFERENCES [realm].[guilds]([guild_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Merchants]  | ||||
|     FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Items]  | ||||
|     FOREIGN KEY ([item_id]) REFERENCES [marketplace].[items]([item_id]); | ||||
|  | ||||
| ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_PaymentAccount]  | ||||
|     FOREIGN KEY ([payment_account_id]) REFERENCES [treasury].[accounts]([account_id]); | ||||
|  | ||||
| -- Note: Testing table reference without schema prefix defaults to dbo schema | ||||
|         `; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // Verify all schemas are recognized | ||||
|         const schemas = new Set(result.tables.map((t) => t.schema)); | ||||
|         expect(schemas.has('realm')).toBe(true); | ||||
|         expect(schemas.has('academy')).toBe(true); | ||||
|         expect(schemas.has('treasury')).toBe(true); | ||||
|         expect(schemas.has('combat')).toBe(true); | ||||
|         expect(schemas.has('marketplace')).toBe(true); | ||||
|  | ||||
|         // Verify table count per schema | ||||
|         const tablesBySchema = { | ||||
|             realm: result.tables.filter((t) => t.schema === 'realm').length, | ||||
|             academy: result.tables.filter((t) => t.schema === 'academy').length, | ||||
|             treasury: result.tables.filter((t) => t.schema === 'treasury') | ||||
|                 .length, | ||||
|             combat: result.tables.filter((t) => t.schema === 'combat').length, | ||||
|             marketplace: result.tables.filter((t) => t.schema === 'marketplace') | ||||
|                 .length, | ||||
|         }; | ||||
|  | ||||
|         expect(tablesBySchema.realm).toBe(3); // kingdoms, cities, guilds | ||||
|         expect(tablesBySchema.academy).toBe(4); // schools, students, courses, enrollments | ||||
|         expect(tablesBySchema.treasury).toBe(4); // currencies, banks, accounts, transactions | ||||
|         expect(tablesBySchema.combat).toBe(4); // warriors, weapons, battles, battle_participants | ||||
|         expect(tablesBySchema.marketplace).toBe(4); // merchants, items, trade_routes, transactions | ||||
|  | ||||
|         // Total tables should be 19 | ||||
|         expect(result.tables.length).toBe(19); | ||||
|  | ||||
|         // Debug: log which relationships are missing | ||||
|         const expectedRelationshipNames = [ | ||||
|             'FK_Cities_Kingdoms', | ||||
|             'FK_Guilds_Cities', | ||||
|             'FK_Schools_Cities', | ||||
|             'FK_Students_Schools', | ||||
|             'FK_Students_Kingdoms', | ||||
|             'FK_Students_Guilds', | ||||
|             'FK_Courses_Schools', | ||||
|             'FK_Enrollments_Students', | ||||
|             'FK_Enrollments_Courses', | ||||
|             'FK_Currencies_Kingdoms', | ||||
|             'FK_Banks_Cities', | ||||
|             'FK_Accounts_Banks', | ||||
|             'FK_Accounts_Currencies', | ||||
|             'FK_Transactions_FromAccount', | ||||
|             'FK_Transactions_ToAccount', | ||||
|             'FK_Transactions_Currency', | ||||
|             'FK_Warriors_Guilds', | ||||
|             'FK_Warriors_Cities', | ||||
|             'FK_Weapons_Warriors', | ||||
|             'FK_Battles_Cities', | ||||
|             'FK_Battles_VictorWarrior', | ||||
|             'FK_BattleParticipants_Battles', | ||||
|             'FK_BattleParticipants_Warriors', | ||||
|             'FK_Merchants_Cities', | ||||
|             'FK_Merchants_BankAccounts', | ||||
|             'FK_Items_Merchants', | ||||
|             'FK_TradeRoutes_FromCity', | ||||
|             'FK_TradeRoutes_ToCity', | ||||
|             'FK_TradeRoutes_Guilds', | ||||
|             'FK_MarketTransactions_Merchants', | ||||
|             'FK_MarketTransactions_Items', | ||||
|             'FK_MarketTransactions_PaymentAccount', | ||||
|         ]; | ||||
|  | ||||
|         const foundRelationshipNames = result.relationships.map((r) => r.name); | ||||
|         const missingRelationships = expectedRelationshipNames.filter( | ||||
|             (name) => !foundRelationshipNames.includes(name) | ||||
|         ); | ||||
|  | ||||
|         if (missingRelationships.length > 0) { | ||||
|             console.log('Missing relationships:', missingRelationships); | ||||
|             console.log('Found relationships:', foundRelationshipNames); | ||||
|         } | ||||
|  | ||||
|         // Verify relationships count - we have 32 working relationships | ||||
|         expect(result.relationships.length).toBe(32); | ||||
|  | ||||
|         // Verify some specific cross-schema relationships | ||||
|         const crossSchemaRelationships = result.relationships.filter( | ||||
|             (r) => r.sourceSchema !== r.targetSchema | ||||
|         ); | ||||
|  | ||||
|         expect(crossSchemaRelationships.length).toBeGreaterThan(10); // Many cross-schema relationships | ||||
|  | ||||
|         // Check specific cross-schema relationships exist | ||||
|         const schoolsToCities = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'schools' && | ||||
|                 r.sourceSchema === 'academy' && | ||||
|                 r.targetTable === 'cities' && | ||||
|                 r.targetSchema === 'realm' | ||||
|         ); | ||||
|         expect(schoolsToCities).toBeDefined(); | ||||
|         expect(schoolsToCities?.name).toBe('FK_Schools_Cities'); | ||||
|  | ||||
|         const studentsToKingdoms = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'students' && | ||||
|                 r.sourceSchema === 'academy' && | ||||
|                 r.targetTable === 'kingdoms' && | ||||
|                 r.targetSchema === 'realm' | ||||
|         ); | ||||
|         expect(studentsToKingdoms).toBeDefined(); | ||||
|         expect(studentsToKingdoms?.name).toBe('FK_Students_Kingdoms'); | ||||
|  | ||||
|         const warriorsToGuilds = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'warriors' && | ||||
|                 r.sourceSchema === 'combat' && | ||||
|                 r.targetTable === 'guilds' && | ||||
|                 r.targetSchema === 'realm' | ||||
|         ); | ||||
|         expect(warriorsToGuilds).toBeDefined(); | ||||
|         expect(warriorsToGuilds?.name).toBe('FK_Warriors_Guilds'); | ||||
|  | ||||
|         const merchantsToAccounts = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'merchants' && | ||||
|                 r.sourceSchema === 'marketplace' && | ||||
|                 r.targetTable === 'accounts' && | ||||
|                 r.targetSchema === 'treasury' | ||||
|         ); | ||||
|         expect(merchantsToAccounts).toBeDefined(); | ||||
|         expect(merchantsToAccounts?.name).toBe('FK_Merchants_BankAccounts'); | ||||
|  | ||||
|         // Verify all relationships have valid source and target table IDs | ||||
|         const validRelationships = result.relationships.filter( | ||||
|             (r) => r.sourceTableId && r.targetTableId | ||||
|         ); | ||||
|         expect(validRelationships.length).toBe(result.relationships.length); | ||||
|  | ||||
|         // Check that table IDs are properly linked | ||||
|         for (const rel of result.relationships) { | ||||
|             const sourceTable = result.tables.find( | ||||
|                 (t) => | ||||
|                     t.name === rel.sourceTable && t.schema === rel.sourceSchema | ||||
|             ); | ||||
|             const targetTable = result.tables.find( | ||||
|                 (t) => | ||||
|                     t.name === rel.targetTable && t.schema === rel.targetSchema | ||||
|             ); | ||||
|  | ||||
|             expect(sourceTable).toBeDefined(); | ||||
|             expect(targetTable).toBeDefined(); | ||||
|             expect(rel.sourceTableId).toBe(sourceTable?.id); | ||||
|             expect(rel.targetTableId).toBe(targetTable?.id); | ||||
|         } | ||||
|  | ||||
|         // Test relationships within the same schema | ||||
|         const withinSchemaRels = result.relationships.filter( | ||||
|             (r) => r.sourceSchema === r.targetSchema | ||||
|         ); | ||||
|         expect(withinSchemaRels.length).toBeGreaterThan(10); | ||||
|  | ||||
|         // Verify specific within-schema relationship | ||||
|         const citiesToKingdoms = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'cities' && | ||||
|                 r.targetTable === 'kingdoms' && | ||||
|                 r.sourceSchema === 'realm' && | ||||
|                 r.targetSchema === 'realm' | ||||
|         ); | ||||
|         expect(citiesToKingdoms).toBeDefined(); | ||||
|  | ||||
|         console.log('Multi-schema test results:'); | ||||
|         console.log('Total schemas:', schemas.size); | ||||
|         console.log('Total tables:', result.tables.length); | ||||
|         console.log('Total relationships:', result.relationships.length); | ||||
|         console.log( | ||||
|             'Cross-schema relationships:', | ||||
|             crossSchemaRelationships.length | ||||
|         ); | ||||
|         console.log('Within-schema relationships:', withinSchemaRels.length); | ||||
|     }); | ||||
|  | ||||
|     it('should handle mixed schema notation formats', async () => { | ||||
|         const sql = ` | ||||
| -- Mix of different schema notation styles | ||||
| CREATE TABLE [dbo].[table1] ( | ||||
|     [id] INT PRIMARY KEY, | ||||
|     [name] NVARCHAR(50) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE table2 ( | ||||
|     id INT PRIMARY KEY, | ||||
|     table1_id INT | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [schema1].[table3] ( | ||||
|     [id] INT PRIMARY KEY, | ||||
|     [value] DECIMAL(10,2) | ||||
| ); | ||||
|  | ||||
| -- Different ALTER TABLE formats | ||||
| ALTER TABLE [dbo].[table1] ADD CONSTRAINT [FK1]  | ||||
|     FOREIGN KEY ([id]) REFERENCES [schema1].[table3]([id]); | ||||
|  | ||||
| ALTER TABLE table2 ADD CONSTRAINT FK2  | ||||
|     FOREIGN KEY (table1_id) REFERENCES [dbo].[table1](id); | ||||
|  | ||||
| ALTER TABLE [schema1].[table3] ADD CONSTRAINT [FK3]  | ||||
|     FOREIGN KEY ([id]) REFERENCES table2(id); | ||||
|         `; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         expect(result.tables.length).toBe(3); | ||||
|         expect(result.relationships.length).toBe(3); | ||||
|  | ||||
|         // Verify schemas are correctly assigned | ||||
|         const table1 = result.tables.find((t) => t.name === 'table1'); | ||||
|         const table2 = result.tables.find((t) => t.name === 'table2'); | ||||
|         const table3 = result.tables.find((t) => t.name === 'table3'); | ||||
|  | ||||
|         expect(table1?.schema).toBe('dbo'); | ||||
|         expect(table2?.schema).toBe('dbo'); | ||||
|         expect(table3?.schema).toBe('schema1'); | ||||
|  | ||||
|         // Verify all relationships are properly linked | ||||
|         for (const rel of result.relationships) { | ||||
|             expect(rel.sourceTableId).toBeTruthy(); | ||||
|             expect(rel.targetTableId).toBeTruthy(); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,132 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server Multiple Tables with Foreign Keys', () => { | ||||
|     it('should parse multiple tables with foreign keys in user format', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE [DBO].[QuestReward]( | ||||
|                 [BOID] (VARCHAR)(32), | ||||
|                 [HASEXTRACOL] BOOLEAN, | ||||
|                 [REWARDCODE] [VARCHAR](128), | ||||
|                 [REWARDFIX] BOOLEAN, | ||||
|                 [ITSQUESTREL] [VARCHAR](32), FOREIGN KEY (itsquestrel) REFERENCES QuestRelation(BOID), | ||||
|                 [SHOWDETAILS] BOOLEAN, | ||||
|             ) ON [PRIMARY] | ||||
|  | ||||
|             CREATE TABLE [DBO].[QuestRelation]( | ||||
|                 [ALIAS] CHAR (50), | ||||
|                 [BOID] (VARCHAR)(32), | ||||
|                 [ISOPTIONAL] BOOLEAN, | ||||
|                 [ITSPARENTREL] [VARCHAR](32), FOREIGN KEY (itsparentrel) REFERENCES QuestRelation(BOID), | ||||
|                 [ITSGUILDMETA] [VARCHAR](32), FOREIGN KEY (itsguildmeta) REFERENCES GuildMeta(BOID), | ||||
|                 [KEYATTR] CHAR (100), | ||||
|             ) ON [PRIMARY] | ||||
|         `; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // Should create both tables | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|  | ||||
|         // Check first table | ||||
|         const questReward = result.tables.find((t) => t.name === 'QuestReward'); | ||||
|         expect(questReward).toBeDefined(); | ||||
|         expect(questReward?.schema).toBe('DBO'); | ||||
|         expect(questReward?.columns).toHaveLength(6); | ||||
|  | ||||
|         // Check second table | ||||
|         const questRelation = result.tables.find( | ||||
|             (t) => t.name === 'QuestRelation' | ||||
|         ); | ||||
|         expect(questRelation).toBeDefined(); | ||||
|         expect(questRelation?.schema).toBe('DBO'); | ||||
|         expect(questRelation?.columns).toHaveLength(6); | ||||
|  | ||||
|         // Check foreign key relationships | ||||
|         expect(result.relationships).toHaveLength(2); // Should have 2 FKs (one self-referential in QuestRelation, one from QuestReward to QuestRelation) | ||||
|  | ||||
|         // Check FK from QuestReward to QuestRelation | ||||
|         const fkToRelation = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'QuestReward' && | ||||
|                 r.targetTable === 'QuestRelation' | ||||
|         ); | ||||
|         expect(fkToRelation).toBeDefined(); | ||||
|         expect(fkToRelation?.sourceColumn).toBe('itsquestrel'); | ||||
|         expect(fkToRelation?.targetColumn).toBe('BOID'); | ||||
|  | ||||
|         // Check self-referential FK in QuestRelation | ||||
|         const selfRefFK = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'QuestRelation' && | ||||
|                 r.targetTable === 'QuestRelation' && | ||||
|                 r.sourceColumn === 'itsparentrel' | ||||
|         ); | ||||
|         expect(selfRefFK).toBeDefined(); | ||||
|         expect(selfRefFK?.targetColumn).toBe('BOID'); | ||||
|     }); | ||||
|  | ||||
|     it('should parse multiple tables with circular dependencies', async () => { | ||||
|         const sql = ` | ||||
|             CREATE TABLE [DBO].[Dragon]( | ||||
|                 [DRAGONID] (VARCHAR)(32), | ||||
|                 [NAME] [VARCHAR](100), | ||||
|                 [ITSLAIRREL] [VARCHAR](32), FOREIGN KEY (itslairrel) REFERENCES DragonLair(LAIRID), | ||||
|                 [POWER] INT, | ||||
|             ) ON [PRIMARY] | ||||
|  | ||||
|             CREATE TABLE [DBO].[DragonLair]( | ||||
|                 [LAIRID] (VARCHAR)(32), | ||||
|                 [LOCATION] [VARCHAR](200), | ||||
|                 [ITSGUARDIAN] [VARCHAR](32), FOREIGN KEY (itsguardian) REFERENCES Dragon(DRAGONID), | ||||
|                 [TREASURES] INT, | ||||
|             ) ON [PRIMARY] | ||||
|         `; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // Should create both tables despite circular dependency | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|  | ||||
|         const dragon = result.tables.find((t) => t.name === 'Dragon'); | ||||
|         expect(dragon).toBeDefined(); | ||||
|  | ||||
|         const dragonLair = result.tables.find((t) => t.name === 'DragonLair'); | ||||
|         expect(dragonLair).toBeDefined(); | ||||
|  | ||||
|         // Check foreign key relationships (may have one or both depending on parser behavior with circular deps) | ||||
|         expect(result.relationships.length).toBeGreaterThanOrEqual(1); | ||||
|     }); | ||||
|  | ||||
|     it('should handle exact user input format', async () => { | ||||
|         // Exact copy of the user's input with fantasy theme | ||||
|         const sql = `CREATE TABLE [DBO].[WizardDef]( | ||||
|   [BOID]  (VARCHAR)(32),     | ||||
|   [HASEXTRACNTCOL] BOOLEAN,   | ||||
|   [HISTORYCD] [VARCHAR](128),   | ||||
|   [HISTORYCDFIX] BOOLEAN,   | ||||
|   [ITSADWIZARDREL]  [VARCHAR](32), FOREIGN KEY (itsadwizardrel) REFERENCES WizardRel(BOID),  | ||||
|   [SHOWDETAILS] BOOLEAN,    ) ON [PRIMARY] | ||||
|  | ||||
| CREATE TABLE [DBO].[WizardRel]( | ||||
|   [ALIAS] CHAR (50),     | ||||
|   [BOID]  (VARCHAR)(32),     | ||||
|   [ISOPTIONAL] BOOLEAN,   | ||||
|   [ITSARWIZARDREL]  [VARCHAR](32), FOREIGN KEY (itsarwizardrel) REFERENCES WizardRel(BOID),  | ||||
|   [ITSARMETABO]  [VARCHAR](32), FOREIGN KEY (itsarmetabo) REFERENCES MetaBO(BOID),  | ||||
|   [KEYATTR] CHAR (100),  ) ON [PRIMARY]`; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // This should create TWO tables, not just one | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|  | ||||
|         const wizardDef = result.tables.find((t) => t.name === 'WizardDef'); | ||||
|         expect(wizardDef).toBeDefined(); | ||||
|         expect(wizardDef?.columns).toHaveLength(6); | ||||
|  | ||||
|         const wizardRel = result.tables.find((t) => t.name === 'WizardRel'); | ||||
|         expect(wizardRel).toBeDefined(); | ||||
|         expect(wizardRel?.columns).toHaveLength(6); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,704 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server Single-Schema Database Tests', () => { | ||||
|     it('should parse a comprehensive fantasy-themed single-schema database with many foreign key relationships', async () => { | ||||
|         // This test simulates a complex single-schema database similar to real-world scenarios | ||||
|         // It tests the fix for parsing ALTER TABLE ADD CONSTRAINT statements without schema prefixes | ||||
|         const sql = ` | ||||
| -- ============================================= | ||||
| -- Enchanted Kingdom Management System | ||||
| -- A comprehensive fantasy database using single schema (dbo) | ||||
| -- ============================================= | ||||
|  | ||||
| -- ============================================= | ||||
| -- Core Kingdom Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [Kingdoms] ( | ||||
|     [KingdomID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [KingdomName] NVARCHAR(100) NOT NULL UNIQUE, | ||||
|     [FoundedYear] INT NOT NULL, | ||||
|     [CurrentRuler] NVARCHAR(100) NOT NULL, | ||||
|     [TreasuryGold] DECIMAL(18, 2) DEFAULT 100000.00, | ||||
|     [Population] BIGINT DEFAULT 0, | ||||
|     [MilitaryStrength] INT DEFAULT 100 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Regions] ( | ||||
|     [RegionID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [RegionName] NVARCHAR(100) NOT NULL, | ||||
|     [KingdomID] BIGINT NOT NULL, | ||||
|     [Terrain] NVARCHAR(50), -- 'Mountains', 'Forest', 'Plains', 'Desert', 'Swamp' | ||||
|     [Population] INT DEFAULT 0, | ||||
|     [TaxRate] DECIMAL(5, 2) DEFAULT 10.00 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Cities] ( | ||||
|     [CityID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CityName] NVARCHAR(100) NOT NULL, | ||||
|     [RegionID] BIGINT NOT NULL, | ||||
|     [Population] INT DEFAULT 1000, | ||||
|     [HasWalls] BIT DEFAULT 0, | ||||
|     [HasMarket] BIT DEFAULT 1, | ||||
|     [DefenseRating] INT DEFAULT 5 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Castles] ( | ||||
|     [CastleID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CastleName] NVARCHAR(100) NOT NULL, | ||||
|     [CityID] BIGINT NOT NULL, | ||||
|     [GarrisonSize] INT DEFAULT 50, | ||||
|     [TowerCount] INT DEFAULT 4, | ||||
|     [MoatDepth] DECIMAL(5, 2) DEFAULT 3.00 | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Character Management Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [CharacterClasses] ( | ||||
|     [ClassID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [ClassName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [ClassType] NVARCHAR(30), -- 'Warrior', 'Mage', 'Rogue', 'Cleric' | ||||
|     [BaseHealth] INT DEFAULT 100, | ||||
|     [BaseMana] INT DEFAULT 50, | ||||
|     [BaseStrength] INT DEFAULT 10, | ||||
|     [BaseIntelligence] INT DEFAULT 10 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Characters] ( | ||||
|     [CharacterID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterName] NVARCHAR(100) NOT NULL, | ||||
|     [ClassID] BIGINT NOT NULL, | ||||
|     [Level] INT DEFAULT 1, | ||||
|     [Experience] BIGINT DEFAULT 0, | ||||
|     [CurrentHealth] INT DEFAULT 100, | ||||
|     [CurrentMana] INT DEFAULT 50, | ||||
|     [HomeCityID] BIGINT NOT NULL, | ||||
|     [Gold] DECIMAL(10, 2) DEFAULT 100.00, | ||||
|     [CreatedDate] DATE NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [CharacterSkills] ( | ||||
|     [SkillID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [SkillName] NVARCHAR(100) NOT NULL, | ||||
|     [RequiredClassID] BIGINT NULL, | ||||
|     [RequiredLevel] INT DEFAULT 1, | ||||
|     [ManaCost] INT DEFAULT 10, | ||||
|     [Cooldown] INT DEFAULT 0, | ||||
|     [Damage] INT DEFAULT 0, | ||||
|     [Description] NVARCHAR(MAX) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [CharacterSkillMapping] ( | ||||
|     [MappingID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [SkillID] BIGINT NOT NULL, | ||||
|     [SkillLevel] INT DEFAULT 1, | ||||
|     [LastUsed] DATETIME NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Guild System Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [GuildTypes] ( | ||||
|     [GuildTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [TypeName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [Description] NVARCHAR(255) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Guilds] ( | ||||
|     [GuildID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [GuildName] NVARCHAR(100) NOT NULL UNIQUE, | ||||
|     [GuildTypeID] BIGINT NOT NULL, | ||||
|     [HeadquartersCityID] BIGINT NOT NULL, | ||||
|     [FoundedDate] DATE NOT NULL, | ||||
|     [GuildMasterID] BIGINT NULL, | ||||
|     [MemberCount] INT DEFAULT 0, | ||||
|     [GuildBank] DECIMAL(18, 2) DEFAULT 0.00, | ||||
|     [Reputation] INT DEFAULT 50 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [GuildMembers] ( | ||||
|     [MembershipID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [GuildID] BIGINT NOT NULL, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [JoinDate] DATE NOT NULL, | ||||
|     [Rank] NVARCHAR(50) DEFAULT 'Member', | ||||
|     [ContributionPoints] INT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [GuildQuests] ( | ||||
|     [QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [QuestName] NVARCHAR(200) NOT NULL, | ||||
|     [GuildID] BIGINT NOT NULL, | ||||
|     [RequiredLevel] INT DEFAULT 1, | ||||
|     [RewardGold] DECIMAL(10, 2) DEFAULT 100.00, | ||||
|     [RewardExperience] INT DEFAULT 100, | ||||
|     [QuestGiverID] BIGINT NULL, | ||||
|     [Status] NVARCHAR(20) DEFAULT 'Available' | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Item and Inventory Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [ItemCategories] ( | ||||
|     [CategoryID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CategoryName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [Description] NVARCHAR(255) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Items] ( | ||||
|     [ItemID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [ItemName] NVARCHAR(150) NOT NULL, | ||||
|     [CategoryID] BIGINT NOT NULL, | ||||
|     [Rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary' | ||||
|     [BaseValue] DECIMAL(10, 2) DEFAULT 1.00, | ||||
|     [Weight] DECIMAL(5, 2) DEFAULT 1.00, | ||||
|     [Stackable] BIT DEFAULT 1, | ||||
|     [MaxStack] INT DEFAULT 99, | ||||
|     [RequiredLevel] INT DEFAULT 1, | ||||
|     [RequiredClassID] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Weapons] ( | ||||
|     [WeaponID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [ItemID] BIGINT NOT NULL UNIQUE, | ||||
|     [WeaponType] NVARCHAR(50), -- 'Sword', 'Axe', 'Bow', 'Staff', 'Dagger' | ||||
|     [MinDamage] INT DEFAULT 1, | ||||
|     [MaxDamage] INT DEFAULT 10, | ||||
|     [AttackSpeed] DECIMAL(3, 2) DEFAULT 1.00, | ||||
|     [Durability] INT DEFAULT 100, | ||||
|     [EnchantmentSlots] INT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Armor] ( | ||||
|     [ArmorID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [ItemID] BIGINT NOT NULL UNIQUE, | ||||
|     [ArmorType] NVARCHAR(50), -- 'Helmet', 'Chest', 'Legs', 'Boots', 'Gloves' | ||||
|     [DefenseValue] INT DEFAULT 1, | ||||
|     [MagicResistance] INT DEFAULT 0, | ||||
|     [Durability] INT DEFAULT 100, | ||||
|     [SetBonusID] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [CharacterInventory] ( | ||||
|     [InventoryID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [ItemID] BIGINT NOT NULL, | ||||
|     [Quantity] INT DEFAULT 1, | ||||
|     [IsEquipped] BIT DEFAULT 0, | ||||
|     [SlotPosition] INT NULL, | ||||
|     [AcquiredDate] DATETIME NOT NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Magic System Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [MagicSchools] ( | ||||
|     [SchoolID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [SchoolName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [Element] NVARCHAR(30), -- 'Fire', 'Water', 'Earth', 'Air', 'Light', 'Dark' | ||||
|     [Description] NVARCHAR(MAX) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Spells] ( | ||||
|     [SpellID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [SpellName] NVARCHAR(100) NOT NULL, | ||||
|     [SchoolID] BIGINT NOT NULL, | ||||
|     [SpellLevel] INT DEFAULT 1, | ||||
|     [ManaCost] INT DEFAULT 10, | ||||
|     [CastTime] DECIMAL(3, 1) DEFAULT 1.0, | ||||
|     [Range] INT DEFAULT 10, | ||||
|     [AreaOfEffect] INT DEFAULT 0, | ||||
|     [BaseDamage] INT DEFAULT 0, | ||||
|     [Description] NVARCHAR(MAX) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [SpellBooks] ( | ||||
|     [SpellBookID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [SpellID] BIGINT NOT NULL, | ||||
|     [DateLearned] DATE NOT NULL, | ||||
|     [MasteryLevel] INT DEFAULT 1, | ||||
|     [TimesUsed] INT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Enchantments] ( | ||||
|     [EnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [EnchantmentName] NVARCHAR(100) NOT NULL, | ||||
|     [RequiredSpellID] BIGINT NULL, | ||||
|     [BonusType] NVARCHAR(50), -- 'Damage', 'Defense', 'Speed', 'Magic' | ||||
|     [BonusValue] INT DEFAULT 1, | ||||
|     [Duration] INT NULL, -- NULL for permanent | ||||
|     [Cost] DECIMAL(10, 2) DEFAULT 100.00 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [ItemEnchantments] ( | ||||
|     [ItemEnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [ItemID] BIGINT NOT NULL, | ||||
|     [EnchantmentID] BIGINT NOT NULL, | ||||
|     [AppliedByCharacterID] BIGINT NOT NULL, | ||||
|     [AppliedDate] DATETIME NOT NULL, | ||||
|     [ExpiryDate] DATETIME NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Quest and Achievement Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [QuestLines] ( | ||||
|     [QuestLineID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [QuestLineName] NVARCHAR(200) NOT NULL, | ||||
|     [MinLevel] INT DEFAULT 1, | ||||
|     [MaxLevel] INT DEFAULT 100, | ||||
|     [TotalQuests] INT DEFAULT 1, | ||||
|     [FinalRewardItemID] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Quests] ( | ||||
|     [QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [QuestName] NVARCHAR(200) NOT NULL, | ||||
|     [QuestLineID] BIGINT NULL, | ||||
|     [QuestGiverNPCID] BIGINT NULL, | ||||
|     [RequiredLevel] INT DEFAULT 1, | ||||
|     [RequiredQuestID] BIGINT NULL, -- Prerequisite quest | ||||
|     [ObjectiveType] NVARCHAR(50), -- 'Kill', 'Collect', 'Deliver', 'Explore' | ||||
|     [ObjectiveCount] INT DEFAULT 1, | ||||
|     [RewardGold] DECIMAL(10, 2) DEFAULT 10.00, | ||||
|     [RewardExperience] INT DEFAULT 100, | ||||
|     [RewardItemID] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [CharacterQuests] ( | ||||
|     [CharacterQuestID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [QuestID] BIGINT NOT NULL, | ||||
|     [StartDate] DATETIME NOT NULL, | ||||
|     [CompletedDate] DATETIME NULL, | ||||
|     [CurrentProgress] INT DEFAULT 0, | ||||
|     [Status] NVARCHAR(20) DEFAULT 'Active' -- 'Active', 'Completed', 'Failed', 'Abandoned' | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Achievements] ( | ||||
|     [AchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [AchievementName] NVARCHAR(100) NOT NULL, | ||||
|     [Description] NVARCHAR(500), | ||||
|     [Points] INT DEFAULT 10, | ||||
|     [Category] NVARCHAR(50), | ||||
|     [RequiredCount] INT DEFAULT 1, | ||||
|     [RewardTitle] NVARCHAR(100) NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [CharacterAchievements] ( | ||||
|     [CharacterAchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [AchievementID] BIGINT NOT NULL, | ||||
|     [EarnedDate] DATETIME NOT NULL, | ||||
|     [Progress] INT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- NPC and Monster Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [NPCTypes] ( | ||||
|     [NPCTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [TypeName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [IsFriendly] BIT DEFAULT 1, | ||||
|     [CanTrade] BIT DEFAULT 0, | ||||
|     [CanGiveQuests] BIT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [NPCs] ( | ||||
|     [NPCID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [NPCName] NVARCHAR(100) NOT NULL, | ||||
|     [NPCTypeID] BIGINT NOT NULL, | ||||
|     [LocationCityID] BIGINT NOT NULL, | ||||
|     [Health] INT DEFAULT 100, | ||||
|     [Level] INT DEFAULT 1, | ||||
|     [DialogueText] NVARCHAR(MAX), | ||||
|     [RespawnTime] INT DEFAULT 300 -- seconds | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Monsters] ( | ||||
|     [MonsterID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [MonsterName] NVARCHAR(100) NOT NULL, | ||||
|     [MonsterType] NVARCHAR(50), -- 'Beast', 'Undead', 'Dragon', 'Elemental', 'Demon' | ||||
|     [Level] INT DEFAULT 1, | ||||
|     [Health] INT DEFAULT 100, | ||||
|     [Damage] INT DEFAULT 10, | ||||
|     [Defense] INT DEFAULT 5, | ||||
|     [ExperienceReward] INT DEFAULT 50, | ||||
|     [GoldDrop] DECIMAL(10, 2) DEFAULT 5.00, | ||||
|     [SpawnRegionID] BIGINT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [MonsterLoot] ( | ||||
|     [LootID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [MonsterID] BIGINT NOT NULL, | ||||
|     [ItemID] BIGINT NOT NULL, | ||||
|     [DropChance] DECIMAL(5, 2) DEFAULT 10.00, -- percentage | ||||
|     [MinQuantity] INT DEFAULT 1, | ||||
|     [MaxQuantity] INT DEFAULT 1 | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Combat and PvP Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [BattleTypes] ( | ||||
|     [BattleTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [TypeName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [MinParticipants] INT DEFAULT 2, | ||||
|     [MaxParticipants] INT DEFAULT 2, | ||||
|     [AllowTeams] BIT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Battles] ( | ||||
|     [BattleID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [BattleTypeID] BIGINT NOT NULL, | ||||
|     [StartTime] DATETIME NOT NULL, | ||||
|     [EndTime] DATETIME NULL, | ||||
|     [LocationCityID] BIGINT NOT NULL, | ||||
|     [WinnerCharacterID] BIGINT NULL, | ||||
|     [TotalDamageDealt] BIGINT DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [BattleParticipants] ( | ||||
|     [ParticipantID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [BattleID] BIGINT NOT NULL, | ||||
|     [CharacterID] BIGINT NOT NULL, | ||||
|     [TeamNumber] INT DEFAULT 0, | ||||
|     [DamageDealt] INT DEFAULT 0, | ||||
|     [DamageTaken] INT DEFAULT 0, | ||||
|     [HealingDone] INT DEFAULT 0, | ||||
|     [KillCount] INT DEFAULT 0, | ||||
|     [DeathCount] INT DEFAULT 0, | ||||
|     [FinalPlacement] INT NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Economy Tables | ||||
| -- ============================================= | ||||
|  | ||||
| CREATE TABLE [Currencies] ( | ||||
|     [CurrencyID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [CurrencyName] NVARCHAR(50) NOT NULL UNIQUE, | ||||
|     [ExchangeRate] DECIMAL(10, 4) DEFAULT 1.0000, -- relative to gold | ||||
|     [IssuingKingdomID] BIGINT NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [MarketListings] ( | ||||
|     [ListingID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [SellerCharacterID] BIGINT NOT NULL, | ||||
|     [ItemID] BIGINT NOT NULL, | ||||
|     [Quantity] INT DEFAULT 1, | ||||
|     [PricePerUnit] DECIMAL(10, 2) NOT NULL, | ||||
|     [CurrencyID] BIGINT NOT NULL, | ||||
|     [ListedDate] DATETIME NOT NULL, | ||||
|     [ExpiryDate] DATETIME NOT NULL, | ||||
|     [Status] NVARCHAR(20) DEFAULT 'Active' | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [Transactions] ( | ||||
|     [TransactionID] BIGINT IDENTITY(1,1) PRIMARY KEY, | ||||
|     [BuyerCharacterID] BIGINT NOT NULL, | ||||
|     [SellerCharacterID] BIGINT NOT NULL, | ||||
|     [ItemID] BIGINT NOT NULL, | ||||
|     [Quantity] INT DEFAULT 1, | ||||
|     [TotalPrice] DECIMAL(10, 2) NOT NULL, | ||||
|     [CurrencyID] BIGINT NOT NULL, | ||||
|     [TransactionDate] DATETIME NOT NULL | ||||
| ); | ||||
|  | ||||
| -- ============================================= | ||||
| -- Foreign Key Constraints (Without Schema Prefix) | ||||
| -- Testing the fix for single-schema foreign key parsing | ||||
| -- ============================================= | ||||
|  | ||||
| -- Kingdom Relationships | ||||
| ALTER TABLE [Regions] ADD CONSTRAINT [FK_Regions_Kingdoms]  | ||||
|     FOREIGN KEY ([KingdomID]) REFERENCES [Kingdoms]([KingdomID]); | ||||
|  | ||||
| ALTER TABLE [Cities] ADD CONSTRAINT [FK_Cities_Regions]  | ||||
|     FOREIGN KEY ([RegionID]) REFERENCES [Regions]([RegionID]); | ||||
|  | ||||
| ALTER TABLE [Castles] ADD CONSTRAINT [FK_Castles_Cities]  | ||||
|     FOREIGN KEY ([CityID]) REFERENCES [Cities]([CityID]); | ||||
|  | ||||
| -- Character Relationships | ||||
| ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Classes]  | ||||
|     FOREIGN KEY ([ClassID]) REFERENCES [CharacterClasses]([ClassID]); | ||||
|  | ||||
| ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Cities]  | ||||
|     FOREIGN KEY ([HomeCityID]) REFERENCES [Cities]([CityID]); | ||||
|  | ||||
| ALTER TABLE [CharacterSkills] ADD CONSTRAINT [FK_CharacterSkills_Classes]  | ||||
|     FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]); | ||||
|  | ||||
| ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Skills]  | ||||
|     FOREIGN KEY ([SkillID]) REFERENCES [CharacterSkills]([SkillID]); | ||||
|  | ||||
| -- Guild Relationships | ||||
| ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildTypes]  | ||||
|     FOREIGN KEY ([GuildTypeID]) REFERENCES [GuildTypes]([GuildTypeID]); | ||||
|  | ||||
| ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_Cities]  | ||||
|     FOREIGN KEY ([HeadquartersCityID]) REFERENCES [Cities]([CityID]); | ||||
|  | ||||
| ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildMaster]  | ||||
|     FOREIGN KEY ([GuildMasterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Guilds]  | ||||
|     FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]); | ||||
|  | ||||
| ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_Guilds]  | ||||
|     FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]); | ||||
|  | ||||
| ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_QuestGiver]  | ||||
|     FOREIGN KEY ([QuestGiverID]) REFERENCES [NPCs]([NPCID]); | ||||
|  | ||||
| -- Item Relationships | ||||
| ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_Categories]  | ||||
|     FOREIGN KEY ([CategoryID]) REFERENCES [ItemCategories]([CategoryID]); | ||||
|  | ||||
| ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_RequiredClass]  | ||||
|     FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]); | ||||
|  | ||||
| ALTER TABLE [Weapons] ADD CONSTRAINT [FK_Weapons_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [Armor] ADD CONSTRAINT [FK_Armor_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| -- Magic Relationships | ||||
| ALTER TABLE [Spells] ADD CONSTRAINT [FK_Spells_Schools]  | ||||
|     FOREIGN KEY ([SchoolID]) REFERENCES [MagicSchools]([SchoolID]); | ||||
|  | ||||
| ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Spells]  | ||||
|     FOREIGN KEY ([SpellID]) REFERENCES [Spells]([SpellID]); | ||||
|  | ||||
| ALTER TABLE [Enchantments] ADD CONSTRAINT [FK_Enchantments_Spells]  | ||||
|     FOREIGN KEY ([RequiredSpellID]) REFERENCES [Spells]([SpellID]); | ||||
|  | ||||
| ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Enchantments]  | ||||
|     FOREIGN KEY ([EnchantmentID]) REFERENCES [Enchantments]([EnchantmentID]); | ||||
|  | ||||
| ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Characters]  | ||||
|     FOREIGN KEY ([AppliedByCharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| -- Quest Relationships | ||||
| ALTER TABLE [QuestLines] ADD CONSTRAINT [FK_QuestLines_FinalReward]  | ||||
|     FOREIGN KEY ([FinalRewardItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestLines]  | ||||
|     FOREIGN KEY ([QuestLineID]) REFERENCES [QuestLines]([QuestLineID]); | ||||
|  | ||||
| ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestGiver]  | ||||
|     FOREIGN KEY ([QuestGiverNPCID]) REFERENCES [NPCs]([NPCID]); | ||||
|  | ||||
| ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_Prerequisites]  | ||||
|     FOREIGN KEY ([RequiredQuestID]) REFERENCES [Quests]([QuestID]); | ||||
|  | ||||
| ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_RewardItem]  | ||||
|     FOREIGN KEY ([RewardItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Quests]  | ||||
|     FOREIGN KEY ([QuestID]) REFERENCES [Quests]([QuestID]); | ||||
|  | ||||
| ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Achievements]  | ||||
|     FOREIGN KEY ([AchievementID]) REFERENCES [Achievements]([AchievementID]); | ||||
|  | ||||
| -- NPC and Monster Relationships | ||||
| ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Types]  | ||||
|     FOREIGN KEY ([NPCTypeID]) REFERENCES [NPCTypes]([NPCTypeID]); | ||||
|  | ||||
| ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Cities]  | ||||
|     FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]); | ||||
|  | ||||
| ALTER TABLE [Monsters] ADD CONSTRAINT [FK_Monsters_Regions]  | ||||
|     FOREIGN KEY ([SpawnRegionID]) REFERENCES [Regions]([RegionID]); | ||||
|  | ||||
| ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Monsters]  | ||||
|     FOREIGN KEY ([MonsterID]) REFERENCES [Monsters]([MonsterID]); | ||||
|  | ||||
| ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| -- Battle Relationships | ||||
| ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Types]  | ||||
|     FOREIGN KEY ([BattleTypeID]) REFERENCES [BattleTypes]([BattleTypeID]); | ||||
|  | ||||
| ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Cities]  | ||||
|     FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]); | ||||
|  | ||||
| ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Winner]  | ||||
|     FOREIGN KEY ([WinnerCharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Battles]  | ||||
|     FOREIGN KEY ([BattleID]) REFERENCES [Battles]([BattleID]); | ||||
|  | ||||
| ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Characters]  | ||||
|     FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| -- Economy Relationships | ||||
| ALTER TABLE [Currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]  | ||||
|     FOREIGN KEY ([IssuingKingdomID]) REFERENCES [Kingdoms]([KingdomID]); | ||||
|  | ||||
| ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Seller]  | ||||
|     FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Currency]  | ||||
|     FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]); | ||||
|  | ||||
| ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Buyer]  | ||||
|     FOREIGN KEY ([BuyerCharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Seller]  | ||||
|     FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]); | ||||
|  | ||||
| ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Items]  | ||||
|     FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]); | ||||
|  | ||||
| ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]  | ||||
|     FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]); | ||||
|         `; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // Debug: log table names to see what's parsed | ||||
|         console.log('Tables found:', result.tables.length); | ||||
|         console.log( | ||||
|             'Table names:', | ||||
|             result.tables.map((t) => t.name) | ||||
|         ); | ||||
|  | ||||
|         // Verify correct number of tables | ||||
|         expect(result.tables.length).toBe(37); // Actually 37 tables after counting | ||||
|  | ||||
|         // Verify all tables use default 'dbo' schema | ||||
|         const schemas = new Set(result.tables.map((t) => t.schema)); | ||||
|         expect(schemas.size).toBe(1); | ||||
|         expect(schemas.has('dbo')).toBe(true); | ||||
|  | ||||
|         // Verify correct number of relationships | ||||
|         console.log('Relationships found:', result.relationships.length); | ||||
|         expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed | ||||
|  | ||||
|         // Verify all relationships have valid source and target table IDs | ||||
|         const validRelationships = result.relationships.filter( | ||||
|             (r) => r.sourceTableId && r.targetTableId | ||||
|         ); | ||||
|         expect(validRelationships.length).toBe(result.relationships.length); | ||||
|  | ||||
|         // Check specific table names exist | ||||
|         const tableNames = result.tables.map((t) => t.name); | ||||
|         expect(tableNames).toContain('Kingdoms'); | ||||
|         expect(tableNames).toContain('Characters'); | ||||
|         expect(tableNames).toContain('Guilds'); | ||||
|         expect(tableNames).toContain('Items'); | ||||
|         expect(tableNames).toContain('Spells'); | ||||
|         expect(tableNames).toContain('Quests'); | ||||
|         expect(tableNames).toContain('Battles'); | ||||
|         expect(tableNames).toContain('Monsters'); | ||||
|  | ||||
|         // Verify some specific relationships exist and are properly linked | ||||
|         const characterToClass = result.relationships.find( | ||||
|             (r) => r.name === 'FK_Characters_Classes' | ||||
|         ); | ||||
|         expect(characterToClass).toBeDefined(); | ||||
|         expect(characterToClass?.sourceTable).toBe('Characters'); | ||||
|         expect(characterToClass?.targetTable).toBe('CharacterClasses'); | ||||
|         expect(characterToClass?.sourceColumn).toBe('ClassID'); | ||||
|         expect(characterToClass?.targetColumn).toBe('ClassID'); | ||||
|  | ||||
|         const guildsToCity = result.relationships.find( | ||||
|             (r) => r.name === 'FK_Guilds_Cities' | ||||
|         ); | ||||
|         expect(guildsToCity).toBeDefined(); | ||||
|         expect(guildsToCity?.sourceTable).toBe('Guilds'); | ||||
|         expect(guildsToCity?.targetTable).toBe('Cities'); | ||||
|  | ||||
|         const inventoryToItems = result.relationships.find( | ||||
|             (r) => r.name === 'FK_Inventory_Items' | ||||
|         ); | ||||
|         expect(inventoryToItems).toBeDefined(); | ||||
|         expect(inventoryToItems?.sourceTable).toBe('CharacterInventory'); | ||||
|         expect(inventoryToItems?.targetTable).toBe('Items'); | ||||
|  | ||||
|         // Check self-referencing relationship | ||||
|         const questPrerequisite = result.relationships.find( | ||||
|             (r) => r.name === 'FK_Quests_Prerequisites' | ||||
|         ); | ||||
|         expect(questPrerequisite).toBeDefined(); | ||||
|         expect(questPrerequisite?.sourceTable).toBe('Quests'); | ||||
|         expect(questPrerequisite?.targetTable).toBe('Quests'); | ||||
|  | ||||
|         // Verify table IDs are correctly linked in relationships | ||||
|         for (const rel of result.relationships) { | ||||
|             const sourceTable = result.tables.find( | ||||
|                 (t) => | ||||
|                     t.name === rel.sourceTable && t.schema === rel.sourceSchema | ||||
|             ); | ||||
|             const targetTable = result.tables.find( | ||||
|                 (t) => | ||||
|                     t.name === rel.targetTable && t.schema === rel.targetSchema | ||||
|             ); | ||||
|  | ||||
|             expect(sourceTable).toBeDefined(); | ||||
|             expect(targetTable).toBeDefined(); | ||||
|             expect(rel.sourceTableId).toBe(sourceTable?.id); | ||||
|             expect(rel.targetTableId).toBe(targetTable?.id); | ||||
|         } | ||||
|  | ||||
|         console.log('Single-schema test results:'); | ||||
|         console.log('Total tables:', result.tables.length); | ||||
|         console.log('Total relationships:', result.relationships.length); | ||||
|         console.log( | ||||
|             'All relationships properly linked:', | ||||
|             validRelationships.length === result.relationships.length | ||||
|         ); | ||||
|  | ||||
|         // Sample of relationship names for verification | ||||
|         const sampleRelationships = result.relationships | ||||
|             .slice(0, 5) | ||||
|             .map((r) => ({ | ||||
|                 name: r.name, | ||||
|                 source: `${r.sourceTable}.${r.sourceColumn}`, | ||||
|                 target: `${r.targetTable}.${r.targetColumn}`, | ||||
|             })); | ||||
|         console.log('Sample relationships:', sampleRelationships); | ||||
|     }); | ||||
| }); | ||||
| @@ -0,0 +1,93 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { fromSQLServer } from '../sqlserver'; | ||||
|  | ||||
| describe('SQL Server FK Verification', () => { | ||||
|     it('should correctly parse FKs from complex fantasy SQL', async () => { | ||||
|         const sql = `CREATE TABLE [DBO].[SpellDefinition]( | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [HASVERBALCOMP] BOOLEAN,   | ||||
|   [INCANTATION] [VARCHAR](128),   | ||||
|   [INCANTATIONFIX] BOOLEAN,   | ||||
|   [ITSCOMPONENTREL]  [VARCHAR](32), FOREIGN KEY (itscomponentrel) REFERENCES SpellComponent(SPELLID),  | ||||
|   [SHOWVISUALS] BOOLEAN,    ) ON [PRIMARY] | ||||
|  | ||||
| CREATE TABLE [DBO].[SpellComponent]( | ||||
|   [ALIAS] CHAR (50),     | ||||
|   [SPELLID]  (VARCHAR)(32),     | ||||
|   [ISOPTIONAL] BOOLEAN,   | ||||
|   [ITSPARENTCOMP]  [VARCHAR](32), FOREIGN KEY (itsparentcomp) REFERENCES SpellComponent(SPELLID),  | ||||
|   [ITSSCHOOLMETA]  [VARCHAR](32), FOREIGN KEY (itsschoolmeta) REFERENCES MagicSchool(SCHOOLID),  | ||||
|   [KEYATTR] CHAR (100),  ) ON [PRIMARY]`; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         // Verify tables | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|         expect(result.tables.map((t) => t.name).sort()).toEqual([ | ||||
|             'SpellComponent', | ||||
|             'SpellDefinition', | ||||
|         ]); | ||||
|  | ||||
|         // Verify that FKs were found (even if MagicSchool doesn't exist) | ||||
|         // The parsing should find 3 FKs initially, but linkRelationships will filter out the one to MagicSchool | ||||
|         expect(result.relationships.length).toBeGreaterThanOrEqual(2); | ||||
|  | ||||
|         // Verify specific FKs that should exist | ||||
|         const fk1 = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'SpellDefinition' && | ||||
|                 r.sourceColumn.toLowerCase() === 'itscomponentrel' && | ||||
|                 r.targetTable === 'SpellComponent' | ||||
|         ); | ||||
|         expect(fk1).toBeDefined(); | ||||
|         expect(fk1?.targetColumn).toBe('SPELLID'); | ||||
|         expect(fk1?.sourceTableId).toBeTruthy(); | ||||
|         expect(fk1?.targetTableId).toBeTruthy(); | ||||
|  | ||||
|         const fk2 = result.relationships.find( | ||||
|             (r) => | ||||
|                 r.sourceTable === 'SpellComponent' && | ||||
|                 r.sourceColumn.toLowerCase() === 'itsparentcomp' && | ||||
|                 r.targetTable === 'SpellComponent' | ||||
|         ); | ||||
|         expect(fk2).toBeDefined(); | ||||
|         expect(fk2?.targetColumn).toBe('SPELLID'); | ||||
|         expect(fk2?.sourceTableId).toBeTruthy(); | ||||
|         expect(fk2?.targetTableId).toBeTruthy(); | ||||
|  | ||||
|         // Log for debugging | ||||
|         console.log('\n=== FK Verification Results ==='); | ||||
|         console.log( | ||||
|             'Tables:', | ||||
|             result.tables.map((t) => `${t.schema}.${t.name}`) | ||||
|         ); | ||||
|         console.log('Total FKs found:', result.relationships.length); | ||||
|         result.relationships.forEach((r, i) => { | ||||
|             console.log( | ||||
|                 `FK ${i + 1}: ${r.sourceTable}.${r.sourceColumn} -> ${r.targetTable}.${r.targetColumn}` | ||||
|             ); | ||||
|             console.log(`  IDs: ${r.sourceTableId} -> ${r.targetTableId}`); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse inline FOREIGN KEY syntax correctly', async () => { | ||||
|         // Simplified test with just one FK to ensure parsing works | ||||
|         const sql = `CREATE TABLE [DBO].[WizardTower]( | ||||
|   [TOWERID] INT, | ||||
|   [MASTERKEY] [VARCHAR](32), FOREIGN KEY (MASTERKEY) REFERENCES ArcaneGuild(GUILDID), | ||||
|   [NAME] VARCHAR(100) | ||||
| ) ON [PRIMARY] | ||||
|  | ||||
| CREATE TABLE [DBO].[ArcaneGuild]( | ||||
|   [GUILDID] [VARCHAR](32), | ||||
|   [GUILDNAME] VARCHAR(100) | ||||
| ) ON [PRIMARY]`; | ||||
|  | ||||
|         const result = await fromSQLServer(sql); | ||||
|  | ||||
|         expect(result.tables).toHaveLength(2); | ||||
|         expect(result.relationships).toHaveLength(1); | ||||
|         expect(result.relationships[0].sourceColumn).toBe('MASTERKEY'); | ||||
|         expect(result.relationships[0].targetColumn).toBe('GUILDID'); | ||||
|     }); | ||||
| }); | ||||
| @@ -162,15 +162,36 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] { | ||||
|         if (match) { | ||||
|             const [ | ||||
|                 , | ||||
|                 sourceSchema = 'dbo', | ||||
|                 sourceTable, | ||||
|                 sourceSchemaOrTable, | ||||
|                 sourceTableIfSchema, | ||||
|                 constraintName, | ||||
|                 sourceColumn, | ||||
|                 targetSchema = 'dbo', | ||||
|                 targetTable, | ||||
|                 targetSchemaOrTable, | ||||
|                 targetTableIfSchema, | ||||
|                 targetColumn, | ||||
|             ] = match; | ||||
|  | ||||
|             // Handle both schema.table and just table formats | ||||
|             let sourceSchema = 'dbo'; | ||||
|             let sourceTable = ''; | ||||
|             let targetSchema = 'dbo'; | ||||
|             let targetTable = ''; | ||||
|  | ||||
|             // If second group is empty, first group is the table name | ||||
|             if (!sourceTableIfSchema) { | ||||
|                 sourceTable = sourceSchemaOrTable; | ||||
|             } else { | ||||
|                 sourceSchema = sourceSchemaOrTable; | ||||
|                 sourceTable = sourceTableIfSchema; | ||||
|             } | ||||
|  | ||||
|             if (!targetTableIfSchema) { | ||||
|                 targetTable = targetSchemaOrTable; | ||||
|             } else { | ||||
|                 targetSchema = targetSchemaOrTable; | ||||
|                 targetTable = targetTableIfSchema; | ||||
|             } | ||||
|  | ||||
|             fkData.push({ | ||||
|                 name: constraintName, | ||||
|                 sourceTable: sourceTable, | ||||
| @@ -321,6 +342,35 @@ function parseCreateTableManually( | ||||
|  | ||||
|     // Process each part (column or constraint) | ||||
|     for (const part of parts) { | ||||
|         // Handle standalone FOREIGN KEY definitions (without CONSTRAINT keyword) | ||||
|         // Format: FOREIGN KEY (column) REFERENCES Table(column) | ||||
|         if (part.match(/^\s*FOREIGN\s+KEY/i)) { | ||||
|             const fkMatch = part.match( | ||||
|                 /FOREIGN\s+KEY\s*\(([^)]+)\)\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i | ||||
|             ); | ||||
|             if (fkMatch) { | ||||
|                 const [ | ||||
|                     , | ||||
|                     sourceCol, | ||||
|                     targetSchema = 'dbo', | ||||
|                     targetTable, | ||||
|                     targetCol, | ||||
|                 ] = fkMatch; | ||||
|                 relationships.push({ | ||||
|                     name: `FK_${tableName}_${sourceCol.trim().replace(/\[|\]/g, '')}`, | ||||
|                     sourceTable: tableName, | ||||
|                     sourceSchema: schema, | ||||
|                     sourceColumn: sourceCol.trim().replace(/\[|\]/g, ''), | ||||
|                     targetTable: targetTable || targetSchema, | ||||
|                     targetSchema: targetTable ? targetSchema : 'dbo', | ||||
|                     targetColumn: targetCol.trim().replace(/\[|\]/g, ''), | ||||
|                     sourceTableId: tableId, | ||||
|                     targetTableId: '', // Will be filled later | ||||
|                 }); | ||||
|             } | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         // Handle constraint definitions | ||||
|         if (part.match(/^\s*CONSTRAINT/i)) { | ||||
|             // Parse constraints | ||||
| @@ -414,6 +464,13 @@ function parseCreateTableManually( | ||||
|             columnMatch = part.match(/^\s*(\w+)\s+(\w+)\s+([\d,\s]+)\s+(.*)$/i); | ||||
|         } | ||||
|  | ||||
|         // Handle unusual format: [COLUMN_NAME] (VARCHAR)(32) | ||||
|         if (!columnMatch) { | ||||
|             columnMatch = part.match( | ||||
|                 /^\s*\[?(\w+)\]?\s+\((\w+)\)\s*\(([\d,\s]+|max)\)(.*)$/i | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if (columnMatch) { | ||||
|             const [, colName, baseType, typeArgs, rest] = columnMatch; | ||||
|  | ||||
| @@ -425,7 +482,37 @@ function parseCreateTableManually( | ||||
|                 const inlineFkMatch = rest.match( | ||||
|                     /FOREIGN\s+KEY\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i | ||||
|                 ); | ||||
|                 if (inlineFkMatch) { | ||||
|  | ||||
|                 // Also check if there's a FOREIGN KEY after a comma with column name | ||||
|                 // Format: , FOREIGN KEY (columnname) REFERENCES Table(column) | ||||
|                 if (!inlineFkMatch && rest.includes('FOREIGN KEY')) { | ||||
|                     const fkWithColumnMatch = rest.match( | ||||
|                         /,\s*FOREIGN\s+KEY\s*\((\w+)\)\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i | ||||
|                     ); | ||||
|                     if (fkWithColumnMatch) { | ||||
|                         const [, srcCol, targetSchema, targetTable, targetCol] = | ||||
|                             fkWithColumnMatch; | ||||
|                         // Only process if srcCol matches current colName (case-insensitive) | ||||
|                         if (srcCol.toLowerCase() === colName.toLowerCase()) { | ||||
|                             // Create FK relationship | ||||
|                             relationships.push({ | ||||
|                                 name: `FK_${tableName}_${colName}`, | ||||
|                                 sourceTable: tableName, | ||||
|                                 sourceSchema: schema, | ||||
|                                 sourceColumn: colName, | ||||
|                                 targetTable: targetTable || targetSchema, | ||||
|                                 targetSchema: targetTable | ||||
|                                     ? targetSchema || 'dbo' | ||||
|                                     : 'dbo', | ||||
|                                 targetColumn: targetCol | ||||
|                                     .trim() | ||||
|                                     .replace(/\[|\]/g, ''), | ||||
|                                 sourceTableId: tableId, | ||||
|                                 targetTableId: '', // Will be filled later | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } else if (inlineFkMatch) { | ||||
|                     const [, targetSchema = 'dbo', targetTable, targetCol] = | ||||
|                         inlineFkMatch; | ||||
|                     relationships.push({ | ||||
| @@ -515,10 +602,36 @@ export async function fromSQLServer( | ||||
|     try { | ||||
|         // First, handle ALTER TABLE statements for foreign keys | ||||
|         // Split by GO or semicolon for SQL Server | ||||
|         const statements = sqlContent | ||||
|         let statements = sqlContent | ||||
|             .split(/(?:GO\s*$|;\s*$)/im) | ||||
|             .filter((stmt) => stmt.trim().length > 0); | ||||
|  | ||||
|         // Additional splitting for CREATE TABLE statements that might not be separated by semicolons | ||||
|         // If we have a statement with multiple CREATE TABLE, split them | ||||
|         const expandedStatements: string[] = []; | ||||
|         for (const stmt of statements) { | ||||
|             // Check if this statement contains multiple CREATE TABLE statements | ||||
|             if ((stmt.match(/CREATE\s+TABLE/gi) || []).length > 1) { | ||||
|                 // Split by ") ON [PRIMARY]" followed by CREATE TABLE | ||||
|                 const parts = stmt.split( | ||||
|                     /\)\s*ON\s*\[PRIMARY\]\s*(?=CREATE\s+TABLE)/gi | ||||
|                 ); | ||||
|                 for (let i = 0; i < parts.length; i++) { | ||||
|                     let part = parts[i].trim(); | ||||
|                     // Re-add ") ON [PRIMARY]" to all parts except the last (which should already have it) | ||||
|                     if (i < parts.length - 1 && part.length > 0) { | ||||
|                         part += ') ON [PRIMARY]'; | ||||
|                     } | ||||
|                     if (part.trim().length > 0) { | ||||
|                         expandedStatements.push(part); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 expandedStatements.push(stmt); | ||||
|             } | ||||
|         } | ||||
|         statements = expandedStatements; | ||||
|  | ||||
|         const alterTableStatements = statements.filter( | ||||
|             (stmt) => | ||||
|                 stmt.trim().toUpperCase().includes('ALTER TABLE') && | ||||
|   | ||||
| @@ -226,6 +226,16 @@ const updateTables = ({ | ||||
|         const targetKey = createObjectKeyFromTable(targetTable); | ||||
|         let sourceTable = sourceTablesByKey.get(targetKey); | ||||
|  | ||||
|         // If no match and target has a schema, try without schema | ||||
|         if (!sourceTable && targetTable.schema) { | ||||
|             const noSchemaKey = createObjectKeyFromTable({ | ||||
|                 ...targetTable, | ||||
|                 schema: undefined, | ||||
|             }); | ||||
|             sourceTable = sourceTablesByKey.get(noSchemaKey); | ||||
|         } | ||||
|  | ||||
|         // If still no match, try with default schema | ||||
|         if (!sourceTable && defaultDatabaseSchema) { | ||||
|             if (!targetTable.schema) { | ||||
|                 // If target table has no schema, try matching with default schema | ||||
| @@ -235,12 +245,7 @@ const updateTables = ({ | ||||
|                 }); | ||||
|                 sourceTable = sourceTablesByKey.get(defaultKey); | ||||
|             } else if (targetTable.schema === defaultDatabaseSchema) { | ||||
|                 // If target table's schema matches default, try matching without schema | ||||
|                 const noSchemaKey = createObjectKeyFromTable({ | ||||
|                     ...targetTable, | ||||
|                     schema: undefined, | ||||
|                 }); | ||||
|                 sourceTable = sourceTablesByKey.get(noSchemaKey); | ||||
|                 // Already tried without schema above | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										7273
									
								
								src/lib/dbml/dbml-export/__tests__/cases/1.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7273
									
								
								src/lib/dbml/dbml-export/__tests__/cases/1.dbml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user