Compare commits

..

1 Commits

Author SHA1 Message Date
johnnyfish
1377bd524b fix: correct foreign key direction based on relationship cardinality in SQL exports 2025-07-23 15:15:30 +03:00
117 changed files with 2967 additions and 13204 deletions

View File

@@ -1,68 +1,5 @@
# Changelog
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
### Features
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
### Bug Fixes
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)

View File

@@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="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

22
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.14.0",
"version": "1.13.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.14.0",
"version": "1.13.2",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
@@ -35,7 +35,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.8.2",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",
@@ -4603,12 +4603,12 @@
"license": "MIT"
},
"node_modules/@xyflow/react": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
"integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.66",
"@xyflow/system": "0.0.50",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -4618,18 +4618,16 @@
}
},
"node_modules/@xyflow/system": {
"version": "0.0.66",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
"integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.14.0",
"version": "1.13.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -43,7 +43,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.8.2",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",

View File

@@ -31,7 +31,6 @@ export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
className?: string;
}
export interface CodeSnippetProps {
@@ -44,8 +43,6 @@ export interface CodeSnippetProps {
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
allowCopy?: boolean;
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -59,8 +56,6 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
isComplete = true,
editorProps,
actions,
actionsTooltipSide,
allowCopy = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -134,37 +129,33 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Suspense fallback={<Spinner />}>
{isComplete ? (
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
{allowCopy ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
) : null}
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
{actions &&
actions.length > 0 &&
@@ -173,10 +164,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<TooltipTrigger asChild>
<span>
<Button
className={cn(
'h-fit p-1.5',
action.className
)}
className="h-fit p-1.5"
variant="outline"
onClick={action.onClick}
>
@@ -186,9 +174,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
<TooltipContent>
{action.label}
</TooltipContent>
</Tooltip>

View File

@@ -1,51 +0,0 @@
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import * as monaco from 'monaco-editor';
export const highlightErrorLine = ({
error,
model,
editorDecorationsCollection,
}: {
error: DBMLError;
model?: monaco.editor.ITextModel | null;
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined;
}) => {
if (!model) return;
if (!editorDecorationsCollection) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
editorDecorationsCollection?.set(decorations);
};
export const clearErrorHighlight = (
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined
) => {
if (editorDecorationsCollection) {
editorDecorationsCollection.clear();
}
};

View File

@@ -37,28 +37,18 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
const datatypePattern = dataTypesNames.join('|');
monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
keywords: ['Table', 'Ref', 'Indexes'],
datatypes: dataTypesNames,
tokenizer: {
root: [
[
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
'keyword',
],
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
[/\[.*?\]/, 'annotation'],
[/'''/, 'string', '@tripleQuoteString'],
[/".*?"/, 'string'],
[/'.*?'/, 'string'],
[/`.*?`/, 'string'],
[/[{}]/, 'delimiter'],
[/[<>]/, 'operator'],
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
],
tripleQuoteString: [
[/[^']+/, 'string'],
[/'''/, 'string', '@pop'],
[/'/, 'string'],
],
},
});
};

View File

@@ -93,8 +93,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(isOpen: boolean) => {
setOpen?.(isOpen);
setIsOpen(isOpen);
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]
);
@@ -229,7 +227,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
onSelect={() =>
handleSelect(
option.value,
matches?.map((match) => match?.toString())
matches?.map((match) => match.toString())
)
}
>

View File

@@ -1,23 +1,22 @@
import React, { type ReactNode, useCallback, useState } from 'react';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import { adjustTablePositions } from '@/lib/domain/db-table';
import {
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} 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, databaseType } =
const { tables, relationships, updateTablesState, filteredSchemas } =
useChartDB();
const { filter } = useDiagramFilter();
const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
@@ -33,18 +32,9 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
mode: 'all',
mode: 'all', // Use 'all' mode for manual reordering
});
const updatedOverlapGraph = findOverlappingTables({
@@ -79,14 +69,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
});
}, 500);
},
[
filter,
relationships,
tables,
updateTablesState,
fitView,
databaseType,
]
[filteredSchemas, relationships, tables, updateTablesState, fitView]
);
return (

View File

@@ -78,8 +78,8 @@ export interface ChartDBContext {
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
highlightedCustomType?: DBCustomType;
highlightCustomTypeId: (id?: string) => void;
filteredSchemas?: string[];
filterSchemas: (schemaIds: string[]) => void;
// General operations
updateDiagramId: (id: string) => Promise<void>;
@@ -92,10 +92,6 @@ export interface ChartDBContext {
updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>;
updateDiagramData: (
diagram: Diagram,
options?: { forceUpdateStorage?: boolean }
) => Promise<void>;
// Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -281,6 +277,11 @@ export interface ChartDBContext {
customType: Partial<DBCustomType>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Filters
hiddenTableIds?: string[];
addHiddenTableId: (tableId: string) => Promise<void>;
removeHiddenTableId: (tableId: string) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -293,7 +294,8 @@ export const chartDBContext = createContext<ChartDBContext>({
areas: [],
customTypes: [],
schemas: [],
highlightCustomTypeId: emptyFn,
filteredSchemas: [],
filterSchemas: emptyFn,
currentDiagram: {
id: '',
name: '',
@@ -311,7 +313,6 @@ export const chartDBContext = createContext<ChartDBContext>({
loadDiagramFromData: emptyFn,
clearDiagramData: emptyFn,
deleteDiagram: emptyFn,
updateDiagramData: emptyFn,
// Database type operations
updateDatabaseType: emptyFn,
@@ -376,4 +377,9 @@ export const chartDBContext = createContext<ChartDBContext>({
removeCustomType: emptyFn,
removeCustomTypes: emptyFn,
updateCustomType: emptyFn,
// Filters
hiddenTableIds: [],
addHiddenTableId: emptyFn,
removeHiddenTableId: emptyFn,
});

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { deepCopy, generateId } from '@/lib/utils';
import { randomColor } from '@/lib/colors';
@@ -17,6 +17,7 @@ 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';
@@ -28,6 +29,7 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
import { useConfig } from '@/hooks/use-config';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -38,12 +40,16 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
const dbStorage = useStorage();
let db = dbStorage;
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const {
getHiddenTablesForDiagram,
hideTableForDiagram,
unhideTableForDiagram,
} = useConfig();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
@@ -65,12 +71,9 @@ export const ChartDBProvider: React.FC<
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? []
);
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
const { events: diffEvents } = useDiff();
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
useState<string>();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
setTables((tables) =>
@@ -89,6 +92,14 @@ export const ChartDBProvider: React.FC<
diffEvents.useSubscription(diffCalculatedHandler);
// Sync hiddenTableIds with config
useEffect(() => {
if (diagramId) {
const hiddenTables = getHiddenTablesForDiagram(diagramId);
setHiddenTableIds(hiddenTables);
}
}, [diagramId, getHiddenTablesForDiagram]);
const defaultSchemaName = defaultSchemas[databaseType];
const readonly = useMemo(
@@ -126,6 +137,34 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
);
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,
@@ -1082,15 +1121,12 @@ 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: targetTableSchema,
targetSchema: sourceTable?.schema,
targetTableId,
sourceFieldId,
targetFieldId,
@@ -1495,37 +1531,22 @@ export const ChartDBProvider: React.FC<
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
);
const highlightCustomTypeId = useCallback(
(id?: string) => setHighlightedCustomTypeId(id),
[setHighlightedCustomTypeId]
);
const highlightedCustomType = useMemo(() => {
return highlightedCustomTypeId
? customTypes.find((type) => type.id === highlightedCustomTypeId)
: undefined;
}, [highlightedCustomTypeId, customTypes]);
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
useCallback(
(diagram) => {
async (diagram) => {
setDiagramId(diagram.id);
setDiagramName(diagram.name);
setDatabaseType(diagram.databaseType);
setDatabaseEdition(diagram.databaseEdition);
setTables(diagram.tables ?? []);
setRelationships(diagram.relationships ?? []);
setDependencies(diagram.dependencies ?? []);
setAreas(diagram.areas ?? []);
setCustomTypes(diagram.customTypes ?? []);
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setAreas(diagram?.areas ?? []);
setCustomTypes(diagram?.customTypes ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
setHighlightedCustomTypeId(undefined);
events.emit({ action: 'load_diagram', data: { diagram } });
resetRedoStack();
resetUndoStack();
},
[
setDiagramId,
@@ -1539,23 +1560,10 @@ export const ChartDBProvider: React.FC<
setCustomTypes,
setDiagramCreatedAt,
setDiagramUpdatedAt,
setHighlightedCustomTypeId,
events,
resetRedoStack,
resetUndoStack,
]
);
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? dbStorage : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, dbStorage, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
@@ -1719,6 +1727,29 @@ export const ChartDBProvider: React.FC<
]
);
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
async (tableId: string) => {
if (!hiddenTableIds.includes(tableId)) {
setHiddenTableIds((prev) => [...prev, tableId]);
await hideTableForDiagram(diagramId, tableId);
}
},
[hiddenTableIds, diagramId, hideTableForDiagram]
);
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
useCallback(
async (tableId: string) => {
if (hiddenTableIds.includes(tableId)) {
setHiddenTableIds((prev) =>
prev.filter((id) => id !== tableId)
);
await unhideTableForDiagram(diagramId, tableId);
}
},
[hiddenTableIds, diagramId, unhideTableForDiagram]
);
return (
<chartDBContext.Provider
value={{
@@ -1731,9 +1762,10 @@ export const ChartDBProvider: React.FC<
areas,
currentDiagram,
schemas,
filteredSchemas,
events,
readonly,
updateDiagramData,
filterSchemas,
updateDiagramId,
updateDiagramName,
loadDiagram,
@@ -1790,8 +1822,9 @@ export const ChartDBProvider: React.FC<
removeCustomType,
removeCustomTypes,
updateCustomType,
highlightCustomTypeId,
highlightedCustomType,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
}}
>
{children}

View File

@@ -8,9 +8,23 @@ export interface ConfigContext {
config?: Partial<ChartDBConfig>;
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
}) => Promise<void>;
getHiddenTablesForDiagram: (diagramId: string) => string[];
setHiddenTablesForDiagram: (
diagramId: string,
hiddenTableIds: string[]
) => Promise<void>;
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
unhideTableForDiagram: (
diagramId: string,
tableId: string
) => Promise<void>;
}
export const ConfigContext = createContext<ConfigContext>({
config: undefined,
updateConfig: emptyFn,
getHiddenTablesForDiagram: () => [],
setHiddenTablesForDiagram: emptyFn,
hideTableForDiagram: emptyFn,
unhideTableForDiagram: emptyFn,
});

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } 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] = useState<ChartDBConfig | undefined>();
const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
useEffect(() => {
const loadConfig = async () => {
@@ -44,11 +44,84 @@ 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}

View File

@@ -1,48 +0,0 @@
import type { DBSchema } from '@/lib/domain';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export interface DiagramFilterContext {
filter?: DiagramFilter;
hasActiveFilter: boolean;
schemasDisplayed: DBSchema[];
// schemas
schemaIdsFilter?: string[];
addSchemaIdsFilter: (...ids: string[]) => void;
removeSchemaIdsFilter: (...ids: string[]) => void;
clearSchemaIdsFilter: () => void;
// tables
tableIdsFilter?: string[];
addTableIdsFilter: (...ids: string[]) => void;
removeTableIdsFilter: (...ids: string[]) => void;
clearTableIdsFilter: () => void;
setTableIdsFilterEmpty: () => void;
// reset
resetFilter: () => void;
// smart filters
toggleSchemaFilter: (schemaId: string) => void;
toggleTableFilter: (tableId: string) => void;
addSchemaIfFiltered: (schemaId: string) => void;
}
export const diagramFilterContext = createContext<DiagramFilterContext>({
hasActiveFilter: false,
addSchemaIdsFilter: emptyFn,
addTableIdsFilter: emptyFn,
clearSchemaIdsFilter: emptyFn,
clearTableIdsFilter: emptyFn,
setTableIdsFilterEmpty: emptyFn,
removeSchemaIdsFilter: emptyFn,
removeTableIdsFilter: emptyFn,
resetFilter: emptyFn,
toggleSchemaFilter: emptyFn,
toggleTableFilter: emptyFn,
addSchemaIfFiltered: emptyFn,
schemasDisplayed: [],
});

View File

@@ -1,442 +0,0 @@
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 } from '@/lib/domain/diagram-filter/diagram-filter';
import { reduceFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import { useStorage } from '@/hooks/use-storage';
import { useChartDB } from '@/hooks/use-chartdb';
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
import { schemaNameToSchemaId } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { diagramId, tables, schemas, databaseType } = useChartDB();
const { getDiagramFilter, updateDiagramFilter } = useStorage();
const [filter, setFilter] = useState<DiagramFilter>({});
const allSchemasIds = useMemo(() => {
return schemas.map((schema) => schema.id);
}, [schemas]);
const allTables = useMemo(() => {
return tables.map((table) => ({
id: table.id,
schemaId: table.schema
? schemaNameToSchemaId(table.schema)
: defaultSchemas[databaseType],
schema: table.schema,
}));
}, [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;
}
const loadFilterFromStorage = async (diagramId: string) => {
if (diagramId) {
const storedFilter = await getDiagramFilter(diagramId);
setFilter(storedFilter ?? {});
}
};
setFilter({});
if (diagramId) {
loadFilterFromStorage(diagramId);
diagramIdOfLoadedFilter.current = diagramId;
}
}, [diagramId, getDiagramFilter]);
// Schema methods
const addSchemaIds: DiagramFilterContext['addSchemaIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
schemaIds: [
...new Set([...(prev.schemaIds || []), ...ids]),
],
}) satisfies DiagramFilter
);
}, []);
const removeSchemaIds: DiagramFilterContext['removeSchemaIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
schemaIds: prev.schemaIds?.filter(
(id) => !ids.includes(id)
),
}) satisfies DiagramFilter
);
}, []);
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
schemaIds: undefined,
}) satisfies DiagramFilter
);
}, []);
// Table methods
const addTableIds: DiagramFilterContext['addTableIdsFilter'] = useCallback(
(...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
tableIds: [
...new Set([...(prev.tableIds || []), ...ids]),
],
}) satisfies DiagramFilter
);
},
[]
);
const removeTableIds: DiagramFilterContext['removeTableIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
tableIds: prev.tableIds?.filter(
(id) => !ids.includes(id)
),
}) 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 = filterSchema({
schemaId,
schemaIdsFilter: currentSchemaIds,
});
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
);
});
},
[allSchemasIds, allTables]
);
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
);
});
},
[allTables]
);
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
useCallback(
(tableId: string) => {
if (!defaultSchemas[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
);
});
},
[allTables, databaseType, toggleTableFilterForNoSchema]
);
const addSchemaIfFiltered: DiagramFilterContext['addSchemaIfFiltered'] =
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 (!filter.schemaIds) {
return schemas;
}
return schemas.filter((schema) =>
filter.schemaIds?.includes(schema.id)
);
}, [filter.schemaIds, schemas]);
const value: DiagramFilterContext = {
filter,
schemaIdsFilter: filter.schemaIds,
addSchemaIdsFilter: addSchemaIds,
removeSchemaIdsFilter: removeSchemaIds,
clearSchemaIdsFilter: clearSchemaIds,
setTableIdsFilterEmpty: setTableIdsEmpty,
tableIdsFilter: filter.tableIds,
addTableIdsFilter: addTableIds,
removeTableIdsFilter: removeTableIds,
clearTableIdsFilter: clearTableIds,
resetFilter,
toggleSchemaFilter,
toggleTableFilter,
addSchemaIfFiltered,
hasActiveFilter,
schemasDisplayed,
};
return (
<diagramFilterContext.Provider value={value}>
{children}
</diagramFilterContext.Provider>
);
};

View File

@@ -1,4 +0,0 @@
import { useContext } from 'react';
import { diagramFilterContext } from './diagram-filter-context';
export const useDiagramFilter = () => useContext(diagramFilterContext);

View File

@@ -32,20 +32,14 @@ export interface DiffContext {
originalDiagram: Diagram | null;
diffMap: DiffMap;
hasDiff: boolean;
isSummaryOnly: boolean;
calculateDiff: ({
diagram,
newDiagram,
options,
}: {
diagram: Diagram;
newDiagram: Diagram;
options?: {
summaryOnly?: boolean;
};
}) => void;
resetDiff: () => void;
// table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
@@ -66,15 +60,6 @@ export interface DiffContext {
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewCharacterMaximumLength: ({
fieldId,
}: {
fieldId: string;
}) => string | null;
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
// relationship diff
checkIfNewRelationship: ({

View File

@@ -32,7 +32,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
const [fieldsChanged, setFieldsChanged] = React.useState<
Map<string, boolean>
>(new Map<string, boolean>());
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
const events = useEventEmitter<DiffEvent>();
@@ -128,7 +127,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
);
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
({ diagram, newDiagram: newDiagramArg, options }) => {
({ diagram, newDiagram: newDiagramArg }) => {
const {
diffMap: newDiffs,
changedTables: newChangedTables,
@@ -140,7 +139,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
setFieldsChanged(newChangedFields);
setNewDiagram(newDiagramArg);
setOriginalDiagram(diagram);
setIsSummaryOnly(options?.summaryOnly ?? false);
events.emit({
action: 'diff_calculated',
@@ -307,117 +305,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const getFieldNewPrimaryKey = useCallback<
DiffContext['getFieldNewPrimaryKey']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'primaryKey',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'nullable',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewCharacterMaximumLength = useCallback<
DiffContext['getFieldNewCharacterMaximumLength']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'characterMaximumLength',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as string;
}
}
return null;
},
[diffMap]
);
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'scale',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const getFieldNewPrecision = useCallback<
DiffContext['getFieldNewPrecision']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'precision',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
@@ -452,15 +339,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
setDiffMap(new Map<string, ChartDBDiff>());
setTablesChanged(new Map<string, boolean>());
setFieldsChanged(new Map<string, boolean>());
setNewDiagram(null);
setOriginalDiagram(null);
setIsSummaryOnly(false);
}, []);
return (
<diffContext.Provider
value={{
@@ -468,10 +346,8 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
originalDiagram,
diffMap,
hasDiff: diffMap.size > 0,
isSummaryOnly,
calculateDiff,
resetDiff,
// table diff
getTableNewName,
@@ -486,11 +362,6 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
checkIfNewField,
getFieldNewName,
getFieldNewType,
getFieldNewPrimaryKey,
getFieldNewNullable,
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
// relationship diff
checkIfNewRelationship,

View File

@@ -36,6 +36,10 @@ export interface LayoutContext {
hideSidePanel: () => void;
showSidePanel: () => void;
toggleSidePanel: () => void;
isSelectSchemaOpen: boolean;
openSelectSchema: () => void;
closeSelectSchema: () => void;
}
export const layoutContext = createContext<LayoutContext>({
@@ -66,4 +70,8 @@ export const layoutContext = createContext<LayoutContext>({
hideSidePanel: emptyFn,
showSidePanel: emptyFn,
toggleSidePanel: emptyFn,
isSelectSchemaOpen: false,
openSelectSchema: emptyFn,
closeSelectSchema: emptyFn,
});

View File

@@ -23,6 +23,8 @@ 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('');
@@ -86,6 +88,11 @@ 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={{
@@ -101,6 +108,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
hideSidePanel,
showSidePanel,
toggleSidePanel,
isSelectSchemaOpen,
openSelectSchema,
closeSelectSchema,
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,

View File

@@ -4,6 +4,8 @@ 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;
@@ -11,11 +13,16 @@ export interface LocalConfigContext {
scrollAction: ScrollAction;
setScrollAction: (action: ScrollAction) => void;
schemasFilter: SchemasFilter;
setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
showCardinality: boolean;
setShowCardinality: (showCardinality: boolean) => void;
showFieldAttributes: boolean;
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
hideMultiSchemaNotification: boolean;
setHideMultiSchemaNotification: (
hideMultiSchemaNotification: boolean
) => void;
githubRepoOpened: boolean;
setGithubRepoOpened: (githubRepoOpened: boolean) => void;
@@ -37,11 +44,14 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
scrollAction: 'pan',
setScrollAction: emptyFn,
schemasFilter: {},
setSchemasFilter: emptyFn,
showCardinality: true,
setShowCardinality: emptyFn,
showFieldAttributes: true,
setShowFieldAttributes: emptyFn,
hideMultiSchemaNotification: false,
setHideMultiSchemaNotification: emptyFn,
githubRepoOpened: false,
setGithubRepoOpened: emptyFn,

View File

@@ -1,12 +1,13 @@
import React, { useEffect } from 'react';
import type { ScrollAction } from './local-config-context';
import type { SchemasFilter, 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';
@@ -23,13 +24,20 @@ 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 [showCardinality, setShowCardinality] = React.useState<boolean>(
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
);
const [showFieldAttributes, setShowFieldAttributes] =
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
React.useState<boolean>(
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
'false') === 'true'
);
const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>(
@@ -63,6 +71,13 @@ 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]);
@@ -71,6 +86,10 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(scrollActionKey, scrollAction);
}, [scrollAction]);
useEffect(() => {
localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter));
}, [schemasFilter]);
useEffect(() => {
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
@@ -96,10 +115,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setTheme,
scrollAction,
setScrollAction,
schemasFilter,
setSchemasFilter,
showCardinality,
setShowCardinality,
showFieldAttributes,
setShowFieldAttributes,
hideMultiSchemaNotification,
setHideMultiSchemaNotification,
setGithubRepoOpened,
githubRepoOpened,
starUsDialogLastOpen,

View File

@@ -7,21 +7,12 @@ 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?: {
@@ -141,10 +132,6 @@ export const storageInitialValue: StorageContext = {
getConfig: emptyFn,
updateConfig: emptyFn,
getDiagramFilter: emptyFn,
updateDiagramFilter: emptyFn,
deleteDiagramFilter: emptyFn,
addDiagram: emptyFn,
listDiagrams: emptyFn,
getDiagram: emptyFn,

View File

@@ -10,7 +10,6 @@ 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,
@@ -45,10 +44,6 @@ 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:
@@ -195,27 +190,6 @@ 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);
@@ -243,32 +217,6 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
[db]
);
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
async (diagramId: string): Promise<DiagramFilter | undefined> => {
return await db.diagram_filters.get({ diagramId });
},
[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({
@@ -808,9 +756,6 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteCustomType,
listCustomTypes,
deleteDiagramCustomTypes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
}}
>
{children}

View File

@@ -61,7 +61,6 @@ export const SelectTables: React.FC<SelectTablesProps> = ({
const [showTables, setShowTables] = useState(true);
const [showViews, setShowViews] = useState(false);
const { t } = useTranslation();
const [isImporting, setIsImporting] = useState(false);
// Prepare all tables and views with their metadata
const allTables = useMemo(() => {
@@ -259,37 +258,22 @@ export const SelectTables: React.FC<SelectTablesProps> = ({
setSelectedTables(new Set());
}, []);
const handleConfirm = useCallback(async () => {
if (isImporting) {
return;
}
const handleConfirm = useCallback(() => {
const selectedTableObjects: SelectedTable[] = Array.from(selectedTables)
.map((key): SelectedTable | null => {
const table = allTables.find((t) => t.key === key);
if (!table) return null;
setIsImporting(true);
return {
schema: table.schema,
table: table.tableName,
type: table.type,
} satisfies SelectedTable;
})
.filter((t): t is SelectedTable => t !== null);
try {
const selectedTableObjects: SelectedTable[] = Array.from(
selectedTables
)
.map((key): SelectedTable | null => {
const table = allTables.find((t) => t.key === key);
if (!table) return null;
return {
schema: table.schema,
table: table.tableName,
type: table.type,
} satisfies SelectedTable;
})
.filter((t): t is SelectedTable => t !== null);
await onImport({
selectedTables: selectedTableObjects,
databaseMetadata,
});
} finally {
setIsImporting(false);
}
}, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
onImport({ selectedTables: selectedTableObjects, databaseMetadata });
}, [selectedTables, allTables, onImport, databaseMetadata]);
const { isMd: isDesktop } = useBreakpoint('md');
@@ -651,29 +635,27 @@ export const SelectTables: React.FC<SelectTablesProps> = ({
</div>
{isDesktop ? renderPagination() : null}
</DialogInternalContent>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
<Button
type="button"
variant="secondary"
onClick={onBack}
disabled={isImporting}
>
<DialogFooter
// className={cn(
// 'gap-2',
// isDesktop
// ? 'flex items-center justify-between'
// : 'flex flex-col'
// )}
className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0"
>
{/* Desktop layout */}
<Button type="button" variant="secondary" onClick={onBack}>
{t('new_diagram_dialog.back')}
</Button>
<Button
onClick={handleConfirm}
disabled={selectedTables.size === 0 || isImporting}
disabled={selectedTables.size === 0}
className="bg-pink-500 text-white hover:bg-pink-600"
>
{isImporting ? (
<>
<Spinner className="mr-2 size-4 text-white" />
Importing...
</>
) : (
`Import ${selectedTables.size} Tables`
)}
Import {selectedTables.size} Tables
</Button>
{!isDesktop ? renderPagination() : null}

View File

@@ -20,18 +20,12 @@ import {
} from '@/lib/data/export-metadata/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;
@@ -42,8 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
targetDatabaseType,
}) => {
const { closeExportSQLDialog } = useDialog();
const { currentDiagram } = useChartDB();
const { filter } = useDiagramFilter();
const { currentDiagram, filteredSchemas } = useChartDB();
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
@@ -55,16 +48,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
@@ -76,20 +60,11 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
sourceTable &&
targetTable &&
filterRelationship({
tableA: {
id: sourceTable.id,
schema: sourceTable.schema,
},
tableB: {
id: targetTable.id,
schema: targetTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
shouldShowTablesBySchemaFilter(
sourceTable,
filteredSchemas
) &&
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
@@ -102,20 +77,11 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
table &&
dependentTable &&
filterDependency({
tableA: {
id: table.id,
schema: table.schema,
},
tableB: {
id: dependentTable.id,
schema: dependentTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
shouldShowTablesBySchemaFilter(
dependentTable,
filteredSchemas
)
);
}),
};
@@ -135,7 +101,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram, filter]);
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
useEffect(() => {
if (!dialog.open) {

View File

@@ -5,7 +5,7 @@ import React, {
Suspense,
useRef,
} from 'react';
import type * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -36,11 +36,45 @@ import type { DBTable } from '@/lib/domain/db-table';
import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import {
clearErrorHighlight,
highlightErrorLine,
} from '@/components/code-snippet/dbml/utils';
interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
@@ -116,8 +150,39 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}, [reorder, reorderTables]);
const highlightErrorLine = useCallback((error: DBMLError) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
clearErrorHighlight(decorationsCollection.current);
decorationsCollection.current?.clear();
}, []);
const validateDBML = useCallback(
@@ -140,12 +205,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})`
);
highlightErrorLine({
error: parsedError,
model: editorRef.current?.getModel(),
editorDecorationsCollection:
decorationsCollection.current,
});
highlightErrorLine(parsedError);
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
@@ -153,7 +213,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}
},
[clearDecorations, t]
[clearDecorations, highlightErrorLine, t]
);
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -17,24 +17,11 @@ import type { DBSchema } from '@/lib/domain/db-schema';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/input/input';
import { Separator } from '@/components/separator/separator';
import { Group, SquarePlus } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { Label } from '@/components/label/label';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
export interface TableSchemaDialogProps extends BaseDialogProps {
table?: DBTable;
schemas: DBSchema[];
onConfirm: ({ schema }: { schema: DBSchema }) => void;
allowSchemaCreation?: boolean;
}
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
@@ -42,33 +29,13 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
table,
schemas,
onConfirm,
allowSchemaCreation = false,
}) => {
const { t } = useTranslation();
const { databaseType } = useChartDB();
const { addSchemaIfFiltered } = useDiagramFilter();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
const [selectedSchemaId, setSelectedSchemaId] = React.useState<string>(
table?.schema
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
);
const allowSchemaSelection = useMemo(
() => schemas && schemas.length > 0,
[schemas]
);
const defaultSchemaName = useMemo(
() => defaultSchemas?.[databaseType],
[databaseType]
);
const [isCreatingNew, setIsCreatingNew] =
useState<boolean>(!allowSchemaSelection);
const [newSchemaName, setNewSchemaName] = useState<string>(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
useEffect(() => {
if (!dialog.open) return;
@@ -77,52 +44,15 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
);
setIsCreatingNew(!allowSchemaSelection);
setNewSchemaName(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
}, [
defaultSchemaName,
dialog.open,
schemas,
table?.schema,
allowSchemaSelection,
allowSchemaCreation,
]);
}, [dialog.open, schemas, table?.schema]);
const { closeTableSchemaDialog } = useDialog();
const handleConfirm = useCallback(() => {
let createdSchemaId: string;
if (isCreatingNew && newSchemaName.trim()) {
const newSchema: DBSchema = {
id: schemaNameToSchemaId(newSchemaName.trim()),
name: newSchemaName.trim(),
tableCount: 0,
};
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
createdSchemaId = newSchema.id;
onConfirm({ schema: newSchema });
} else {
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
createdSchemaId = schema.id;
onConfirm({ schema });
}
addSchemaIfFiltered(createdSchemaId);
}, [
onConfirm,
selectedSchemaId,
schemas,
isCreatingNew,
newSchemaName,
addSchemaIfFiltered,
]);
onConfirm({ schema });
}, [onConfirm, selectedSchemaId, schemas]);
const schemaOptions: SelectBoxOption[] = useMemo(
() =>
@@ -133,25 +63,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
[schemas]
);
const renderSwitchCreateOrSelectButton = useCallback(
() => (
<Button
variant="outline"
className="w-full justify-start"
onClick={() => setIsCreatingNew(!isCreatingNew)}
disabled={!allowSchemaSelection || !allowSchemaCreation}
>
{!isCreatingNew ? (
<SquarePlus className="mr-2 size-4 " />
) : (
<Group className="mr-2 size-4 " />
)}
{isCreatingNew ? 'Select existing schema' : 'Create new schema'}
</Button>
),
[isCreatingNew, allowSchemaSelection, allowSchemaCreation]
);
return (
<Dialog
{...dialog}
@@ -159,106 +70,48 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
if (!open) {
closeTableSchemaDialog();
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.title')
: table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
{table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
</DialogTitle>
<DialogDescription>
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.description')
: table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
{table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
{!isCreatingNew ? (
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchemaId}
onChange={(value) =>
setSelectedSchemaId(value as string)
}
/>
) : (
<div className="flex flex-col gap-2">
{allowSchemaCreation &&
!allowSchemaSelection ? (
<Label htmlFor="new-schema-name">
Schema Name
</Label>
) : null}
<Input
id="new-schema-name"
value={newSchemaName}
onChange={(e) =>
setNewSchemaName(e.target.value)
}
placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
autoFocus
/>
</div>
)}
{allowSchemaCreation && allowSchemaSelection ? (
<>
<div className="relative">
<Separator className="my-2" />
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
or
</span>
</div>
{allowSchemaSelection ? (
renderSwitchCreateOrSelectButton()
) : (
<Tooltip>
<TooltipTrigger asChild>
<span>
{renderSwitchCreateOrSelectButton()}
</span>
</TooltipTrigger>
<TooltipContent>
<p>No existing schemas available</p>
</TooltipContent>
</Tooltip>
)}
</>
) : null}
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchemaId}
onChange={(value) =>
setSelectedSchemaId(value as string)
}
/>
</div>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{isCreatingNew
? t('create_table_schema_dialog.cancel')
: table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
{table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button
onClick={handleConfirm}
disabled={isCreatingNew && !newSchemaName.trim()}
>
{isCreatingNew
? t('create_table_schema_dialog.create')
: table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
<Button onClick={handleConfirm}>
{table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
</Button>
</DialogClose>
</DialogFooter>

View File

@@ -83,7 +83,6 @@
}
body {
@apply bg-background text-foreground;
overscroll-behavior-x: none;
}
.text-editable {
@@ -155,29 +154,3 @@
background-size: 650%;
}
}
/* Edit button emphasis animation */
@keyframes dbml_edit-button-emphasis {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
background-color: rgba(59, 130, 246, 0);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0.1);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0);
}
}
.dbml-edit-button-emphasis {
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
animation-iteration-count: 1;
position: relative;
z-index: 10;
}

View File

@@ -23,25 +23,23 @@ import { bn, bnMetadata } from './locales/bn';
import { gu, guMetadata } from './locales/gu';
import { vi, viMetadata } from './locales/vi';
import { ar, arMetadata } from './locales/ar';
import { hr, hrMetadata } from './locales/hr';
export const languages: LanguageMetadata[] = [
enMetadata,
esMetadata,
frMetadata,
deMetadata,
esMetadata,
ukMetadata,
ruMetadata,
trMetadata,
hrMetadata,
pt_BRMetadata,
hiMetadata,
jaMetadata,
ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata,
zh_TWMetadata,
neMetadata,
mrMetadata,
trMetadata,
id_IDMetadata,
teMetadata,
bnMetadata,
@@ -72,7 +70,6 @@ const resources = {
gu,
vi,
ar,
hr,
};
i18n.use(LanguageDetector)

View File

@@ -26,8 +26,6 @@ export const ar: LanguageTranslation = {
hide_sidebar: 'إخفاء الشريط الجانبي',
hide_cardinality: 'إخفاء الكاردينالية',
show_cardinality: 'إظهار الكاردينالية',
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
@@ -72,6 +70,15 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
},
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
// TODO: Translate
show_me: 'Show me',
none: 'لا شيء',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'فشل النسخ',
@@ -106,6 +113,10 @@ export const ar: LanguageTranslation = {
copied: '!تم النسخ',
side_panel: {
schema: ':المخطط',
filter_by_schema: 'تصفية حسب المخطط',
search_schema: '...بحث في المخطط',
no_schemas_found: '.لم يتم العثور على مخططات',
view_all_options: '...عرض جميع الخيارات',
tables_section: {
tables: 'الجداول',
@@ -140,8 +151,6 @@ export const ar: LanguageTranslation = {
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
precision: 'الدقة',
scale: 'النطاق',
default_value: 'Default Value',
no_default: 'No default',
},
@@ -244,12 +253,9 @@ export const ar: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,9 +273,6 @@ export const ar: LanguageTranslation = {
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
// TODO: Translate
filter: 'Filter Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
},
new_diagram_dialog: {
@@ -401,13 +404,6 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
confirm: 'تغيير',
},
create_table_schema_dialog: {
title: 'إنشاء مخطط جديد',
description:
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
create: 'إنشاء',
cancel: 'إلغاء',
},
star_us_dialog: {
title: '!ساعدنا على التحسن',

View File

@@ -26,8 +26,6 @@ export const bn: LanguageTranslation = {
hide_sidebar: 'সাইডবার লুকান',
hide_cardinality: 'কার্ডিনালিটি লুকান',
show_cardinality: 'কার্ডিনালিটি দেখান',
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
@@ -73,6 +71,15 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
},
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'কিছুই না',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'কপি ব্যর্থ হয়েছে',
@@ -107,6 +114,10 @@ export const bn: LanguageTranslation = {
copied: 'অনুলিপি সম্পন্ন!',
side_panel: {
schema: 'স্কিমা:',
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
search_schema: 'স্কিমা খুঁজুন...',
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
view_all_options: 'সমস্ত বিকল্প দেখুন...',
tables_section: {
tables: 'টেবিল',
@@ -144,8 +155,6 @@ export const bn: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'নির্ভুলতা',
scale: 'স্কেল',
},
index_actions: {
title: 'ইনডেক্স কর্ম',
@@ -245,12 +254,9 @@ export const bn: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,11 +272,7 @@ export const bn: LanguageTranslation = {
redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
},
@@ -403,13 +405,6 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
confirm: 'পরিবর্তন করুন',
},
create_table_schema_dialog: {
title: 'নতুন স্কিমা তৈরি করুন',
description:
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
create: 'তৈরি করুন',
cancel: 'বাতিল করুন',
},
star_us_dialog: {
title: 'আমাদের উন্নত করতে সাহায্য করুন!',

View File

@@ -26,8 +26,6 @@ export const de: LanguageTranslation = {
hide_sidebar: 'Seitenleiste ausblenden',
hide_cardinality: 'Kardinalität ausblenden',
show_cardinality: 'Kardinalität anzeigen',
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen',
@@ -73,6 +71,15 @@ export const de: LanguageTranslation = {
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',
@@ -108,6 +115,10 @@ 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',
@@ -145,8 +156,6 @@ export const de: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Präzision',
scale: 'Skalierung',
},
index_actions: {
title: 'Indexattribute',
@@ -247,12 +256,9 @@ export const de: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -267,11 +273,6 @@ export const de: LanguageTranslation = {
undo: 'Rückgängig',
redo: 'Wiederholen',
reorder_diagram: 'Diagramm neu anordnen',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
// TODO: Translate
filter: 'Filter Tables',
@@ -407,13 +408,6 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
confirm: 'Ändern',
},
create_table_schema_dialog: {
title: 'Neues Schema erstellen',
description:
'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
create: 'Erstellen',
cancel: 'Abbrechen',
},
star_us_dialog: {
title: 'Hilf uns, uns zu verbessern!',

View File

@@ -26,8 +26,6 @@ export const en = {
hide_sidebar: 'Hide Sidebar',
hide_cardinality: 'Hide Cardinality',
show_cardinality: 'Show Cardinality',
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll',
theme: 'Theme',
show_dependencies: 'Show Dependencies',
@@ -71,6 +69,14 @@ export const en = {
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',
@@ -105,6 +111,10 @@ 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',
@@ -133,8 +143,6 @@ export const en = {
title: 'Field Attributes',
unique: 'Unique',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
comments: 'Comments',
no_comments: 'No comments',
default_value: 'Default Value',
@@ -238,11 +246,8 @@ export const en = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
clear_field_highlight: 'Clear Highlight',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
@@ -259,9 +264,6 @@ export const en = {
redo: 'Redo',
reorder_diagram: 'Reorder Diagram',
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
},
@@ -394,14 +396,6 @@ export const en = {
confirm: 'Change',
},
create_table_schema_dialog: {
title: 'Create New Schema',
description:
'No schemas exist yet. Create your first schema to organize your tables.',
create: 'Create',
cancel: 'Cancel',
},
star_us_dialog: {
title: 'Help us improve!',
description:

View File

@@ -24,8 +24,6 @@ export const es: LanguageTranslation = {
view: 'Ver',
hide_cardinality: 'Ocultar Cardinalidad',
show_cardinality: 'Mostrar Cardinalidad',
show_field_attributes: 'Mostrar Atributos de Campo',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
@@ -106,6 +104,10 @@ 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',
@@ -143,8 +145,6 @@ export const es: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precisión',
scale: 'Escala',
},
index_actions: {
title: 'Atributos del Índice',
@@ -244,12 +244,9 @@ export const es: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,10 +261,6 @@ export const es: LanguageTranslation = {
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
// TODO: Translate
filter: 'Filter Tables',
@@ -404,13 +397,6 @@ export const es: LanguageTranslation = {
cancel: 'Cancelar',
confirm: 'Cambiar',
},
create_table_schema_dialog: {
title: 'Crear Nuevo Esquema',
description:
'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
create: 'Crear',
cancel: 'Cancelar',
},
star_us_dialog: {
title: '¡Ayúdanos a mejorar!',
@@ -420,6 +406,14 @@ 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',

View File

@@ -26,8 +26,6 @@ export const fr: LanguageTranslation = {
hide_sidebar: 'Cacher la Barre Latérale',
hide_cardinality: 'Cacher la Cardinalité',
show_cardinality: 'Afficher la Cardinalité',
hide_field_attributes: 'Masquer les Attributs de Champ',
show_field_attributes: 'Afficher les Attributs de Champ',
zoom_on_scroll: 'Zoom sur le Défilement',
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
@@ -105,6 +103,10 @@ 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',
@@ -141,8 +143,6 @@ export const fr: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Précision',
scale: 'Échelle',
},
index_actions: {
title: "Attributs de l'Index",
@@ -242,12 +242,9 @@ export const fr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -262,10 +259,6 @@ export const fr: LanguageTranslation = {
undo: 'Annuler',
redo: 'Rétablir',
reorder_diagram: 'Réorganiser le Diagramme',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Surligner les tables chevauchées',
// TODO: Translate
filter: 'Filter Tables',
@@ -353,6 +346,15 @@ 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:
@@ -375,13 +377,6 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
confirm: 'Modifier',
},
create_table_schema_dialog: {
title: 'Créer un Nouveau Schéma',
description:
"Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
create: 'Créer',
cancel: 'Annuler',
},
create_relationship_dialog: {
title: 'Créer une Relation',

View File

@@ -26,8 +26,6 @@ export const gu: LanguageTranslation = {
hide_sidebar: 'સાઇડબાર છુપાવો',
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
@@ -73,6 +71,15 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો',
},
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'નકલ નિષ્ફળ',
@@ -107,6 +114,10 @@ export const gu: LanguageTranslation = {
copied: 'નકલ થયું!',
side_panel: {
schema: 'સ્કીમા:',
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
search_schema: 'સ્કીમા શોધો...',
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
view_all_options: 'બધા વિકલ્પો જુઓ...',
tables_section: {
tables: 'ટેબલ્સ',
@@ -145,8 +156,6 @@ export const gu: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'ચોકસાઈ',
scale: 'માપ',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',
@@ -246,12 +255,9 @@ export const gu: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,10 +272,6 @@ export const gu: LanguageTranslation = {
undo: 'અનડુ',
redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
// TODO: Translate
filter: 'Filter Tables',
@@ -404,14 +406,6 @@ export const gu: LanguageTranslation = {
confirm: 'બદલો',
},
create_table_schema_dialog: {
title: 'નવું સ્કીમા બનાવો',
description:
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
create: 'બનાવો',
cancel: 'રદ કરો',
},
star_us_dialog: {
title: 'અમને સુધારવામાં મદદ કરો!',
description:

View File

@@ -26,8 +26,6 @@ export const hi: LanguageTranslation = {
hide_sidebar: 'साइडबार छिपाएँ',
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
@@ -72,6 +70,15 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी असफल',
@@ -107,6 +114,10 @@ export const hi: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
search_schema: 'स्कीमा खोजें...',
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
view_all_options: 'सभी विकल्प देखें...',
tables_section: {
tables: 'तालिकाएँ',
@@ -144,8 +155,6 @@ export const hi: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',
@@ -246,12 +255,9 @@ export const hi: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,10 +272,6 @@ export const hi: LanguageTranslation = {
undo: 'पूर्ववत करें',
redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
// TODO: Translate
filter: 'Filter Tables',
@@ -407,14 +409,6 @@ export const hi: LanguageTranslation = {
confirm: 'बदलें',
},
create_table_schema_dialog: {
title: 'नया स्कीमा बनाएं',
description:
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
create: 'बनाएं',
cancel: 'रद्द करें',
},
star_us_dialog: {
title: 'हमें सुधारने में मदद करें!',
description:

View File

@@ -1,491 +0,0 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hr: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'Datoteka',
new: 'Nova',
open: 'Otvori',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
exit: 'Izađi',
},
edit: {
edit: 'Uredi',
undo: 'Poništi',
redo: 'Ponovi',
clear: 'Očisti',
},
view: {
view: 'Prikaz',
show_sidebar: 'Prikaži bočnu traku',
hide_sidebar: 'Sakrij bočnu traku',
hide_cardinality: 'Sakrij kardinalnost',
show_cardinality: 'Prikaži kardinalnost',
hide_field_attributes: 'Sakrij atribute polja',
show_field_attributes: 'Prikaži atribute polja',
zoom_on_scroll: 'Zumiranje pri skrolanju',
theme: 'Tema',
show_dependencies: 'Prikaži ovisnosti',
hide_dependencies: 'Sakrij ovisnosti',
show_minimap: 'Prikaži mini kartu',
hide_minimap: 'Sakrij mini kartu',
},
backup: {
backup: 'Sigurnosna kopija',
export_diagram: 'Izvezi dijagram',
restore_diagram: 'Vrati dijagram',
},
help: {
help: 'Pomoć',
docs_website: 'Dokumentacija',
join_discord: 'Pridružite nam se na Discordu',
},
},
delete_diagram_alert: {
title: 'Izbriši dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati dijagram.',
cancel: 'Odustani',
delete: 'Izbriši',
},
clear_diagram_alert: {
title: 'Očisti dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve podatke u dijagramu.',
cancel: 'Odustani',
clear: 'Očisti',
},
reorder_diagram_alert: {
title: 'Preuredi dijagram',
description:
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
reorder: 'Preuredi',
cancel: 'Odustani',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopiranje neuspješno',
description: 'Međuspremnik nije podržan.',
},
failed: {
title: 'Kopiranje neuspješno',
description: 'Nešto je pošlo po zlu. Molimo pokušajte ponovno.',
},
},
theme: {
system: 'Sustav',
light: 'Svijetla',
dark: 'Tamna',
},
zoom: {
on: 'Uključeno',
off: 'Isključeno',
},
last_saved: 'Zadnje spremljeno',
saved: 'Spremljeno',
loading_diagram: 'Učitavanje dijagrama...',
deselect_all: 'Odznači sve',
select_all: 'Označi sve',
clear: 'Očisti',
show_more: 'Prikaži više',
show_less: 'Prikaži manje',
copy_to_clipboard: 'Kopiraj u međuspremnik',
copied: 'Kopirano!',
side_panel: {
view_all_options: 'Prikaži sve opcije...',
tables_section: {
tables: 'Tablice',
add_table: 'Dodaj tablicu',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
clear: 'Očisti filter',
no_results:
'Nema pronađenih tablica koje odgovaraju vašem filteru.',
show_list: 'Prikaži popis tablica',
show_dbml: 'Prikaži DBML uređivač',
table: {
fields: 'Polja',
nullable: 'Može biti null?',
primary_key: 'Primarni ključ',
indexes: 'Indeksi',
comments: 'Komentari',
no_comments: 'Nema komentara',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
index_select_fields: 'Odaberi polja',
no_types_found: 'Nema pronađenih tipova',
field_name: 'Naziv',
field_type: 'Tip',
field_actions: {
title: 'Atributi polja',
unique: 'Jedinstven',
character_length: 'Maksimalna dužina',
precision: 'Preciznost',
scale: 'Skala',
comments: 'Komentari',
no_comments: 'Nema komentara',
default_value: 'Zadana vrijednost',
no_default: 'Nema zadane vrijednosti',
delete_field: 'Izbriši polje',
},
index_actions: {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
delete_index: 'Izbriši indeks',
},
table_actions: {
title: 'Radnje nad tablicom',
change_schema: 'Promijeni shemu',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
},
},
empty_state: {
title: 'Nema tablica',
description: 'Stvorite tablicu za početak',
},
},
relationships_section: {
relationships: 'Veze',
filter: 'Filtriraj',
add_relationship: 'Dodaj vezu',
collapse: 'Sažmi sve',
relationship: {
primary: 'Primarna tablica',
foreign: 'Referentna tablica',
cardinality: 'Kardinalnost',
delete_relationship: 'Izbriši',
relationship_actions: {
title: 'Radnje',
delete_relationship: 'Izbriši',
},
},
empty_state: {
title: 'Nema veza',
description: 'Stvorite vezu za povezivanje tablica',
},
},
dependencies_section: {
dependencies: 'Ovisnosti',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
dependency: {
table: 'Tablica',
dependent_table: 'Ovisni pogled',
delete_dependency: 'Izbriši',
dependency_actions: {
title: 'Radnje',
delete_dependency: 'Izbriši',
},
},
empty_state: {
title: 'Nema ovisnosti',
description: 'Stvorite pogled za početak',
},
},
areas_section: {
areas: 'Područja',
add_area: 'Dodaj područje',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih područja koja odgovaraju vašem filteru.',
area: {
area_actions: {
title: 'Radnje nad područjem',
edit_name: 'Uredi naziv',
delete_area: 'Izbriši područje',
},
},
empty_state: {
title: 'Nema područja',
description: 'Stvorite područje za početak',
},
},
custom_types_section: {
custom_types: 'Prilagođeni tipovi',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.',
empty_state: {
title: 'Nema prilagođenih tipova',
description:
'Prilagođeni tipovi će se pojaviti ovdje kada budu dostupni u vašoj bazi podataka',
},
custom_type: {
kind: 'Vrsta',
enum_values: 'Enum vrijednosti',
composite_fields: 'Polja',
no_fields: 'Nema definiranih polja',
field_name_placeholder: 'Naziv polja',
field_type_placeholder: 'Odaberi tip',
add_field: 'Dodaj polje',
no_fields_tooltip:
'Nema definiranih polja za ovaj prilagođeni tip',
custom_type_actions: {
title: 'Radnje',
highlight_fields: 'Istakni polja',
clear_field_highlight: 'Ukloni isticanje',
delete_custom_type: 'Izbriši',
},
delete_custom_type: 'Izbriši tip',
},
},
},
toolbar: {
zoom_in: 'Uvećaj',
zoom_out: 'Smanji',
save: 'Spremi',
show_all: 'Prikaži sve',
undo: 'Poništi',
redo: 'Ponovi',
reorder_diagram: 'Preuredi dijagram',
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
custom_type_highlight_tooltip:
'Isticanje "{{typeName}}" - Kliknite za uklanjanje',
filter: 'Filtriraj tablice',
},
new_diagram_dialog: {
database_selection: {
title: 'Koja je vaša baza podataka?',
description:
'Svaka baza podataka ima svoje jedinstvene značajke i mogućnosti.',
check_examples_long: 'Pogledaj primjere',
check_examples_short: 'Primjeri',
},
import_database: {
title: 'Uvezite svoju bazu podataka',
database_edition: 'Verzija baze podataka:',
step_1: 'Pokrenite ovu skriptu u svojoj bazi podataka:',
step_2: 'Zalijepite rezultat skripte u ovaj dio →',
script_results_placeholder: 'Rezultati skripte ovdje...',
ssms_instructions: {
button_text: 'SSMS upute',
title: 'Upute',
step_1: 'Idite na Tools > Options > Query Results > SQL Server.',
step_2: 'Ako koristite "Results to Grid," promijenite Maximum Characters Retrieved za Non-XML podatke (postavite na 9999999).',
},
instructions_link: 'Trebate pomoć? Pogledajte kako',
check_script_result: 'Provjeri rezultat skripte',
},
cancel: 'Odustani',
import_from_file: 'Uvezi iz datoteke',
back: 'Natrag',
empty_diagram: 'Prazan dijagram',
continue: 'Nastavi',
import: 'Uvezi',
},
open_diagram_dialog: {
title: 'Otvori dijagram',
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
table_columns: {
name: 'Naziv',
created_at: 'Stvoreno',
last_modified: 'Zadnje izmijenjeno',
tables_count: 'Tablice',
},
cancel: 'Odustani',
open: 'Otvori',
},
export_sql_dialog: {
title: 'Izvezi SQL',
description:
'Izvezite shemu vašeg dijagrama u {{databaseType}} skriptu',
close: 'Zatvori',
loading: {
text: 'AI generira SQL za {{databaseType}}...',
description: 'Ovo bi trebalo potrajati do 30 sekundi.',
},
error: {
message:
'Greška pri generiranju SQL skripte. Molimo pokušajte ponovno kasnije ili <0>kontaktirajte nas</0>.',
description:
'Slobodno koristite svoj OPENAI_TOKEN, pogledajte priručnik <0>ovdje</0>.',
},
},
create_relationship_dialog: {
title: 'Kreiraj vezu',
primary_table: 'Primarna tablica',
primary_field: 'Primarno polje',
referenced_table: 'Referentna tablica',
referenced_field: 'Referentno polje',
primary_table_placeholder: 'Odaberi tablicu',
primary_field_placeholder: 'Odaberi polje',
referenced_table_placeholder: 'Odaberi tablicu',
referenced_field_placeholder: 'Odaberi polje',
no_tables_found: 'Nema pronađenih tablica',
no_fields_found: 'Nema pronađenih polja',
create: 'Kreiraj',
cancel: 'Odustani',
},
import_database_dialog: {
title: 'Uvezi u trenutni dijagram',
override_alert: {
title: 'Uvezi bazu podataka',
content: {
alert: 'Uvoz ovog dijagrama će utjecati na postojeće tablice i veze.',
new_tables:
'<bold>{{newTablesNumber}}</bold> novih tablica će biti dodano.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> novih veza će biti stvoreno.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> tablica će biti prepisano.',
proceed: 'Želite li nastaviti?',
},
import: 'Uvezi',
cancel: 'Odustani',
},
},
export_image_dialog: {
title: 'Izvezi sliku',
description: 'Odaberite faktor veličine za izvoz:',
scale_1x: '1x Obično',
scale_2x: '2x (Preporučeno)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Odustani',
export: 'Izvezi',
advanced_options: 'Napredne opcije',
pattern: 'Uključi pozadinski uzorak',
pattern_description: 'Dodaj suptilni mrežni uzorak u pozadinu.',
transparent: 'Prozirna pozadina',
transparent_description: 'Ukloni boju pozadine iz slike.',
},
new_table_schema_dialog: {
title: 'Odaberi shemu',
description:
'Trenutno je prikazano više shema. Odaberite jednu za novu tablicu.',
cancel: 'Odustani',
confirm: 'Potvrdi',
},
update_table_schema_dialog: {
title: 'Promijeni shemu',
description: 'Ažuriraj shemu tablice "{{tableName}}"',
cancel: 'Odustani',
confirm: 'Promijeni',
},
create_table_schema_dialog: {
title: 'Stvori novu shemu',
description:
'Još ne postoje sheme. Stvorite svoju prvu shemu za organiziranje tablica.',
create: 'Stvori',
cancel: 'Odustani',
},
star_us_dialog: {
title: 'Pomozite nam da se poboljšamo!',
description:
'Želite li nam dati zvjezdicu na GitHubu? Samo je jedan klik!',
close: 'Ne sada',
confirm: 'Naravno!',
},
export_diagram_dialog: {
title: 'Izvezi dijagram',
description: 'Odaberite format za izvoz:',
format_json: 'JSON',
cancel: 'Odustani',
export: 'Izvezi',
error: {
title: 'Greška pri izvozu dijagrama',
description:
'Nešto je pošlo po zlu. Trebate pomoć? support@chartdb.io',
},
},
import_diagram_dialog: {
title: 'Uvezi dijagram',
description: 'Uvezite dijagram iz JSON datoteke.',
cancel: 'Odustani',
import: 'Uvezi',
error: {
title: 'Greška pri uvozu dijagrama',
description:
'JSON dijagrama je nevažeći. Molimo provjerite JSON i pokušajte ponovno. Trebate pomoć? support@chartdb.io',
},
},
import_dbml_dialog: {
example_title: 'Uvezi primjer DBML-a',
title: 'Uvezi DBML',
description: 'Uvezite shemu baze podataka iz DBML formata.',
import: 'Uvezi',
cancel: 'Odustani',
skip_and_empty: 'Preskoči i isprazni',
show_example: 'Prikaži primjer',
error: {
title: 'Greška pri uvozu DBML-a',
description:
'Neuspješno parsiranje DBML-a. Molimo provjerite sintaksu.',
},
},
relationship_type: {
one_to_one: 'Jedan na jedan',
one_to_many: 'Jedan na više',
many_to_one: 'Više na jedan',
many_to_many: 'Više na više',
},
canvas_context_menu: {
new_table: 'Nova tablica',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
},
table_node_context_menu: {
edit_table: 'Uredi tablicu',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
add_relationship: 'Dodaj vezu',
},
snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})',
tool_tips: {
double_click_to_edit: 'Dvostruki klik za uređivanje',
},
language_select: {
change_language: 'Jezik',
},
},
};
export const hrMetadata: LanguageMetadata = {
name: 'Croatian',
nativeName: 'Hrvatski',
code: 'hr',
};

View File

@@ -26,8 +26,6 @@ export const id_ID: LanguageTranslation = {
hide_sidebar: 'Sembunyikan Sidebar',
hide_cardinality: 'Sembunyikan Kardinalitas',
show_cardinality: 'Tampilkan Kardinalitas',
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll',
theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi',
@@ -72,6 +70,15 @@ export const id_ID: LanguageTranslation = {
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',
@@ -106,6 +113,10 @@ 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',
@@ -143,8 +154,6 @@ export const id_ID: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Presisi',
scale: 'Skala',
},
index_actions: {
title: 'Atribut Indeks',
@@ -244,12 +253,9 @@ export const id_ID: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,10 +270,6 @@ export const id_ID: LanguageTranslation = {
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Atur Ulang Diagram',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
// TODO: Translate
filter: 'Filter Tables',
@@ -402,14 +404,6 @@ export const id_ID: LanguageTranslation = {
confirm: 'Ubah',
},
create_table_schema_dialog: {
title: 'Buat Skema Baru',
description:
'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
create: 'Buat',
cancel: 'Batal',
},
star_us_dialog: {
title: 'Bantu kami meningkatkan!',
description:

View File

@@ -26,8 +26,6 @@ export const ja: LanguageTranslation = {
hide_sidebar: 'サイドバーを非表示',
hide_cardinality: 'カーディナリティを非表示',
show_cardinality: 'カーディナリティを表示',
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
theme: 'テーマ',
// TODO: Translate
@@ -74,6 +72,15 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
},
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: 'なし',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'コピー失敗',
@@ -110,6 +117,10 @@ export const ja: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'スキーマ:',
filter_by_schema: 'スキーマでフィルタ',
search_schema: 'スキーマを検索...',
no_schemas_found: 'スキーマが見つかりません。',
view_all_options: 'すべてのオプションを表示...',
tables_section: {
tables: 'テーブル',
@@ -147,8 +158,6 @@ export const ja: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小数点以下桁数',
},
index_actions: {
title: 'インデックス属性',
@@ -250,12 +259,9 @@ export const ja: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -272,9 +278,7 @@ export const ja: LanguageTranslation = {
reorder_diagram: 'ダイアグラムを並べ替え',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
// TODO: Translate
filter: 'Filter Tables',
},
@@ -409,14 +413,6 @@ export const ja: LanguageTranslation = {
confirm: '変更',
},
create_table_schema_dialog: {
title: '新しいスキーマを作成',
description:
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
create: '作成',
cancel: 'キャンセル',
},
star_us_dialog: {
title: '改善をサポートしてください!',
description:

View File

@@ -26,8 +26,6 @@ export const ko_KR: LanguageTranslation = {
hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기',
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
theme: '테마',
show_dependencies: '종속성 보이기',
@@ -72,6 +70,15 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: '없음',
},
copy_to_clipboard_toast: {
unsupported: {
title: '복사 실패',
@@ -106,6 +113,10 @@ export const ko_KR: LanguageTranslation = {
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
@@ -143,8 +154,6 @@ export const ko_KR: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '정밀도',
scale: '소수점 자릿수',
},
index_actions: {
title: '인덱스 속성',
@@ -244,12 +253,9 @@ export const ko_KR: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,10 +270,6 @@ export const ko_KR: LanguageTranslation = {
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
// TODO: Translate
filter: 'Filter Tables',
@@ -402,14 +404,6 @@ export const ko_KR: LanguageTranslation = {
confirm: '변경',
},
create_table_schema_dialog: {
title: '새 스키마 생성',
description:
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
create: '생성',
cancel: '취소',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:

View File

@@ -26,8 +26,6 @@ export const mr: LanguageTranslation = {
hide_sidebar: 'साइडबार लपवा',
hide_cardinality: 'कार्डिनॅलिटी लपवा',
show_cardinality: 'कार्डिनॅलिटी दाखवा',
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
@@ -73,6 +71,15 @@ export const mr: LanguageTranslation = {
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी अयशस्वी',
@@ -109,6 +116,10 @@ export const mr: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...',
tables_section: {
tables: 'टेबल्स',
@@ -146,8 +157,6 @@ export const mr: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'अचूकता',
scale: 'प्रमाण',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
@@ -249,12 +258,9 @@ export const mr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -269,10 +275,6 @@ export const mr: LanguageTranslation = {
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
// TODO: Translate
filter: 'Filter Tables',
@@ -410,14 +412,6 @@ export const mr: LanguageTranslation = {
confirm: 'बदला',
},
create_table_schema_dialog: {
title: 'नवीन स्कीमा तयार करा',
description:
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
create: 'तयार करा',
cancel: 'रद्द करा',
},
star_us_dialog: {
title: 'आम्हाला सुधारण्यास मदत करा!',
description:

View File

@@ -26,8 +26,6 @@ export const ne: LanguageTranslation = {
hide_sidebar: 'साइडबार लुकाउनुहोस्',
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
@@ -73,6 +71,15 @@ export const ne: LanguageTranslation = {
cancel: 'रद्द गर्नुहोस्',
},
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कुनै पनि छैन',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'प्रतिलिपि असफल',
@@ -107,6 +114,10 @@ export const ne: LanguageTranslation = {
copied: 'प्रतिलिपि गरियो!',
side_panel: {
schema: 'स्कीम:',
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
search_schema: 'स्कीम खोज्नुहोस्...',
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
tables_section: {
tables: 'तालिकाहरू',
@@ -144,8 +155,6 @@ export const ne: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'परिशुद्धता',
scale: 'स्केल',
},
index_actions: {
title: 'सूचक विशेषताहरू',
@@ -246,12 +255,9 @@ export const ne: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,10 +272,6 @@ export const ne: LanguageTranslation = {
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables:
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
// TODO: Translate
@@ -407,14 +409,6 @@ export const ne: LanguageTranslation = {
confirm: 'परिवर्तन गर्नुहोस्',
},
create_table_schema_dialog: {
title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
description:
'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
create: 'सिर्जना गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
star_us_dialog: {
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
description:

View File

@@ -26,8 +26,6 @@ export const pt_BR: LanguageTranslation = {
hide_sidebar: 'Ocultar Barra Lateral',
hide_cardinality: 'Ocultar Cardinalidade',
show_cardinality: 'Mostrar Cardinalidade',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_field_attributes: 'Mostrar Atributos de Campo',
zoom_on_scroll: 'Zoom ao Rolar',
theme: 'Tema',
show_dependencies: 'Mostrar Dependências',
@@ -73,6 +71,15 @@ export const pt_BR: LanguageTranslation = {
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',
@@ -107,6 +114,10 @@ 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',
@@ -144,8 +155,6 @@ export const pt_BR: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precisão',
scale: 'Escala',
},
index_actions: {
title: 'Atributos do Índice',
@@ -245,12 +254,9 @@ export const pt_BR: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -265,10 +271,6 @@ export const pt_BR: LanguageTranslation = {
undo: 'Desfazer',
redo: 'Refazer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
// TODO: Translate
filter: 'Filter Tables',
@@ -405,14 +407,6 @@ export const pt_BR: LanguageTranslation = {
confirm: 'Alterar',
},
create_table_schema_dialog: {
title: 'Criar Novo Esquema',
description:
'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.',
create: 'Criar',
cancel: 'Cancelar',
},
star_us_dialog: {
title: 'Ajude-nos a melhorar!',
description:

View File

@@ -26,8 +26,6 @@ export const ru: LanguageTranslation = {
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть виды связи',
show_cardinality: 'Показать виды связи',
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
@@ -71,6 +69,15 @@ export const ru: LanguageTranslation = {
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'никто',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Ошибка копирования',
@@ -104,6 +111,10 @@ export const ru: LanguageTranslation = {
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
@@ -140,8 +151,6 @@ export const ru: LanguageTranslation = {
default_value: 'Default Value',
no_default: 'No default',
character_length: 'Макс. длина',
precision: 'Точность',
scale: 'Масштаб',
},
index_actions: {
title: 'Атрибуты индекса',
@@ -242,12 +251,9 @@ export const ru: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -262,10 +268,6 @@ export const ru: LanguageTranslation = {
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
// TODO: Translate
filter: 'Filter Tables',
@@ -402,14 +404,6 @@ export const ru: LanguageTranslation = {
confirm: 'Изменить',
},
create_table_schema_dialog: {
title: 'Создать новую схему',
description:
'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
create: 'Создать',
cancel: 'Отменить',
},
star_us_dialog: {
title: 'Помогите нам стать лучше!',
description:

View File

@@ -26,8 +26,6 @@ export const te: LanguageTranslation = {
hide_sidebar: 'సైడ్‌బార్ దాచండి',
hide_cardinality: 'కార్డినాలిటీని దాచండి',
show_cardinality: 'కార్డినాలిటీని చూపించండి',
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి',
@@ -73,6 +71,15 @@ export const te: LanguageTranslation = {
cancel: 'రద్దు',
},
multiple_schemas_alert: {
title: 'బహుళ స్కీమాలు',
description:
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'ఎదరికాదు',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'కాపీ విఫలమైంది',
@@ -107,6 +114,10 @@ export const te: LanguageTranslation = {
copied: 'కాపీ చేయబడింది!',
side_panel: {
schema: 'స్కీమా:',
filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి',
search_schema: 'స్కీమా కోసం శోధించండి...',
no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.',
view_all_options: 'అన్ని ఎంపికలను చూడండి...',
tables_section: {
tables: 'పట్టికలు',
@@ -144,8 +155,6 @@ export const te: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'సూక్ష్మత',
scale: 'స్కేల్',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',
@@ -246,12 +255,9 @@ export const te: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -266,10 +272,6 @@ export const te: LanguageTranslation = {
undo: 'తిరిగి చేయు',
redo: 'మరలా చేయు',
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
// TODO: Translate
filter: 'Filter Tables',
@@ -406,14 +408,6 @@ export const te: LanguageTranslation = {
confirm: 'మార్చు',
},
create_table_schema_dialog: {
title: 'కొత్త స్కీమా సృష్టించండి',
description:
'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
create: 'సృష్టించు',
cancel: 'రద్దు',
},
star_us_dialog: {
title: 'మా సహాయంతో మెరుగుపరచండి!',
description:

View File

@@ -26,8 +26,6 @@ export const tr: LanguageTranslation = {
hide_sidebar: 'Kenar Çubuğunu Gizle',
hide_cardinality: 'Kardinaliteyi Gizle',
show_cardinality: 'Kardinaliteyi Göster',
show_field_attributes: 'Alan Özelliklerini Göster',
hide_field_attributes: 'Alan Özelliklerini Gizle',
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
theme: 'Tema',
show_dependencies: 'Bağımlılıkları Göster',
@@ -73,6 +71,15 @@ export const tr: LanguageTranslation = {
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',
@@ -106,6 +113,10 @@ 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',
@@ -143,8 +154,6 @@ export const tr: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Hassasiyet',
scale: 'Ölçek',
},
index_actions: {
title: 'İndeks Özellikleri',
@@ -245,12 +254,9 @@ export const tr: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,10 +270,6 @@ export const tr: LanguageTranslation = {
undo: 'Geri Al',
redo: 'Yinele',
reorder_diagram: 'Diyagramı Yeniden Sırala',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
// TODO: Translate
filter: 'Filter Tables',
@@ -395,14 +397,6 @@ export const tr: LanguageTranslation = {
cancel: 'İptal',
confirm: 'Değiştir',
},
create_table_schema_dialog: {
title: 'Yeni Şema Oluştur',
description:
'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.',
create: 'Oluştur',
cancel: 'İptal',
},
star_us_dialog: {
title: 'Bize yardım et!',
description:

View File

@@ -26,8 +26,6 @@ export const uk: LanguageTranslation = {
hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність',
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням',
theme: 'Тема',
show_dependencies: 'Показати залежності',
@@ -71,6 +69,15 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
},
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'немає',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Помилка копіювання',
@@ -105,6 +112,10 @@ export const uk: LanguageTranslation = {
copied: 'Скопійовано!',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Пошук схеми…',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри…',
tables_section: {
tables: 'Таблиці',
@@ -142,8 +153,6 @@ export const uk: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Точність',
scale: 'Масштаб',
},
index_actions: {
title: 'Атрибути індексу',
@@ -243,12 +252,9 @@ export const uk: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -263,10 +269,6 @@ export const uk: LanguageTranslation = {
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Перевпорядкувати діаграму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
// TODO: Translate
filter: 'Filter Tables',
@@ -403,14 +405,6 @@ export const uk: LanguageTranslation = {
confirm: 'Змінити',
},
create_table_schema_dialog: {
title: 'Створити нову схему',
description:
'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
create: 'Створити',
cancel: 'Скасувати',
},
star_us_dialog: {
title: 'Допоможіть нам покращитися!',
description: 'Поставне на зірку на GitHub? Це лише один клік!',

View File

@@ -26,8 +26,6 @@ export const vi: LanguageTranslation = {
hide_sidebar: 'Ẩn thanh bên',
hide_cardinality: 'Ẩn số lượng',
show_cardinality: 'Hiển thị số lượng',
show_field_attributes: 'Hiển thị thuộc tính trường',
hide_field_attributes: 'Ẩn thuộc tính trường',
zoom_on_scroll: 'Thu phóng khi cuộn',
theme: 'Chủ đề',
show_dependencies: 'Hiển thị các phụ thuộc',
@@ -72,6 +70,15 @@ export const vi: LanguageTranslation = {
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',
@@ -106,6 +113,10 @@ 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',
@@ -143,8 +154,6 @@ export const vi: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Độ chính xác',
scale: 'Tỷ lệ',
},
index_actions: {
title: 'Thuộc tính chỉ mục',
@@ -244,12 +253,9 @@ export const vi: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -264,10 +270,6 @@ export const vi: LanguageTranslation = {
undo: 'Hoàn tác',
redo: 'Làm lại',
reorder_diagram: 'Sắp xếp lại sơ đồ',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
// TODO: Translate
filter: 'Filter Tables',
@@ -402,14 +404,6 @@ export const vi: LanguageTranslation = {
confirm: 'Xác nhận',
},
create_table_schema_dialog: {
title: 'Tạo lược đồ mới',
description:
'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.',
create: 'Tạo',
cancel: 'Hủy',
},
star_us_dialog: {
title: 'Hãy giúp chúng tôi cải thiện!',
description:

View File

@@ -26,8 +26,6 @@ export const zh_CN: LanguageTranslation = {
hide_sidebar: '隐藏侧边栏',
hide_cardinality: '隐藏基数',
show_cardinality: '展示基数',
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放',
theme: '主题',
show_dependencies: '展示依赖',
@@ -69,6 +67,15 @@ export const zh_CN: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: '无',
},
copy_to_clipboard_toast: {
unsupported: {
title: '复制失败',
@@ -103,6 +110,10 @@ export const zh_CN: LanguageTranslation = {
copied: '复制了!',
side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...',
tables_section: {
tables: '表',
@@ -140,8 +151,6 @@ export const zh_CN: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小数位',
},
index_actions: {
title: '索引属性',
@@ -241,12 +250,9 @@ export const zh_CN: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -261,10 +267,6 @@ export const zh_CN: LanguageTranslation = {
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出显示重叠的表',
// TODO: Translate
filter: 'Filter Tables',
@@ -398,13 +400,6 @@ export const zh_CN: LanguageTranslation = {
confirm: '更改',
},
create_table_schema_dialog: {
title: '创建新模式',
description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
create: '创建',
cancel: '取消',
},
star_us_dialog: {
title: '帮助我们改进!',
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',

View File

@@ -26,8 +26,6 @@ export const zh_TW: LanguageTranslation = {
hide_sidebar: '隱藏側邊欄',
hide_cardinality: '隱藏基數',
show_cardinality: '顯示基數',
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放',
theme: '主題',
show_dependencies: '顯示相依性',
@@ -69,6 +67,15 @@ export const zh_TW: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: '無',
},
copy_to_clipboard_toast: {
unsupported: {
title: '複製失敗',
@@ -103,6 +110,10 @@ 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: '表格',
@@ -140,8 +151,6 @@ export const zh_TW: LanguageTranslation = {
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小數位',
},
index_actions: {
title: '索引屬性',
@@ -241,12 +250,9 @@ export const zh_TW: LanguageTranslation = {
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
@@ -261,10 +267,6 @@ export const zh_TW: LanguageTranslation = {
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出顯示重疊表格',
// TODO: Translate
filter: 'Filter Tables',
@@ -397,14 +399,6 @@ export const zh_TW: LanguageTranslation = {
confirm: '變更',
},
create_table_schema_dialog: {
title: '建立新 Schema',
description:
'尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
create: '建立',
cancel: '取消',
},
star_us_dialog: {
title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',

View File

@@ -1,4 +1,3 @@
import type { DBCustomType } from './domain';
import type { Area } from './domain/area';
import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field';
@@ -49,10 +48,6 @@ const generateIdsMapFromDiagram = (
idsMap.set(area.id, generateId());
});
diagram.customTypes?.forEach((customType) => {
idsMap.set(customType.id, generateId());
});
return idsMap;
};
@@ -218,22 +213,6 @@ export const cloneDiagram = (
})
.filter((area): area is Area => area !== null) ?? [];
const customTypes: DBCustomType[] =
diagram.customTypes
?.map((customType) => {
const id = getNewId(customType.id);
if (!id) {
return null;
}
return {
...customType,
id,
} satisfies DBCustomType;
})
.filter(
(customType): customType is DBCustomType => customType !== null
) ?? [];
return {
diagram: {
...diagram,
@@ -242,7 +221,6 @@ export const cloneDiagram = (
relationships,
tables,
areas,
customTypes,
createdAt: diagram.createdAt
? new Date(diagram.createdAt)
: new Date(),

View File

@@ -48,30 +48,18 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char large object', id: 'char_large_object' },
{
name: 'char varying',
id: 'char_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
{ name: 'character large object', id: 'character_large_object' },
{
name: 'character varying',
id: 'character_varying',
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{ name: 'nchar large object', id: 'nchar_large_object' },
{
name: 'nchar varying',
id: 'nchar_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
{
name: 'national character large object',
id: 'national_character_large_object',
@@ -79,34 +67,22 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{
name: 'national character varying',
id: 'national_character_varying',
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{
name: 'national char varying',
id: 'national_char_varying',
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{
name: 'national character',
id: 'national_character',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national char',
id: 'national_char',
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
{ name: 'binary large object', id: 'binary_large_object' },
{
name: 'binary varying',
id: 'binary_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'fixedstring',
id: 'fixedstring',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
{ name: 'string', id: 'string' },
// Date Types

View File

@@ -14,23 +14,9 @@ export interface DataType {
name: string;
}
export interface FieldAttributeRange {
max: number;
min: number;
default: number;
}
interface FieldAttributes {
hasCharMaxLength?: boolean;
hasCharMaxLengthOption?: boolean;
precision?: FieldAttributeRange;
scale?: FieldAttributeRange;
maxLength?: number;
}
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
fieldAttributes?: FieldAttributes;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({

View File

@@ -2,12 +2,7 @@ import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
@@ -15,62 +10,23 @@ export const genericDataTypes: readonly DataTypeData[] = [
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
// Level 2 - Second most common types
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ name: 'bigint', id: 'bigint' },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'blob', id: 'blob' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'double', id: 'double' },
{ name: 'enum', id: 'enum' },
{ name: 'float', id: 'float' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'set', id: 'set' },
{ name: 'smallint', id: 'smallint' },
{ name: 'time', id: 'time' },
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
] as const;

View File

@@ -4,32 +4,12 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 1,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
// Level 2 - Second most common types
@@ -40,39 +20,16 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'numeric', id: 'numeric' },
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -3,12 +3,7 @@ import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -16,23 +11,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
@@ -43,7 +22,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },

View File

@@ -2,30 +2,15 @@ import type { DataTypeData } from './data-types';
export const oracleDataTypes: readonly DataTypeData[] = [
// Character types
{
name: 'VARCHAR2',
id: 'varchar2',
usageLevel: 1,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
{
name: 'NVARCHAR2',
id: 'nvarchar2',
usageLevel: 1,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'CHAR',
id: 'char',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'NCHAR',
id: 'nchar',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{ name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
{ name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
{ name: 'CLOB', id: 'clob', usageLevel: 2 },
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 },
@@ -64,12 +49,7 @@ export const oracleDataTypes: readonly DataTypeData[] = [
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
// Other types
{
name: 'RAW',
id: 'raw',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },

View File

@@ -3,12 +3,7 @@ import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'integer', id: 'integer', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -16,23 +11,7 @@ export const postgresDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
@@ -44,33 +23,18 @@ export const postgresDataTypes: readonly DataTypeData[] = [
},
// Less common types
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'double precision', id: 'double_precision' },
{ name: 'smallserial', id: 'smallserial' },
{ name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' },
{ name: 'smallint', id: 'smallint' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
fieldAttributes: { hasCharMaxLength: true },
hasCharMaxLength: true,
},
{ name: 'time', id: 'time' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },

View File

@@ -4,93 +4,32 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bit', id: 'bit', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
usageLevel: 1,
},
{
name: 'nvarchar',
id: 'nvarchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 4000,
},
usageLevel: 1,
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types
{
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'numeric', id: 'numeric' },
{ name: 'smallint', id: 'smallint' },
{ name: 'smallmoney', id: 'smallmoney' },
{ name: 'tinyint', id: 'tinyint' },
{ name: 'money', id: 'money' },
{ name: 'float', id: 'float' },
{ name: 'real', id: 'real' },
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'ntext', id: 'ntext' },
{
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
},
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'image', id: 'image' },
{ name: 'datetimeoffset', id: 'datetimeoffset' },
{ name: 'smalldatetime', id: 'smalldatetime' },

View File

@@ -10,48 +10,25 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
// SQLite type aliases and common types
{ name: 'int', id: 'int', usageLevel: 1 },
{
name: 'varchar',
id: 'varchar',
fieldAttributes: {
hasCharMaxLength: true,
},
usageLevel: 1,
},
{
name: 'timestamp',
id: 'timestamp',
usageLevel: 1,
},
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'float', id: 'float', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
},
{ name: 'double', id: 'double', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types (all map to SQLite storage classes)
{
name: 'char',
id: 'char',
fieldAttributes: {
hasCharMaxLength: true,
},
usageLevel: 2,
},
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'smallint', id: 'smallint' },
{ 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;

View File

@@ -4,5 +4,4 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
[DatabaseType.POSTGRESQL]: 'public',
[DatabaseType.SQL_SERVER]: 'dbo',
[DatabaseType.CLICKHOUSE]: 'default',
[DatabaseType.COCKROACHDB]: 'public',
};

View File

@@ -227,7 +227,7 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).not.toContain('DEFAULT DEFAULT has default');
// The fields should still be in the table
expect(sql).toContain('is_active boolean');
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
expect(sql).toContain('stock_count int NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {

View File

@@ -73,13 +73,7 @@ function parseMSSQLDefault(field: DBField): string {
return `'${defaultValue}'`;
}
export function exportMSSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
export function exportMSSQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -89,254 +83,202 @@ export function exportMSSQL({
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
if (!onlyRelationships) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
});
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
return `${
table.comments ? formatMSSQLTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
return `${
table.comments
? formatMSSQLTableComment(table.comments)
: ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n\n${table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
})()}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
})
.join('')}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
sqlScript += `\n${relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
if (!sourceField || !targetField) {
return '';
}
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
// 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 (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return '';
}
const fkTableName = fkTable.schema
? `[${fkTable.schema}].[${fkTable.name}]`
: `[${fkTable.name}]`;
const refTableName = refTable.schema
? `[${refTable.schema}].[${refTable.name}]`
: `[${refTable.name}]`;
const fkTableName = fkTable.schema
? `[${fkTable.schema}].[${fkTable.name}]`
: `[${fkTable.name}]`;
const refTableName = refTable.schema
? `[${refTable.schema}].[${refTable.name}]`
: `[${refTable.name}]`;
return {
schema: fkTable.schema || 'dbo',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
};
})
.filter(Boolean); // Remove empty objects
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return `ALTER TABLE ${fkTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
return sqlScript;
}

View File

@@ -170,13 +170,7 @@ function mapMySQLType(typeName: string): string {
return typeName;
}
export function exportMySQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
export function exportMySQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -185,245 +179,224 @@ export function exportMySQL({
const relationships = diagram.relationships;
// Start SQL script
let sqlScript = '-- MySQL database export\n';
let sqlScript = '-- MySQL database export\n\n';
if (!onlyRelationships) {
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n';
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n\n';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
if (schemas.size > 0) {
sqlScript += '\n';
}
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
return `${
table.comments ? formatTableComment(table.comments) : ''
}\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Handle MySQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'varbinary'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
}
if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
return `${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Set a default size for VARCHAR columns if not specified
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Handle MySQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'varbinary'
) {
typeWithSize = `${typeName}(255)`;
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
} else if (field.precision && field.scale) {
if (
field.primaryKey &&
(field.default
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
// Set a default size for VARCHAR columns if not specified
if (
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
) {
typeWithSize = `${typeName}(255)`;
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
?.toLowerCase()
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
? ` DEFAULT ${parseMySQLDefault(field)}`
: '';
// MySQL supports inline comments
const comment = field.comments
? ` COMMENT '${escapeSQLComment(field.comments)}'`
// Handle default value
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
? ` DEFAULT ${parseMySQLDefault(field)}`
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
}\n)${
// MySQL supports table comments
table.comments
? ` COMMENT='${escapeSQLComment(table.comments)}'`
: ''
};\n${
// Add indexes - MySQL creates them separately from the table definition
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// MySQL supports inline comments
const comment = field.comments
? ` COMMENT '${escapeSQLComment(field.comments)}'`
: '';
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
) {
return '';
}
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
const fieldNamesForIndex = indexFields
.map((field) => field?.name || '')
.join('_');
const uniqueIndicator = index.unique
? '_unique'
: '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `\`${field.name}\`` : ''
)
.filter(Boolean);
// Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some(
(field) => {
const typeName =
field?.type.name.toLowerCase() ||
'';
return (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
);
}
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
}\n)${
// MySQL supports table comments
table.comments
? ` COMMENT='${escapeSQLComment(table.comments)}'`
: ''
};\n\n${
// Add indexes - MySQL creates them separately from the table definition
table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
// If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob
? indexFieldNames.map((name) => {
const field = indexFields.find(
(f) => `\`${f?.name}\`` === name
);
if (!field) return name;
const typeName =
field.type.name.toLowerCase();
if (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
) {
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
return `${name}(255)`;
}
return name;
})
: indexFieldNames;
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: '';
return field ? field : null;
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
const fieldNamesForIndex = indexFields
.map((field) => field?.name || '')
.join('_');
const uniqueIndicator = index.unique ? '_unique' : '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `\`${field.name}\`` : ''))
.filter(Boolean);
// Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some((field) => {
const typeName =
field?.type.name.toLowerCase() || '';
return (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
);
});
// If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob
? indexFieldNames.map((name) => {
const field = indexFields.find(
(f) => `\`${f?.name}\`` === name
);
if (!field) return name;
const typeName =
field.type.name.toLowerCase();
if (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
) {
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
return `${name}(255)`;
}
return name;
})
: indexFieldNames;
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
})
.filter(Boolean)
.join('\n')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
sqlScript += '\n-- Foreign key constraints\n\n';
const foreignKeys = relationships
sqlScript += relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
@@ -499,15 +472,14 @@ export function exportMySQL({
const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
// MySQL supports ON DELETE and ON UPDATE actions
return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
return `ALTER TABLE ${fkTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
})
.filter(Boolean); // Remove empty strings
sqlScript += foreignKeys.join('\n');
.filter(Boolean) // Remove empty strings
.join('\n');
}
// Commit transaction
sqlScript += '\n\nCOMMIT;\n';
sqlScript += '\nCOMMIT;\n';
return sqlScript;
}

View File

@@ -142,16 +142,10 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
}
});
return typesSql ? typesSql + '\n' : '';
return typesSql + '\n';
}
export function exportPostgreSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
export function exportPostgreSQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -162,391 +156,326 @@ export function exportPostgreSQL({
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
if (!onlyRelationships) {
const schemas = new Set<string>();
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
// Add sequence creation statements
const sequences = new Set<string>();
tables.forEach((table) => {
table.fields.forEach((field) => {
if (field.default) {
// Match nextval('schema.sequence_name') or nextval('sequence_name')
const match = field.default.match(
/nextval\('([^']+)'(?:::[^)]+)?\)/
);
if (match) {
sequences.add(match[1]);
}
}
});
});
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
if (sequences.size > 0) {
sqlScript += '\n';
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
sqlScript += '\n';
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
// Add sequence creation statements
const sequences = new Set<string>();
tables.forEach((table) => {
table.fields.forEach((field) => {
if (field.default) {
// Match nextval('schema.sequence_name') or nextval('sequence_name')
const match = field.default.match(
/nextval\('([^']+)'(?:::[^)]+)?\)/
);
if (match) {
sequences.add(match[1]);
}
}
});
});
return `${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
sqlScript += '\n';
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
);
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
if (field.increment && !field.nullable) {
if (
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
}
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() ===
'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
}
if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
return `${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
);
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
// Handle identity generation
let identity = '';
if (field.increment && !field.nullable) {
if (
field.default &&
field.default.includes('nextval')
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
// PostgreSQL already handles this with DEFAULT nextval()
} else if (
field.default &&
field.default.toLowerCase().includes('identity')
) {
identity = ' GENERATED BY DEFAULT AS IDENTITY';
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
}
// Only add UNIQUE constraint if the field is not part of the primary key
// This avoids redundant uniqueness constraints
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
// Handle default value using PostgreSQL specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parsePostgresDefault(field)}`
: '';
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize = typeWithSize.replace('[]', '') + '[]';
}
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);${
// Add table comments
table.comments
? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
)
.join('')
}${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
const notNull = field.nullable ? '' : ' NOT NULL';
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
) {
return '';
}
// Handle identity generation
let identity = '';
if (field.default && field.default.includes('nextval')) {
// PostgreSQL already handles this with DEFAULT nextval()
} else if (
field.default &&
field.default.toLowerCase().includes('identity')
) {
identity = ' GENERATED BY DEFAULT AS IDENTITY';
}
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
// Only add UNIQUE constraint if the field is not part of the primary key
// This avoids redundant uniqueness constraints
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using PostgreSQL specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parsePostgresDefault(field)}`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n\n${
// Add table comments
table.comments
? `COMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';\n\n`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';\n`
)
.join('')
}\n${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
const safeIndexName = index.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
let combinedName = `${safeTableName}_${safeIndexName}`;
if (combinedName.length > 60) {
// If too long, use just the index name or a truncated version
combinedName =
safeIndexName.length > 60
? safeIndexName.substring(0, 60)
: safeIndexName;
}
const indexName = `"${combinedName}"`;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
: '';
return field ? field : null;
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
const safeIndexName = index.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
let combinedName = `${safeTableName}_${safeIndexName}`;
if (combinedName.length > 60) {
// If too long, use just the index name or a truncated version
combinedName =
safeIndexName.length > 60
? safeIndexName.substring(0, 60)
: safeIndexName;
}
const indexName = `"${combinedName}"`;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : ''))
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
})
.filter(Boolean)
.join('')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
sqlScript += `\n${relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
if (!sourceField || !targetField) {
return '';
}
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
// 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 (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return '';
}
const fkTableName = fkTable.schema
? `"${fkTable.schema}"."${fkTable.name}"`
: `"${fkTable.name}"`;
const refTableName = refTable.schema
? `"${refTable.schema}"."${refTable.name}"`
: `"${refTable.name}"`;
const fkTableName = fkTable.schema
? `"${fkTable.schema}"."${fkTable.name}"`
: `"${fkTable.name}"`;
const refTableName = refTable.schema
? `"${refTable.schema}"."${refTable.name}"`
: `"${refTable.name}"`;
// Create a unique constraint name by combining table and field names
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
// and doesn't get truncated in a way that breaks SQL syntax
const baseName = `fk_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.name}`;
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
const safeConstraintName =
baseName.length > 60
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
// Create a unique constraint name by combining table and field names
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
// and doesn't get truncated in a way that breaks SQL syntax
const baseName = `fk_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.name}`;
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
const safeConstraintName =
baseName.length > 60
? baseName
.substring(0, 60)
.replace(/[^a-zA-Z0-9_]/g, '_')
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
const constraintName = `"${safeConstraintName}"`;
const constraintName = `"${safeConstraintName}"`;
return {
schema: fkTable.schema || 'public',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");`,
};
})
.filter(Boolean); // Remove empty objects
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return `ALTER TABLE ${fkTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
return sqlScript;
}

View File

@@ -67,9 +67,8 @@ function parseSQLiteDefault(field: DBField): string {
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Preserve original types for SQLite export (only map when necessary)
// Map problematic types to SQLite compatible types
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)
@@ -77,62 +76,59 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
}
// Preserve original type names that SQLite accepts
// Map common types to SQLite's simplified type system
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':
case 'json':
return typeName.toUpperCase();
return 'INTEGER';
// Only map types that SQLite truly doesn't recognize
case 'decimal':
case 'numeric':
case 'float':
case 'double':
case 'real':
return 'REAL';
case 'char':
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 'DATETIME';
return 'TEXT'; // SQLite doesn't have dedicated date types
case 'blob':
case 'binary':
case 'varbinary':
case 'image':
return 'BLOB';
case 'bit':
return 'BOOLEAN';
case 'boolean':
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
case 'user-defined':
case 'json':
case 'jsonb':
return 'TEXT';
return 'TEXT'; // Store as JSON text
case 'array':
return 'TEXT';
return 'TEXT'; // Store as serialized array text
case 'geometry':
case 'geography':
return 'BLOB';
case 'mediumint':
return 'INTEGER';
return 'BLOB'; // Store spatial data as BLOB in SQLite
}
// If type has array notation (ends with []), treat as TEXT
@@ -140,17 +136,11 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'TEXT';
}
// For any other types, preserve the original
return originalType.toUpperCase();
// For any other types, default to TEXT
return typeName;
}
export function exportSQLite({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
export function exportSQLite(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -159,15 +149,10 @@ export function exportSQLite({
const relationships = diagram.relationships;
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
let sqlScript = '-- SQLite database export\n';
// Add PRAGMA foreign_keys = ON if there are relationships
if (relationships && relationships.length > 0) {
sqlScript += 'PRAGMA foreign_keys = ON;\n\n';
}
let sqlScript = '-- SQLite database export\n\n';
// Begin transaction for faster import
sqlScript += 'BEGIN TRANSACTION;\n';
sqlScript += 'BEGIN TRANSACTION;\n\n';
// SQLite doesn't have sequences, so we skip sequence creation
@@ -181,280 +166,227 @@ export function exportSQLite({
'sqlite_master',
];
if (!onlyRelationships) {
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Skip SQLite system tables
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
return `-- Skipping SQLite system table: "${table.name}"\n`;
}
// Skip SQLite system tables
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
return `-- Skipping SQLite system table: "${table.name}"\n`;
}
// SQLite doesn't use schema prefixes, so we use just the table name
// Include the schema in a comment if it exists
const schemaComment = table.schema
? `-- Original schema: ${table.schema}\n`
: '';
const tableName = `"${table.name}"`;
// SQLite doesn't use schema prefixes, so we use just the table name
// Include the schema in a comment if it exists
const schemaComment = table.schema
? `-- Original schema: ${table.schema}\n`
: '';
const tableName = `"${table.name}"`;
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() ===
'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
// 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
return `${schemaComment}${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// SQLite ignores length specifiers, so we don't add them
// We'll keep this simple without size info
const typeWithoutSize = typeName;
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle autoincrement - only works with INTEGER PRIMARY KEY
let autoIncrement = '';
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
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types
const baseTypeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// 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';
// Handle autoincrement - only works with INTEGER PRIMARY KEY
let autoIncrement = '';
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
?.toLowerCase()
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTOINCREMENT';
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTOINCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value - Special handling for datetime() function
let defaultValue = '';
if (
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
) {
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
if (field.default.includes("datetime(''now'')")) {
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
} else {
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
}
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
// Handle default value - Special handling for datetime() function
let defaultValue = '';
if (
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
) {
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
if (field.default.includes("datetime(''now'')")) {
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
} else {
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
}
}
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithParams}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}${
// Add foreign key constraints
tableForeignKeys.length > 0
? ',\n' + tableForeignKeys.join(',\n')
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
(() => {
const validIndexes = table.indexes
.map((index) => {
// Skip indexes that exactly match the primary key
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
) {
return '';
}
// Create safe index name
const safeIndexName =
`${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
table.indexes
.map((index) => {
// Skip indexes that exactly match the primary key
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : ''))
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
}
// Create safe index name
const safeIndexName = `${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
})
.filter(Boolean)
.join('\n')
}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate table constraints and triggers for foreign keys
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
// But we'll also provide individual ALTER TABLE statements as comments for reference
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\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';

View File

@@ -11,7 +11,23 @@ import { exportMySQL } from './export-per-type/mysql';
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
const typeMap: Record<string, string> = {};
const typeMap: Record<string, string> = {
'character varying': 'varchar',
'char varying': 'varchar',
integer: 'int',
int4: 'int',
int8: 'bigint',
serial4: 'serial',
serial8: 'bigserial',
float8: 'double precision',
float4: 'real',
bool: 'boolean',
character: 'char',
'timestamp without time zone': 'timestamp',
'timestamp with time zone': 'timestamptz',
'time without time zone': 'time',
'time with time zone': 'timetz',
};
return typeMap[typeName.toLowerCase()] || typeName;
};
@@ -20,12 +36,10 @@ export const exportBaseSQL = ({
diagram,
targetDatabaseType,
isDBMLFlow = false,
onlyRelationships = false,
}: {
diagram: Diagram;
targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean;
onlyRelationships?: boolean;
}): string => {
const { tables, relationships } = diagram;
@@ -36,16 +50,16 @@ export const exportBaseSQL = ({
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
switch (diagram.databaseType) {
case DatabaseType.SQL_SERVER:
return exportMSSQL({ diagram, onlyRelationships });
return exportMSSQL(diagram);
case DatabaseType.POSTGRESQL:
return exportPostgreSQL({ diagram, onlyRelationships });
return exportPostgreSQL(diagram);
case DatabaseType.SQLITE:
return exportSQLite({ diagram, onlyRelationships });
return exportSQLite(diagram);
case DatabaseType.MYSQL:
case DatabaseType.MARIADB:
return exportMySQL({ diagram, onlyRelationships });
return exportMySQL(diagram);
default:
return exportPostgreSQL({ diagram, onlyRelationships });
return exportPostgreSQL(diagram);
}
}
@@ -117,23 +131,7 @@ export const exportBaseSQL = ({
}
}
});
if (
diagram.customTypes.some(
(ct) =>
(ct.kind === 'enum' &&
ct.values &&
ct.values.length > 0 &&
targetDatabaseType === DatabaseType.POSTGRESQL &&
!isDBMLFlow) ||
(ct.kind === 'composite' &&
ct.fields &&
ct.fields.length > 0 &&
(targetDatabaseType === DatabaseType.POSTGRESQL ||
isDBMLFlow))
)
) {
sqlScript += '\n';
}
sqlScript += '\n'; // Add a newline if custom types were processed
}
// Add CREATE SEQUENCE statements
@@ -156,9 +154,7 @@ export const exportBaseSQL = ({
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
if (sequences.size > 0) {
sqlScript += '\n';
}
sqlScript += '\n';
// Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => {
@@ -238,8 +234,7 @@ export const exportBaseSQL = ({
// Add size for character types
if (
field.characterMaximumLength &&
parseInt(field.characterMaximumLength) > 0 &&
field.type.name.toLowerCase() !== 'decimal'
parseInt(field.characterMaximumLength) > 0
) {
sqlScript += `(${field.characterMaximumLength})`;
} else if (field.type.name.toLowerCase().includes('varchar')) {
@@ -321,7 +316,7 @@ export const exportBaseSQL = ({
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
}
sqlScript += '\n);\n';
sqlScript += '\n);\n\n';
// Add table comment
if (table.comments) {
@@ -346,18 +341,15 @@ export const exportBaseSQL = ({
.join(', ');
if (fieldNames) {
const indexName =
table.schema && !isDBMLFlow
? `${table.schema}_${index.name}`
: index.name;
const indexName = table.schema
? `${table.schema}_${index.name}`
: index.name;
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
}
});
});
if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
sqlScript += '\n';
}
});
// Handle relationships (foreign keys)
relationships?.forEach((relationship) => {

View File

@@ -2,8 +2,7 @@ const withExtras = false;
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
const withoutDefault = `""`;
export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
SELECT CAST(CONCAT(
export const mariaDBQuery = `SELECT CAST(CONCAT(
'{"fk_info": [',
IFNULL((SELECT GROUP_CONCAT(
CONCAT('{"schema":"', cast(fk.table_schema as CHAR),

View File

@@ -18,14 +18,11 @@ export interface SQLColumn {
nullable: boolean;
primaryKey: boolean;
unique: boolean;
typeArgs?:
| {
length?: number;
precision?: number;
scale?: number;
}
| number[]
| string;
typeArgs?: {
length?: number;
precision?: number;
scale?: number;
};
comment?: string;
default?: string;
increment?: boolean;
@@ -562,38 +559,6 @@ export function convertToChartDBDiagram(
id: column.type.toLowerCase(),
name: column.type,
};
}
// Handle SQL Server types specifically
else if (
sourceDatabaseType === DatabaseType.SQL_SERVER &&
targetDatabaseType === DatabaseType.SQL_SERVER
) {
const normalizedType = column.type.toLowerCase();
// Preserve SQL Server specific types when target is also SQL Server
if (
normalizedType === 'nvarchar' ||
normalizedType === 'nchar' ||
normalizedType === 'ntext' ||
normalizedType === 'uniqueidentifier' ||
normalizedType === 'datetime2' ||
normalizedType === 'datetimeoffset' ||
normalizedType === 'money' ||
normalizedType === 'smallmoney' ||
normalizedType === 'bit' ||
normalizedType === 'xml' ||
normalizedType === 'hierarchyid' ||
normalizedType === 'geography' ||
normalizedType === 'geometry'
) {
mappedType = { id: normalizedType, name: normalizedType };
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
sourceDatabaseType
);
}
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
@@ -616,68 +581,22 @@ export function convertToChartDBDiagram(
// Add type arguments if present
if (column.typeArgs) {
// Handle string typeArgs (e.g., 'max' for varchar(max))
if (typeof column.typeArgs === 'string') {
if (
(field.type.id === 'varchar' ||
field.type.id === 'nvarchar') &&
column.typeArgs === 'max'
) {
field.characterMaximumLength = 'max';
}
}
// Handle array typeArgs (SQL Server format)
else if (
Array.isArray(column.typeArgs) &&
column.typeArgs.length > 0
// Transfer length for varchar/char types
if (
column.typeArgs.length !== undefined &&
(field.type.id === 'varchar' || field.type.id === 'char')
) {
if (
field.type.id === 'varchar' ||
field.type.id === 'nvarchar' ||
field.type.id === 'char' ||
field.type.id === 'nchar'
) {
field.characterMaximumLength =
column.typeArgs[0].toString();
} else if (
(field.type.id === 'numeric' ||
field.type.id === 'decimal') &&
column.typeArgs.length >= 2
) {
field.precision = column.typeArgs[0];
field.scale = column.typeArgs[1];
}
field.characterMaximumLength =
column.typeArgs.length.toString();
}
// Handle object typeArgs (standard format)
else if (
typeof column.typeArgs === 'object' &&
!Array.isArray(column.typeArgs)
// Transfer precision/scale for numeric types
if (
column.typeArgs.precision !== undefined &&
(field.type.id === 'numeric' || field.type.id === 'decimal')
) {
const typeArgsObj = column.typeArgs as {
length?: number;
precision?: number;
scale?: number;
};
// Transfer length for varchar/char types
if (
typeArgsObj.length !== undefined &&
(field.type.id === 'varchar' ||
field.type.id === 'char')
) {
field.characterMaximumLength =
typeArgsObj.length.toString();
}
// Transfer precision/scale for numeric types
if (
typeArgsObj.precision !== undefined &&
(field.type.id === 'numeric' ||
field.type.id === 'decimal')
) {
field.precision = typeArgsObj.precision;
field.scale = typeArgsObj.scale;
}
field.precision = column.typeArgs.precision;
field.scale = column.typeArgs.scale;
}
}

View File

@@ -1,350 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Core Parser Tests', () => {
it('should parse basic tables', async () => {
const sql = `
CREATE TABLE wizards (
id INT PRIMARY KEY,
name NVARCHAR(255) NOT NULL
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('wizards');
expect(result.tables[0].columns).toHaveLength(2);
});
it('should parse tables with schemas', async () => {
const sql = `
CREATE TABLE [magic].[spells] (
id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
name NVARCHAR(100) NOT NULL,
level INT NOT NULL
);
CREATE TABLE [dbo].[wizards] (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(255) NOT NULL
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.tables.find((t) => t.name === 'spells')).toBeDefined();
expect(result.tables.find((t) => t.name === 'spells')?.schema).toBe(
'magic'
);
expect(result.tables.find((t) => t.name === 'wizards')?.schema).toBe(
'dbo'
);
});
it('should parse foreign key relationships', async () => {
const sql = `
CREATE TABLE guilds (id INT PRIMARY KEY);
CREATE TABLE mages (
id INT PRIMARY KEY,
guild_id INT FOREIGN KEY REFERENCES guilds(id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('mages');
expect(result.relationships[0].targetTable).toBe('guilds');
expect(result.relationships[0].sourceColumn).toBe('guild_id');
expect(result.relationships[0].targetColumn).toBe('id');
});
it('should parse foreign keys with schema references', async () => {
const sql = `
CREATE TABLE [magic].[schools] (
id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
name NVARCHAR(100) NOT NULL
);
CREATE TABLE [magic].[towers] (
id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
school_id UNIQUEIDENTIFIER NOT NULL,
name NVARCHAR(100) NOT NULL,
CONSTRAINT FK_towers_schools FOREIGN KEY (school_id) REFERENCES [magic].[schools](id)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('towers');
expect(result.relationships[0].targetTable).toBe('schools');
expect(result.relationships[0].sourceSchema).toBe('magic');
expect(result.relationships[0].targetSchema).toBe('magic');
});
it('should handle GO statements and SQL Server specific syntax', async () => {
const sql = `
USE [MagicalRealm]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[enchantments] (
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](max) NOT NULL,
[Power] [decimal](18, 2) NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
CONSTRAINT [PK_enchantments] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe('enchantments');
expect(result.tables[0].columns).toHaveLength(4);
expect(
result.tables[0].columns.find((c) => c.name === 'Power')?.type
).toBe('decimal');
});
it('should parse ALTER TABLE ADD CONSTRAINT for foreign keys', async () => {
const sql = `
CREATE TABLE [calibration].[Calibration] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Average] [decimal](18, 2) NOT NULL
);
CREATE TABLE [calibration].[CalibrationProcess] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[CalibrationId] [uniqueidentifier] NOT NULL
);
ALTER TABLE [calibration].[CalibrationProcess]
ADD CONSTRAINT [FK_CalibrationProcess_Calibration]
FOREIGN KEY ([CalibrationId])
REFERENCES [calibration].[Calibration]([Id]);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('CalibrationProcess');
expect(result.relationships[0].targetTable).toBe('Calibration');
expect(result.relationships[0].name).toBe(
'FK_CalibrationProcess_Calibration'
);
});
it('should handle multiple schemas from the test file', async () => {
const sql = `
CREATE SCHEMA [magic]
GO
CREATE SCHEMA [artifacts]
GO
CREATE TABLE [magic].[wizards] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Name] [nvarchar](255) NOT NULL
);
CREATE TABLE [artifacts].[wands] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[WizardId] [uniqueidentifier] NOT NULL,
[WoodType] [nvarchar](50) NOT NULL,
CONSTRAINT [FK_wands_wizards] FOREIGN KEY ([WizardId]) REFERENCES [magic].[wizards]([Id])
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.tables.find((t) => t.schema === 'magic')).toBeDefined();
expect(
result.tables.find((t) => t.schema === 'artifacts')
).toBeDefined();
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceSchema).toBe('artifacts');
expect(result.relationships[0].targetSchema).toBe('magic');
});
it('should handle SQL Server data types correctly', async () => {
const sql = `
CREATE TABLE [magic].[spell_components] (
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[Quantity] [int] NOT NULL,
[Weight] [decimal](10, 2) NOT NULL,
[IsPowerful] [bit] NOT NULL,
[DiscoveredAt] [datetime2](7) NOT NULL,
[Description] [nvarchar](max) NULL,
[RarityLevel] [tinyint] NOT NULL,
[MarketValue] [money] NOT NULL,
[AlchemicalFormula] [xml] NULL,
PRIMARY KEY ([Id])
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const columns = result.tables[0].columns;
expect(columns.find((c) => c.name === 'Id')?.type).toBe(
'uniqueidentifier'
);
expect(columns.find((c) => c.name === 'Name')?.type).toBe('nvarchar');
expect(columns.find((c) => c.name === 'Quantity')?.type).toBe('int');
expect(columns.find((c) => c.name === 'Weight')?.type).toBe('decimal');
expect(columns.find((c) => c.name === 'IsPowerful')?.type).toBe('bit');
expect(columns.find((c) => c.name === 'DiscoveredAt')?.type).toBe(
'datetime2'
);
expect(columns.find((c) => c.name === 'Description')?.type).toBe(
'nvarchar'
);
expect(columns.find((c) => c.name === 'RarityLevel')?.type).toBe(
'tinyint'
);
expect(columns.find((c) => c.name === 'MarketValue')?.type).toBe(
'money'
);
expect(columns.find((c) => c.name === 'AlchemicalFormula')?.type).toBe(
'xml'
);
});
it('should handle IDENTITY columns', async () => {
const sql = `
CREATE TABLE [dbo].[magical_creatures] (
[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[Name] [nvarchar](100) NOT NULL,
[PowerLevel] [int] NOT NULL
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const idColumn = result.tables[0].columns.find((c) => c.name === 'Id');
expect(idColumn?.increment).toBe(true);
});
it('should parse composite primary keys', async () => {
const sql = `
CREATE TABLE [magic].[spell_ingredients] (
[SpellId] [uniqueidentifier] NOT NULL,
[IngredientId] [uniqueidentifier] NOT NULL,
[Quantity] [int] NOT NULL,
CONSTRAINT [PK_spell_ingredients] PRIMARY KEY CLUSTERED
(
[SpellId] ASC,
[IngredientId] ASC
)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.columns.filter((c) => c.primaryKey)).toHaveLength(2);
expect(
table.columns.find((c) => c.name === 'SpellId')?.primaryKey
).toBe(true);
expect(
table.columns.find((c) => c.name === 'IngredientId')?.primaryKey
).toBe(true);
});
it('should handle unique constraints', async () => {
const sql = `
CREATE TABLE [dbo].[arcane_libraries] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Code] [nvarchar](50) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
CONSTRAINT [UQ_arcane_libraries_code] UNIQUE ([Code])
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
expect(result.tables[0].indexes).toHaveLength(1);
expect(result.tables[0].indexes[0].name).toBe(
'UQ_arcane_libraries_code'
);
expect(result.tables[0].indexes[0].unique).toBe(true);
expect(result.tables[0].indexes[0].columns).toContain('Code');
});
it('should handle default values', async () => {
const sql = `
CREATE TABLE [dbo].[potion_recipes] (
[Id] [uniqueidentifier] NOT NULL DEFAULT NEWID(),
[Name] [nvarchar](255) NOT NULL,
[IsActive] [bit] NOT NULL DEFAULT 1,
[CreatedAt] [datetime2](7) NOT NULL DEFAULT GETDATE(),
[Difficulty] [int] NOT NULL DEFAULT 5,
PRIMARY KEY ([Id])
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const columns = result.tables[0].columns;
expect(columns.find((c) => c.name === 'Id')?.default).toBeDefined();
expect(columns.find((c) => c.name === 'IsActive')?.default).toBe('1');
expect(
columns.find((c) => c.name === 'CreatedAt')?.default
).toBeDefined();
expect(columns.find((c) => c.name === 'Difficulty')?.default).toBe('5');
});
it('should parse indexes created separately', async () => {
const sql = `
CREATE TABLE [dbo].[spell_books] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Title] [nvarchar](255) NOT NULL,
[Author] [nvarchar](255) NOT NULL,
[PublishedYear] [int] NOT NULL
);
CREATE INDEX [IX_spell_books_author] ON [dbo].[spell_books] ([Author]);
CREATE UNIQUE INDEX [UIX_spell_books_title] ON [dbo].[spell_books] ([Title]);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
expect(result.tables[0].indexes).toHaveLength(2);
const authorIndex = result.tables[0].indexes.find(
(i) => i.name === 'IX_spell_books_author'
);
expect(authorIndex?.unique).toBe(false);
expect(authorIndex?.columns).toContain('Author');
const titleIndex = result.tables[0].indexes.find(
(i) => i.name === 'UIX_spell_books_title'
);
expect(titleIndex?.unique).toBe(true);
expect(titleIndex?.columns).toContain('Title');
});
});

View File

@@ -1,478 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Real-World Examples', () => {
describe('Magical Academy Example', () => {
it('should parse the magical academy example with all 16 tables', async () => {
const sql = `
CREATE TABLE [dbo].[schools](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[name] [nvarchar](255) NOT NULL,
[created_at] [datetime2](7) NOT NULL DEFAULT GETDATE()
);
CREATE TABLE [dbo].[towers](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[name] [nvarchar](255) NOT NULL,
CONSTRAINT [FK_towers_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[ranks](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[name] [nvarchar](255) NOT NULL,
CONSTRAINT [FK_ranks_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[spell_permissions](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[spell_type] [nvarchar](255) NOT NULL,
[casting_level] [nvarchar](255) NOT NULL
);
CREATE TABLE [dbo].[rank_spell_permissions](
[rank_id] [uniqueidentifier] NOT NULL,
[spell_permission_id] [uniqueidentifier] NOT NULL,
PRIMARY KEY ([rank_id], [spell_permission_id]),
CONSTRAINT [FK_rsp_ranks] FOREIGN KEY ([rank_id]) REFERENCES [dbo].[ranks]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_rsp_permissions] FOREIGN KEY ([spell_permission_id]) REFERENCES [dbo].[spell_permissions]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[grimoire_types](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[name] [nvarchar](255) NOT NULL,
CONSTRAINT [FK_grimoire_types_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[wizards](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[wizard_name] [nvarchar](255) NOT NULL,
[email] [nvarchar](255) NOT NULL,
CONSTRAINT [FK_wizards_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_wizards_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE,
CONSTRAINT [UQ_wizards_school_name] UNIQUE ([school_id], [wizard_name])
);
CREATE TABLE [dbo].[wizard_ranks](
[wizard_id] [uniqueidentifier] NOT NULL,
[rank_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[assigned_at] [datetime2](7) NOT NULL DEFAULT GETDATE(),
PRIMARY KEY ([wizard_id], [rank_id], [tower_id]),
CONSTRAINT [FK_wr_wizards] FOREIGN KEY ([wizard_id]) REFERENCES [dbo].[wizards]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_wr_ranks] FOREIGN KEY ([rank_id]) REFERENCES [dbo].[ranks]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_wr_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[apprentices](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[first_name] [nvarchar](255) NOT NULL,
[last_name] [nvarchar](255) NOT NULL,
[enrollment_date] [date] NOT NULL,
[primary_mentor] [uniqueidentifier] NULL,
[sponsoring_wizard] [uniqueidentifier] NULL,
CONSTRAINT [FK_apprentices_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_apprentices_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_apprentices_mentor] FOREIGN KEY ([primary_mentor]) REFERENCES [dbo].[wizards]([id]),
CONSTRAINT [FK_apprentices_sponsor] FOREIGN KEY ([sponsoring_wizard]) REFERENCES [dbo].[wizards]([id])
);
CREATE TABLE [dbo].[spell_lessons](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[apprentice_id] [uniqueidentifier] NOT NULL,
[instructor_id] [uniqueidentifier] NOT NULL,
[lesson_date] [datetime2](7) NOT NULL,
CONSTRAINT [FK_sl_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_sl_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_sl_apprentices] FOREIGN KEY ([apprentice_id]) REFERENCES [dbo].[apprentices]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_sl_instructors] FOREIGN KEY ([instructor_id]) REFERENCES [dbo].[wizards]([id])
);
CREATE TABLE [dbo].[grimoires](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[apprentice_id] [uniqueidentifier] NOT NULL,
[grimoire_type_id] [uniqueidentifier] NOT NULL,
[author_wizard_id] [uniqueidentifier] NOT NULL,
[content] [nvarchar](max) NOT NULL,
CONSTRAINT [FK_g_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_g_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_g_apprentices] FOREIGN KEY ([apprentice_id]) REFERENCES [dbo].[apprentices]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_g_types] FOREIGN KEY ([grimoire_type_id]) REFERENCES [dbo].[grimoire_types]([id]),
CONSTRAINT [FK_g_authors] FOREIGN KEY ([author_wizard_id]) REFERENCES [dbo].[wizards]([id])
);
CREATE TABLE [dbo].[tuition_scrolls](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[school_id] [uniqueidentifier] NOT NULL,
[tower_id] [uniqueidentifier] NOT NULL,
[apprentice_id] [uniqueidentifier] NOT NULL,
[total_amount] [decimal](10,2) NOT NULL,
[status] [nvarchar](50) NOT NULL,
CONSTRAINT [FK_ts_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_ts_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_ts_apprentices] FOREIGN KEY ([apprentice_id]) REFERENCES [dbo].[apprentices]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[tuition_items](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[tuition_scroll_id] [uniqueidentifier] NOT NULL,
[description] [nvarchar](max) NOT NULL,
[amount] [decimal](10,2) NOT NULL,
CONSTRAINT [FK_ti_scrolls] FOREIGN KEY ([tuition_scroll_id]) REFERENCES [dbo].[tuition_scrolls]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[patron_sponsorships](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[tuition_scroll_id] [uniqueidentifier] NOT NULL,
[patron_house] [nvarchar](255) NOT NULL,
[sponsorship_code] [nvarchar](50) NOT NULL,
[status] [nvarchar](50) NOT NULL,
CONSTRAINT [FK_ps_scrolls] FOREIGN KEY ([tuition_scroll_id]) REFERENCES [dbo].[tuition_scrolls]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[gold_payments](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[tuition_scroll_id] [uniqueidentifier] NOT NULL,
[amount] [decimal](10,2) NOT NULL,
[payment_date] [datetime2](7) NOT NULL DEFAULT GETDATE(),
CONSTRAINT [FK_gp_scrolls] FOREIGN KEY ([tuition_scroll_id]) REFERENCES [dbo].[tuition_scrolls]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[arcane_logs](
[id] [bigint] IDENTITY(1,1) PRIMARY KEY,
[school_id] [uniqueidentifier] NULL,
[wizard_id] [uniqueidentifier] NULL,
[tower_id] [uniqueidentifier] NULL,
[table_name] [nvarchar](255) NOT NULL,
[operation] [nvarchar](50) NOT NULL,
[record_id] [uniqueidentifier] NULL,
[changes] [nvarchar](max) NULL,
[created_at] [datetime2](7) NOT NULL DEFAULT GETDATE(),
CONSTRAINT [FK_al_schools] FOREIGN KEY ([school_id]) REFERENCES [dbo].[schools]([id]) ON DELETE SET NULL,
CONSTRAINT [FK_al_wizards] FOREIGN KEY ([wizard_id]) REFERENCES [dbo].[wizards]([id]) ON DELETE SET NULL,
CONSTRAINT [FK_al_towers] FOREIGN KEY ([tower_id]) REFERENCES [dbo].[towers]([id]) ON DELETE SET NULL
);
`;
const result = await fromSQLServer(sql);
// Should find all 16 tables
const expectedTables = [
'apprentices',
'arcane_logs',
'gold_payments',
'grimoire_types',
'grimoires',
'patron_sponsorships',
'rank_spell_permissions',
'ranks',
'schools',
'spell_lessons',
'spell_permissions',
'towers',
'tuition_items',
'tuition_scrolls',
'wizard_ranks',
'wizards',
];
expect(result.tables).toHaveLength(16);
expect(result.tables.map((t) => t.name).sort()).toEqual(
expectedTables
);
// Verify key relationships exist
const relationships = result.relationships;
// Check some critical relationships
expect(
relationships.some(
(r) =>
r.sourceTable === 'wizards' &&
r.targetTable === 'schools' &&
r.sourceColumn === 'school_id'
)
).toBe(true);
expect(
relationships.some(
(r) =>
r.sourceTable === 'wizard_ranks' &&
r.targetTable === 'wizards' &&
r.sourceColumn === 'wizard_id'
)
).toBe(true);
expect(
relationships.some(
(r) =>
r.sourceTable === 'apprentices' &&
r.targetTable === 'wizards' &&
r.sourceColumn === 'primary_mentor'
)
).toBe(true);
});
});
describe('Enchanted Bazaar Example', () => {
it('should parse the enchanted bazaar example with complex features', async () => {
const sql = `
-- Enchanted Bazaar tables with complex features
CREATE TABLE [dbo].[merchants](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[name] [nvarchar](255) NOT NULL,
[email] [nvarchar](255) NOT NULL,
[created_at] [datetime] DEFAULT GETDATE(),
CONSTRAINT [UQ_merchants_email] UNIQUE ([email])
);
CREATE TABLE [dbo].[artifacts](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[merchant_id] [int] NOT NULL,
[name] [nvarchar](255) NOT NULL,
[price] [decimal](10, 2) NOT NULL CHECK ([price] >= 0),
[enchantment_charges] [int] DEFAULT 0 CHECK ([enchantment_charges] >= 0),
CONSTRAINT [FK_artifacts_merchants] FOREIGN KEY ([merchant_id]) REFERENCES [dbo].[merchants]([id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[trades](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[created_at] [datetime] DEFAULT GETDATE(),
[status] [varchar](50) DEFAULT 'negotiating'
);
CREATE TABLE [dbo].[trade_items](
[trade_id] [int] NOT NULL,
[artifact_id] [int] NOT NULL,
[quantity] [int] NOT NULL CHECK ([quantity] > 0),
[agreed_price] [decimal](10, 2) NOT NULL,
PRIMARY KEY ([trade_id], [artifact_id]),
CONSTRAINT [FK_ti_trades] FOREIGN KEY ([trade_id]) REFERENCES [dbo].[trades]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_ti_artifacts] FOREIGN KEY ([artifact_id]) REFERENCES [dbo].[artifacts]([id])
);
-- Create indexes
CREATE INDEX [IX_artifacts_merchant_id] ON [dbo].[artifacts] ([merchant_id]);
CREATE INDEX [IX_artifacts_price] ON [dbo].[artifacts] ([price] DESC);
CREATE UNIQUE INDEX [UIX_artifacts_name_merchant] ON [dbo].[artifacts] ([name], [merchant_id]);
`;
const result = await fromSQLServer(sql);
// Should parse all tables
expect(result.tables.length).toBeGreaterThanOrEqual(4);
// Check for specific tables
const tableNames = result.tables.map((t) => t.name);
expect(tableNames).toContain('merchants');
expect(tableNames).toContain('artifacts');
expect(tableNames).toContain('trades');
expect(tableNames).toContain('trade_items');
// Check relationships
expect(
result.relationships.some(
(r) =>
r.sourceTable === 'artifacts' &&
r.targetTable === 'merchants'
)
).toBe(true);
expect(
result.relationships.some(
(r) =>
r.sourceTable === 'trade_items' &&
r.targetTable === 'trades'
)
).toBe(true);
// Check indexes were created
const artifactsTable = result.tables.find(
(t) => t.name === 'artifacts'
);
expect(artifactsTable?.indexes.length).toBeGreaterThanOrEqual(2);
expect(
artifactsTable?.indexes.some(
(i) => i.name === 'IX_artifacts_merchant_id'
)
).toBe(true);
expect(
artifactsTable?.indexes.some(
(i) => i.unique && i.name === 'UIX_artifacts_name_merchant'
)
).toBe(true);
});
});
describe('Complex SQL Server Schema Example', () => {
it('should parse complex multi-schema database with various SQL Server features', async () => {
const sql = `
CREATE SCHEMA [magic];
GO
CREATE SCHEMA [inventory];
GO
CREATE SCHEMA [academy];
GO
-- Magic schema tables
CREATE TABLE [magic].[spell_categories](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
[name] [nvarchar](100) NOT NULL,
[description] [nvarchar](max) NULL,
[is_forbidden] [bit] NOT NULL DEFAULT 0,
[created_at] [datetime2](7) NOT NULL DEFAULT SYSDATETIME()
);
CREATE TABLE [magic].[spells](
[id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
[category_id] [uniqueidentifier] NOT NULL,
[name] [nvarchar](200) NOT NULL,
[mana_cost] [smallint] NOT NULL CHECK ([mana_cost] > 0),
[damage_output] [decimal](10,2) NULL,
[cast_time_ms] [int] NOT NULL DEFAULT 1000,
[is_active] [bit] NOT NULL DEFAULT 1,
[metadata] [xml] NULL,
CONSTRAINT [FK_spells_categories] FOREIGN KEY ([category_id])
REFERENCES [magic].[spell_categories]([id]) ON DELETE CASCADE,
CONSTRAINT [UQ_spells_name] UNIQUE ([name])
);
-- Inventory schema tables
CREATE TABLE [inventory].[item_types](
[id] [int] IDENTITY(1,1) PRIMARY KEY,
[type_code] [char](3) NOT NULL UNIQUE,
[type_name] [varchar](50) NOT NULL,
[max_stack_size] [tinyint] NOT NULL DEFAULT 99
);
CREATE TABLE [inventory].[magical_items](
[id] [bigint] IDENTITY(1,1) PRIMARY KEY,
[item_type_id] [int] NOT NULL,
[item_name] [nvarchar](255) NOT NULL,
[rarity] [varchar](20) NOT NULL,
[weight_kg] [float] NOT NULL,
[base_value] [money] NOT NULL,
[enchantment_level] [tinyint] NULL CHECK ([enchantment_level] BETWEEN 0 AND 10),
[discovered_date] [date] NULL,
[discovered_time] [time](7) NULL,
[full_discovered_at] [datetimeoffset](7) NULL,
CONSTRAINT [FK_items_types] FOREIGN KEY ([item_type_id])
REFERENCES [inventory].[item_types]([id])
);
-- Academy schema tables
CREATE TABLE [academy].[courses](
[course_id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[course_code] [nvarchar](10) NOT NULL UNIQUE,
[course_name] [nvarchar](200) NOT NULL,
[credits] [decimal](3,1) NOT NULL,
[prerequisite_spell_id] [uniqueidentifier] NULL,
CONSTRAINT [FK_courses_spells] FOREIGN KEY ([prerequisite_spell_id])
REFERENCES [magic].[spells]([id])
);
CREATE TABLE [academy].[enrollments](
[enrollment_id] [bigint] IDENTITY(1,1) PRIMARY KEY,
[student_id] [uniqueidentifier] NOT NULL,
[course_id] [uniqueidentifier] NOT NULL,
[enrollment_date] [datetime2](0) NOT NULL DEFAULT GETDATE(),
[grade] [decimal](4,2) NULL CHECK ([grade] >= 0 AND [grade] <= 100),
[completion_status] [nvarchar](20) NOT NULL DEFAULT 'enrolled',
CONSTRAINT [FK_enrollments_courses] FOREIGN KEY ([course_id])
REFERENCES [academy].[courses]([course_id]) ON DELETE CASCADE,
CONSTRAINT [UQ_enrollment] UNIQUE ([student_id], [course_id])
);
-- Cross-schema relationships
CREATE TABLE [inventory].[spell_reagents](
[spell_id] [uniqueidentifier] NOT NULL,
[item_id] [bigint] NOT NULL,
[quantity_required] [smallint] NOT NULL DEFAULT 1,
PRIMARY KEY ([spell_id], [item_id]),
CONSTRAINT [FK_reagents_spells] FOREIGN KEY ([spell_id])
REFERENCES [magic].[spells]([id]) ON DELETE CASCADE,
CONSTRAINT [FK_reagents_items] FOREIGN KEY ([item_id])
REFERENCES [inventory].[magical_items]([id]) ON DELETE CASCADE
);
-- Additional indexes
CREATE INDEX [IX_spells_category] ON [magic].[spells] ([category_id]);
CREATE INDEX [IX_items_type_rarity] ON [inventory].[magical_items] ([item_type_id], [rarity]);
CREATE UNIQUE INDEX [UIX_items_name_type] ON [inventory].[magical_items] ([item_name], [item_type_id]);
`;
const result = await fromSQLServer(sql);
// Verify all tables are parsed
expect(result.tables).toHaveLength(7);
// Check schema assignment
expect(
result.tables.filter((t) => t.schema === 'magic')
).toHaveLength(2);
expect(
result.tables.filter((t) => t.schema === 'inventory')
).toHaveLength(3);
expect(
result.tables.filter((t) => t.schema === 'academy')
).toHaveLength(2);
// Verify cross-schema relationships
const crossSchemaRel = result.relationships.find(
(r) => r.sourceTable === 'courses' && r.targetTable === 'spells'
);
expect(crossSchemaRel).toBeDefined();
expect(crossSchemaRel?.sourceSchema).toBe('academy');
expect(crossSchemaRel?.targetSchema).toBe('magic');
// Check various SQL Server data types
const spellsTable = result.tables.find((t) => t.name === 'spells');
expect(
spellsTable?.columns.find((c) => c.name === 'mana_cost')?.type
).toBe('smallint');
expect(
spellsTable?.columns.find((c) => c.name === 'metadata')?.type
).toBe('xml');
const itemsTable = result.tables.find(
(t) => t.name === 'magical_items'
);
expect(
itemsTable?.columns.find((c) => c.name === 'weight_kg')?.type
).toBe('float');
expect(
itemsTable?.columns.find((c) => c.name === 'base_value')?.type
).toBe('money');
expect(
itemsTable?.columns.find((c) => c.name === 'discovered_date')
?.type
).toBe('date');
expect(
itemsTable?.columns.find((c) => c.name === 'discovered_time')
?.type
).toBe('time');
expect(
itemsTable?.columns.find((c) => c.name === 'full_discovered_at')
?.type
).toBe('datetimeoffset');
// Verify IDENTITY columns
const itemTypesTable = result.tables.find(
(t) => t.name === 'item_types'
);
expect(
itemTypesTable?.columns.find((c) => c.name === 'id')?.increment
).toBe(true);
});
});
});

View File

@@ -1,675 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Fantasy Database Import Tests', () => {
it('should parse the magical realm database correctly', async () => {
// Fantasy-themed SQL Server database with multiple schemas
const sql = `
USE [MagicalRealmDB]
GO
/****** Object: Schema [spellcasting] Script Date: 25.7.2025. 9:42:07 ******/
CREATE SCHEMA [spellcasting]
GO
/****** Object: Schema [enchantments] Script Date: 25.7.2025. 9:42:07 ******/
CREATE SCHEMA [enchantments]
GO
/****** Object: Schema [artifacts] Script Date: 25.7.2025. 9:42:07 ******/
CREATE SCHEMA [artifacts]
GO
/****** Object: Schema [wizards] Script Date: 25.7.2025. 9:42:07 ******/
CREATE SCHEMA [wizards]
GO
/****** Object: Table [spellcasting].[Spell] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [spellcasting].[Spell](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[RealmId] [uniqueidentifier] NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedById] [uniqueidentifier] NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[UpdatedBy] [uniqueidentifier] NULL,
[UpdatedAt] [datetime2](7) NULL,
[PowerLevel] [decimal](18, 2) NOT NULL,
[Incantation] [nvarchar](max) NULL,
[ParentId] [uniqueidentifier] NULL,
[Name] [nvarchar](255) NOT NULL,
[Description] [nvarchar](max) NOT NULL,
[RunicInscription] [varchar](max) NULL,
CONSTRAINT [PK_Spell] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [spellcasting].[SpellCasting] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [spellcasting].[SpellCasting](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[RealmId] [uniqueidentifier] NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedById] [uniqueidentifier] NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[UpdatedBy] [uniqueidentifier] NULL,
[UpdatedAt] [datetime2](7) NULL,
[WizardLevel] [int] NOT NULL,
[ManaCost] [decimal](18, 2) NOT NULL,
[CastingTime] [decimal](18, 2) NULL,
[Components] [nvarchar](max) NULL,
[CastingNumber] [int] NULL,
[SuccessRate] [decimal](18, 2) NULL,
[CriticalChance] [decimal](18, 2) NULL,
[ExtendedDuration] [decimal](18, 2) NULL,
[Status] [int] NULL,
[SpellId] [uniqueidentifier] NOT NULL,
[CastingNotes] [nvarchar](max) NULL,
[ParentId] [uniqueidentifier] NULL,
CONSTRAINT [PK_SpellCasting] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [enchantments].[MagicalItem] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [enchantments].[MagicalItem](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[WandId] [uniqueidentifier] NOT NULL,
[EnchanterId] [uniqueidentifier] NOT NULL,
[OrderNumber] [nvarchar](max) NOT NULL,
[EnchantmentDate] [datetime2](7) NOT NULL,
[IsCertified] [bit] NOT NULL,
[CertificationCode] [nvarchar](max) NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedById] [uniqueidentifier] NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[ParentId] [uniqueidentifier] NULL,
[ReasonForAction] [nvarchar](max) NULL,
[EnchantmentLevel] [int] NOT NULL,
CONSTRAINT [PK_MagicalItem] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [enchantments].[EnchantmentFormula] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [enchantments].[EnchantmentFormula](
[Id] [uniqueidentifier] NOT NULL,
[RealmId] [uniqueidentifier] NOT NULL,
[ParentId] [uniqueidentifier] NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedById] [uniqueidentifier] NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[ReasonForAction] [nvarchar](max) NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[FormulaTypeId] [int] NOT NULL,
[Definition] [nvarchar](max) NOT NULL,
[Name] [nvarchar](max) NOT NULL,
[HasMultipleApplications] [bit] NOT NULL,
[StepNumber] [int] NOT NULL,
[Identifier] [int] NOT NULL,
CONSTRAINT [PK_EnchantmentFormula] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [wizards].[Wizard] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [wizards].[Wizard](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedById] [uniqueidentifier] NULL,
[ParentId] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[Title] [nvarchar](255) NULL,
[Biography] [nvarchar](max) NULL,
[SpecialtySchool] [nvarchar](100) NULL,
[PowerLevel] [int] NOT NULL,
[JoinedGuildDate] [datetime2](7) NOT NULL,
[IsActive] [bit] NOT NULL,
[MagicalSignature] [nvarchar](max) NOT NULL,
[TowerId] [uniqueidentifier] NOT NULL,
[MentorId] [uniqueidentifier] NULL,
[SpellbookNotes] [varchar](max) NULL,
CONSTRAINT [PK_Wizard] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
CONSTRAINT [AK_Wizard_HelpId] UNIQUE NONCLUSTERED
(
[HelpId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [wizards].[WizardSpellbook] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [wizards].[WizardSpellbook](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[DeletedByEmail] [nvarchar](max) NULL,
[DeletedByFullName] [nvarchar](max) NULL,
[DeletedById] [uniqueidentifier] NULL,
[SuccessRate] [decimal](18, 2) NOT NULL,
[ManaCostReduction] [decimal](18, 2) NOT NULL,
[CriticalBonus] [decimal](18, 2) NOT NULL,
[PageNumber] [int] NOT NULL,
[WizardId] [uniqueidentifier] NOT NULL,
[TowerId] [uniqueidentifier] NOT NULL,
[ParentId] [uniqueidentifier] NULL,
[ReasonForAction] [nvarchar](max) NULL,
[SpellId] [uniqueidentifier] NOT NULL,
[EnchanterId] [uniqueidentifier] NOT NULL,
[OrderNumber] [nvarchar](max) NOT NULL,
[LearnedDate] [datetime2](7) NOT NULL,
[IsMastered] [bit] NOT NULL,
[MasteryCertificate] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_WizardSpellbook] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [artifacts].[MagicSchool] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [artifacts].[MagicSchool](
[Id] [int] IDENTITY(1,1) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[Value] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_MagicSchool] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [artifacts].[ArtifactType] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [artifacts].[ArtifactType](
[Id] [int] IDENTITY(1,1) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[Name] [nvarchar](max) NOT NULL,
[Key] [nvarchar](max) NOT NULL,
[ItemCategoryId] [int] NOT NULL,
CONSTRAINT [PK_ArtifactType] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [artifacts].[AncientRelic] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [artifacts].[AncientRelic](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[DiscoveryDate] [datetime2](7) NULL,
[VaultId] [uniqueidentifier] NULL,
[AppraiserId] [uniqueidentifier] NULL,
[NumberOfRunes] [int] NULL,
[MagicalAura] [decimal](18, 2) NULL,
[AuraReadingDeviceId] [uniqueidentifier] NULL,
[PowerOutput] [decimal](18, 2) NULL,
[PowerGaugeTypeId] [int] NULL,
[AgeInCenturies] [decimal](18, 2) NULL,
[CarbonDatingDeviceId] [uniqueidentifier] NULL,
[HistoricalEra] [nvarchar](max) NULL,
[EraVerificationMethod] [int] NULL,
[Curse] [nvarchar](max) NULL,
[CurseDetectorId] [uniqueidentifier] NULL,
[CurseStrength] [decimal](18, 2) NULL,
[ProtectionLevel] [int] NULL,
[MagicalResonance] [decimal](18, 2) NULL,
[ResonanceWithAdjustment] [decimal](18, 2) NULL,
[AuthenticityVerified] [bit] NOT NULL,
[VerificationWizardId] [uniqueidentifier] NULL,
[RestorationNeeded] [bit] NOT NULL,
[RestorationCost] [decimal](18, 2) NULL,
[EstimatedValue] [decimal](18, 2) NULL,
[MarketDemand] [decimal](18, 2) NULL,
[ArtifactCatalogId] [uniqueidentifier] NULL,
[OriginRealm] [nvarchar](max) NULL,
[CreatorWizard] [nvarchar](max) NULL,
[LegendaryStatus] [bit] NOT NULL,
[ParentId] [uniqueidentifier] NULL,
[IsSealed] [bit] NOT NULL,
CONSTRAINT [PK_AncientRelic] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
CONSTRAINT [AK_AncientRelic_HelpId] UNIQUE NONCLUSTERED
(
[HelpId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [artifacts].[RelicPowerMeasurements] Script Date: 25.7.2025. 9:42:07 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [artifacts].[RelicPowerMeasurements](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[ParentId] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[MagicalEnergyMeasured] [decimal](31, 15) NOT NULL,
[AuraIntensityMeasured] [decimal](31, 15) NOT NULL,
[ResonanceFrequencyMeasured] [decimal](31, 15) NOT NULL,
[DimensionalFluxMeasured] [decimal](31, 15) NOT NULL,
[MagicalEnergyCorrection] [decimal](31, 15) NULL,
[AuraIntensityCorrection] [decimal](31, 15) NULL,
[ResonanceFrequencyCorrection] [decimal](31, 15) NULL,
[DimensionalFluxCorrection] [decimal](31, 15) NULL,
[MagicalEnergyCalculated] [decimal](31, 15) NULL,
[AuraIntensityCalculated] [decimal](31, 15) NULL,
[ResonanceFrequencyCalculated] [decimal](31, 15) NULL,
[DimensionalFluxCalculated] [decimal](31, 15) NULL,
[MagicalEnergyUncertainty] [decimal](31, 15) NULL,
[AuraIntensityUncertainty] [decimal](31, 15) NULL,
[ResonanceFrequencyUncertainty] [decimal](31, 15) NULL,
[DimensionalFluxUncertainty] [decimal](31, 15) NULL,
[MagicalEnergyDrift] [decimal](31, 15) NULL,
[AuraIntensityDrift] [decimal](31, 15) NULL,
[ResonanceFrequencyDrift] [decimal](31, 15) NULL,
[DimensionalFluxDrift] [decimal](31, 15) NULL,
[AncientRelicId] [uniqueidentifier] NULL,
CONSTRAINT [PK_RelicPowerMeasurements] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
`;
const result = await fromSQLServer(sql);
// Get unique schemas from parsed tables
const foundSchemas = [
...new Set(result.tables.map((t) => t.schema || 'dbo')),
];
// Verify we found tables in multiple schemas
expect(foundSchemas.length).toBeGreaterThan(1);
expect(foundSchemas).toContain('spellcasting');
expect(foundSchemas).toContain('enchantments');
expect(foundSchemas).toContain('wizards');
expect(foundSchemas).toContain('artifacts');
// Check for some specific tables we know should exist
expect(
result.tables.some(
(t) => t.name === 'Spell' && t.schema === 'spellcasting'
)
).toBe(true);
expect(
result.tables.some(
(t) => t.name === 'SpellCasting' && t.schema === 'spellcasting'
)
).toBe(true);
expect(
result.tables.some(
(t) => t.name === 'Wizard' && t.schema === 'wizards'
)
).toBe(true);
// Check data types are handled correctly
const spellTable = result.tables.find(
(t) => t.name === 'Spell' && t.schema === 'spellcasting'
);
expect(spellTable).toBeDefined();
if (spellTable) {
expect(spellTable.columns.find((c) => c.name === 'Id')?.type).toBe(
'uniqueidentifier'
);
expect(
spellTable.columns.find((c) => c.name === 'PowerLevel')?.type
).toBe('decimal');
expect(
spellTable.columns.find((c) => c.name === 'IsDeleted')?.type
).toBe('bit');
expect(
spellTable.columns.find((c) => c.name === 'CreatedAt')?.type
).toBe('datetime2');
// Check nvarchar(max) fields
const incantationField = spellTable.columns.find(
(c) => c.name === 'Incantation'
);
expect(incantationField?.type).toBe('nvarchar');
expect(incantationField?.typeArgs).toBe('max');
// Check varchar(max) fields
const runicField = spellTable.columns.find(
(c) => c.name === 'RunicInscription'
);
expect(runicField?.type).toBe('varchar');
expect(runicField?.typeArgs).toBe('max');
}
// Check IDENTITY columns
const magicSchoolTable = result.tables.find(
(t) => t.name === 'MagicSchool' && t.schema === 'artifacts'
);
expect(magicSchoolTable).toBeDefined();
if (magicSchoolTable) {
const idColumn = magicSchoolTable.columns.find(
(c) => c.name === 'Id'
);
expect(idColumn?.increment).toBe(true);
expect(idColumn?.type).toBe('int');
}
// Check unique constraints converted to indexes
const wizardTable = result.tables.find(
(t) => t.name === 'Wizard' && t.schema === 'wizards'
);
expect(wizardTable).toBeDefined();
if (wizardTable) {
expect(wizardTable.indexes).toHaveLength(1);
expect(wizardTable.indexes[0].unique).toBe(true);
expect(wizardTable.indexes[0].columns).toContain('HelpId');
expect(wizardTable.indexes[0].name).toBe('AK_Wizard_HelpId');
}
});
it('should handle ALTER TABLE ADD CONSTRAINT statements for magical artifacts', async () => {
const sql = `
CREATE TABLE [artifacts].[MagicalArtifact] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Name] [nvarchar](255) NOT NULL,
[PowerLevel] [int] NOT NULL
);
CREATE TABLE [enchantments].[ArtifactEnchantment] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[PrimaryArtifactId] [uniqueidentifier] NOT NULL,
[SecondaryArtifactId] [uniqueidentifier] NOT NULL,
[EnchantmentStrength] [decimal](18, 2) NOT NULL
);
ALTER TABLE [enchantments].[ArtifactEnchantment]
ADD CONSTRAINT [FK_ArtifactEnchantment_Primary]
FOREIGN KEY ([PrimaryArtifactId])
REFERENCES [artifacts].[MagicalArtifact]([Id]);
ALTER TABLE [enchantments].[ArtifactEnchantment]
ADD CONSTRAINT [FK_ArtifactEnchantment_Secondary]
FOREIGN KEY ([SecondaryArtifactId])
REFERENCES [artifacts].[MagicalArtifact]([Id]);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(2);
// Check both foreign keys were parsed
const primaryRel = result.relationships.find(
(r) =>
r.sourceColumn === 'PrimaryArtifactId' &&
r.name === 'FK_ArtifactEnchantment_Primary'
);
expect(primaryRel).toBeDefined();
expect(primaryRel?.sourceTable).toBe('ArtifactEnchantment');
expect(primaryRel?.targetTable).toBe('MagicalArtifact');
const secondaryRel = result.relationships.find(
(r) =>
r.sourceColumn === 'SecondaryArtifactId' &&
r.name === 'FK_ArtifactEnchantment_Secondary'
);
expect(secondaryRel).toBeDefined();
expect(secondaryRel?.sourceTable).toBe('ArtifactEnchantment');
expect(secondaryRel?.targetTable).toBe('MagicalArtifact');
});
it('should handle tables with many columns including nvarchar(max)', async () => {
const sql = `
CREATE TABLE [wizards].[SpellResearchEnvironment](
[Id] [uniqueidentifier] NOT NULL,
[HelpId] [uniqueidentifier] NOT NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedAt] [datetime2](7) NOT NULL,
[CreatedById] [uniqueidentifier] NULL,
[CreatedByUsername] [nvarchar](max) NOT NULL,
[ResearchDate] [datetime2](7) NULL,
[LaboratoryId] [uniqueidentifier] NULL,
[EvaluationCriteriaId] [uniqueidentifier] NULL,
[NumberOfExperiments] [int] NULL,
[ManaLevelStart] [decimal](18, 2) NULL,
[ManaGaugeId] [uniqueidentifier] NULL,
[ManaLevelEnd] [decimal](18, 2) NULL,
[ManaGaugeTypeId] [int] NULL,
[AetherDensityStart] [decimal](18, 2) NULL,
[AetherGaugeId] [uniqueidentifier] NULL,
[AetherDensityEnd] [decimal](18, 2) NULL,
[AetherGaugeTypeId] [int] NULL,
[MagicalFieldStart] [decimal](18, 2) NULL,
[MagicalFieldGaugeId] [uniqueidentifier] NULL,
[MagicalFieldEnd] [decimal](18, 2) NULL,
[MagicalFieldGaugeTypeId] [int] NULL,
[MagicalFieldWithCorrection] [decimal](18, 2) NULL,
[AetherDensityWithCorrection] [decimal](18, 2) NULL,
[ElementalBalanceStart] [decimal](18, 2) NULL,
[ElementalBalanceGaugeId] [uniqueidentifier] NULL,
[ElementalBalanceEnd] [decimal](18, 2) NULL,
[ElementalBalanceGaugeTypeId] [int] NULL,
[ManaLevelWithCorrection] [decimal](18, 2) NULL,
[ElementalBalanceWithCorrection] [decimal](18, 2) NULL,
[SpellResearchId] [uniqueidentifier] NULL,
[AetherDensityValue] [decimal](18, 2) NULL,
[MagicalFieldValue] [decimal](18, 2) NULL,
[ManaLevelValue] [decimal](18, 2) NULL,
[ElementalBalanceValue] [decimal](18, 2) NULL,
[ParentId] [uniqueidentifier] NULL,
[IsLocked] [bit] NOT NULL,
CONSTRAINT [PK_SpellResearchEnvironment] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [AK_SpellResearchEnvironment_HelpId] UNIQUE NONCLUSTERED ([HelpId] ASC)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
// Should have all columns
expect(table.columns.length).toBeGreaterThan(30);
// Check nvarchar(max) handling
expect(
table.columns.find((c) => c.name === 'CreatedByUsername')?.type
).toBe('nvarchar');
// Check decimal precision handling
const decimalColumn = table.columns.find(
(c) => c.name === 'ManaLevelStart'
);
expect(decimalColumn?.type).toBe('decimal');
expect(decimalColumn?.typeArgs).toEqual([18, 2]);
// Check unique constraint was converted to index
expect(table.indexes).toHaveLength(1);
expect(table.indexes[0].name).toBe(
'AK_SpellResearchEnvironment_HelpId'
);
expect(table.indexes[0].unique).toBe(true);
expect(table.indexes[0].columns).toContain('HelpId');
});
it('should handle complex decimal types like decimal(31, 15)', async () => {
const sql = `
CREATE TABLE [artifacts].[RelicPowerCalculatedValues](
[Id] [uniqueidentifier] NOT NULL,
[MagicalEnergyMeasured] [decimal](31, 15) NOT NULL,
[AuraIntensityMeasured] [decimal](31, 15) NOT NULL,
[ResonanceFrequencyMeasured] [decimal](31, 15) NOT NULL,
[DimensionalFluxMeasured] [decimal](31, 15) NOT NULL,
[MagicalEnergyCorrection] [decimal](31, 15) NULL,
[AuraIntensityCorrection] [decimal](31, 15) NULL,
[ResonanceFrequencyCorrection] [decimal](31, 15) NULL,
[DimensionalFluxCorrection] [decimal](31, 15) NULL,
CONSTRAINT [PK_RelicPowerCalculatedValues] PRIMARY KEY CLUSTERED ([Id] ASC)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
// Check high precision decimal handling
const magicalEnergyColumn = table.columns.find(
(c) => c.name === 'MagicalEnergyMeasured'
);
expect(magicalEnergyColumn?.type).toBe('decimal');
expect(magicalEnergyColumn?.typeArgs).toEqual([31, 15]);
});
it('should handle IDENTITY columns in artifact lookup tables', async () => {
const sql = `
CREATE TABLE [artifacts].[SpellComponent](
[Id] [int] IDENTITY(1,1) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[ComponentName] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_SpellComponent] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [artifacts].[RuneType](
[Id] [int] IDENTITY(1,1) NOT NULL,
[IsDeleted] [bit] NOT NULL,
[DeletedAt] [datetime2](7) NULL,
[Name] [nvarchar](max) NOT NULL,
[Symbol] [nvarchar](max) NOT NULL,
[MagicSchoolId] [int] NOT NULL,
CONSTRAINT [PK_RuneType] PRIMARY KEY CLUSTERED ([Id] ASC)
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
// Both tables should have IDENTITY columns
result.tables.forEach((table) => {
const idColumn = table.columns.find((c) => c.name === 'Id');
expect(idColumn?.increment).toBe(true);
expect(idColumn?.type).toBe('int');
});
});
it('should parse all table constraints with complex WITH options', async () => {
const sql = `
CREATE TABLE [dbo].[MagicalRegistry](
[Id] [uniqueidentifier] NOT NULL,
[RegistrationCode] [nvarchar](50) NOT NULL,
[PowerLevel] [int] NOT NULL,
CONSTRAINT [PK_MagicalRegistry] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
CONSTRAINT [UQ_MagicalRegistry_Code] UNIQUE NONCLUSTERED
(
[RegistrationCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
// Primary key should be set
expect(table.columns.find((c) => c.name === 'Id')?.primaryKey).toBe(
true
);
// Unique constraint should be converted to index
expect(table.indexes).toHaveLength(1);
expect(table.indexes[0].unique).toBe(true);
expect(table.indexes[0].columns).toContain('RegistrationCode');
});
});

View File

@@ -1,253 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Foreign Key Relationship Tests', () => {
it('should properly link foreign key relationships with correct table IDs', async () => {
const sql = `
CREATE TABLE [magic].[schools] (
[id] [uniqueidentifier] PRIMARY KEY,
[name] [nvarchar](100) NOT NULL
);
CREATE TABLE [magic].[wizards] (
[id] [uniqueidentifier] PRIMARY KEY,
[school_id] [uniqueidentifier] NOT NULL,
[name] [nvarchar](100) NOT NULL
);
ALTER TABLE [magic].[wizards] WITH CHECK ADD CONSTRAINT [FK_wizards_schools]
FOREIGN KEY ([school_id]) REFERENCES [magic].[schools]([id]);
`;
const result = await fromSQLServer(sql);
// Check tables are parsed
expect(result.tables).toHaveLength(2);
const schoolsTable = result.tables.find((t) => t.name === 'schools');
const wizardsTable = result.tables.find((t) => t.name === 'wizards');
expect(schoolsTable).toBeDefined();
expect(wizardsTable).toBeDefined();
// Check relationship is parsed
expect(result.relationships).toHaveLength(1);
const rel = result.relationships[0];
// Verify the relationship has proper table IDs
expect(rel.sourceTableId).toBe(wizardsTable!.id);
expect(rel.targetTableId).toBe(schoolsTable!.id);
// Verify other relationship properties
expect(rel.sourceTable).toBe('wizards');
expect(rel.targetTable).toBe('schools');
expect(rel.sourceColumn).toBe('school_id');
expect(rel.targetColumn).toBe('id');
expect(rel.sourceSchema).toBe('magic');
expect(rel.targetSchema).toBe('magic');
});
it('should handle cross-schema foreign key relationships', async () => {
const sql = `
CREATE TABLE [users].[accounts] (
[id] [int] PRIMARY KEY,
[username] [nvarchar](50) NOT NULL
);
CREATE TABLE [orders].[purchases] (
[id] [int] PRIMARY KEY,
[account_id] [int] NOT NULL
);
ALTER TABLE [orders].[purchases] ADD CONSTRAINT [FK_purchases_accounts]
FOREIGN KEY ([account_id]) REFERENCES [users].[accounts]([id]);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const rel = result.relationships[0];
const accountsTable = result.tables.find(
(t) => t.name === 'accounts' && t.schema === 'users'
);
const purchasesTable = result.tables.find(
(t) => t.name === 'purchases' && t.schema === 'orders'
);
// Verify cross-schema relationship IDs are properly linked
expect(rel.sourceTableId).toBe(purchasesTable!.id);
expect(rel.targetTableId).toBe(accountsTable!.id);
});
it('should parse complex foreign keys from magical realm database with proper table IDs', async () => {
// Fantasy-themed SQL with multiple schemas and relationships
const sql = `
-- Spell casting schema
CREATE SCHEMA [spellcasting];
GO
-- Create spell table
CREATE TABLE [spellcasting].[Spell] (
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[School] [nvarchar](100) NOT NULL,
[Level] [int] NOT NULL,
[Description] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Spell] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
-- Create spell casting process table
CREATE TABLE [spellcasting].[SpellCastingProcess] (
[Id] [uniqueidentifier] NOT NULL,
[SpellId] [uniqueidentifier] NOT NULL,
[WizardId] [uniqueidentifier] NOT NULL,
[CastingDate] [datetime2](7) NOT NULL,
[SuccessRate] [decimal](18, 2) NOT NULL,
[ManaCost] [int] NOT NULL,
[Notes] [nvarchar](max) NULL,
CONSTRAINT [PK_SpellCastingProcess] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
-- Wizards schema
CREATE SCHEMA [wizards];
GO
-- Create wizard table
CREATE TABLE [wizards].[Wizard] (
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[Title] [nvarchar](100) NULL,
[Level] [int] NOT NULL,
[Specialization] [nvarchar](100) NULL,
CONSTRAINT [PK_Wizard] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
-- Create wizard apprentice table
CREATE TABLE [wizards].[Apprentice] (
[Id] [uniqueidentifier] NOT NULL,
[WizardId] [uniqueidentifier] NOT NULL,
[MentorId] [uniqueidentifier] NOT NULL,
[StartDate] [datetime2](7) NOT NULL,
[EndDate] [datetime2](7) NULL,
CONSTRAINT [PK_Apprentice] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
-- Add foreign key constraints
ALTER TABLE [spellcasting].[SpellCastingProcess]
ADD CONSTRAINT [FK_SpellCastingProcess_Spell]
FOREIGN KEY ([SpellId])
REFERENCES [spellcasting].[Spell]([Id]);
GO
ALTER TABLE [spellcasting].[SpellCastingProcess]
ADD CONSTRAINT [FK_SpellCastingProcess_Wizard]
FOREIGN KEY ([WizardId])
REFERENCES [wizards].[Wizard]([Id]);
GO
ALTER TABLE [wizards].[Apprentice]
ADD CONSTRAINT [FK_Apprentice_Wizard]
FOREIGN KEY ([WizardId])
REFERENCES [wizards].[Wizard]([Id]);
GO
ALTER TABLE [wizards].[Apprentice]
ADD CONSTRAINT [FK_Apprentice_Mentor]
FOREIGN KEY ([MentorId])
REFERENCES [wizards].[Wizard]([Id]);
GO
`;
const result = await fromSQLServer(sql);
// Debug output
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
// Check if we have the expected number of tables and relationships
expect(result.tables).toHaveLength(4);
expect(result.relationships).toHaveLength(4);
// Check a specific relationship we know should exist
const spellCastingRel = result.relationships.find(
(r) =>
r.sourceTable === 'SpellCastingProcess' &&
r.targetTable === 'Spell' &&
r.sourceColumn === 'SpellId'
);
expect(spellCastingRel).toBeDefined();
if (spellCastingRel) {
// Find the corresponding tables
const spellTable = result.tables.find(
(t) => t.name === 'Spell' && t.schema === 'spellcasting'
);
const spellCastingProcessTable = result.tables.find(
(t) =>
t.name === 'SpellCastingProcess' &&
t.schema === 'spellcasting'
);
console.log('SpellCastingProcess relationship:', {
sourceTableId: spellCastingRel.sourceTableId,
targetTableId: spellCastingRel.targetTableId,
spellCastingProcessTableId: spellCastingProcessTable?.id,
spellTableId: spellTable?.id,
isSourceIdValid:
spellCastingRel.sourceTableId ===
spellCastingProcessTable?.id,
isTargetIdValid:
spellCastingRel.targetTableId === spellTable?.id,
});
// Verify the IDs are properly linked
expect(spellCastingRel.sourceTableId).toBeTruthy();
expect(spellCastingRel.targetTableId).toBeTruthy();
expect(spellCastingRel.sourceTableId).toBe(
spellCastingProcessTable!.id
);
expect(spellCastingRel.targetTableId).toBe(spellTable!.id);
}
// Check the apprentice self-referencing relationships
const apprenticeWizardRel = result.relationships.find(
(r) =>
r.sourceTable === 'Apprentice' &&
r.targetTable === 'Wizard' &&
r.sourceColumn === 'WizardId'
);
const apprenticeMentorRel = result.relationships.find(
(r) =>
r.sourceTable === 'Apprentice' &&
r.targetTable === 'Wizard' &&
r.sourceColumn === 'MentorId'
);
expect(apprenticeWizardRel).toBeDefined();
expect(apprenticeMentorRel).toBeDefined();
// Check that all relationships have valid table IDs
const relationshipsWithMissingIds = result.relationships.filter(
(r) =>
!r.sourceTableId ||
!r.targetTableId ||
r.sourceTableId === '' ||
r.targetTableId === ''
);
if (relationshipsWithMissingIds.length > 0) {
console.log(
'Relationships with missing IDs:',
relationshipsWithMissingIds.slice(0, 5)
);
}
expect(relationshipsWithMissingIds).toHaveLength(0);
});
});

View File

@@ -1,198 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromSQLServer } from '../sqlserver';
import { convertToChartDBDiagram } from '../../../common';
import { DatabaseType } from '@/lib/domain/database-type';
describe('SQL Server varchar(max) and nvarchar(max) preservation', () => {
it('should preserve varchar(max) and nvarchar(max) in column definitions', async () => {
const sql = `
CREATE TABLE [dbo].[magical_texts] (
[Id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Title] [nvarchar](255) NOT NULL,
[Description] [nvarchar](max) NULL,
[Content] [varchar](max) NOT NULL,
[ShortNote] [varchar](100) NULL,
[Metadata] [nvarchar](4000) NULL
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.columns).toHaveLength(6);
// Check that max is preserved in typeArgs
const descriptionCol = table.columns.find(
(c) => c.name === 'Description'
);
expect(descriptionCol).toBeDefined();
expect(descriptionCol?.type).toBe('nvarchar');
expect(descriptionCol?.typeArgs).toBe('max');
const contentCol = table.columns.find((c) => c.name === 'Content');
expect(contentCol).toBeDefined();
expect(contentCol?.type).toBe('varchar');
expect(contentCol?.typeArgs).toBe('max');
// Check that numeric lengths are preserved as arrays
const titleCol = table.columns.find((c) => c.name === 'Title');
expect(titleCol).toBeDefined();
expect(titleCol?.type).toBe('nvarchar');
expect(titleCol?.typeArgs).toEqual([255]);
const shortNoteCol = table.columns.find((c) => c.name === 'ShortNote');
expect(shortNoteCol).toBeDefined();
expect(shortNoteCol?.type).toBe('varchar');
expect(shortNoteCol?.typeArgs).toEqual([100]);
});
it('should convert varchar(max) to characterMaximumLength field in diagram', async () => {
const sql = `
CREATE TABLE [dbo].[spell_scrolls] (
[Id] [int] IDENTITY(1,1) PRIMARY KEY,
[SpellName] [nvarchar](50) NOT NULL,
[Incantation] [nvarchar](max) NOT NULL,
[Instructions] [varchar](max) NULL,
[PowerLevel] [decimal](10, 2) NOT NULL
);
`;
const result = await fromSQLServer(sql);
const diagram = convertToChartDBDiagram(
result,
DatabaseType.SQL_SERVER,
DatabaseType.SQL_SERVER
);
expect(diagram.tables).toBeDefined();
expect(diagram.tables).toHaveLength(1);
const table = diagram.tables![0];
// Check that 'max' is preserved in characterMaximumLength
const incantationField = table.fields.find(
(f) => f.name === 'Incantation'
);
expect(incantationField).toBeDefined();
expect(incantationField?.characterMaximumLength).toBe('max');
const instructionsField = table.fields.find(
(f) => f.name === 'Instructions'
);
expect(instructionsField).toBeDefined();
expect(instructionsField?.characterMaximumLength).toBe('max');
// Check that numeric lengths are preserved
const spellNameField = table.fields.find((f) => f.name === 'SpellName');
expect(spellNameField).toBeDefined();
expect(spellNameField?.characterMaximumLength).toBe('50');
// Check decimal precision/scale
const powerLevelField = table.fields.find(
(f) => f.name === 'PowerLevel'
);
expect(powerLevelField).toBeDefined();
expect(powerLevelField?.precision).toBe(10);
expect(powerLevelField?.scale).toBe(2);
});
it('should handle mixed varchar types with schema and relationships', async () => {
const sql = `
CREATE TABLE [content].[authors] (
[Id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[Name] [nvarchar](100) NOT NULL,
[Bio] [nvarchar](max) NULL
);
CREATE TABLE [content].[books] (
[Id] [uniqueidentifier] PRIMARY KEY DEFAULT NEWID(),
[AuthorId] [uniqueidentifier] NOT NULL,
[Title] [nvarchar](500) NOT NULL,
[Summary] [nvarchar](max) NULL,
[FullText] [varchar](max) NOT NULL,
[ISBN] [varchar](13) NULL,
CONSTRAINT [FK_books_authors] FOREIGN KEY ([AuthorId]) REFERENCES [content].[authors]([Id])
);
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(2);
// Check authors table
const authorsTable = result.tables.find((t) => t.name === 'authors');
expect(authorsTable).toBeDefined();
const bioCol = authorsTable?.columns.find((c) => c.name === 'Bio');
expect(bioCol?.typeArgs).toBe('max');
// Check books table
const booksTable = result.tables.find((t) => t.name === 'books');
expect(booksTable).toBeDefined();
const summaryCol = booksTable?.columns.find(
(c) => c.name === 'Summary'
);
expect(summaryCol?.typeArgs).toBe('max');
const fullTextCol = booksTable?.columns.find(
(c) => c.name === 'FullText'
);
expect(fullTextCol?.typeArgs).toBe('max');
const isbnCol = booksTable?.columns.find((c) => c.name === 'ISBN');
expect(isbnCol?.typeArgs).toEqual([13]);
// Verify relationship is preserved
expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].sourceTable).toBe('books');
expect(result.relationships[0].targetTable).toBe('authors');
});
it('should handle complex table with various SQL Server features including varchar(max)', async () => {
const sql = `
CREATE TABLE [reporting].[wizard_performance](\
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[WizardId] [uniqueidentifier] NOT NULL,
[EvaluationDate] [datetime2](7) NOT NULL,
[PerformanceScore] [decimal](5, 2) NOT NULL,
[Comments] [nvarchar](max) NULL,
[DetailedReport] [varchar](max) NULL,
[Signature] [varbinary](max) NULL,
[ReviewerNotes] [text] NULL,
[IsActive] [bit] NOT NULL DEFAULT 1,
CONSTRAINT [PK_wizard_performance] PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];
`;
const result = await fromSQLServer(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
// Check varchar(max) columns
const commentsCol = table.columns.find((c) => c.name === 'Comments');
expect(commentsCol?.type).toBe('nvarchar');
expect(commentsCol?.typeArgs).toBe('max');
const reportCol = table.columns.find(
(c) => c.name === 'DetailedReport'
);
expect(reportCol?.type).toBe('varchar');
expect(reportCol?.typeArgs).toBe('max');
// Note: varbinary(max) should also be preserved but might need special handling
const signatureCol = table.columns.find((c) => c.name === 'Signature');
expect(signatureCol?.type).toBe('varbinary');
// varbinary(max) handling might differ
// Check other column types
const scoreCol = table.columns.find(
(c) => c.name === 'PerformanceScore'
);
expect(scoreCol?.typeArgs).toEqual([5, 2]);
const idCol = table.columns.find((c) => c.name === 'Id');
expect(idCol?.increment).toBe(true);
});
});

View File

@@ -7,126 +7,111 @@ import type {
SQLForeignKey,
SQLASTNode,
} from '../../common';
import { buildSQLFromAST } from '../../common';
import { DatabaseType } from '@/lib/domain/database-type';
import type {
TableReference,
ColumnReference,
ColumnDefinition,
ConstraintDefinition,
CreateTableStatement,
CreateIndexStatement,
AlterTableStatement,
} from './sqlserver-common';
import {
parserOpts,
extractColumnName,
getTypeArgs,
findTableWithSchemaSupport,
} from './sqlserver-common';
/**
* Helper function to safely build SQL from AST nodes, handling null/undefined/invalid cases
*/
function safelyBuildSQLFromAST(ast: unknown): string | undefined {
if (!ast) return undefined;
// Make sure it's a valid AST node with a 'type' property
if (typeof ast === 'object' && ast !== null && 'type' in ast) {
return buildSQLFromAST(ast as SQLASTNode, DatabaseType.SQL_SERVER);
}
// Return string representation for non-AST objects
if (ast !== null && (typeof ast === 'string' || typeof ast === 'number')) {
return String(ast);
}
return undefined;
}
/**
* Preprocess SQL Server script to remove or modify parts that the parser can't handle
*/
function preprocessSQLServerScript(sqlContent: string): string {
// 1. Remove USE statements
sqlContent = sqlContent.replace(/USE\s+\[[^\]]+\]\s*;?/gi, '');
// 2. Remove SET statements
sqlContent = sqlContent.replace(/SET\s+\w+\s+\w+\s*;?/gi, '');
// 3. Remove GO statements (batch separators)
sqlContent = sqlContent.replace(/\bGO\b/gi, ';');
// 4. Remove CREATE SCHEMA statements
sqlContent = sqlContent.replace(/CREATE\s+SCHEMA\s+\[[^\]]+\]\s*;?/gi, '');
// 5. Remove IF NOT EXISTS ... BEGIN ... END blocks
// 1. Remove IF NOT EXISTS ... BEGIN ... END blocks (typically used for schema creation)
sqlContent = sqlContent.replace(
/IF\s+NOT\s+EXISTS\s*\([^)]+\)\s*BEGIN\s+[^;]+;\s*END;?/gi,
''
);
// 6. Remove any EXEC statements
// 2. Remove any GO statements (batch separators)
sqlContent = sqlContent.replace(/\bGO\b/gi, ';');
// 3. Remove any EXEC statements
sqlContent = sqlContent.replace(/EXEC\s*\([^)]+\)\s*;?/gi, '');
sqlContent = sqlContent.replace(/EXEC\s+[^;]+;/gi, '');
// 7. Replace any remaining procedural code blocks
// 4. Replace any remaining procedural code blocks that might cause issues
sqlContent = sqlContent.replace(
/BEGIN\s+TRANSACTION|COMMIT\s+TRANSACTION|ROLLBACK\s+TRANSACTION/gi,
'-- $&'
);
// 8. Remove square brackets (SQL Server specific)
sqlContent = sqlContent.replace(/\[/g, '');
sqlContent = sqlContent.replace(/\]/g, '');
// 5. Special handling for CREATE TABLE with reserved keywords as column names
// Find CREATE TABLE statements
const createTablePattern =
/CREATE\s+TABLE\s+\[?([^\]]*)\]?\.?\[?([^\]]*)\]?\s*\(([^;]*)\)/gi;
// 9. Remove ON PRIMARY and TEXTIMAGE_ON PRIMARY clauses
sqlContent = sqlContent.replace(
/ON\s+PRIMARY(\s+TEXTIMAGE_ON\s+PRIMARY)?/gi,
''
createTablePattern,
(_, schema, tableName, columnDefs) => {
// Process column definitions to rename problematic columns
let processedColumnDefs = columnDefs;
// Replace any column named "column" with "column_name"
processedColumnDefs = processedColumnDefs.replace(
/\[column\]/gi,
'[column_name]'
);
// Replace any column named "int" with "int_col"
processedColumnDefs = processedColumnDefs.replace(
/\[int\]/gi,
'[int_col]'
);
// Replace any column named "time" with "time_col"
processedColumnDefs = processedColumnDefs.replace(
/\[time\]/gi,
'[time_col]'
);
// Replace any column named "order" with "order_column"
processedColumnDefs = processedColumnDefs.replace(
/\[order\]/gi,
'[order_column]'
);
// Rebuild the CREATE TABLE statement
return `CREATE TABLE [${schema || 'dbo'}].[${tableName}] (${processedColumnDefs})`;
}
);
// 10. Remove WITH options from constraints
sqlContent = sqlContent.replace(/WITH\s*\([^)]+\)/gi, '');
// 11. Handle default value expressions with functions
sqlContent = sqlContent.replace(/DEFAULT\s+NEWID\(\)/gi, "DEFAULT 'newid'");
sqlContent = sqlContent.replace(
/DEFAULT\s+NEWSEQUENTIALID\(\)/gi,
"DEFAULT 'newsequentialid'"
);
sqlContent = sqlContent.replace(
/DEFAULT\s+GETDATE\(\)/gi,
"DEFAULT 'getdate'"
);
sqlContent = sqlContent.replace(
/DEFAULT\s+SYSDATETIME\(\)/gi,
"DEFAULT 'sysdatetime'"
);
// Don't replace numeric defaults or simple values
// 6. Handle default value expressions with functions - replace with simpler defaults
sqlContent = sqlContent.replace(/DEFAULT\s+'\([^)]+\)'/gi, "DEFAULT '0'");
// Only replace function calls in DEFAULT, not numeric literals
sqlContent = sqlContent.replace(
/DEFAULT\s+(\w+)\s*\([^)]*\)/gi,
"DEFAULT '0'"
);
sqlContent = sqlContent.replace(/DEFAULT\s+\([^)]+\)/gi, 'DEFAULT 0');
// 12. Replace SQL Server specific data types with standard types
// Note: We preserve varchar(max) and nvarchar(max) for accurate export
sqlContent = sqlContent.replace(/\buniqueid\b/gi, 'uniqueidentifier'); // Fix common typo
sqlContent = sqlContent.replace(
/\bdatetime2\s*\(\s*\d+\s*\)/gi,
'datetime2'
);
sqlContent = sqlContent.replace(/\btime\s*\(\s*\d+\s*\)/gi, 'time');
sqlContent = sqlContent.replace(
/\bdatetimeoffset\s*\(\s*\d+\s*\)/gi,
'datetimeoffset'
);
// 13. Handle IDENTITY columns - convert to a simpler format
sqlContent = sqlContent.replace(
/IDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\)/gi,
'AUTO_INCREMENT'
);
sqlContent = sqlContent.replace(/IDENTITY/gi, 'AUTO_INCREMENT');
// 14. Replace CHECK constraints with comments (parser doesn't handle well)
sqlContent = sqlContent.replace(
/CHECK\s*\([^)]+\)/gi,
'/* CHECK CONSTRAINT */'
);
// 15. Handle FOREIGN KEY constraints within CREATE TABLE
// Convert inline foreign key syntax to be more parser-friendly
sqlContent = sqlContent.replace(
/(\w+)\s+(\w+(?:\s*\(\s*\d+(?:\s*,\s*\d+)?\s*\))?)\s+(?:NOT\s+NULL\s+)?FOREIGN\s+KEY\s+REFERENCES\s+(\w+)\.?(\w+)\s*\((\w+)\)/gi,
'$1 $2 /* FK TO $3.$4($5) */'
);
// Handle standalone FOREIGN KEY constraints
sqlContent = sqlContent.replace(
/CONSTRAINT\s+(\w+)\s+FOREIGN\s+KEY\s*\((\w+)\)\s+REFERENCES\s+(\w+)\.?(\w+)?\s*\((\w+)\)(?:\s+ON\s+DELETE\s+(\w+))?(?:\s+ON\s+UPDATE\s+(\w+))?/gi,
'/* CONSTRAINT $1 FK($2) REF $3.$4($5) */'
);
// 16. Split into individual statements to handle them separately
// 7. Split into individual statements to handle them separately
const statements = sqlContent
.split(';')
.filter((stmt) => stmt.trim().length > 0);
@@ -135,27 +120,30 @@ function preprocessSQLServerScript(sqlContent: string): string {
const filteredStatements = statements.filter((stmt) => {
const trimmedStmt = stmt.trim().toUpperCase();
return (
trimmedStmt.includes('CREATE TABLE') ||
trimmedStmt.includes('CREATE UNIQUE INDEX') ||
trimmedStmt.includes('CREATE INDEX') ||
trimmedStmt.includes('ALTER TABLE')
trimmedStmt.startsWith('CREATE TABLE') ||
trimmedStmt.startsWith('CREATE UNIQUE INDEX') ||
trimmedStmt.startsWith('CREATE INDEX') ||
trimmedStmt.startsWith('ALTER TABLE')
);
});
return filteredStatements.join(';\n') + ';';
return filteredStatements.join(';') + ';';
}
/**
* Manual parsing of ALTER TABLE ADD CONSTRAINT statements
* This is a fallback for when the node-sql-parser fails to properly parse the constraints
*/
function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
function parseAlterTableAddConstraint(statements: string[]): {
fkData: SQLForeignKey[];
tableMap: Record<string, string>;
} {
const fkData: SQLForeignKey[] = [];
const tableMap: Record<string, string> = {};
// Regular expressions to extract information from ALTER TABLE statements
// Handle multi-line ALTER TABLE statements
const alterTableRegex =
/ALTER\s+TABLE\s+\[?([^\]]*)\]?\.?\[?([^\]]*)\]?\s+(?:WITH\s+CHECK\s+)?ADD\s+CONSTRAINT\s+\[?([^\]]*)\]?\s+FOREIGN\s+KEY\s*\(\[?([^\]]*)\]?\)\s*REFERENCES\s+\[?([^\]]*)\]?\.?\[?([^\]]*)\]?\s*\(\[?([^\]]*)\]?\)/is;
/ALTER\s+TABLE\s+\[?([^\]]*)\]?\.?\[?([^\]]*)\]?\s+ADD\s+CONSTRAINT\s+\[?([^\]]*)\]?\s+FOREIGN\s+KEY\s*\(\[?([^\]]*)\]?\)\s+REFERENCES\s+\[?([^\]]*)\]?\.?\[?([^\]]*)\]?\s*\(\[?([^\]]*)\]?\)/i;
for (const stmt of statements) {
const match = stmt.match(alterTableRegex);
@@ -171,6 +159,18 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
targetColumn,
] = match;
// Generate IDs for tables if they don't already exist
const sourceTableKey = `${sourceSchema}.${sourceTable}`;
const targetTableKey = `${targetSchema}.${targetTable}`;
if (!tableMap[sourceTableKey]) {
tableMap[sourceTableKey] = generateId();
}
if (!tableMap[targetTableKey]) {
tableMap[targetTableKey] = generateId();
}
fkData.push({
name: constraintName,
sourceTable: sourceTable,
@@ -179,13 +179,13 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
targetTable: targetTable,
targetSchema: targetSchema,
targetColumn: targetColumn,
sourceTableId: '', // Will be filled by linkRelationships
targetTableId: '', // Will be filled by linkRelationships
sourceTableId: tableMap[sourceTableKey],
targetTableId: tableMap[targetTableKey],
});
}
}
return fkData;
return { fkData, tableMap };
}
/**
@@ -267,239 +267,6 @@ function normalizeSQLServerDataType(dataType: string): string {
}
}
/**
* Manual parsing of CREATE TABLE statements when node-sql-parser fails
*/
function parseCreateTableManually(
statement: string,
tables: SQLTable[],
tableMap: Record<string, string>,
relationships: SQLForeignKey[]
): void {
// Extract table name and schema (handling square brackets)
const tableMatch = statement.match(
/CREATE\s+TABLE\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(/i
);
if (!tableMatch) return;
const [, schema = 'dbo', tableName] = tableMatch;
// Generate table ID
const tableId = generateId();
const tableKey = `${schema}.${tableName}`;
tableMap[tableKey] = tableId;
// Extract column definitions
const columns: SQLColumn[] = [];
const indexes: SQLIndex[] = [];
// Find the content between the parentheses
const tableContentMatch = statement.match(
/CREATE\s+TABLE\s+[^(]+\(([\s\S]*)\)\s*(?:ON\s+|$)/i
);
if (!tableContentMatch) return;
const tableContent = tableContentMatch[1];
// Split table content by commas but not within parentheses
const parts = [];
let current = '';
let parenDepth = 0;
for (let i = 0; i < tableContent.length; i++) {
const char = tableContent[i];
if (char === '(') parenDepth++;
else if (char === ')') parenDepth--;
else if (char === ',' && parenDepth === 0) {
parts.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.trim()) parts.push(current.trim());
// Process each part (column or constraint)
for (const part of parts) {
// Handle constraint definitions
if (part.match(/^\s*CONSTRAINT/i)) {
// Parse constraints
const constraintMatch = part.match(
/CONSTRAINT\s+\[?(\w+)\]?\s+(PRIMARY\s+KEY|UNIQUE|FOREIGN\s+KEY)/i
);
if (constraintMatch) {
const [, constraintName, constraintType] = constraintMatch;
if (constraintType.match(/PRIMARY\s+KEY/i)) {
// Extract columns from PRIMARY KEY constraint - handle multi-line format
const pkColumnsMatch = part.match(
/PRIMARY\s+KEY(?:\s+CLUSTERED)?\s*\(([\s\S]+?)\)/i
);
if (pkColumnsMatch) {
const pkColumns = pkColumnsMatch[1]
.split(',')
.map((c) =>
c
.trim()
.replace(/\[|\]|\s+(ASC|DESC)/gi, '')
.trim()
);
pkColumns.forEach((col) => {
const column = columns.find((c) => c.name === col);
if (column) column.primaryKey = true;
});
}
} else if (constraintType === 'UNIQUE') {
// Extract columns from UNIQUE constraint
const uniqueColumnsMatch = part.match(
/UNIQUE(?:\s+NONCLUSTERED)?\s*\(([\s\S]+?)\)/i
);
if (uniqueColumnsMatch) {
const uniqueColumns = uniqueColumnsMatch[1]
.split(',')
.map((c) =>
c
.trim()
.replace(/\[|\]|\s+(ASC|DESC)/gi, '')
.trim()
);
indexes.push({
name: constraintName,
columns: uniqueColumns,
unique: true,
});
}
} else if (constraintType.match(/FOREIGN\s+KEY/i)) {
// Parse foreign key constraint
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: constraintName,
sourceTable: tableName,
sourceSchema: schema,
sourceColumn: sourceCol
.trim()
.replace(/\[|\]/g, ''),
targetTable: targetTable,
targetSchema: targetSchema,
targetColumn: targetCol
.trim()
.replace(/\[|\]/g, ''),
sourceTableId: tableId,
targetTableId: '', // Will be filled later
});
}
}
}
continue;
}
// Parse column definition - handle both numeric args and 'max'
// Handle brackets around column names and types
let columnMatch = part.match(
/^\s*\[?(\w+)\]?\s+\[?(\w+)\]?(?:\s*\(\s*([\d,\s]+|max)\s*\))?(.*)$/i
);
// If no match, try pattern for preprocessed types without parentheses
if (!columnMatch) {
columnMatch = part.match(/^\s*(\w+)\s+(\w+)\s+([\d,\s]+)\s+(.*)$/i);
}
if (columnMatch) {
const [, colName, baseType, typeArgs, rest] = columnMatch;
if (
colName &&
!colName.match(/^(PRIMARY|FOREIGN|UNIQUE|CHECK)$/i)
) {
// Check for inline foreign key
const inlineFkMatch = rest.match(
/FOREIGN\s+KEY\s+REFERENCES\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i
);
if (inlineFkMatch) {
const [, targetSchema = 'dbo', targetTable, targetCol] =
inlineFkMatch;
relationships.push({
name: `FK_${tableName}_${colName}`,
sourceTable: tableName,
sourceSchema: schema,
sourceColumn: colName,
targetTable: targetTable,
targetSchema: targetSchema,
targetColumn: targetCol.trim().replace(/\[|\]/g, ''),
sourceTableId: tableId,
targetTableId: '', // Will be filled later
});
}
const isPrimaryKey = !!rest.match(/PRIMARY\s+KEY/i);
const isNotNull = !!rest.match(/NOT\s+NULL/i);
const isIdentity = !!rest.match(
/IDENTITY(?:\s*\(\s*\d+\s*,\s*\d+\s*\))?/i
);
const isUnique = !!rest.match(/UNIQUE/i);
const defaultMatch = rest.match(/DEFAULT\s+([^,]+)/i);
// Parse type arguments
let parsedTypeArgs: number[] | string | undefined;
if (typeArgs) {
if (typeArgs.toLowerCase() === 'max') {
// Preserve 'max' keyword for varchar/nvarchar types
parsedTypeArgs = 'max';
} else {
// Parse numeric args
parsedTypeArgs = typeArgs
.split(',')
.map((arg) => parseInt(arg.trim()));
}
}
const column: SQLColumn = {
name: colName,
type: normalizeSQLServerDataType(baseType.trim()),
nullable: !isNotNull && !isPrimaryKey,
primaryKey: isPrimaryKey,
unique: isUnique,
increment: isIdentity,
default: defaultMatch ? defaultMatch[1].trim() : undefined,
};
// Add type arguments if present
if (parsedTypeArgs) {
if (typeof parsedTypeArgs === 'string') {
// For 'max' keyword
column.typeArgs = parsedTypeArgs;
} else if (parsedTypeArgs.length > 0) {
// For numeric arguments
column.typeArgs = parsedTypeArgs;
}
}
columns.push(column);
}
}
}
// Add the table
tables.push({
id: tableId,
name: tableName,
schema: schema,
columns,
indexes,
order: tables.length,
});
}
/**
* Parse SQL Server DDL scripts and extract database structure
* @param sqlContent SQL Server DDL content as string
@@ -513,130 +280,83 @@ export async function fromSQLServer(
const tableMap: Record<string, string> = {}; // Maps table name to its ID
try {
// First, handle ALTER TABLE statements for foreign keys
// Split by GO or semicolon for SQL Server
const statements = sqlContent
.split(/(?:GO\s*$|;\s*$)/im)
.filter((stmt) => stmt.trim().length > 0);
// Preprocess the SQL content to handle T-SQL specific syntax
const preprocessedSQL = preprocessSQLServerScript(sqlContent);
const statements = sqlContent
.split(';')
.filter((stmt) => stmt.trim().length > 0);
const alterTableStatements = statements.filter(
(stmt) =>
stmt.trim().toUpperCase().includes('ALTER TABLE') &&
stmt.trim().toUpperCase().startsWith('ALTER TABLE') &&
stmt.includes('FOREIGN KEY')
);
if (alterTableStatements.length > 0) {
const fkData = parseAlterTableAddConstraint(alterTableStatements);
const { fkData, tableMap: fkTableMap } =
parseAlterTableAddConstraint(alterTableStatements);
// Store table IDs from alter statements
Object.assign(tableMap, fkTableMap);
// Store foreign key relationships for later processing
relationships.push(...fkData);
}
// Parse CREATE TABLE statements manually first
const createTableStatements = statements.filter((stmt) =>
stmt.trim().toUpperCase().includes('CREATE TABLE')
);
for (const stmt of createTableStatements) {
parseCreateTableManually(stmt, tables, tableMap, relationships);
}
// Preprocess the SQL content for node-sql-parser
const preprocessedSQL = preprocessSQLServerScript(sqlContent);
// Try to use node-sql-parser for additional parsing
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
let ast;
try {
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
let ast;
try {
ast = parser.astify(preprocessedSQL, parserOpts);
} catch {
// Fallback: Try to parse each statement individually
const statements = preprocessedSQL
.split(';')
.filter((stmt) => stmt.trim().length > 0);
ast = [];
ast = parser.astify(preprocessedSQL, parserOpts);
} catch {
// Fallback: Try to parse each statement individually
const statements = preprocessedSQL
.split(';')
.filter((stmt) => stmt.trim().length > 0);
ast = [];
for (const stmt of statements) {
try {
const stmtAst = parser.astify(stmt + ';', parserOpts);
if (Array.isArray(stmtAst)) {
ast.push(...stmtAst);
} else if (stmtAst) {
ast.push(stmtAst);
}
} catch {
// Skip statements that can't be parsed
for (const stmt of statements) {
try {
const stmtAst = parser.astify(stmt + ';', parserOpts);
if (Array.isArray(stmtAst)) {
ast.push(...stmtAst);
} else if (stmtAst) {
ast.push(stmtAst);
}
} catch {
// Skip statements that can't be parsed
}
}
if (Array.isArray(ast) && ast.length > 0) {
// Process each statement
(ast as unknown as SQLASTNode[]).forEach((stmt) => {
// Process CREATE INDEX statements
if (stmt.type === 'create' && stmt.keyword === 'index') {
processCreateIndex(
stmt as CreateIndexStatement,
tables
);
}
// Process ALTER TABLE statements for non-FK constraints
else if (
stmt.type === 'alter' &&
stmt.keyword === 'table'
) {
processAlterTable(
stmt as AlterTableStatement,
tables,
relationships
);
}
});
}
} catch (parserError) {
// If parser fails completely, continue with manual parsing results
console.warn(
'node-sql-parser failed, using manual parsing only:',
parserError
);
}
// Parse CREATE INDEX statements manually
const createIndexStatements = statements.filter(
(stmt) =>
stmt.trim().toUpperCase().includes('CREATE') &&
stmt.trim().toUpperCase().includes('INDEX')
);
if (!Array.isArray(ast) || ast.length === 0) {
throw new Error('Failed to parse SQL DDL - Empty or invalid AST');
}
for (const stmt of createIndexStatements) {
const indexMatch = stmt.match(
/CREATE\s+(UNIQUE\s+)?INDEX\s+\[?(\w+)\]?\s+ON\s+(?:\[?(\w+)\]?\.)??\[?(\w+)\]?\s*\(([^)]+)\)/i
);
if (indexMatch) {
const [
,
unique,
indexName,
schema = 'dbo',
tableName,
columnsStr,
] = indexMatch;
const table = tables.find(
(t) => t.name === tableName && t.schema === schema
// Process each statement
(ast as unknown as SQLASTNode[]).forEach((stmt) => {
// Process CREATE TABLE statements
if (stmt.type === 'create' && stmt.keyword === 'table') {
processCreateTable(
stmt as CreateTableStatement,
tables,
tableMap,
relationships
);
if (table) {
const columns = columnsStr
.split(',')
.map((c) => c.trim().replace(/\[|\]/g, ''));
table.indexes.push({
name: indexName,
columns,
unique: !!unique,
});
}
}
}
// Process CREATE INDEX statements
else if (stmt.type === 'create' && stmt.keyword === 'index') {
processCreateIndex(stmt as CreateIndexStatement, tables);
}
// Process ALTER TABLE statements
else if (stmt.type === 'alter' && stmt.keyword === 'table') {
processAlterTable(
stmt as AlterTableStatement,
tables,
relationships
);
}
});
// Link relationships to ensure all targetTableId and sourceTableId fields are filled
const validRelationships = linkRelationships(
@@ -659,6 +379,233 @@ export async function fromSQLServer(
}
}
/**
* Process a CREATE TABLE statement
*/
function processCreateTable(
stmt: CreateTableStatement,
tables: SQLTable[],
tableMap: Record<string, string>,
relationships: SQLForeignKey[]
): void {
let tableName = '';
let schemaName = '';
// Extract table name and schema
if (stmt.table && typeof stmt.table === 'object') {
// Handle array of tables if needed
if (Array.isArray(stmt.table) && stmt.table.length > 0) {
const tableObj = stmt.table[0];
tableName = tableObj.table || '';
// SQL Server uses 'schema' or 'db' field
schemaName = tableObj.schema || tableObj.db || '';
} else {
// Direct object reference
const tableObj = stmt.table as TableReference;
tableName = tableObj.table || '';
schemaName = tableObj.schema || tableObj.db || '';
}
}
if (!tableName) {
return;
}
// If no schema specified, use default 'dbo' schema for SQL Server
if (!schemaName) {
schemaName = 'dbo';
}
// Generate a unique ID for the table
const tableId = generateId();
const tableKey = `${schemaName ? schemaName + '.' : ''}${tableName}`;
tableMap[tableKey] = tableId;
// Process table columns
const columns: SQLColumn[] = [];
const indexes: SQLIndex[] = [];
if (stmt.create_definitions && Array.isArray(stmt.create_definitions)) {
stmt.create_definitions.forEach(
(def: ColumnDefinition | ConstraintDefinition) => {
if (def.resource === 'column') {
// Process column definition
const columnDef = def as ColumnDefinition;
const columnName = extractColumnName(columnDef.column);
const rawDataType = columnDef.definition?.dataType || '';
const normalizedDataType =
normalizeSQLServerDataType(rawDataType);
if (columnName) {
// Check for SQL Server specific column properties
const isPrimaryKey =
columnDef.primary_key === 'primary key';
// For SQL Server, check for IDENTITY property in suffixes
const hasIdentity = columnDef.definition?.suffix?.some(
(suffix) =>
suffix.toLowerCase().includes('identity')
);
columns.push({
name: columnName,
type: normalizedDataType,
nullable: columnDef.nullable?.type !== 'not null',
primaryKey: isPrimaryKey,
unique: columnDef.unique === 'unique',
typeArgs: getTypeArgs(columnDef.definition),
default: columnDef.default_val
? safelyBuildSQLFromAST(columnDef.default_val)
: undefined,
increment: hasIdentity,
});
}
} else if (def.resource === 'constraint') {
// Handle constraint definitions
const constraintDef = def as ConstraintDefinition;
// Handle PRIMARY KEY constraints
if (constraintDef.constraint_type === 'primary key') {
if (Array.isArray(constraintDef.definition)) {
// Extract column names from primary key constraint
for (const colDef of constraintDef.definition) {
if (
colDef &&
typeof colDef === 'object' &&
'type' in colDef &&
colDef.type === 'column_ref' &&
'column' in colDef
) {
const pkColumnName = extractColumnName(
colDef as ColumnReference
);
// Find and mark the column as primary key
const column = columns.find(
(col) => col.name === pkColumnName
);
if (column) {
column.primaryKey = true;
}
}
}
}
}
// Handle UNIQUE constraints
else if (constraintDef.constraint_type === 'unique') {
if (Array.isArray(constraintDef.definition)) {
const uniqueColumns: string[] = [];
// Extract column names from unique constraint
for (const colDef of constraintDef.definition) {
if (
colDef &&
typeof colDef === 'object' &&
'type' in colDef &&
colDef.type === 'column_ref' &&
'column' in colDef
) {
const uniqueColumnName = extractColumnName(
colDef as ColumnReference
);
uniqueColumns.push(uniqueColumnName);
}
}
// Add as an index
if (uniqueColumns.length > 0) {
indexes.push({
name:
constraintDef.constraint ||
`unique_${tableName}_${uniqueColumns.join('_')}`,
columns: uniqueColumns,
unique: true,
});
}
}
}
// Handle FOREIGN KEY constraints
else if (
constraintDef.constraint_type === 'foreign key' &&
constraintDef.reference
) {
const reference = constraintDef.reference;
if (
reference &&
reference.table &&
reference.columns &&
reference.columns.length > 0
) {
// Extract target table info
const targetTable =
reference.table as TableReference;
const targetTableName = targetTable.table;
const targetSchemaName =
targetTable.schema || targetTable.db || 'dbo';
// Extract source column
let sourceColumnName = '';
if (
Array.isArray(constraintDef.definition) &&
constraintDef.definition.length > 0
) {
const sourceColDef =
constraintDef.definition[0];
if (
sourceColDef &&
typeof sourceColDef === 'object' &&
'type' in sourceColDef &&
sourceColDef.type === 'column_ref'
) {
sourceColumnName = extractColumnName(
sourceColDef as ColumnReference
);
}
}
// Extract target column
const targetColumnName = extractColumnName(
reference.columns[0]
);
if (
sourceColumnName &&
targetTableName &&
targetColumnName
) {
// Create a foreign key relationship
relationships.push({
name:
constraintDef.constraint ||
`fk_${tableName}_${sourceColumnName}`,
sourceTable: tableName,
sourceSchema: schemaName,
sourceColumn: sourceColumnName,
targetTable: targetTableName,
targetSchema: targetSchemaName,
targetColumn: targetColumnName,
sourceTableId: tableId,
targetTableId: '', // Will be filled later
updateAction: reference.on_update,
deleteAction: reference.on_delete,
});
}
}
}
}
}
);
}
// Create the table object
tables.push({
id: tableId,
name: tableName,
schema: schemaName,
columns,
indexes,
order: tables.length,
});
}
/**
* Process a CREATE INDEX statement
*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,624 +0,0 @@
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { Area } from '../../domain/area';
import {
DBCustomTypeKind,
type DBCustomType,
} from '../../domain/db-custom-type';
import type { DBDependency } from '../../domain/db-dependency';
import type { DBField } from '../../domain/db-field';
import type { DBIndex } from '../../domain/db-index';
import type { DBRelationship } from '../../domain/db-relationship';
import type { DBTable } from '../../domain/db-table';
import type { Diagram } from '../../domain/diagram';
type SourceIdToDataMap = Record<
string,
{ schema?: string | null; name: string; color?: string }
>;
type IdMappings = {
tables: Record<string, string>;
fields: Record<string, string>;
};
// Key generation functions remain the same for consistency
const createObjectKey = ({
type,
schema,
otherSchema,
parentName,
otherParentName,
name,
otherName,
}: {
type:
| 'table'
| 'field'
| 'index'
| 'relationship'
| 'customType'
| 'dependency'
| 'area';
schema?: string | null;
otherSchema?: string | null;
parentName?: string | null;
otherParentName?: string | null;
name: string;
otherName?: string | null;
}) =>
`${type}-${schema ? `${schema}.` : ''}${otherSchema ? `${otherSchema}.` : ''}${parentName ? `${parentName}.` : ''}${otherParentName ? `${otherParentName}.` : ''}${name}${otherName ? `.${otherName}` : ''}`;
const createObjectKeyFromTable = (table: DBTable) =>
createObjectKey({
type: 'table',
schema: table.schema,
name: table.name,
});
const createObjectKeyFromField = (table: DBTable, field: DBField) =>
createObjectKey({
type: 'field',
schema: table.schema,
parentName: table.name,
name: field.name,
});
const createObjectKeyFromIndex = (table: DBTable, index: DBIndex) =>
createObjectKey({
type: 'index',
schema: table.schema,
parentName: table.name,
name: index.name,
});
const createObjectKeyFromRelationship = (
relationship: DBRelationship,
sourceIdToNameMap: SourceIdToDataMap
) => {
const sourceTable = sourceIdToNameMap[relationship.sourceTableId];
const targetTable = sourceIdToNameMap[relationship.targetTableId];
const sourceField = sourceIdToNameMap[relationship.sourceFieldId];
const targetField = sourceIdToNameMap[relationship.targetFieldId];
if (!sourceTable || !targetTable || !sourceField || !targetField) {
return null;
}
return createObjectKey({
type: 'relationship',
schema: sourceTable.schema,
otherSchema: targetTable.schema,
parentName: sourceTable.name,
otherParentName: targetTable.name,
name: sourceField.name,
otherName: targetField.name,
});
};
const createObjectKeyFromCustomType = (customType: DBCustomType) =>
createObjectKey({
type: 'customType',
schema: customType.schema,
name: customType.name,
});
const createObjectKeyFromDependency = (
dependency: DBDependency,
sourceIdToNameMap: SourceIdToDataMap
) => {
const dependentTable = sourceIdToNameMap[dependency.dependentTableId];
const table = sourceIdToNameMap[dependency.tableId];
if (!dependentTable || !table) {
return null;
}
return createObjectKey({
type: 'dependency',
schema: dependentTable.schema,
otherSchema: table.schema,
name: dependentTable.name,
otherName: table.name,
});
};
const createObjectKeyFromArea = (area: Area) =>
createObjectKey({
type: 'area',
name: area.name,
});
// Helper function to build source mappings
const buildSourceMappings = (sourceDiagram: Diagram) => {
const objectKeysToIdsMap: Record<string, string> = {};
const sourceIdToDataMap: SourceIdToDataMap = {};
// Map tables and their fields/indexes
sourceDiagram.tables?.forEach((table) => {
const tableKey = createObjectKeyFromTable(table);
objectKeysToIdsMap[tableKey] = table.id;
sourceIdToDataMap[table.id] = {
schema: table.schema,
name: table.name,
color: table.color,
};
table.fields?.forEach((field) => {
const fieldKey = createObjectKeyFromField(table, field);
objectKeysToIdsMap[fieldKey] = field.id;
sourceIdToDataMap[field.id] = {
schema: table.schema,
name: field.name,
};
});
table.indexes?.forEach((index) => {
const indexKey = createObjectKeyFromIndex(table, index);
objectKeysToIdsMap[indexKey] = index.id;
});
});
// Map relationships
sourceDiagram.relationships?.forEach((relationship) => {
const key = createObjectKeyFromRelationship(
relationship,
sourceIdToDataMap
);
if (key) {
objectKeysToIdsMap[key] = relationship.id;
}
});
// Map custom types
sourceDiagram.customTypes?.forEach((customType) => {
const key = createObjectKeyFromCustomType(customType);
objectKeysToIdsMap[key] = customType.id;
});
// Map dependencies
sourceDiagram.dependencies?.forEach((dependency) => {
const key = createObjectKeyFromDependency(
dependency,
sourceIdToDataMap
);
if (key) {
objectKeysToIdsMap[key] = dependency.id;
}
});
// Map areas
sourceDiagram.areas?.forEach((area) => {
const key = createObjectKeyFromArea(area);
objectKeysToIdsMap[key] = area.id;
});
return { objectKeysToIdsMap, sourceIdToDataMap };
};
// Functional helper to update tables and collect ID mappings
const updateTables = ({
targetTables,
sourceTables,
defaultDatabaseSchema,
}: {
targetTables: DBTable[] | undefined;
sourceTables: DBTable[] | undefined;
objectKeysToIdsMap: Record<string, string>;
sourceIdToDataMap: SourceIdToDataMap;
defaultDatabaseSchema?: string;
}): { tables: DBTable[]; idMappings: IdMappings } => {
if (!targetTables)
return { tables: [], idMappings: { tables: {}, fields: {} } };
if (!sourceTables)
return { tables: targetTables, idMappings: { tables: {}, fields: {} } };
const idMappings: IdMappings = { tables: {}, fields: {} };
// Create a map of source tables by schema + name
const sourceTablesByKey = new Map<string, DBTable>();
sourceTables.forEach((table) => {
const key = createObjectKeyFromTable(table);
sourceTablesByKey.set(key, table);
});
const updatedTables = targetTables.map((targetTable) => {
// Try to find matching source table by schema + name
const targetKey = createObjectKeyFromTable(targetTable);
let sourceTable = sourceTablesByKey.get(targetKey);
if (!sourceTable && defaultDatabaseSchema) {
if (!targetTable.schema) {
// If target table has no schema, try matching with default schema
const defaultKey = createObjectKeyFromTable({
...targetTable,
schema: defaultDatabaseSchema,
});
sourceTable = sourceTablesByKey.get(defaultKey);
} else if (targetTable.schema === defaultDatabaseSchema) {
// If target table's schema matches default, try matching without schema
const noSchemaKey = createObjectKeyFromTable({
...targetTable,
schema: undefined,
});
sourceTable = sourceTablesByKey.get(noSchemaKey);
}
}
if (!sourceTable) {
// No matching source table found - keep target as-is
return targetTable;
}
const sourceId = sourceTable.id;
idMappings.tables[targetTable.id] = sourceId;
// Update fields by matching on name within the table
const sourceFieldsByName = new Map<string, DBField>();
sourceTable.fields?.forEach((field) => {
sourceFieldsByName.set(field.name, field);
});
const updatedFields = targetTable.fields?.map((targetField) => {
const sourceField = sourceFieldsByName.get(targetField.name);
if (sourceField) {
idMappings.fields[targetField.id] = sourceField.id;
// Use source field properties when there's a match
return {
...targetField,
id: sourceField.id,
createdAt: sourceField.createdAt,
};
}
// For new fields not in source, keep target field as-is
return targetField;
});
// Update indexes by matching on name within the table
const sourceIndexesByName = new Map<string, DBIndex>();
sourceTable.indexes?.forEach((index) => {
sourceIndexesByName.set(index.name, index);
});
const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
const sourceIndex = sourceIndexesByName.get(targetIndex.name);
if (sourceIndex) {
return {
...targetIndex,
id: sourceIndex.id,
createdAt: sourceIndex.createdAt,
};
}
return targetIndex;
});
// Build the result table, preserving source structure
const resultTable: DBTable = {
...sourceTable,
fields: updatedFields,
indexes: updatedIndexes,
comments: targetTable.comments,
};
// Update nullable, unique, primaryKey from target fields
if (targetTable.fields) {
resultTable.fields = resultTable.fields?.map((field) => {
const targetField = targetTable.fields?.find(
(f) => f.name === field.name
);
if (targetField) {
return {
...field,
nullable: targetField.nullable,
unique: targetField.unique,
primaryKey: targetField.primaryKey,
type: targetField.type,
};
}
return field;
});
}
return resultTable;
});
return { tables: updatedTables, idMappings };
};
// Functional helper to update custom types
const updateCustomTypes = (
customTypes: DBCustomType[] | undefined,
objectKeysToIdsMap: Record<string, string>
): DBCustomType[] => {
if (!customTypes) return [];
return customTypes.map((customType) => {
const key = createObjectKeyFromCustomType(customType);
const sourceId = objectKeysToIdsMap[key];
if (sourceId) {
return { ...customType, id: sourceId };
}
return customType;
});
};
// Functional helper to update relationships
const updateRelationships = (
targetRelationships: DBRelationship[] | undefined,
sourceRelationships: DBRelationship[] | undefined,
idMappings: IdMappings
): DBRelationship[] => {
// If target has no relationships, return empty array (relationships were removed)
if (!targetRelationships || targetRelationships.length === 0) return [];
// If source has no relationships, we need to add the target relationships with updated IDs
if (!sourceRelationships || sourceRelationships.length === 0) {
return targetRelationships.map((targetRel) => {
// Find the source IDs by reversing the mapping lookup
let sourceTableId = targetRel.sourceTableId;
let targetTableId = targetRel.targetTableId;
let sourceFieldId = targetRel.sourceFieldId;
let targetFieldId = targetRel.targetFieldId;
// Find source table/field IDs from the mappings
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
if (targetId === targetRel.sourceTableId) {
sourceTableId = srcId;
}
if (targetId === targetRel.targetTableId) {
targetTableId = srcId;
}
}
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
if (targetId === targetRel.sourceFieldId) {
sourceFieldId = srcId;
}
if (targetId === targetRel.targetFieldId) {
targetFieldId = srcId;
}
}
return {
...targetRel,
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
};
});
}
// Map source relationships that have matches in target
const resultRelationships: DBRelationship[] = [];
const matchedTargetRelIds = new Set<string>();
sourceRelationships.forEach((sourceRel) => {
// Find matching target relationship by checking if the target has a relationship
// between the same tables and fields (using the ID mappings)
const targetRel = targetRelationships.find((tgtRel) => {
const mappedSourceTableId = idMappings.tables[tgtRel.sourceTableId];
const mappedTargetTableId = idMappings.tables[tgtRel.targetTableId];
const mappedSourceFieldId = idMappings.fields[tgtRel.sourceFieldId];
const mappedTargetFieldId = idMappings.fields[tgtRel.targetFieldId];
// Check both directions since relationships can be defined in either direction
const directMatch =
sourceRel.sourceTableId === mappedSourceTableId &&
sourceRel.targetTableId === mappedTargetTableId &&
sourceRel.sourceFieldId === mappedSourceFieldId &&
sourceRel.targetFieldId === mappedTargetFieldId;
const reverseMatch =
sourceRel.sourceTableId === mappedTargetTableId &&
sourceRel.targetTableId === mappedSourceTableId &&
sourceRel.sourceFieldId === mappedTargetFieldId &&
sourceRel.targetFieldId === mappedSourceFieldId;
return directMatch || reverseMatch;
});
if (targetRel) {
matchedTargetRelIds.add(targetRel.id);
// Preserve source relationship but update cardinalities from target
const result: DBRelationship = {
...sourceRel,
sourceCardinality: targetRel.sourceCardinality,
targetCardinality: targetRel.targetCardinality,
};
// Only include schema fields if they exist in the source relationship
if (!sourceRel.sourceSchema) {
delete result.sourceSchema;
}
if (!sourceRel.targetSchema) {
delete result.targetSchema;
}
resultRelationships.push(result);
}
});
// Add any target relationships that weren't matched (new relationships)
targetRelationships.forEach((targetRel) => {
if (!matchedTargetRelIds.has(targetRel.id)) {
// Find the source IDs by reversing the mapping lookup
let sourceTableId = targetRel.sourceTableId;
let targetTableId = targetRel.targetTableId;
let sourceFieldId = targetRel.sourceFieldId;
let targetFieldId = targetRel.targetFieldId;
// Find source table/field IDs from the mappings
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
if (targetId === targetRel.sourceTableId) {
sourceTableId = srcId;
}
if (targetId === targetRel.targetTableId) {
targetTableId = srcId;
}
}
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
if (targetId === targetRel.sourceFieldId) {
sourceFieldId = srcId;
}
if (targetId === targetRel.targetFieldId) {
targetFieldId = srcId;
}
}
resultRelationships.push({
...targetRel,
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
});
}
});
return resultRelationships;
};
// Functional helper to update dependencies
const updateDependencies = (
targetDependencies: DBDependency[] | undefined,
sourceDependencies: DBDependency[] | undefined,
idMappings: IdMappings
): DBDependency[] => {
if (!targetDependencies) return [];
if (!sourceDependencies) return targetDependencies;
return targetDependencies.map((targetDep) => {
// Find matching source dependency
const sourceDep = sourceDependencies.find((srcDep) => {
const srcTableId = idMappings.tables[targetDep.tableId];
const srcDependentTableId =
idMappings.tables[targetDep.dependentTableId];
return (
srcDep.tableId === srcTableId &&
srcDep.dependentTableId === srcDependentTableId
);
});
if (sourceDep) {
return {
...targetDep,
id: sourceDep.id,
tableId:
idMappings.tables[targetDep.tableId] || targetDep.tableId,
dependentTableId:
idMappings.tables[targetDep.dependentTableId] ||
targetDep.dependentTableId,
};
}
// If no match found, just update the table references
return {
...targetDep,
tableId: idMappings.tables[targetDep.tableId] || targetDep.tableId,
dependentTableId:
idMappings.tables[targetDep.dependentTableId] ||
targetDep.dependentTableId,
};
});
};
// Functional helper to update index field references
const updateIndexFieldReferences = (
tables: DBTable[] | undefined,
idMappings: IdMappings
): DBTable[] => {
if (!tables) return [];
return tables.map((table) => ({
...table,
indexes: table.indexes?.map((index) => ({
...index,
fieldIds: index.fieldIds.map(
(fieldId) => idMappings.fields[fieldId] || fieldId
),
})),
}));
};
export const applyDBMLChanges = ({
sourceDiagram,
targetDiagram,
}: {
sourceDiagram: Diagram;
targetDiagram: Diagram;
}): Diagram => {
// Step 1: Build mappings from source diagram
const { objectKeysToIdsMap, sourceIdToDataMap } =
buildSourceMappings(sourceDiagram);
// Step 2: Update tables and collect ID mappings
const { tables: updatedTables, idMappings } = updateTables({
targetTables: targetDiagram.tables,
sourceTables: sourceDiagram.tables,
objectKeysToIdsMap,
sourceIdToDataMap,
defaultDatabaseSchema: defaultSchemas[sourceDiagram.databaseType],
});
// Step 3: Update all other entities functionally
const newCustomTypes = updateCustomTypes(
targetDiagram.customTypes,
objectKeysToIdsMap
);
const updatedCustomTypes = [
...(sourceDiagram.customTypes?.filter(
(ct) => ct.kind === DBCustomTypeKind.composite
) ?? []),
...newCustomTypes,
];
const updatedRelationships = updateRelationships(
targetDiagram.relationships,
sourceDiagram.relationships,
idMappings
);
const updatedDependencies = updateDependencies(
targetDiagram.dependencies,
sourceDiagram.dependencies,
idMappings
);
// Step 4: Update index field references
const finalTables = updateIndexFieldReferences(updatedTables, idMappings);
// Sort relationships to match source order
const sortedRelationships = [...updatedRelationships].sort((a, b) => {
// Find source relationships to get their order
const sourceRelA = sourceDiagram.relationships?.find(
(r) => r.id === a.id
);
const sourceRelB = sourceDiagram.relationships?.find(
(r) => r.id === b.id
);
if (!sourceRelA || !sourceRelB) return 0;
const indexA = sourceDiagram.relationships?.indexOf(sourceRelA) ?? 0;
const indexB = sourceDiagram.relationships?.indexOf(sourceRelB) ?? 0;
return indexA - indexB;
});
// Return a new diagram object with tables sorted by order
const result: Diagram = {
...sourceDiagram,
tables: finalTables.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
areas: targetDiagram.areas,
relationships: sortedRelationships,
dependencies: updatedDependencies,
customTypes: updatedCustomTypes,
};
return result;
};

View File

@@ -210,17 +210,14 @@ export const sanitizeSQLforDBML = (sql: string): string => {
// Post-process DBML to convert separate Ref statements to inline refs
const convertToInlineRefs = (dbml: string): string => {
// Extract all Ref statements - Updated pattern to handle schema.table.field format
// Matches both "table"."field" and "schema"."table"."field" formats
// Extract all Ref statements - Corrected pattern
const refPattern =
/Ref\s+"([^"]+)"\s*:\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"\s*([<>*])\s*(?:"([^"]+)"\.)?"([^"]+)"\."([^"]+)"/g;
/Ref\s+"([^"]+)"\s*:\s*"([^"]+)"\."([^"]+)"\s*([<>*])\s*"([^"]+)"\."([^"]+)"/g;
const refs: Array<{
refName: string;
sourceSchema?: string;
sourceTable: string;
sourceField: string;
direction: string;
targetSchema?: string;
targetTable: string;
targetField: string;
}> = [];
@@ -229,86 +226,28 @@ const convertToInlineRefs = (dbml: string): string => {
while ((match = refPattern.exec(dbml)) !== null) {
refs.push({
refName: match[1], // Reference name
sourceSchema: match[2] || undefined, // Source schema (optional)
sourceTable: match[3], // Source table
sourceField: match[4], // Source field
direction: match[5], // Direction (<, >)
targetSchema: match[6] || undefined, // Target schema (optional)
targetTable: match[7], // Target table
targetField: match[8], // Target field
sourceTable: match[2], // Source table
sourceField: match[3], // Source field
direction: match[4], // Direction (<, >)
targetTable: match[5], // Target table
targetField: match[6], // Target field
});
}
// Extract all table definitions - Support both quoted and bracketed table names
// Extract all table definitions - Corrected pattern and handling
const tables: {
[key: string]: {
start: number;
end: number;
content: string;
fullMatch: string;
};
[key: string]: { start: number; end: number; content: string };
} = {};
const tablePattern = /Table\s+"([^"]+)"\s*{([^}]*)}/g; // Simpler pattern, assuming content doesn't have {}
// Use a more sophisticated approach to handle nested braces
let currentPos = 0;
while (currentPos < dbml.length) {
// Find the next table definition
const tableStartPattern =
/Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{/g;
tableStartPattern.lastIndex = currentPos;
const tableStartMatch = tableStartPattern.exec(dbml);
if (!tableStartMatch) break;
// Extract table name
let tableName;
if (tableStartMatch[1] && tableStartMatch[2]) {
tableName = `${tableStartMatch[1]}.${tableStartMatch[2]}`;
} else if (tableStartMatch[1]) {
tableName = tableStartMatch[1];
} else {
tableName = tableStartMatch[3] || tableStartMatch[4];
}
// Clean up any bracket syntax from table names
const cleanTableName = tableName.replace(/\[([^\]]+)\]/g, '$1');
// Find the matching closing brace by counting nested braces
const openBracePos =
tableStartMatch.index + tableStartMatch[0].length - 1;
let braceCount = 1;
const contentStart = openBracePos + 1;
let contentEnd = contentStart;
for (let i = contentStart; i < dbml.length && braceCount > 0; i++) {
if (dbml[i] === '{') braceCount++;
else if (dbml[i] === '}') {
braceCount--;
if (braceCount === 0) {
contentEnd = i;
}
}
}
if (braceCount === 0) {
const content = dbml.substring(contentStart, contentEnd);
const fullMatch = dbml.substring(
tableStartMatch.index,
contentEnd + 1
);
tables[cleanTableName] = {
start: tableStartMatch.index,
end: contentEnd + 1,
content: content,
fullMatch: fullMatch,
};
currentPos = contentEnd + 1;
} else {
// Malformed DBML, skip this table
currentPos = tableStartMatch.index + tableStartMatch[0].length;
}
let tableMatch;
while ((tableMatch = tablePattern.exec(dbml)) !== null) {
const tableName = tableMatch[1];
tables[tableName] = {
start: tableMatch.index,
end: tableMatch.index + tableMatch[0].length,
content: tableMatch[2],
};
}
if (refs.length === 0 || Object.keys(tables).length === 0) {
@@ -318,102 +257,50 @@ const convertToInlineRefs = (dbml: string): string => {
// Create a map for faster table lookup
const tableMap = new Map(Object.entries(tables));
// 1. First, collect all refs per field
const fieldRefs = new Map<
string,
{ table: string; refs: string[]; relatedTables: string[] }
>();
// 1. Add inline refs to table contents
refs.forEach((ref) => {
let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
let targetTableName, fieldNameToModify, inlineRefSyntax;
if (ref.direction === '<') {
targetTableName = ref.targetSchema
? `${ref.targetSchema}.${ref.targetTable}`
: ref.targetTable;
targetTableName = ref.targetTable;
fieldNameToModify = ref.targetField;
const sourceRef = ref.sourceSchema
? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
: `"${ref.sourceTable}"."${ref.sourceField}"`;
inlineRefSyntax = `ref: < ${sourceRef}`;
relatedTable = ref.sourceTable;
inlineRefSyntax = `[ref: < "${ref.sourceTable}"."${ref.sourceField}"]`;
} else {
targetTableName = ref.sourceSchema
? `${ref.sourceSchema}.${ref.sourceTable}`
: ref.sourceTable;
targetTableName = ref.sourceTable;
fieldNameToModify = ref.sourceField;
const targetRef = ref.targetSchema
? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
: `"${ref.targetTable}"."${ref.targetField}"`;
inlineRefSyntax = `ref: > ${targetRef}`;
relatedTable = ref.targetTable;
inlineRefSyntax = `[ref: > "${ref.targetTable}"."${ref.targetField}"]`;
}
const fieldKey = `${targetTableName}.${fieldNameToModify}`;
const existing = fieldRefs.get(fieldKey) || {
table: targetTableName,
refs: [],
relatedTables: [],
};
existing.refs.push(inlineRefSyntax);
existing.relatedTables.push(relatedTable);
fieldRefs.set(fieldKey, existing);
});
// 2. Apply all refs to fields
fieldRefs.forEach((fieldData, fieldKey) => {
// fieldKey might be "schema.table.field" or just "table.field"
const lastDotIndex = fieldKey.lastIndexOf('.');
const tableName = fieldKey.substring(0, lastDotIndex);
const fieldName = fieldKey.substring(lastDotIndex + 1);
const tableData = tableMap.get(tableName);
const tableData = tableMap.get(targetTableName);
if (tableData) {
// Updated pattern to capture field definition and all existing attributes in brackets
const fieldPattern = new RegExp(
`^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
'gm'
`("(${fieldNameToModify})"[^\n]*?)([ \t]*[[].*?[]])?([ \t]*//.*)?$`,
'm'
);
let newContent = tableData.content;
newContent = newContent.replace(
fieldPattern,
(lineMatch, fieldPart, existingBrackets, commentPart) => {
// Collect all attributes from existing brackets
const allAttributes: string[] = [];
if (existingBrackets) {
// Extract all bracket contents
const bracketPattern = /\[([^\]]*)\]/g;
let bracketMatch;
while (
(bracketMatch = bracketPattern.exec(lineMatch)) !==
null
) {
const content = bracketMatch[1].trim();
if (content) {
allAttributes.push(content);
}
}
(
lineMatch,
fieldPart,
_fieldName,
existingAttributes,
commentPart
) => {
// Avoid adding duplicate refs
if (lineMatch.includes('[ref:')) {
return lineMatch;
}
// Add all refs for this field
allAttributes.push(...fieldData.refs);
// Combine all attributes into a single bracket
const combinedAttributes = allAttributes.join(', ');
// Preserve original spacing from fieldPart
const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
const fieldDefWithoutSpaces = fieldPart.trim();
return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
return `${fieldPart.trim()} ${inlineRefSyntax}${existingAttributes || ''}${commentPart || ''}`;
}
);
// Update the table content if modified
if (newContent !== tableData.content) {
tableData.content = newContent;
tableMap.set(tableName, tableData);
tableMap.set(targetTableName, tableData);
}
}
});
@@ -425,53 +312,9 @@ const convertToInlineRefs = (dbml: string): string => {
([, a], [, b]) => a.start - b.start
);
for (const [, tableData] of sortedTables) {
for (const [tableName, tableData] of sortedTables) {
reconstructedDbml += dbml.substring(lastIndex, tableData.start);
// Preserve the original table definition format but with updated content
const originalTableDef = tableData.fullMatch;
let formattedContent = tableData.content;
// Clean up content formatting:
// 1. Split into lines to handle each line individually
const lines = formattedContent.split('\n');
// 2. Process lines to ensure proper formatting
const processedLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trimEnd();
// Skip empty lines at the end if followed by a closing brace
if (trimmedLine === '' && i === lines.length - 1) {
continue;
}
// Skip empty lines before a closing brace
if (
trimmedLine === '' &&
i < lines.length - 1 &&
lines[i + 1].trim().startsWith('}')
) {
continue;
}
processedLines.push(line);
}
formattedContent = processedLines.join('\n');
// Ensure content ends with a newline before the table's closing brace
if (!formattedContent.endsWith('\n')) {
formattedContent = formattedContent + '\n';
}
// Since we properly extracted content with nested braces, we need to rebuild the table definition
const tableHeader = originalTableDef.substring(
0,
originalTableDef.indexOf('{') + 1
);
const updatedTableDef = `${tableHeader}${formattedContent}}`;
reconstructedDbml += updatedTableDef;
reconstructedDbml += `Table "${tableName}" {${tableData.content}}`;
lastIndex = tableData.end;
}
reconstructedDbml += dbml.substring(lastIndex);
@@ -482,13 +325,7 @@ const convertToInlineRefs = (dbml: string): string => {
.filter((line) => !line.trim().startsWith('Ref '));
const finalDbml = finalLines.join('\n').trim();
// Clean up excessive empty lines - replace multiple consecutive empty lines with just one
// But ensure there's at least one blank line between tables
const cleanedDbml = finalDbml
.replace(/\n\s*\n\s*\n/g, '\n\n')
.replace(/}\n(?=Table)/g, '}\n\n');
return cleanedDbml;
return finalDbml;
};
// Function to check for SQL keywords (add more if needed)
@@ -573,125 +410,6 @@ const normalizeCharTypeFormat = (dbml: string): string => {
.replace(/character \(([0-9]+)\)/g, 'character($1)');
};
// Fix table definitions with incorrect bracket syntax
const fixTableBracketSyntax = (dbml: string): string => {
// Fix patterns like Table [schema].[table] to Table "schema"."table"
return dbml.replace(
/Table\s+\[([^\]]+)\]\.\[([^\]]+)\]/g,
'Table "$1"."$2"'
);
};
// Restore schema information that may have been stripped by the DBML importer
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
// Group tables by name to handle duplicates
const tablesByName = new Map<
string,
Array<{ table: DBTable; index: number }>
>();
tables.forEach((table, index) => {
const existing = tablesByName.get(table.name) || [];
existing.push({ table, index });
tablesByName.set(table.name, existing);
});
let result = dbml;
// Process each group of tables with the same name
tablesByName.forEach((tablesGroup, tableName) => {
if (tablesGroup.length === 1) {
// Single table with this name - simple case
const table = tablesGroup[0].table;
if (table.schema) {
// Match table definition without schema (e.g., Table "users" {)
const tablePattern = new RegExp(
`Table\\s+"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s*{`,
'g'
);
const schemaTableName = `Table "${table.schema}"."${table.name}" {`;
result = result.replace(tablePattern, schemaTableName);
// Update references in Ref statements
const escapedTableName = table.name.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
// Pattern 1: In Ref definitions - :"tablename"."field"
const refDefPattern = new RegExp(
`(Ref\\s+"[^"]+")\\s*:\\s*"${escapedTableName}"\\."([^"]+)"`,
'g'
);
result = result.replace(
refDefPattern,
`$1:"${table.schema}"."${table.name}"."$2"`
);
// Pattern 2: In Ref targets - [<>] "tablename"."field"
const refTargetPattern = new RegExp(
`([<>])\\s*"${escapedTableName}"\\."([^"]+)"`,
'g'
);
result = result.replace(
refTargetPattern,
`$1 "${table.schema}"."${table.name}"."$2"`
);
}
} else {
// Multiple tables with the same name - need to be more careful
// Find all table definitions for this name
const escapedTableName = tableName.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
// Get tables that need schema restoration (those without schema in DBML)
const tablesNeedingSchema = tablesGroup.filter(({ table }) => {
// Check if this table's schema is already in the DBML
const schemaPattern = new RegExp(
`Table\\s+"${table.schema}"\\.\\s*"${escapedTableName}"\\s*{`,
'g'
);
return !result.match(schemaPattern);
});
// Then handle tables without schema in DBML
const noSchemaTablePattern = new RegExp(
`Table\\s+"${escapedTableName}"\\s*{`,
'g'
);
let noSchemaMatchIndex = 0;
result = result.replace(noSchemaTablePattern, (match) => {
// We need to match based on the order in the DBML output
// For PostgreSQL DBML, the @dbml/core sorts tables by:
// 1. Tables with schemas (alphabetically)
// 2. Tables without schemas
// Since both our tables have schemas, they should appear in order
// Only process tables that need schema restoration
if (noSchemaMatchIndex >= tablesNeedingSchema.length) {
return match;
}
const correspondingTable =
tablesNeedingSchema[noSchemaMatchIndex];
noSchemaMatchIndex++;
if (correspondingTable && correspondingTable.table.schema) {
return `Table "${correspondingTable.table.schema}"."${tableName}" {`;
}
// If the table doesn't have a schema, keep it as is
return match;
});
}
});
return result;
};
export interface DBMLExportResult {
standardDbml: string;
inlineDbml: string;
@@ -711,18 +429,13 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
};
}) ?? [];
// Remove duplicate tables (consider both schema and table name)
const seenTableIdentifiers = new Set<string>();
// Remove duplicate tables (keep first occurrence by table name)
const seenTableNames = new Set<string>();
const uniqueTables = sanitizedTables.filter((table) => {
// Create a unique identifier combining schema and table name
const tableIdentifier = table.schema
? `${table.schema}.${table.name}`
: table.name;
if (seenTableIdentifiers.has(tableIdentifier)) {
if (seenTableNames.has(table.name)) {
return false; // Skip duplicate
}
seenTableIdentifiers.add(tableIdentifier);
seenTableNames.add(table.name);
return true; // Keep unique table
});
@@ -770,58 +483,43 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
const processTable = (table: DBTable) => {
const originalName = table.name;
let safeTableName = originalName;
// If name contains spaces or special characters, wrap in quotes
if (/[^\w]/.test(originalName)) {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
let safeTableName = originalName.replace(/[^\w]/g, '_');
// Rename table if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(originalName)) {
const newName = `${originalName}_table`;
if (shouldRenameKeywords && isSQLKeyword(safeTableName)) {
const newName = `${safeTableName}_table`;
sqlRenamedTables.set(newName, originalName);
safeTableName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
safeTableName = newName;
}
const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => {
let finalSafeName = field.name;
// If field name contains spaces or special characters, wrap in quotes
if (/[^\w]/.test(field.name)) {
finalSafeName = `"${field.name.replace(/"/g, '\\"')}"`;
}
const originalSafeName = field.name.replace(/[^\w]/g, '_');
let finalSafeName = originalSafeName;
// Handle duplicate field names
const count = fieldNameCounts.get(field.name) || 0;
const count = fieldNameCounts.get(originalSafeName) || 0;
if (count > 0) {
const newName = `${field.name}_${count + 1}`;
finalSafeName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
finalSafeName = `${originalSafeName}_${count + 1}`;
}
fieldNameCounts.set(field.name, count + 1);
fieldNameCounts.set(originalSafeName, count + 1);
// Create sanitized field
const sanitizedField: DBField = {
...field,
name: finalSafeName,
};
delete sanitizedField.comments;
// Rename field if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(field.name)) {
const newFieldName = `${field.name}_field`;
if (shouldRenameKeywords && isSQLKeyword(finalSafeName)) {
const newFieldName = `${finalSafeName}_field`;
fieldRenames.push({
table: safeTableName,
originalName: field.name,
originalName: finalSafeName,
newName: newFieldName,
});
sanitizedField.name = /[^\w]/.test(newFieldName)
? `"${newFieldName.replace(/"/g, '\\"')}"`
: newFieldName;
sanitizedField.name = newFieldName;
}
return sanitizedField;
@@ -834,9 +532,7 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
indexes: (table.indexes || []).map((index) => ({
...index,
name: index.name
? /[^\w]/.test(index.name)
? `"${index.name.replace(/"/g, '\\"')}"`
: index.name
? index.name.replace(/[^\w]/g, '_')
: `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
};
@@ -846,15 +542,10 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
...cleanDiagram,
tables: cleanDiagram.tables?.map(processTable) ?? [],
relationships:
cleanDiagram.relationships?.map((rel, index) => {
const safeName = rel.name
? rel.name.replace(/[^\w]/g, '_')
: Math.random().toString(36).substring(2, 8);
return {
...rel,
name: `fk_${index}_${safeName}`,
};
}) ?? [],
cleanDiagram.relationships?.map((rel, index) => ({
...rel,
name: `fk_${index}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`,
})) ?? [],
} as Diagram);
let standard = '';
@@ -886,35 +577,16 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
}
standard = normalizeCharTypeFormat(
fixTableBracketSyntax(
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
)
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
)
);
// Restore schema information that may have been stripped by DBML importer
standard = restoreTableSchemas(standard, uniqueTables);
// Prepend Enum DBML to the standard output
if (enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
}
standard = enumsDBML + '\n' + standard;
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
// Clean up excessive empty lines in both outputs
standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');
inline = inline.replace(/\n\s*\n\s*\n/g, '\n\n');
// Ensure proper formatting with newline at end
if (!standard.endsWith('\n')) {
standard += '\n';
}
if (!inline.endsWith('\n')) {
inline += '\n';
}
} catch (error: unknown) {
console.error(
'Error during DBML generation process:',
@@ -930,11 +602,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
// If an error occurred, still prepend enums if they exist, or they'll be lost.
// The error message will then follow.
if (standard.startsWith('// Error generating DBML:') && enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
if (standard.startsWith('// Error generating DBML:')) {
standard = enumsDBML + standard;
}
if (inline.startsWith('// Error generating DBML:') && enumsDBML) {
inline = enumsDBML + '\n\n' + inline;
if (inline.startsWith('// Error generating DBML:')) {
inline = enumsDBML + inline;
}
}

View File

@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
describe('DBML Import - Fantasy Examples', () => {
describe('Magical Academy System', () => {
@@ -614,228 +613,6 @@ Note quest_system_note {
});
});
describe('Enum Support', () => {
it('should import enums as customTypes', async () => {
const dbmlWithEnums = `
// Test DBML with various enum definitions
enum job_status {
created [note: 'Waiting to be processed']
running
done
failure
}
// Enum with schema
enum hr.employee_type {
full_time
part_time
contractor
intern
}
// Enum with special characters and spaces
enum grade {
"A+"
"A"
"A-"
"Not Yet Set"
}
Table employees {
id integer [pk]
name varchar(200) [not null]
status job_status
type hr.employee_type
performance_grade grade
created_at timestamp [default: 'now()']
}
Table projects {
id integer [pk]
name varchar(300) [not null]
status job_status [not null]
priority enum // inline enum without values - will be converted to varchar
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnums);
// Verify customTypes are created for enums
expect(diagram.customTypes).toBeDefined();
expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade
// Check job_status enum
const jobStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'job_status' && !ct.schema
);
expect(jobStatusEnum).toBeDefined();
expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(jobStatusEnum?.values).toEqual([
'created',
'running',
'done',
'failure',
]);
// Check hr.employee_type enum with schema
const employeeTypeEnum = diagram.customTypes?.find(
(ct) => ct.name === 'employee_type' && ct.schema === 'hr'
);
expect(employeeTypeEnum).toBeDefined();
expect(employeeTypeEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(employeeTypeEnum?.values).toEqual([
'full_time',
'part_time',
'contractor',
'intern',
]);
// Check grade enum with quoted values
const gradeEnum = diagram.customTypes?.find(
(ct) => ct.name === 'grade' && !ct.schema
);
expect(gradeEnum).toBeDefined();
expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(gradeEnum?.values).toEqual(['A+', 'A', 'A-', 'Not Yet Set']);
// Verify tables are created
expect(diagram.tables).toHaveLength(2);
// Check that enum fields in tables reference the custom types
const employeesTable = diagram.tables?.find(
(t) => t.name === 'employees'
);
const statusField = employeesTable?.fields.find(
(f) => f.name === 'status'
);
const typeField = employeesTable?.fields.find(
(f) => f.name === 'type'
);
const gradeField = employeesTable?.fields.find(
(f) => f.name === 'performance_grade'
);
// Verify fields have correct types
expect(statusField?.type.id).toBe('job_status');
expect(typeField?.type.id).toBe('employee_type');
expect(gradeField?.type.id).toBe('grade');
// Check inline enum was converted to varchar
const projectsTable = diagram.tables?.find(
(t) => t.name === 'projects'
);
const priorityField = projectsTable?.fields.find(
(f) => f.name === 'priority'
);
expect(priorityField?.type.id).toBe('varchar');
});
it('should handle enum values with notes', async () => {
const dbmlWithEnumNotes = `
enum order_status {
pending [note: 'Order has been placed but not confirmed']
confirmed [note: 'Payment received and order confirmed']
shipped [note: 'Order has been dispatched']
delivered [note: 'Order delivered to customer']
cancelled [note: 'Order cancelled by customer or system']
}
Table orders {
id integer [pk]
status order_status [not null]
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
// Verify enum is created
expect(diagram.customTypes).toHaveLength(1);
const orderStatusEnum = diagram.customTypes?.[0];
expect(orderStatusEnum?.name).toBe('order_status');
expect(orderStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(orderStatusEnum?.values).toEqual([
'pending',
'confirmed',
'shipped',
'delivered',
'cancelled',
]);
});
it('should handle multiple schemas with same enum names', async () => {
const dbmlWithSameEnumNames = `
// Public schema status enum
enum status {
active
inactive
deleted
}
// Admin schema status enum with different values
enum admin.status {
pending_approval
approved
rejected
suspended
}
Table public.users {
id integer [pk]
status status
}
Table admin.users {
id integer [pk]
status admin.status
}`;
const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
// Verify both enums are created
expect(diagram.customTypes).toHaveLength(2);
// Check public.status enum
const publicStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'status' && !ct.schema
);
expect(publicStatusEnum).toBeDefined();
expect(publicStatusEnum?.values).toEqual([
'active',
'inactive',
'deleted',
]);
// Check admin.status enum
const adminStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'status' && ct.schema === 'admin'
);
expect(adminStatusEnum).toBeDefined();
expect(adminStatusEnum?.values).toEqual([
'pending_approval',
'approved',
'rejected',
'suspended',
]);
// Verify fields reference correct enums
const publicUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'public'
);
const adminUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'admin'
);
const publicStatusField = publicUsersTable?.fields.find(
(f) => f.name === 'status'
);
const adminStatusField = adminUsersTable?.fields.find(
(f) => f.name === 'status'
);
expect(publicStatusField?.type.id).toBe('status');
expect(adminStatusField?.type.id).toBe('status');
});
});
describe('Edge Cases and Special Features', () => {
it('should handle tables with all DBML features', async () => {
const edgeCaseDBML = `
@@ -918,34 +695,13 @@ Note dragon_note {
// Check that table header color was removed
expect(hoardsTable).toBeDefined();
// Verify all indexes are imported correctly
expect(hoardsTable?.indexes).toHaveLength(3); // Should have 3 indexes as defined in DBML
// Verify named indexes
const uniqueDragonIndex = hoardsTable?.indexes.find(
(idx) => idx.name === 'idx_unique_dragon'
);
expect(uniqueDragonIndex).toBeDefined();
expect(uniqueDragonIndex?.name).toBe('idx_unique_dragon'); // Verify exact name from DBML
expect(uniqueDragonIndex?.unique).toBe(true);
expect(uniqueDragonIndex?.fieldIds).toHaveLength(1);
const hoardValueIndex = hoardsTable?.indexes.find(
(idx) => idx.name === 'idx_hoard_value'
);
expect(hoardValueIndex).toBeDefined();
expect(hoardValueIndex?.name).toBe('idx_hoard_value'); // Verify exact name from DBML
expect(hoardValueIndex?.unique).toBe(false);
expect(hoardValueIndex?.fieldIds).toHaveLength(1);
const dragonActiveIndex = hoardsTable?.indexes.find(
(idx) => idx.name === 'idx_dragon_active'
);
expect(dragonActiveIndex).toBeDefined();
expect(dragonActiveIndex?.name).toBe('idx_dragon_active'); // Verify exact name from DBML
expect(dragonActiveIndex?.unique).toBe(false);
expect(dragonActiveIndex?.fieldIds).toHaveLength(2);
// Check relationship
expect(diagram.relationships).toHaveLength(1);
const relationship = diagram.relationships?.[0];
@@ -985,306 +741,5 @@ Table empty_table {
expect(diagram.tables?.[0]?.fields).toHaveLength(1);
expect(diagram.tables?.[0]?.name).toBe('empty_table');
});
it('should import tables with same name but different schemas', async () => {
const dbml = `
Table "aa"."users" {
id integer [primary key]
}
Table "bb"."users" {
id integer [primary key]
}`;
const diagram = await importDBMLToDiagram(dbml);
expect(diagram.tables).toHaveLength(2);
const aaUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'aa'
);
const bbUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'bb'
);
expect(aaUsersTable).toBeDefined();
expect(bbUsersTable).toBeDefined();
expect(aaUsersTable?.schema).toBe('aa');
expect(bbUsersTable?.schema).toBe('bb');
expect(aaUsersTable?.fields).toHaveLength(1);
expect(bbUsersTable?.fields).toHaveLength(1);
expect(aaUsersTable?.fields[0].name).toBe('id');
expect(aaUsersTable?.fields[0].type.id).toBe('integer');
expect(aaUsersTable?.fields[0].primaryKey).toBe(true);
expect(bbUsersTable?.fields[0].name).toBe('id');
expect(bbUsersTable?.fields[0].type.id).toBe('integer');
expect(bbUsersTable?.fields[0].primaryKey).toBe(true);
});
it('should import complex multi-schema DBML with inline refs and various indexes', async () => {
// This test validates:
// - 3 tables across different schemas (public, public_2, public_3)
// - Table-level notes (Note: 'my comment' on users table)
// - 3 indexes:
// * Composite unique index: (content, user_id) on posts table
// * Single non-unique index: created_at on posts table
// * Single unique index: id on comments table
// - 3 inline foreign key relationships:
// * posts.user_id -> users.id
// * comments.post_id -> posts.id
// * comments.user_id -> users.id
// - Quoted identifiers for all table and field names
const dbml = `
Table "public"."users" {
"id" varchar(500) [pk]
"name" varchar(500)
"email" varchar(500)
Note: 'my comment'
}
Table "public_2"."posts" {
"id" varchar(500) [pk]
"title" varchar(500)
"content" text
"user_id" varchar(500) [ref: < "public"."users"."id"]
"created_at" timestamp
Indexes {
(content, user_id) [unique, name: "public_2_content_user_id_idx"]
created_at [name: "public_2_index_2"]
}
}
Table "public_3"."comments" {
"id" varchar(500) [pk]
"content" text
"post_id" varchar(500) [ref: < "public_2"."posts"."id"]
"user_id" varchar(500) [ref: < "public"."users"."id"]
"created_at" timestamp
Indexes {
id [unique, name: "public_3_index_1"]
}
}`;
const diagram = await importDBMLToDiagram(dbml);
// Verify tables
expect(diagram.tables).toHaveLength(3);
const usersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'public'
);
const postsTable = diagram.tables?.find(
(t) => t.name === 'posts' && t.schema === 'public_2'
);
const commentsTable = diagram.tables?.find(
(t) => t.name === 'comments' && t.schema === 'public_3'
);
expect(usersTable).toBeDefined();
expect(postsTable).toBeDefined();
expect(commentsTable).toBeDefined();
// Check users table
expect(usersTable?.fields).toHaveLength(3);
expect(
usersTable?.fields.find((f) => f.name === 'id')?.primaryKey
).toBe(true);
expect(
usersTable?.fields.find((f) => f.name === 'id')?.type.id
).toBe('varchar');
expect(
usersTable?.fields.find((f) => f.name === 'name')?.type.id
).toBe('varchar');
expect(
usersTable?.fields.find((f) => f.name === 'email')?.type.id
).toBe('varchar');
// Check if table note is preserved
expect(usersTable?.comments).toBe('my comment');
// Check posts table
expect(postsTable?.fields).toHaveLength(5);
expect(
postsTable?.fields.find((f) => f.name === 'content')?.type.id
).toBe('text');
expect(
postsTable?.fields.find((f) => f.name === 'created_at')?.type.id
).toBe('timestamp');
// Check posts indexes thoroughly
expect(postsTable?.indexes).toHaveLength(2);
// Index 1: Composite unique index on (content, user_id)
const compositeIndex = postsTable?.indexes.find(
(idx) => idx.name === 'public_2_content_user_id_idx'
);
expect(compositeIndex).toBeDefined();
expect(compositeIndex?.name).toBe('public_2_content_user_id_idx'); // Verify exact name from DBML
expect(compositeIndex?.unique).toBe(true);
expect(compositeIndex?.fieldIds).toHaveLength(2);
// Verify it includes the correct fields
const contentFieldId = postsTable?.fields.find(
(f) => f.name === 'content'
)?.id;
const userIdFieldId = postsTable?.fields.find(
(f) => f.name === 'user_id'
)?.id;
expect(compositeIndex?.fieldIds).toContain(contentFieldId);
expect(compositeIndex?.fieldIds).toContain(userIdFieldId);
// Index 2: Non-unique index on created_at
const singleIndex = postsTable?.indexes.find(
(idx) => idx.name === 'public_2_index_2'
);
expect(singleIndex).toBeDefined();
expect(singleIndex?.name).toBe('public_2_index_2'); // Verify exact name from DBML
expect(singleIndex?.unique).toBe(false);
expect(singleIndex?.fieldIds).toHaveLength(1);
const createdAtFieldId = postsTable?.fields.find(
(f) => f.name === 'created_at'
)?.id;
expect(singleIndex?.fieldIds[0]).toBe(createdAtFieldId);
// Check comments table
expect(commentsTable?.fields).toHaveLength(5);
expect(commentsTable?.indexes).toHaveLength(1);
// Index: Unique index on id
const idIndex = commentsTable?.indexes.find(
(idx) => idx.name === 'public_3_index_1'
);
expect(idIndex).toBeDefined();
expect(idIndex?.name).toBe('public_3_index_1'); // Verify exact name from DBML
expect(idIndex?.unique).toBe(true);
expect(idIndex?.fieldIds).toHaveLength(1);
const idFieldId = commentsTable?.fields.find(
(f) => f.name === 'id'
)?.id;
expect(idIndex?.fieldIds[0]).toBe(idFieldId);
// Verify relationships (inline refs should create relationships)
// From DBML:
// 1. posts.user_id -> users.id
// 2. comments.post_id -> posts.id
// 3. comments.user_id -> users.id
expect(diagram.relationships).toHaveLength(3);
// Find relationships - check the actual field references
const findRelationshipByFields = (
sourceTableId: string,
sourceFieldName: string,
targetTableId: string,
targetFieldName: string
) => {
const sourceField = diagram.tables
?.find((t) => t.id === sourceTableId)
?.fields.find((f) => f.name === sourceFieldName);
const targetField = diagram.tables
?.find((t) => t.id === targetTableId)
?.fields.find((f) => f.name === targetFieldName);
return diagram.relationships?.find(
(r) =>
(r.sourceFieldId === sourceField?.id &&
r.targetFieldId === targetField?.id) ||
(r.sourceFieldId === targetField?.id &&
r.targetFieldId === sourceField?.id)
);
};
// Relationship 1: posts.user_id -> users.id
const postsUsersRel = findRelationshipByFields(
postsTable!.id,
'user_id',
usersTable!.id,
'id'
);
expect(postsUsersRel).toBeDefined();
expect(postsUsersRel?.sourceSchema).toBeDefined();
expect(postsUsersRel?.targetSchema).toBeDefined();
// Relationship 2: comments.post_id -> posts.id
const commentsPostsRel = findRelationshipByFields(
commentsTable!.id,
'post_id',
postsTable!.id,
'id'
);
expect(commentsPostsRel).toBeDefined();
// Relationship 3: comments.user_id -> users.id
const commentsUsersRel = findRelationshipByFields(
commentsTable!.id,
'user_id',
usersTable!.id,
'id'
);
expect(commentsUsersRel).toBeDefined();
// Verify all relationships have the expected cardinality
// In DBML, inline refs create relationships where the referenced table (with PK)
// is the "one" side and the referencing table (with FK) is the "many" side
const allOneToMany = diagram.relationships?.every(
(r) =>
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
);
expect(allOneToMany).toBe(true);
// Verify schemas are preserved in relationships
const relationshipsHaveSchemas = diagram.relationships?.every(
(r) =>
r.sourceSchema !== undefined && r.targetSchema !== undefined
);
expect(relationshipsHaveSchemas).toBe(true);
});
});
describe('Notes Support', () => {
it('should import table with note', async () => {
const dbmlWithTableNote = `
Table products {
id integer [pk]
name varchar(100)
Note: 'This table stores product information'
}`;
const diagram = await importDBMLToDiagram(dbmlWithTableNote);
expect(diagram.tables).toHaveLength(1);
const productsTable = diagram.tables?.[0];
expect(productsTable?.name).toBe('products');
expect(productsTable?.comments).toBe(
'This table stores product information'
);
});
it('should import field with note', async () => {
const dbmlWithFieldNote = `
Table orders {
id integer [pk]
total numeric(10,2) [note: 'Order total including tax']
}`;
const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
expect(diagram.tables).toHaveLength(1);
const ordersTable = diagram.tables?.[0];
expect(ordersTable?.fields).toHaveLength(2);
const totalField = ordersTable?.fields.find(
(f) => f.name === 'total'
);
// Field notes should be imported
expect(totalField).toBeDefined();
expect(totalField?.name).toBe('total');
expect(totalField?.comments).toBe('Order total including tax');
});
});
});

View File

@@ -1,40 +0,0 @@
export interface DBMLError {
message: string;
line: number;
column: number;
}
export function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}

View File

@@ -4,16 +4,10 @@ import { generateDiagramId, generateId } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
import type { DataType } from '@/lib/data/data-types/data-types';
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
import { randomColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type Field from '@dbml/core/types/model_structure/field';
import type { DBIndex } from '@/lib/domain';
import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
// Preprocess DBML to handle unsupported features
export const preprocessDBML = (content: string): string => {
@@ -25,8 +19,8 @@ export const preprocessDBML = (content: string): string => {
// Remove Note blocks
processed = processed.replace(/Note\s+\w+\s*\{[^}]*\}/gs, '');
// Don't remove enum definitions - we'll parse them
// processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
// Remove enum definitions (blocks)
processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
// Handle array types by converting them to text
processed = processed.replace(/(\w+)\[\]/g, 'text');
@@ -38,9 +32,8 @@ export const preprocessDBML = (content: string): string => {
);
// Handle Table headers with color attributes
// This regex handles both simple table names and schema.table patterns with quotes
processed = processed.replace(
/Table\s+((?:"[^"]+"\."[^"]+")|(?:\w+))\s*\[[^\]]*\]\s*\{/g,
/Table\s+(\w+)\s*\[[^\]]*\]\s*\{/g,
'Table $1 {'
);
@@ -83,10 +76,6 @@ interface DBMLField {
pk?: boolean;
not_null?: boolean;
increment?: boolean;
characterMaximumLength?: string | null;
precision?: number | null;
scale?: number | null;
note?: string | { value: string } | null;
}
interface DBMLIndexColumn {
@@ -97,7 +86,7 @@ interface DBMLIndexColumn {
}
interface DBMLIndex {
columns: (string | DBMLIndexColumn)[];
columns: string | (string | DBMLIndexColumn)[];
unique?: boolean;
name?: string;
}
@@ -107,7 +96,6 @@ interface DBMLTable {
schema?: string | { name: string };
fields: DBMLField[];
indexes?: DBMLIndex[];
note?: string | { value: string } | null;
}
interface DBMLEndpoint {
@@ -120,51 +108,32 @@ interface DBMLRef {
endpoints: [DBMLEndpoint, DBMLEndpoint];
}
interface DBMLEnum {
name: string;
schema?: string | { name: string };
values: Array<{ name: string; note?: string }>;
note?: string | { value: string } | null;
}
const mapDBMLTypeToDataType = (
dbmlType: string,
options?: { databaseType?: DatabaseType; enums?: DBMLEnum[] }
): DataTypeData => {
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
// Check if it's an enum type
if (options?.enums) {
const enumDef = options.enums.find((e) => {
// Check both with and without schema prefix
const enumName = e.name.toLowerCase();
const enumFullName = e.schema
? `${e.schema}.${enumName}`
: enumName;
return (
normalizedType === enumName || normalizedType === enumFullName
);
});
if (enumDef) {
// Return enum as custom type reference
return {
id: enumDef.name,
name: enumDef.name,
} satisfies DataTypeData;
}
}
const matchedType = findDataTypeDataById(
normalizedType,
options?.databaseType
);
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
if (matchedType) return matchedType;
return {
id: normalizedType.split(' ').join('_').toLowerCase(),
name: normalizedType,
} satisfies DataTypeData;
const typeMap: Record<string, string> = {
int: 'integer',
varchar: 'varchar',
bool: 'boolean',
number: 'numeric',
string: 'varchar',
text: 'text',
timestamp: 'timestamp',
datetime: 'timestamp',
float: 'float',
double: 'double',
decimal: 'decimal',
bigint: 'bigint',
smallint: 'smallint',
char: 'char',
};
const mappedType = typeMap[normalizedType];
if (mappedType) {
const foundType = genericDataTypes.find((t) => t.id === mappedType);
if (foundType) return foundType;
}
return genericDataTypes.find((t) => t.id === 'varchar')!;
};
const determineCardinality = (
@@ -185,10 +154,7 @@ const determineCardinality = (
};
export const importDBMLToDiagram = async (
dbmlContent: string,
options?: {
databaseType?: DatabaseType;
}
dbmlContent: string
): Promise<Diagram> => {
try {
// Handle empty content
@@ -196,7 +162,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
databaseType: DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -214,7 +180,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
databaseType: DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -223,13 +189,14 @@ export const importDBMLToDiagram = async (
}
const parsedData = parser.parse(sanitizedContent, 'dbml');
const dbmlData = parsedData.schemas[0];
// Handle case where no schemas are found
if (!parsedData.schemas || parsedData.schemas.length === 0) {
// Handle case where no schema is found
if (!dbmlData || !dbmlData.tables) {
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
databaseType: DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -237,186 +204,71 @@ export const importDBMLToDiagram = async (
};
}
// Process all schemas, not just the first one
const allTables: DBMLTable[] = [];
const allRefs: DBMLRef[] = [];
const allEnums: DBMLEnum[] = [];
const getFieldExtraAttributes = (
field: Field,
enums: DBMLEnum[]
): Partial<DBMLField> => {
if (!field.type || !field.type.args) {
return {};
}
const args = field.type.args.split(',') as string[];
const dataType = mapDBMLTypeToDataType(field.type.type_name, {
...options,
enums,
});
if (dataType.fieldAttributes?.hasCharMaxLength) {
const charMaxLength = args?.[0];
return {
characterMaximumLength: charMaxLength,
};
} else if (
dataType.fieldAttributes?.precision &&
dataType.fieldAttributes?.scale
) {
const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;
const precision = precisionNum
? isNaN(precisionNum)
? undefined
: precisionNum
: undefined;
const scale = scaleNum
? isNaN(scaleNum)
? undefined
: scaleNum
: undefined;
return {
precision,
scale,
};
}
return {};
};
parsedData.schemas.forEach((schema) => {
if (schema.tables) {
schema.tables.forEach((table) => {
// For tables with explicit schema, use the schema name
// For tables without explicit schema, use empty string
const schemaName =
typeof table.schema === 'string'
? table.schema
: table.schema?.name || '';
allTables.push({
name: table.name,
schema: schemaName,
note: table.note,
fields: table.fields.map((field): DBMLField => {
return {
name: field.name,
type: field.type,
unique: field.unique,
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
note: field.note,
...getFieldExtraAttributes(field, allEnums),
} satisfies DBMLField;
}),
indexes:
table.indexes?.map((dbmlIndex) => {
let indexColumns: string[];
// Handle both string and array formats
if (typeof dbmlIndex.columns === 'string') {
// Handle composite index case "(col1, col2)"
// @ts-expect-error "columns" can be a string in some DBML versions
if (dbmlIndex.columns.includes('(')) {
const columnsStr: string =
// @ts-expect-error "columns" can be a string in some DBML versions
dbmlIndex.columns.replace(
/[()]/g,
''
);
indexColumns = columnsStr
.split(',')
.map((c) => c.trim());
} else {
// Single column as string
indexColumns = [
// @ts-expect-error "columns" can be a string in some DBML versions
dbmlIndex.columns.trim(),
];
}
} else {
// Handle array of columns
indexColumns = dbmlIndex.columns.map(
(col) => {
if (typeof col === 'string') {
// @ts-expect-error "columns" can be a string in some DBML versions
return col.trim();
} else if (
typeof col === 'object' &&
'value' in col
) {
return col.value.trim();
} else {
return String(col).trim();
}
}
);
}
// Generate a consistent index name
const indexName =
dbmlIndex.name ||
`idx_${table.name}_${indexColumns.join('_')}`;
return {
columns: indexColumns,
unique: dbmlIndex.unique || false,
name: indexName,
};
}) || [],
});
});
}
if (schema.refs) {
schema.refs.forEach((ref) => {
// Convert the ref to ensure it has exactly two endpoints
if (ref.endpoints && ref.endpoints.length >= 2) {
allRefs.push({
endpoints: [ref.endpoints[0], ref.endpoints[1]] as [
DBMLEndpoint,
DBMLEndpoint,
],
});
}
});
}
if (schema.enums) {
schema.enums.forEach((enumDef) => {
// Get schema name from enum or use schema's name
const enumSchema =
typeof enumDef.schema === 'string'
? enumDef.schema
: enumDef.schema?.name || schema.name;
allEnums.push({
name: enumDef.name,
schema: enumSchema === 'public' ? '' : enumSchema,
values: enumDef.values || [],
note: enumDef.note,
});
});
}
});
// Extract only the necessary data from the parsed DBML
const extractedData: {
tables: DBMLTable[];
refs: DBMLRef[];
enums: DBMLEnum[];
} = {
tables: allTables,
refs: allRefs,
enums: allEnums,
const extractedData = {
tables: (dbmlData.tables as unknown as DBMLTable[]).map(
(table) => ({
name: table.name,
schema: table.schema,
fields: table.fields.map((field: DBMLField) => ({
name: field.name,
type: field.type,
unique: field.unique,
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
})),
indexes:
table.indexes?.map((dbmlIndex) => {
let indexColumns: string[];
// Handle composite index case "(col1, col2)"
if (typeof dbmlIndex.columns === 'string') {
if (dbmlIndex.columns.includes('(')) {
// Composite index
const columnsStr =
dbmlIndex.columns.replace(/[()]/g, '');
indexColumns = columnsStr
.split(',')
.map((c) => c.trim());
} else {
// Single column
indexColumns = [dbmlIndex.columns.trim()];
}
} else {
// Handle array of columns
indexColumns = Array.isArray(dbmlIndex.columns)
? dbmlIndex.columns.map((col) =>
typeof col === 'object' &&
'value' in col
? (col.value as string).trim()
: (col as string).trim()
)
: [String(dbmlIndex.columns).trim()];
}
// Generate a consistent index name
const indexName =
dbmlIndex.name ||
`idx_${table.name}_${indexColumns.join('_')}`;
return {
columns: indexColumns,
unique: dbmlIndex.unique || false,
name: indexName,
};
}) || [],
})
),
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
(endpoint) => ({
tableName: endpoint.tableName,
fieldNames: endpoint.fieldNames,
relation: endpoint.relation,
})
),
})),
};
// Convert DBML tables to ChartDB table objects
@@ -426,40 +278,18 @@ export const importDBMLToDiagram = async (
const tableSpacing = 300;
// Create fields first so we have their IDs
const fields: DBField[] = table.fields.map((field) => {
// Extract field note/comment
let fieldComment: string | undefined;
if (field.note) {
if (typeof field.note === 'string') {
fieldComment = field.note;
} else if (
typeof field.note === 'object' &&
'value' in field.note
) {
fieldComment = field.note.value;
}
}
return {
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToDataType(field.type.type_name, {
...options,
enums: extractedData.enums,
}),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
characterMaximumLength: field.characterMaximumLength,
precision: field.precision,
scale: field.scale,
...(fieldComment ? { comments: fieldComment } : {}),
};
});
const fields = table.fields.map((field) => ({
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToGenericType(field.type.type_name),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
}));
// Convert DBML indexes to ChartDB indexes
const indexes: DBIndex[] =
const indexes =
table.indexes?.map((dbmlIndex) => {
const fieldIds = dbmlIndex.columns.map((columnName) => {
const field = fields.find((f) => f.name === columnName);
@@ -475,26 +305,13 @@ export const importDBMLToDiagram = async (
id: generateId(),
name:
dbmlIndex.name ||
`idx_${table.name}_${(dbmlIndex.columns as string[]).join('_')}`,
`idx_${table.name}_${dbmlIndex.columns.join('_')}`,
fieldIds,
unique: dbmlIndex.unique || false,
createdAt: Date.now(),
};
}) || [];
// Extract table note/comment
let tableComment: string | undefined;
if (table.note) {
if (typeof table.note === 'string') {
tableComment = table.note;
} else if (
typeof table.note === 'object' &&
'value' in table.note
) {
tableComment = table.note.value;
}
}
return {
id: generateId(),
name: table.name.replace(/['"]/g, ''),
@@ -510,8 +327,7 @@ export const importDBMLToDiagram = async (
color: randomColor(),
isView: false,
createdAt: Date.now(),
comments: tableComment,
} as DBTable;
};
});
// Create relationships using the refs
@@ -565,43 +381,12 @@ export const importDBMLToDiagram = async (
}
);
// Convert DBML enums to custom types
const customTypes: DBCustomType[] = extractedData.enums.map(
(enumDef) => {
// Extract values from enum
const values = enumDef.values
.map((v) => {
// Handle both string values and objects with name property
if (typeof v === 'string') {
return v;
} else if (v && typeof v === 'object' && 'name' in v) {
return v.name.replace(/["']/g, ''); // Remove quotes from values
}
return '';
})
.filter((v) => v !== '');
return {
id: generateId(),
schema:
typeof enumDef.schema === 'string'
? enumDef.schema
: undefined,
name: enumDef.name,
kind: DBCustomTypeKind.enum,
values,
order: 0,
} satisfies DBCustomType;
}
);
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
databaseType: DatabaseType.GENERIC,
tables,
relationships,
customTypes,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -1,4 +1,5 @@
export interface ChartDBConfig {
defaultDiagramId: string;
exportActions?: Date[];
hiddenTablesByDiagram?: Record<string, string[]>; // Maps diagram ID to array of hidden table IDs
}

View File

@@ -1,7 +1,10 @@
import { z } from 'zod';
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { DatabaseType } from './database-type';
import { schemaNameToDomainSchemaName } from './db-schema';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import { decodeViewDefinition, type DBTable } from './db-table';
import { generateId } from '@/lib/utils';
import type { AST } from 'node-sql-parser';
@@ -24,6 +27,18 @@ export const dbDependencySchema: z.ZodType<DBDependency> = z.object({
createdAt: z.number(),
});
export const shouldShowDependencyBySchemaFilter = (
dependency: DBDependency,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!dependency.schema ||
!dependency.dependentSchema ||
(filteredSchemas.includes(schemaNameToSchemaId(dependency.schema)) &&
filteredSchemas.includes(
schemaNameToSchemaId(dependency.dependentSchema)
));
const astDatabaseTypes: Record<DatabaseType, string> = {
[DatabaseType.POSTGRESQL]: 'postgresql',
[DatabaseType.MYSQL]: 'postgresql',

View File

@@ -1,15 +1,10 @@
import { z } from 'zod';
import {
dataTypeSchema,
findDataTypeDataById,
type DataType,
} from '../data/data-types/data-types';
import { dataTypeSchema, type DataType } from '../data/data-types/data-types';
import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-info';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
import { generateId } from '../utils';
import type { DatabaseType } from './database-type';
export interface DBField {
id: string;
@@ -102,80 +97,3 @@ export const createFieldsFromMetadata = ({
})
);
};
export const generateDBFieldSuffix = (
field: DBField,
{
databaseType,
forceExtended = false,
typeId,
}: {
databaseType?: DatabaseType;
forceExtended?: boolean;
typeId?: string;
} = {}
): string => {
if (databaseType && forceExtended && typeId) {
return generateExtendedSuffix(field, databaseType, typeId);
}
return generateStandardSuffix(field);
};
const generateExtendedSuffix = (
field: DBField,
databaseType: DatabaseType,
typeId: string
): string => {
const type = findDataTypeDataById(typeId, databaseType);
if (!type?.fieldAttributes) {
return '';
}
const { fieldAttributes } = type;
// Character maximum length types (e.g., VARCHAR)
if (fieldAttributes.hasCharMaxLength) {
const maxLength = field.characterMaximumLength ?? 'n';
return `(${maxLength})`;
}
// Precision and scale types (e.g., DECIMAL)
if (fieldAttributes.precision && fieldAttributes.scale) {
return formatPrecisionAndScale(field.precision, field.scale, '(p, s)');
}
// Precision only types (e.g., FLOAT)
if (fieldAttributes.precision) {
const precision = field.precision ?? 'p';
return `(${precision})`;
}
return '';
};
const generateStandardSuffix = (field: DBField): string => {
// Character maximum length
if (field.characterMaximumLength) {
return `(${field.characterMaximumLength})`;
}
return formatPrecisionAndScale(field.precision, field.scale, '');
};
const formatPrecisionAndScale = (
precision: number | null | undefined,
scale: number | null | undefined,
fallback: string
): string => {
if (precision && scale) {
return `(${precision}, ${scale})`;
}
if (precision) {
return `(${precision})`;
}
return fallback;
};

View File

@@ -1,7 +1,10 @@
import { z } from 'zod';
import type { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
import type { DBField } from './db-field';
import { schemaNameToDomainSchemaName } from './db-schema';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import type { DBTable } from './db-table';
import { generateId } from '@/lib/utils';
@@ -40,6 +43,20 @@ export type RelationshipType =
| 'many_to_many';
export type Cardinality = 'one' | 'many';
export const shouldShowRelationshipBySchemaFilter = (
relationship: DBRelationship,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!relationship.sourceSchema ||
!relationship.targetSchema ||
(filteredSchemas.includes(
schemaNameToSchemaId(relationship.sourceSchema)
) &&
filteredSchemas.includes(
schemaNameToSchemaId(relationship.targetSchema)
));
const determineCardinality = (
field: DBField,
isTablePKComplex: boolean

View File

@@ -18,7 +18,10 @@ import {
deepCopy,
generateId,
} from '../utils';
import { schemaNameToDomainSchemaName } from './db-schema';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
@@ -74,6 +77,26 @@ export const generateTableKey = ({
tableName: string;
}) => `${schemaNameToDomainSchemaName(schemaName) ?? ''}.${tableName}`;
export const shouldShowTableSchemaBySchemaFilter = ({
filteredSchemas,
tableSchema,
}: {
tableSchema?: string | null;
filteredSchemas?: string[];
}): boolean =>
!filteredSchemas ||
!tableSchema ||
filteredSchemas.includes(schemaNameToSchemaId(tableSchema));
export const shouldShowTablesBySchemaFilter = (
table: DBTable,
filteredSchemas?: string[]
): boolean =>
shouldShowTableSchemaBySchemaFilter({
filteredSchemas,
tableSchema: table?.schema,
});
export const decodeViewDefinition = (
databaseType: DatabaseType,
viewDefinition?: string

View File

@@ -1,147 +0,0 @@
// union logic filter
export interface DiagramFilter {
schemaIds?: string[];
tableIds?: string[];
}
export interface TableInfo {
id: string;
schemaId?: string;
}
/**
* Reduces/optimizes a DiagramFilter by removing redundant entries
* - Removes tableIds that belong to schemas already in schemaIds (union logic)
* - Consolidates complete schemas: if all tables from a schema are in tableIds, adds the schema to schemaIds
* - Returns undefined for both fields if everything is displayed
* - Returns empty arrays if nothing should be displayed
*/
export function reduceFilter(
filter: DiagramFilter,
tables: TableInfo[]
): DiagramFilter {
let { schemaIds, tableIds } = filter;
// If no filters are defined, everything is visible
if (!schemaIds && !tableIds) {
return { schemaIds: undefined, tableIds: undefined };
}
// Get all unique schema IDs from tables
const allSchemaIds = [
...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!)),
];
const allTableIds = tables.map((t) => t.id);
// in case its db with no schemas
if (allSchemaIds.length === 0) {
const tableSet = new Set(tableIds);
if (tableSet.size === allTableIds.length) {
return { schemaIds: undefined, tableIds: undefined };
}
return { schemaIds: undefined, tableIds: Array.from(tableSet) };
}
// Build a map of schema to its tables
const schemaToTables = new Map<string, string[]>();
tables.forEach((table) => {
if (table.schemaId) {
if (!schemaToTables.has(table.schemaId)) {
schemaToTables.set(table.schemaId, []);
}
schemaToTables.get(table.schemaId)!.push(table.id);
}
});
// Consolidate complete schemas: if all tables from a schema are in tableIds, add schema to schemaIds
if (tableIds) {
const tableSet = new Set(tableIds);
const consolidatedSchemaIds = new Set(schemaIds || []);
let consolidatedTableIds = [...tableIds];
for (const [schemaId, schemaTables] of schemaToTables.entries()) {
// Check if all tables from this schema are in tableIds
if (schemaTables.every((tableId) => tableSet.has(tableId))) {
// Add schema to schemaIds
consolidatedSchemaIds.add(schemaId);
// Remove these tables from tableIds
consolidatedTableIds = consolidatedTableIds.filter(
(id) => !schemaTables.includes(id)
);
}
}
schemaIds =
consolidatedSchemaIds.size > 0
? Array.from(consolidatedSchemaIds)
: schemaIds;
tableIds =
consolidatedTableIds.length > 0 ? consolidatedTableIds : undefined;
}
// If all schemas are in the filter, everything is visible
if (schemaIds && schemaIds.length === allSchemaIds.length) {
const schemasSet = new Set(schemaIds);
const allSchemasIncluded = allSchemaIds.every((id) =>
schemasSet.has(id)
);
if (allSchemasIncluded) {
return { schemaIds: undefined, tableIds: undefined };
}
}
// If schemaIds is defined, remove tables from tableIds that belong to those schemas
let reducedTableIds = tableIds;
if (schemaIds && tableIds) {
const schemaSet = new Set(schemaIds);
reducedTableIds = tableIds.filter((tableId) => {
const table = tables.find((t) => t.id === tableId);
// Keep table in tableIds only if it doesn't belong to a schema in schemaIds
return !table?.schemaId || !schemaSet.has(table.schemaId);
});
// If no tables remain after reduction, set to undefined
if (reducedTableIds.length === 0) {
reducedTableIds = undefined;
}
}
// Check if all tables are now visible (either through schemas or direct table IDs)
if (schemaIds && reducedTableIds) {
const schemaSet = new Set(schemaIds);
const tableSet = new Set(reducedTableIds);
const visibleTables = tables.filter((table) => {
// Table is visible if it's in tableIds OR its schema is in schemaIds
return (
tableSet.has(table.id) ||
(table.schemaId && schemaSet.has(table.schemaId))
);
});
if (visibleTables.length === tables.length) {
return { schemaIds: undefined, tableIds: undefined };
}
} else if (schemaIds && !reducedTableIds) {
// Only schemaIds is defined, check if all tables are covered by schemas
const schemaSet = new Set(schemaIds);
const visibleTables = tables.filter(
(table) => table.schemaId && schemaSet.has(table.schemaId)
);
if (visibleTables.length === tables.length) {
return { schemaIds: undefined, tableIds: undefined };
}
} else if (!schemaIds && reducedTableIds) {
// Only tableIds is defined, check if all tables are in the filter
if (reducedTableIds.length === allTableIds.length) {
return { schemaIds: undefined, tableIds: undefined };
}
}
return {
schemaIds,
tableIds: reducedTableIds,
};
}

View File

@@ -1,114 +0,0 @@
import { schemaNameToSchemaId } from '../db-schema';
import type { DiagramFilter } from './diagram-filter';
export const filterTable = ({
table,
filter,
options = { defaultSchema: undefined },
}: {
table: { id: string; schema?: string | null };
filter?: DiagramFilter;
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!filter) {
return true;
}
if (!filter.tableIds && !filter.schemaIds) {
return true;
}
if (filter.tableIds && filter.tableIds.includes(table.id)) {
return true;
}
const tableSchema = table.schema ?? options.defaultSchema;
if (
tableSchema &&
filter.schemaIds &&
filter.schemaIds.includes(schemaNameToSchemaId(tableSchema))
) {
return true;
}
return false;
};
export const filterTableBySchema = ({
table,
schemaIdsFilter,
options = { defaultSchema: undefined },
}: {
table: { id: string; schema?: string | null };
schemaIdsFilter?: string[];
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!schemaIdsFilter) {
return true;
}
const tableSchemaId = table.schema ?? options.defaultSchema;
if (tableSchemaId) {
return schemaIdsFilter.includes(schemaNameToSchemaId(tableSchemaId));
}
return false;
};
export const filterSchema = ({
schemaId,
schemaIdsFilter,
}: {
schemaId?: string;
schemaIdsFilter?: string[];
}): boolean => {
if (!schemaIdsFilter) {
return true;
}
if (!schemaId) {
return false;
}
return schemaIdsFilter.includes(schemaId);
};
export const filterRelationship = ({
tableA: { id: tableAId, schema: tableASchema },
tableB: { id: tableBId, schema: tableBSchema },
filter,
options = { defaultSchema: undefined },
}: {
tableA: { id: string; schema?: string | null };
tableB: { id: string; schema?: string | null };
filter?: DiagramFilter;
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!filter) {
return true;
}
const isTableAVisible = filterTable({
table: { id: tableAId, schema: tableASchema },
filter,
options,
});
const isTableBVisible = filterTable({
table: { id: tableBId, schema: tableBSchema },
filter,
options,
});
return isTableAVisible && isTableBVisible;
};
export const filterDependency = filterRelationship;

View File

@@ -108,7 +108,7 @@ export const loadFromDatabaseMetadata = async ({
return a.isView ? 1 : -1;
});
const diagram: Diagram = {
const diagram = {
id: generateDiagramId(),
name: databaseMetadata.database_name
? `${databaseMetadata.database_name}-db`

View File

@@ -329,27 +329,6 @@ function compareFieldProperties({
changedAttributes.push('comments');
}
if (
(newField.characterMaximumLength || oldField.characterMaximumLength) &&
oldField.characterMaximumLength !== newField.characterMaximumLength
) {
changedAttributes.push('characterMaximumLength');
}
if (
(newField.scale || oldField.scale) &&
oldField.scale !== newField.scale
) {
changedAttributes.push('scale');
}
if (
(newField.precision || oldField.precision) &&
oldField.precision !== newField.precision
) {
changedAttributes.push('precision');
}
if (changedAttributes.length > 0) {
for (const attribute of changedAttributes) {
diffMap.set(

View File

@@ -12,10 +12,7 @@ export type FieldDiffAttribute =
| 'primaryKey'
| 'unique'
| 'nullable'
| 'comments'
| 'characterMaximumLength'
| 'precision'
| 'scale';
| 'comments';
export const fieldDiffAttributeSchema: z.ZodType<FieldDiffAttribute> = z.union([
z.literal('name'),
@@ -64,8 +61,8 @@ export interface FieldDiffChanged {
fieldId: string;
tableId: string;
attribute: FieldDiffAttribute;
oldValue: string | boolean | DataType | number;
newValue: string | boolean | DataType | number;
oldValue: string | boolean | DataType;
newValue: string | boolean | DataType;
}
export const fieldDiffChangedSchema: z.ZodType<FieldDiffChanged> = z.object({

View File

@@ -80,7 +80,7 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
<NodeResizer
isVisible={focused}
lineClassName="!border-4 !border-transparent"
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
handleClassName="!h-[18px] !w-[18px] !rounded-full !bg-pink-600"
minHeight={100}
minWidth={100}
/>

View File

@@ -1,10 +1,6 @@
import type { DBTable } from '@/lib/domain/db-table';
import type { Area } from '@/lib/domain/area';
import { calcTableHeight } from '@/lib/domain/db-table';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { DatabaseType } from '@/lib/domain/database-type';
/**
* Check if a table is inside an area based on their positions and dimensions
@@ -34,54 +30,16 @@ const isTableInsideArea = (table: DBTable, area: Area): boolean => {
);
};
/**
* Check if an area is visible based on its tables
*/
const isAreaVisible = (
area: Area,
tables: DBTable[],
filter?: DiagramFilter,
databaseType?: DatabaseType
): boolean => {
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
// If area has no tables, consider it visible
if (tablesInArea.length === 0) return true;
// Area is visible if at least one table in it is visible
return tablesInArea.some((table) =>
filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: {
defaultSchema:
defaultSchemas[databaseType || DatabaseType.GENERIC],
},
})
);
};
/**
* Find which area contains a table
*/
const findContainingArea = (
table: DBTable,
areas: Area[],
tables: DBTable[],
filter?: DiagramFilter,
databaseType?: DatabaseType
): Area | null => {
const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
// Sort areas by order (if available) to prioritize top-most areas
const sortedAreas = [...areas].sort(
(a, b) => (b.order ?? 0) - (a.order ?? 0)
);
for (const area of sortedAreas) {
// Skip hidden areas - they shouldn't capture tables
if (!isAreaVisible(area, tables, filter, databaseType)) {
continue;
}
if (isTableInsideArea(table, area)) {
return area;
}
@@ -95,33 +53,10 @@ const findContainingArea = (
*/
export const updateTablesParentAreas = (
tables: DBTable[],
areas: Area[],
filter?: DiagramFilter,
databaseType?: DatabaseType
areas: Area[]
): DBTable[] => {
return tables.map((table) => {
// Skip hidden tables - they shouldn't be assigned to areas
const isTableVisible = filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: {
defaultSchema:
defaultSchemas[databaseType || DatabaseType.GENERIC],
},
});
if (!isTableVisible) {
// Hidden tables keep their current parent area (don't change)
return table;
}
const containingArea = findContainingArea(
table,
areas,
tables,
filter,
databaseType
);
const containingArea = findContainingArea(table, areas);
const newParentAreaId = containingArea?.id || null;
// Only update if parentAreaId has changed
@@ -145,26 +80,3 @@ export const getTablesInArea = (
): DBTable[] => {
return tables.filter((table) => table.parentAreaId === areaId);
};
/**
* Get visible tables that are inside a specific area
*/
export const getVisibleTablesInArea = (
areaId: string,
tables: DBTable[],
filter?: DiagramFilter,
databaseType?: DatabaseType
): DBTable[] => {
return tables.filter((table) => {
if (table.parentAreaId !== areaId) return false;
return filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: {
defaultSchema:
defaultSchemas[databaseType || DatabaseType.GENERIC],
},
});
});
};

View File

@@ -11,13 +11,12 @@ import { useReactFlow } from '@xyflow/react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, Workflow, Group } from 'lucide-react';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { createTable, readonly, createArea } = useChartDB();
const { schemasDisplayed } = useDiagramFilter();
const { createTable, filteredSchemas, schemas, readonly, createArea } =
useChartDB();
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
const { screenToFlowPosition } = useReactFlow();
const { t } = useTranslation();
@@ -31,7 +30,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: event.clientY,
});
if (schemasDisplayed.length > 1) {
if ((filteredSchemas?.length ?? 0) > 1) {
openTableSchemaDialog({
onConfirm: ({ schema }) =>
createTable({
@@ -39,12 +38,14 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: position.y,
schema: schema.name,
}),
schemas: schemasDisplayed,
schemas: schemas.filter((schema) =>
filteredSchemas?.includes(schema.id)
),
});
} else {
const schema =
schemasDisplayed?.length === 1
? schemasDisplayed[0]?.name
filteredSchemas?.length === 1
? schemas.find((s) => s.id === filteredSchemas[0])?.name
: undefined;
createTable({
x: position.x,
@@ -57,7 +58,8 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
createTable,
screenToFlowPosition,
openTableSchemaDialog,
schemasDisplayed,
schemas,
filteredSchemas,
]
);
@@ -80,15 +82,13 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
openCreateRelationshipDialog();
}, [openCreateRelationshipDialog]);
if (!isDesktop) {
if (!isDesktop || readonly) {
return <>{children}</>;
}
return (
<ContextMenu>
<ContextMenuTrigger disabled={readonly}>
{children}
</ContextMenuTrigger>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={createTableHandler}

View File

@@ -5,49 +5,32 @@ import React, {
useEffect,
useRef,
} from 'react';
import {
X,
Search,
Eye,
EyeOff,
Database,
Table,
Funnel,
Layers,
Box,
} from 'lucide-react';
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
import { useChartDB } from '@/hooks/use-chartdb';
import type { DBTable } from '@/lib/domain/db-table';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { shouldShowTableSchemaBySchemaFilter } from '@/lib/domain/db-table';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useReactFlow } from '@xyflow/react';
import { TreeView } from '@/components/tree-view/tree-view';
import type { TreeNode } from '@/components/tree-view/tree';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
export interface CanvasFilterProps {
onClose: () => void;
}
type NodeType = 'schema' | 'area' | 'table';
type GroupingMode = 'schema' | 'area';
type NodeType = 'schema' | 'table';
type SchemaContext = { name: string; visible: boolean };
type AreaContext = { id: string; name: string; visible: boolean };
type SchemaContext = { name: string };
type TableContext = {
tableSchema?: string | null;
visible: boolean;
hidden: boolean;
};
type NodeContext = {
schema: SchemaContext;
area: AreaContext;
table: TableContext;
};
@@ -59,19 +42,19 @@ type RelevantTableData = {
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
const { t } = useTranslation();
const { tables, databaseType, areas } = useChartDB();
const {
filter,
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
} = useDiagramFilter();
tables,
databaseType,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
filteredSchemas,
filterSchemas,
} = useChartDB();
const { fitView, setNodes } = useReactFlow();
const [searchQuery, setSearchQuery] = useState('');
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
const searchInputRef = useRef<HTMLInputElement>(null);
// Extract only the properties needed for tree data
@@ -85,262 +68,72 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
[tables]
);
const databaseWithSchemas = useMemo(
() => !!defaultSchemas[databaseType],
[databaseType]
);
// Convert tables to tree nodes
const treeData = useMemo(() => {
// Group tables by schema
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
const schema =
table.schema ?? defaultSchemas[databaseType] ?? 'default';
if (!tablesBySchema.has(schema)) {
tablesBySchema.set(schema, []);
}
tablesBySchema.get(schema)!.push(table);
});
// Sort tables within each schema
tablesBySchema.forEach((tables) => {
tables.sort((a, b) => a.name.localeCompare(b.name));
});
// Convert to tree nodes
const nodes: TreeNode<NodeType, NodeContext>[] = [];
if (groupingMode === 'area') {
// Group tables by area
const tablesByArea = new Map<string | null, DBTable[]>();
const tablesWithoutArea: DBTable[] = [];
tables.forEach((table) => {
if (table.parentAreaId) {
if (!tablesByArea.has(table.parentAreaId)) {
tablesByArea.set(table.parentAreaId, []);
}
tablesByArea.get(table.parentAreaId)!.push(table);
} else {
tablesWithoutArea.push(table);
}
});
// Sort tables within each area
tablesByArea.forEach((areaTables) => {
areaTables.sort((a, b) => a.name.localeCompare(b.name));
});
tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
// Create nodes for areas
areas.forEach((area) => {
const areaTables = tablesByArea.get(area.id) || [];
// Check if at least one table in the area is visible
const areaVisible =
areaTables.length === 0 ||
areaTables.some((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
);
const areaNode: TreeNode<NodeType, NodeContext> = {
id: `area-${area.id}`,
name: `${area.name} (${areaTables.length})`,
type: 'area',
isFolder: true,
icon: Box,
context: {
id: area.id,
name: area.name,
visible: areaVisible,
} as AreaContext,
className: !areaVisible ? 'opacity-50' : '',
children: areaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
tablesBySchema.forEach((schemaTables, schemaName) => {
const schemaId = schemaNameToSchemaId(schemaName);
const schemaHidden = filteredSchemas
? !filteredSchemas.includes(schemaId)
: false;
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
type: 'schema',
isFolder: true,
icon: Database,
context: { name: schemaName },
className: schemaHidden ? 'opacity-50' : '',
children: schemaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableHidden =
hiddenTableIds?.includes(table.id) ?? false;
const visibleBySchema =
shouldShowTableSchemaBySchemaFilter({
tableSchema: table.schema,
filteredSchemas,
});
const hidden = tableHidden || !visibleBySchema;
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} as TableContext,
className: !tableVisible ? 'opacity-50' : '',
};
}
),
};
if (areaTables.length > 0) {
nodes.push(areaNode);
}
});
// Add ungrouped tables
if (tablesWithoutArea.length > 0) {
const ungroupedVisible = tablesWithoutArea.some((table) =>
filterTable({
table: {
return {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
);
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
id: 'ungrouped',
name: `Ungrouped (${tablesWithoutArea.length})`,
type: 'area',
isFolder: true,
icon: Layers,
context: {
id: 'ungrouped',
name: 'Ungrouped',
visible: ungroupedVisible,
} as AreaContext,
className: !ungroupedVisible ? 'opacity-50' : '',
children: tablesWithoutArea.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} as TableContext,
className: !tableVisible ? 'opacity-50' : '',
};
}
),
};
nodes.push(ungroupedNode);
}
} else {
// Group tables by schema (existing logic)
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
const schema = !databaseWithSchemas
? 'All Tables'
: (table.schema ??
defaultSchemas[databaseType] ??
'default');
if (!tablesBySchema.has(schema)) {
tablesBySchema.set(schema, []);
}
tablesBySchema.get(schema)!.push(table);
});
// Sort tables within each schema
tablesBySchema.forEach((tables) => {
tables.sort((a, b) => a.name.localeCompare(b.name));
});
tablesBySchema.forEach((schemaTables, schemaName) => {
let schemaVisible;
if (databaseWithSchemas) {
const schemaId = schemaNameToSchemaId(schemaName);
schemaVisible = filterSchema({
schemaId,
schemaIdsFilter: filter?.schemaIds,
});
} else {
// if at least one table is visible, the schema is considered visible
schemaVisible = schemaTables.some((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
hidden: tableHidden,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
);
}
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
type: 'schema',
isFolder: true,
icon: Database,
context: {
name: schemaName,
visible: schemaVisible,
} as SchemaContext,
className: !schemaVisible ? 'opacity-50' : '',
children: schemaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
const hidden = !tableVisible;
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} as TableContext,
className: hidden ? 'opacity-50' : '',
};
}
),
};
nodes.push(schemaNode);
});
}
className: hidden ? 'opacity-50' : '',
};
}
),
};
nodes.push(schemaNode);
});
return nodes;
}, [
relevantTableData,
tables,
databaseType,
filter,
databaseWithSchemas,
groupingMode,
areas,
]);
}, [relevantTableData, databaseType, hiddenTableIds, filteredSchemas]);
// Initialize expanded state with all schemas expanded
useMemo(() => {
@@ -376,6 +169,17 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
return result;
}, [treeData, searchQuery]);
const toggleTableVisibility = useCallback(
async (tableId: string, hidden: boolean) => {
if (hidden) {
await addHiddenTableId(tableId);
} else {
await removeHiddenTableId(tableId);
}
},
[addHiddenTableId, removeHiddenTableId]
);
const focusOnTable = useCallback(
(tableId: string) => {
// Make sure the table is visible
@@ -415,12 +219,11 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
const renderActions = useCallback(
(node: TreeNode<NodeType, NodeContext>) => {
if (node.type === 'schema') {
const context = node.context as SchemaContext;
const schemaVisible = context.visible;
const schemaName = context.name;
if (!schemaName) return null;
const schemaId = schemaNameToSchemaId(schemaName);
const schemaContext = node.context as SchemaContext;
const schemaId = schemaNameToSchemaId(schemaContext.name);
const schemaHidden = filteredSchemas
? !filteredSchemas.includes(schemaId)
: false;
return (
<Button
@@ -429,93 +232,30 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
if (databaseWithSchemas) {
toggleSchemaFilter(schemaId);
} else {
// Toggle visibility of all tables in this schema
if (schemaVisible) {
setTableIdsFilterEmpty();
} else {
clearTableIdsFilter();
// unhide all tables in this schema
node.children?.forEach((child) => {
if (
child.type === 'table' &&
hiddenTableIds?.includes(child.id)
) {
removeHiddenTableId(child.id);
}
}
}}
>
{!schemaVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
if (node.type === 'area') {
const context = node.context as AreaContext;
const areaVisible = context.visible;
const areaId = context.id;
if (!areaId) return null;
// Get all tables in this area
const areaTables =
areaId === 'ungrouped'
? tables.filter((t) => !t.parentAreaId)
: tables.filter((t) => t.parentAreaId === areaId);
const tableIds = areaTables.map((t) => t.id);
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
// Toggle all tables in this area
if (areaVisible) {
// Hide all tables in this area
tableIds.forEach((id) => {
const isVisible = filterTable({
table: {
id,
schema: tables.find(
(t) => t.id === id
)?.schema,
},
filter,
options: {
defaultSchema:
defaultSchemas[databaseType],
},
});
if (isVisible) {
toggleTableFilter(id);
}
});
});
if (schemaHidden) {
filterSchemas([
...(filteredSchemas ?? []),
schemaId,
]);
} else {
// Show all tables in this area
tableIds.forEach((id) => {
const isVisible = filterTable({
table: {
id,
schema: tables.find(
(t) => t.id === id
)?.schema,
},
filter,
options: {
defaultSchema:
defaultSchemas[databaseType],
},
});
if (!isVisible) {
toggleTableFilter(id);
}
});
filterSchemas(
filteredSchemas?.filter(
(s) => s !== schemaId
) ?? []
);
}
}}
>
{!areaVisible ? (
{schemaHidden ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
@@ -526,8 +266,14 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
if (node.type === 'table') {
const tableId = node.id;
const context = node.context as TableContext;
const tableVisible = context.visible;
const tableContext = node.context as TableContext;
const hidden = tableContext.hidden;
const tableSchema = tableContext.tableSchema;
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
tableSchema,
filteredSchemas,
});
return (
<Button
@@ -536,10 +282,35 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
toggleTableFilter(tableId);
if (!visibleBySchema && tableSchema) {
// Unhide schema and hide all other tables
const schemaId =
schemaNameToSchemaId(tableSchema);
filterSchemas([
...(filteredSchemas ?? []),
schemaId,
]);
const schemaNode = treeData.find(
(s) =>
(s.context as SchemaContext).name ===
tableSchema
);
if (schemaNode) {
schemaNode.children?.forEach((child) => {
if (
child.id !== tableId &&
!hiddenTableIds?.includes(child.id)
) {
addHiddenTableId(child.id);
}
});
}
} else {
toggleTableVisibility(tableId, !hidden);
}
}}
>
{!tableVisible ? (
{hidden || !visibleBySchema ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
@@ -551,14 +322,13 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
return null;
},
[
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
databaseWithSchemas,
tables,
filter,
databaseType,
toggleTableVisibility,
filteredSchemas,
filterSchemas,
treeData,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
]
);
@@ -566,16 +336,20 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
const handleNodeClick = useCallback(
(node: TreeNode<NodeType, NodeContext>) => {
if (node.type === 'table') {
const context = node.context as TableContext;
const isTableVisible = context.visible;
const tableContext = node.context as TableContext;
const tableSchema = tableContext.tableSchema;
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
tableSchema,
filteredSchemas,
});
// Only focus if table is visible
if (isTableVisible) {
// Only focus if neither table is hidden nor filtered by schema
if (!tableContext.hidden && visibleBySchema) {
focusOnTable(node.id);
}
}
},
[focusOnTable]
[focusOnTable, filteredSchemas]
);
// Animate in on mount and focus search input
@@ -630,40 +404,19 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
</div>
</div>
{/* Grouping Toggle */}
<div className="border-b p-2">
<ToggleGroup
type="single"
value={groupingMode}
onValueChange={(value) => {
if (value) setGroupingMode(value as GroupingMode);
}}
className="w-full"
>
<ToggleGroupItem value="schema" className="flex-1 text-xs">
<Database className="mr-1.5 size-3.5" />
{t('canvas_filter.group_by_schema', 'Group by Schema')}
</ToggleGroupItem>
<ToggleGroupItem value="area" className="flex-1 text-xs">
<Box className="mr-1.5 size-3.5" />
{t('canvas_filter.group_by_area', 'Group by Area')}
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Table Tree */}
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
<div className="flex-1 overflow-y-auto rounded-b-lg">
<TreeView
data={filteredTreeData}
onNodeClick={handleNodeClick}
renderActionsComponent={renderActions}
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
defaultFolderIcon={Database}
defaultIcon={Table}
expanded={expanded}
setExpanded={setExpanded}
className="py-2"
/>
</ScrollArea>
</div>
</div>
);
};

View File

@@ -40,13 +40,7 @@ import {
} from './table-node/table-node-field';
import { Toolbar } from './toolbar/toolbar';
import { useToast } from '@/components/toast/use-toast';
import {
Pencil,
LayoutGrid,
AlertTriangle,
Magnet,
Highlighter,
} from 'lucide-react';
import { Pencil, LayoutGrid, AlertTriangle, Magnet } from 'lucide-react';
import { Button } from '@/components/button/button';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
@@ -54,7 +48,10 @@ import { Badge } from '@/components/badge/badge';
import { useTheme } from '@/hooks/use-theme';
import { useTranslation } from 'react-i18next';
import type { DBTable } from '@/lib/domain/db-table';
import { MIN_TABLE_SIZE } from '@/lib/domain/db-table';
import {
MIN_TABLE_SIZE,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { useLocalConfig } from '@/hooks/use-local-config';
import {
Tooltip,
@@ -86,15 +83,9 @@ import { useCanvas } from '@/hooks/use-canvas';
import type { AreaNodeType } from './area-node/area-node';
import { AreaNode } from './area-node/area-node';
import type { Area } from '@/lib/domain/area';
import { updateTablesParentAreas, getVisibleTablesInArea } from './area-utils';
import { updateTablesParentAreas, getTablesInArea } from './area-utils';
import { CanvasFilter } from './canvas-filter/canvas-filter';
import { useHotkeys } from 'react-hotkeys-hook';
import { ShowAllButton } from './show-all-button';
import { useIsLostInCanvas } from './hooks/use-is-lost-in-canvas';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
const HIGHLIGHTED_EDGE_Z_INDEX = 1;
const DEFAULT_EDGE_Z_INDEX = 0;
@@ -119,8 +110,13 @@ const initialEdges: EdgeType[] = [];
const tableToTableNode = (
table: DBTable,
filter: DiagramFilter | undefined,
databaseType: DatabaseType
{
filteredSchemas,
hiddenTableIds,
}: {
filteredSchemas?: string[];
hiddenTableIds?: string[];
}
): TableNodeType => {
// Always use absolute position for now
const position = { x: table.x, y: table.y };
@@ -134,48 +130,21 @@ const tableToTableNode = (
isOverlapping: false,
},
width: table.width ?? MIN_TABLE_SIZE,
hidden: !filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: { defaultSchema: defaultSchemas[databaseType] },
}),
hidden:
!shouldShowTablesBySchemaFilter(table, filteredSchemas) ||
(hiddenTableIds?.includes(table.id) ?? false),
};
};
const areaToAreaNode = (
area: Area,
tables: DBTable[],
filter?: DiagramFilter,
databaseType?: DatabaseType
): AreaNodeType => {
// Get all tables in this area
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
// Check if at least one table in the area is visible
const hasVisibleTable =
tablesInArea.length === 0 ||
tablesInArea.some((table) =>
filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: {
defaultSchema:
defaultSchemas[databaseType || DatabaseType.GENERIC],
},
})
);
return {
id: area.id,
type: 'area',
position: { x: area.x, y: area.y },
data: { area },
width: area.width,
height: area.height,
zIndex: -10,
hidden: !hasVisibleTable,
};
};
const areaToAreaNode = (area: Area): AreaNodeType => ({
id: area.id,
type: 'area',
position: { x: area.x, y: area.y },
data: { area },
width: area.width,
height: area.height,
zIndex: -10,
});
export interface CanvasProps {
initialTables: DBTable[];
@@ -189,7 +158,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
>([]);
const { toast } = useToast();
const { t } = useTranslation();
const { isLostInCanvas } = useIsLostInCanvas();
const {
tables,
areas,
@@ -201,13 +169,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
removeDependencies,
getField,
databaseType,
filteredSchemas,
events,
dependencies,
readonly,
removeArea,
updateArea,
highlightedCustomType,
highlightCustomTypeId,
hiddenTableIds,
} = useChartDB();
const { showSidePanel } = useLayout();
const { effectiveTheme } = useTheme();
@@ -225,13 +193,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
showFilter,
setShowFilter,
} = useCanvas();
const { filter } = useDiagramFilter();
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
initialTables.map((table) =>
tableToTableNode(table, filter, databaseType)
tableToTableNode(table, { filteredSchemas, hiddenTableIds })
)
);
const [edges, setEdges, onEdgesChange] =
@@ -245,12 +212,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
useEffect(() => {
const initialNodes = initialTables.map((table) =>
tableToTableNode(table, filter, databaseType)
tableToTableNode(table, { filteredSchemas, hiddenTableIds })
);
if (equal(initialNodes, nodes)) {
setIsInitialLoadingNodes(false);
}
}, [initialTables, nodes, filter, databaseType]);
}, [initialTables, nodes, filteredSchemas, hiddenTableIds]);
useEffect(() => {
if (!isInitialLoadingNodes) {
@@ -413,16 +380,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
...tables.map((table) => {
const isOverlapping =
(overlapGraph.graph.get(table.id) ?? []).length > 0;
const node = tableToTableNode(table, filter, databaseType);
// Check if table uses the highlighted custom type
let hasHighlightedCustomType = false;
if (highlightedCustomType) {
hasHighlightedCustomType = table.fields.some(
(field) =>
field.type.name === highlightedCustomType.name
);
}
const node = tableToTableNode(table, {
filteredSchemas,
hiddenTableIds,
});
return {
...node,
@@ -430,13 +391,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
...node.data,
isOverlapping,
highlightOverlappingTables,
hasHighlightedCustomType,
},
};
}),
...areas.map((area) =>
areaToAreaNode(area, tables, filter, databaseType)
),
...areas.map(areaToAreaNode),
];
// Check if nodes actually changed
@@ -450,30 +408,20 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
tables,
areas,
setNodes,
filter,
databaseType,
filteredSchemas,
hiddenTableIds,
overlapGraph.lastUpdated,
overlapGraph.graph,
highlightOverlappingTables,
highlightedCustomType,
]);
const prevFilter = useRef<DiagramFilter | undefined>(undefined);
const prevFilteredSchemas = useRef<string[] | undefined>(undefined);
useEffect(() => {
if (!equal(filter, prevFilter.current)) {
if (!equal(filteredSchemas, prevFilteredSchemas.current)) {
debounce(() => {
const overlappingTablesInDiagram = findOverlappingTables({
tables: tables.filter((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
});
setOverlapGraph(overlappingTablesInDiagram);
@@ -483,9 +431,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
maxZoom: 0.8,
});
}, 500)();
prevFilter.current = filter;
prevFilteredSchemas.current = filteredSchemas;
}
}, [filter, fitView, tables, setOverlapGraph, databaseType]);
}, [filteredSchemas, fitView, tables, setOverlapGraph]);
// Handle parent area updates when tables move
const tablePositions = useMemo(
@@ -495,12 +443,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
useEffect(() => {
const checkParentAreas = debounce(() => {
const updatedTables = updateTablesParentAreas(
tables,
areas,
filter,
databaseType
);
const updatedTables = updateTablesParentAreas(tables, areas);
const needsUpdate: Array<{
id: string;
parentAreaId: string | null;
@@ -510,7 +453,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const oldTable = tables[index];
if (
oldTable &&
(!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
newTable.parentAreaId !== oldTable.parentAreaId
) {
needsUpdate.push({
@@ -541,14 +483,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
}, 300);
checkParentAreas();
}, [
tablePositions,
areas,
updateTablesState,
tables,
filter,
databaseType,
]);
}, [tablePositions, areas, updateTablesState, tables]);
const onConnectHandler = useCallback(
async (params: AddEdgeParams) => {
@@ -927,37 +862,16 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const deltaX = change.position.x - currentArea.x;
const deltaY = change.position.y - currentArea.y;
// Only move visible child tables
const childTables = getVisibleTablesInArea(
const childTables = getTablesInArea(
change.id,
tables,
filter,
databaseType
tables
);
// Update child table positions in storage
if (childTables.length > 0) {
updateTablesState((currentTables) =>
currentTables.map((table) => {
// Only move visible tables that are in this area
const isVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema:
defaultSchemas[
databaseType
],
},
});
if (
table.parentAreaId === change.id &&
isVisible
) {
if (table.parentAreaId === change.id) {
return {
id: table.id,
x: table.x + deltaX,
@@ -1021,8 +935,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
tables,
areas,
getNode,
databaseType,
filter,
]
);
@@ -1078,21 +990,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
overlapGraph
);
setOverlapGraph(newOverlappingGraph);
setTimeout(() => {
setNodes((prevNodes) =>
prevNodes.map((n) => {
if (n.id === event.data.id) {
return {
...n,
measured,
};
}
return n;
})
);
}, 0);
} else if (
event.action === 'add_field' ||
event.action === 'remove_field'
@@ -1126,30 +1023,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const diagramTables = event.data.diagram.tables ?? [];
const overlappingTablesInDiagram = findOverlappingTables({
tables: diagramTables.filter((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
});
setOverlapGraph(overlappingTablesInDiagram);
}
},
[
overlapGraph,
setOverlapGraph,
getNode,
nodes,
filter,
setNodes,
databaseType,
]
[overlapGraph, setOverlapGraph, getNode, nodes, filteredSchemas]
);
events.useSubscription(eventConsumer);
@@ -1282,34 +1162,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
})}
</TooltipContent>
</Tooltip>
{highlightedCustomType ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 border border-yellow-400 bg-yellow-200 p-1 shadow-none hover:bg-yellow-300 dark:border-yellow-700 dark:bg-yellow-800 dark:hover:bg-yellow-700"
onClick={() =>
highlightCustomTypeId(
undefined
)
}
>
<Highlighter className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
'toolbar.custom_type_highlight_tooltip',
{
typeName:
highlightedCustomType.name,
}
)}
</TooltipContent>
</Tooltip>
) : null}
</>
) : null}
@@ -1376,25 +1228,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
</Button>
</Controls>
) : null}
{isLostInCanvas ? (
<Controls
position={
isDesktop ? 'bottom-center' : 'top-center'
}
orientation="horizontal"
showZoom={false}
showFitView={false}
showInteractive={false}
className="!shadow-none"
style={{
[isDesktop ? 'bottom' : 'top']: isDesktop
? '70px'
: '70px',
}}
>
<ShowAllButton />
</Controls>
) : null}
<Controls
position={isDesktop ? 'bottom-center' : 'top-center'}
orientation="horizontal"

View File

@@ -1,65 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '@/components/button/button';
import { Info } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useCanvas } from '@/hooks/use-canvas';
export interface ShowAllButtonProps {}
export const ShowAllButton: React.FC<ShowAllButtonProps> = () => {
const { fitView } = useCanvas();
const [visible, setVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setVisible(true);
}, 300);
return () => clearTimeout(timer);
}, []);
const showAll = useCallback(() => {
fitView({
duration: 500,
padding: 0.1,
maxZoom: 0.8,
});
}, [fitView]);
return (
<div
className={cn(
'transition-all duration-300 ease-in-out',
visible
? 'translate-y-0 opacity-100'
: 'pointer-events-none translate-y-4 opacity-0'
)}
>
<div className="sm:hidden">
<Button
onClick={showAll}
size="sm"
className="h-fit rounded-lg bg-slate-900 px-4 py-1.5 text-xs text-white shadow-lg hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600"
>
Show All
</Button>
</div>
<div className="hidden items-center gap-2 rounded-lg bg-slate-900 px-3 py-2 shadow-lg sm:flex">
<div className="flex size-6 items-center justify-center rounded-full bg-pink-600">
<Info className="size-4 text-white" />
</div>
<span className="text-sm text-white">
Your content is out of view
</span>
<Button
onClick={showAll}
size="sm"
className="ml-2 h-fit rounded-lg bg-slate-700 px-4 py-1.5 text-xs text-white hover:bg-slate-600 dark:hover:bg-slate-800"
>
Show All
</Button>
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More