Compare commits

..

1 Commits

Author SHA1 Message Date
johnnyfish
21ba816a6d feat: add array type support for PostgreSQL fields 2025-08-26 15:19:48 +03:00
46 changed files with 294 additions and 2375 deletions

View File

@@ -1,55 +1,5 @@
# Changelog
## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
### Bug Fixes
* add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0))
* **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671))
* handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872))
## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26)
### Features
* add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103))
* **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4))
* include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992))
* **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006))
* support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c))
### Bug Fixes
* area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026))
* **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037))
* **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f))
* **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de))
* DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3))
* filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02))
* **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d))
* **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22))
* for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2))
* improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1))
* initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9))
* **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195))
* merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015))
* move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51))
* open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e))
* preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771))
* prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b))
* remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c))
* reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20))
* **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268))
* set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925))
* show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb))
* **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4))
* **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca))
* **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a))
* update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4))
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.15.1",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.15.1",
"version": "1.14.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.15.1",
"version": "1.14.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,98 +0,0 @@
import React, { useCallback } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { Button } from '@/components/button/button';
import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
import { useChartDB } from '@/hooks/use-chartdb';
import type { Diagram } from '@/lib/domain';
import { useStorage } from '@/hooks/use-storage';
import { cloneDiagram } from '@/lib/clone';
import { useTranslation } from 'react-i18next';
interface DiagramRowActionsMenuProps {
diagram: Diagram;
onOpen: () => void;
refetch: () => void;
numberOfDiagrams: number;
}
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
diagram,
onOpen,
refetch,
numberOfDiagrams,
}) => {
const { diagramId } = useChartDB();
const { deleteDiagram, addDiagram } = useStorage();
const { t } = useTranslation();
const onDelete = useCallback(async () => {
deleteDiagram(diagram.id);
refetch();
if (diagram.id === diagramId || numberOfDiagrams <= 1) {
window.location.href = '/';
}
}, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]);
const onDuplicate = useCallback(async () => {
const duplicatedDiagram = cloneDiagram(diagram);
const diagramToAdd = duplicatedDiagram.diagram;
if (!diagramToAdd) {
return;
}
diagramToAdd.name = `${diagram.name} (Copy)`;
addDiagram({ diagram: diagramToAdd });
refetch();
}, [addDiagram, refetch, diagram]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={onOpen}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.open')}
<SquareArrowOutUpRight className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDuplicate}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.duplicate')}
<Layers2 className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className="flex justify-between gap-4 text-red-700"
>
{t('open_diagram_dialog.diagram_actions.delete')}
<Trash2 className="size-3.5 text-red-700" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -27,7 +27,6 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useDebounce } from '@/hooks/use-debounce';
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
export interface OpenDiagramDialogProps extends BaseDialogProps {
canClose?: boolean;
@@ -47,22 +46,21 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
string | undefined
>();
const fetchDiagrams = useCallback(async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
}, [listDiagrams]);
useEffect(() => {
setSelectedDiagramId(undefined);
}, [dialog.open]);
useEffect(() => {
if (!dialog.open) {
return;
}
setSelectedDiagramId(undefined);
const fetchDiagrams = async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
};
fetchDiagrams();
}, [dialog.open, fetchDiagrams]);
}, [listDiagrams, setDiagrams, dialog.open]);
const openDiagram = useCallback(
(diagramId: string) => {
@@ -168,7 +166,6 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
'open_diagram_dialog.table_columns.tables_count'
)}
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
@@ -224,19 +221,6 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<TableCell className="text-center">
{diagram.tables?.length}
</TableCell>
<TableCell className="items-center p-0 pr-1 text-right">
<DiagramRowActionsMenu
diagram={diagram}
onOpen={() => {
openDiagram(diagram.id);
closeOpenDiagramDialog();
}}
numberOfDiagrams={
diagrams.length
}
refetch={fetchDiagrams}
/>
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -12,8 +12,8 @@ export const ar: LanguageTranslation = {
custom_types: 'الأنواع المخصصة',
},
menu: {
actions: {
actions: 'الإجراءات',
databases: {
databases: 'قواعد البيانات',
new: 'مخطط جديد',
browse: 'تصفح...',
save: 'حفظ',
@@ -323,12 +323,6 @@ export const ar: LanguageTranslation = {
},
cancel: 'إلغاء',
open: 'فتح',
diagram_actions: {
open: 'فتح',
duplicate: 'تكرار',
delete: 'حذف الرسم التخطيطي',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const bn: LanguageTranslation = {
custom_types: 'কাস্টম টাইপ',
},
menu: {
actions: {
actions: 'কার্য',
databases: {
databases: 'ডাটাবেস',
new: 'নতুন ডায়াগ্রাম',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
@@ -325,12 +325,6 @@ export const bn: LanguageTranslation = {
},
cancel: 'বাতিল করুন',
open: 'খুলুন',
diagram_actions: {
open: 'খুলুন',
duplicate: 'ডুপ্লিকেট',
delete: 'ডায়াগ্রাম মুছুন',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const de: LanguageTranslation = {
custom_types: 'Benutzerdefinierte Typen',
},
menu: {
actions: {
actions: 'Aktionen',
databases: {
databases: 'Datenbanken',
new: 'Neues Diagramm',
browse: 'Durchsuchen...',
save: 'Speichern',
@@ -328,12 +328,6 @@ export const de: LanguageTranslation = {
},
cancel: 'Abbrechen',
open: 'Öffnen',
diagram_actions: {
open: 'Öffnen',
duplicate: 'Duplizieren',
delete: 'Diagramm löschen',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const en = {
custom_types: 'Custom Types',
},
menu: {
actions: {
actions: 'Actions',
databases: {
databases: 'Databases',
new: 'New Diagram',
browse: 'Browse...',
save: 'Save',
@@ -143,6 +143,7 @@ export const en = {
title: 'Field Attributes',
unique: 'Unique',
auto_increment: 'Auto Increment',
array: 'Declare Array',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
@@ -316,12 +317,6 @@ export const en = {
},
cancel: 'Cancel',
open: 'Open',
diagram_actions: {
open: 'Open',
duplicate: 'Duplicate',
delete: 'Delete Diagram',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const es: LanguageTranslation = {
custom_types: 'Tipos Personalizados',
},
menu: {
actions: {
actions: 'Acciones',
databases: {
databases: 'Bases de Datos',
new: 'Nuevo Diagrama',
browse: 'Examinar...',
save: 'Guardar',
@@ -326,12 +326,6 @@ export const es: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Eliminar Diagrama',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const fr: LanguageTranslation = {
custom_types: 'Types Personnalisés',
},
menu: {
actions: {
actions: 'Actions',
databases: {
databases: 'Bases de Données',
new: 'Nouveau Diagramme',
browse: 'Parcourir...',
save: 'Enregistrer',
@@ -323,12 +323,6 @@ export const fr: LanguageTranslation = {
},
cancel: 'Annuler',
open: 'Ouvrir',
diagram_actions: {
open: 'Ouvrir',
duplicate: 'Dupliquer',
delete: 'Supprimer le diagramme',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const gu: LanguageTranslation = {
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: {
actions: {
actions: 'ક્રિયાઓ',
databases: {
databases: 'ડેટાબેસેસ',
new: 'નવું ડાયાગ્રામ',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
@@ -325,12 +325,6 @@ export const gu: LanguageTranslation = {
},
cancel: 'રદ કરો',
open: 'ખોલો',
diagram_actions: {
open: 'ખોલો',
duplicate: 'ડુપ્લિકેટ',
delete: 'ડાયાગ્રામ કાઢી નાખો',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const hi: LanguageTranslation = {
custom_types: 'कस्टम टाइप',
},
menu: {
actions: {
actions: 'कार्य',
databases: {
databases: 'डेटाबेस',
new: 'नया आरेख',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
@@ -327,12 +327,6 @@ export const hi: LanguageTranslation = {
},
cancel: 'रद्द करें',
open: 'खोलें',
diagram_actions: {
open: 'खोलें',
duplicate: 'डुप्लिकेट',
delete: 'डायग्राम हटाएं',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const hr: LanguageTranslation = {
custom_types: 'Prilagođeni Tipovi',
},
menu: {
actions: {
actions: 'Akcije',
databases: {
databases: 'Baze Podataka',
new: 'Novi Dijagram',
browse: 'Pregledaj...',
save: 'Spremi',
@@ -320,12 +320,6 @@ export const hr: LanguageTranslation = {
},
cancel: 'Odustani',
open: 'Otvori',
diagram_actions: {
open: 'Otvori',
duplicate: 'Dupliciraj',
delete: 'Obriši dijagram',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const id_ID: LanguageTranslation = {
custom_types: 'Tipe Kustom',
},
menu: {
actions: {
actions: 'Aksi',
databases: {
databases: 'Basis Data',
new: 'Diagram Baru',
browse: 'Jelajahi...',
save: 'Simpan',
@@ -324,12 +324,6 @@ export const id_ID: LanguageTranslation = {
},
cancel: 'Batal',
open: 'Buka',
diagram_actions: {
open: 'Buka',
duplicate: 'Duplikat',
delete: 'Hapus Diagram',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const ja: LanguageTranslation = {
custom_types: 'カスタムタイプ',
},
menu: {
actions: {
actions: 'アクション',
databases: {
databases: 'データベース',
new: '新しいダイアグラム',
browse: '参照...',
save: '保存',
@@ -329,12 +329,6 @@ export const ja: LanguageTranslation = {
},
cancel: 'キャンセル',
open: '開く',
diagram_actions: {
open: '開く',
duplicate: '複製',
delete: 'ダイアグラムを削除',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const ko_KR: LanguageTranslation = {
custom_types: '사용자 지정 타입',
},
menu: {
actions: {
actions: '작업',
databases: {
databases: '데이터베이스',
new: '새 다이어그램',
browse: '찾아보기...',
save: '저장',
@@ -324,12 +324,6 @@ export const ko_KR: LanguageTranslation = {
},
cancel: '취소',
open: '열기',
diagram_actions: {
open: '열기',
duplicate: '복제',
delete: '다이어그램 삭제',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const mr: LanguageTranslation = {
custom_types: 'कस्टम प्रकार',
},
menu: {
actions: {
actions: 'क्रिया',
databases: {
databases: 'डेटाबेस',
new: 'नवीन आरेख',
browse: 'ब्राउज करा...',
save: 'जतन करा',
@@ -330,12 +330,6 @@ export const mr: LanguageTranslation = {
},
cancel: 'रद्द करा',
open: 'उघडा',
diagram_actions: {
open: 'उघडा',
duplicate: 'डुप्लिकेट',
delete: 'आरेख हटवा',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const ne: LanguageTranslation = {
custom_types: 'कस्टम प्रकारहरू',
},
menu: {
actions: {
actions: 'कार्यहरू',
databases: {
databases: 'डाटाबेसहरू',
new: 'नयाँ डायाग्राम',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
@@ -327,12 +327,6 @@ export const ne: LanguageTranslation = {
},
cancel: 'रद्द गर्नुहोस्',
open: 'खोल्नुहोस्',
diagram_actions: {
open: 'खोल्नुहोस्',
duplicate: 'डुप्लिकेट',
delete: 'डायग्राम मेटाउनुहोस्',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const pt_BR: LanguageTranslation = {
custom_types: 'Tipos Personalizados',
},
menu: {
actions: {
actions: 'Ações',
databases: {
databases: 'Bancos de Dados',
new: 'Novo Diagrama',
browse: 'Navegar...',
save: 'Salvar',
@@ -326,12 +326,6 @@ export const pt_BR: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Excluir Diagrama',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const ru: LanguageTranslation = {
custom_types: 'Пользовательские типы',
},
menu: {
actions: {
actions: 'Действия',
databases: {
databases: 'Базы данных',
new: 'Новая диаграмма',
browse: 'Обзор...',
save: 'Сохранить',
@@ -323,12 +323,6 @@ export const ru: LanguageTranslation = {
},
cancel: 'Отмена',
open: 'Открыть',
diagram_actions: {
open: 'Открыть',
duplicate: 'Дублировать',
delete: 'Удалить диаграмму',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const te: LanguageTranslation = {
custom_types: 'కస్టమ్ టైప్స్',
},
menu: {
actions: {
actions: 'చర్యలు',
databases: {
databases: 'డేటాబేస్లు',
new: 'కొత్త డైగ్రాం',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
@@ -327,12 +327,6 @@ export const te: LanguageTranslation = {
},
cancel: 'రద్దు',
open: 'తెరవు',
diagram_actions: {
open: 'తెరవు',
duplicate: 'నకలు',
delete: 'డైగ్రామ్ తొలగించు',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const tr: LanguageTranslation = {
custom_types: 'Özel Tipler',
},
menu: {
actions: {
actions: 'Eylemler',
databases: {
databases: 'Veritabanları',
new: 'Yeni Diyagram',
browse: 'Gözat...',
save: 'Kaydet',
@@ -322,12 +322,6 @@ export const tr: LanguageTranslation = {
},
cancel: 'İptal',
open: 'Aç',
diagram_actions: {
open: 'Aç',
duplicate: 'Kopyala',
delete: 'Diyagramı Sil',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const uk: LanguageTranslation = {
custom_types: 'Користувацькі типи',
},
menu: {
actions: {
actions: 'Дії',
databases: {
databases: 'Бази даних',
new: 'Нова діаграма',
browse: 'Огляд...',
save: 'Зберегти',
@@ -324,12 +324,6 @@ export const uk: LanguageTranslation = {
},
cancel: 'Скасувати',
open: 'Відкрити',
diagram_actions: {
open: 'Відкрити',
duplicate: 'Дублювати',
delete: 'Видалити діаграму',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const vi: LanguageTranslation = {
custom_types: 'Kiểu tùy chỉnh',
},
menu: {
actions: {
actions: 'Hành động',
databases: {
databases: 'Cơ sở dữ liệu',
new: 'Sơ đồ mới',
browse: 'Duyệt...',
save: 'Lưu',
@@ -324,12 +324,6 @@ export const vi: LanguageTranslation = {
},
cancel: 'Hủy',
open: 'Mở',
diagram_actions: {
open: 'Mở',
duplicate: 'Nhân bản',
delete: 'Xóa sơ đồ',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const zh_CN: LanguageTranslation = {
custom_types: '自定义类型',
},
menu: {
actions: {
actions: '操作',
databases: {
databases: '数据库',
new: '新建关系图',
browse: '浏览...',
save: '保存',
@@ -321,12 +321,6 @@ export const zh_CN: LanguageTranslation = {
},
cancel: '取消',
open: '打开',
diagram_actions: {
open: '打开',
duplicate: '复制',
delete: '删除图表',
},
},
export_sql_dialog: {

View File

@@ -12,8 +12,8 @@ export const zh_TW: LanguageTranslation = {
custom_types: '自定義類型',
},
menu: {
actions: {
actions: '操作',
databases: {
databases: '資料庫',
new: '新增圖表',
browse: '瀏覽...',
save: '儲存',
@@ -320,12 +320,6 @@ export const zh_TW: LanguageTranslation = {
},
cancel: '取消',
open: '開啟',
diagram_actions: {
open: '開啟',
duplicate: '複製',
delete: '刪除圖表',
},
},
export_sql_dialog: {

View File

@@ -165,3 +165,21 @@ export const supportsAutoIncrementDataType = (
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};
export const supportsArrayDataType = (dataTypeName: string): boolean => {
// Types that do NOT support arrays in PostgreSQL
const unsupportedTypes = [
'serial',
'bigserial',
'smallserial',
'serial2',
'serial4',
'serial8',
'xml',
'money',
];
// Check if the type is in the unsupported list
const normalizedType = dataTypeName.toLowerCase();
return !unsupportedTypes.includes(normalizedType);
};

View File

@@ -1,10 +1,20 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { exportBaseSQL } from '../export-sql-script';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
// Mock the dbml/core importer
vi.mock('@dbml/core', () => ({
importer: {
import: vi.fn((sql: string) => {
// Return a simplified DBML for testing
return sql;
}),
},
}));
describe('DBML Export - SQL Generation Tests', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
@@ -596,7 +606,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create a valid table without primary key
expect(sql).toContain('CREATE TABLE "experiment_logs"');
expect(sql).toContain('CREATE TABLE experiment_logs');
expect(sql).not.toContain('PRIMARY KEY');
});
@@ -711,11 +721,11 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create both tables
expect(sql).toContain('CREATE TABLE "guilds"');
expect(sql).toContain('CREATE TABLE "guild_members"');
expect(sql).toContain('CREATE TABLE guilds');
expect(sql).toContain('CREATE TABLE guild_members');
// Should create foreign key
expect(sql).toContain(
'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES "guilds" (id);'
'ALTER TABLE guild_members ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES guilds (id)'
);
});
});
@@ -789,9 +799,12 @@ describe('DBML Export - SQL Generation Tests', () => {
isDBMLFlow: true,
});
// Should create schemas
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation');
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic');
// Should use schema-qualified table names
expect(sql).toContain('CREATE TABLE "transportation"."portals"');
expect(sql).toContain('CREATE TABLE "magic"."spells"');
expect(sql).toContain('CREATE TABLE transportation.portals');
expect(sql).toContain('CREATE TABLE magic.spells');
});
});
@@ -838,7 +851,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should still create table structure
expect(sql).toContain('CREATE TABLE "empty_table"');
expect(sql).toContain('CREATE TABLE empty_table');
expect(sql).toContain('(\n\n)');
});

View File

@@ -286,10 +286,14 @@ export function exportPostgreSQL({
}
}
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
// Handle array types (check if the field has array property or type name ends with '[]')
if (field.array || typeName.endsWith('[]')) {
if (!typeName.endsWith('[]')) {
typeWithSize = typeWithSize + '[]';
} else {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
}
const notNull = field.nullable ? '' : ' NOT NULL';

View File

@@ -20,38 +20,6 @@ const simplifyDataType = (typeName: string): string => {
return typeMap[typeName.toLowerCase()] || typeName;
};
// Helper function to properly quote table/schema names with special characters
const getQuotedTableName = (
table: DBTable,
isDBMLFlow: boolean = false
): string => {
// Check if a name is already quoted
const isAlreadyQuoted = (name: string) => {
return (
(name.startsWith('"') && name.endsWith('"')) ||
(name.startsWith('`') && name.endsWith('`')) ||
(name.startsWith('[') && name.endsWith(']'))
);
};
// Only add quotes if needed and not already quoted
const quoteIfNeeded = (name: string) => {
if (isAlreadyQuoted(name)) {
return name;
}
const needsQuoting = /[^a-zA-Z0-9_]/.test(name) || isDBMLFlow;
return needsQuoting ? `"${name}"` : name;
};
if (table.schema) {
const quotedSchema = quoteIfNeeded(table.schema);
const quotedTable = quoteIfNeeded(table.name);
return `${quotedSchema}.${quotedTable}`;
} else {
return quoteIfNeeded(table.name);
}
};
export const exportBaseSQL = ({
diagram,
targetDatabaseType,
@@ -95,21 +63,18 @@ export const exportBaseSQL = ({
let sqlScript = '';
// First create the CREATE SCHEMA statements for all the found schemas based on tables
// Skip schema creation for DBML flow as DBML doesn't support CREATE SCHEMA syntax
if (!isDBMLFlow) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
}
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
// Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
if (diagram.customTypes && diagram.customTypes.length > 0) {
@@ -201,7 +166,9 @@ export const exportBaseSQL = ({
// Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => {
const tableName = getQuotedTableName(table, isDBMLFlow);
const tableName = table.schema
? `${table.schema}.${table.name}`
: table.name;
sqlScript += `CREATE TABLE ${tableName} (\n`;
// Check for composite primary keys
@@ -498,9 +465,12 @@ export const exportBaseSQL = ({
return;
}
const fkTableName = getQuotedTableName(fkTable, isDBMLFlow);
const refTableName = getQuotedTableName(refTable, isDBMLFlow);
const fkTableName = fkTable.schema
? `${fkTable.schema}.${fkTable.name}`
: fkTable.name;
const refTableName = refTable.schema
? `${refTable.schema}.${refTable.name}`
: refTable.name;
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
}
});

View File

@@ -29,6 +29,7 @@ export interface SQLColumn {
comment?: string;
default?: string;
increment?: boolean;
array?: boolean;
}
export interface SQLTable {
@@ -612,6 +613,7 @@ export function convertToChartDBDiagram(
default: column.default || '',
createdAt: Date.now(),
increment: column.increment,
array: column.array,
};
// Add type arguments if present

View File

@@ -1,350 +0,0 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL Import - Quoted Identifiers with Special Characters', () => {
describe('CREATE TABLE with quoted identifiers', () => {
it('should handle tables with quoted schema and table names', async () => {
const sql = `
CREATE TABLE "my-schema"."user-profiles" (
id serial PRIMARY KEY,
name text NOT NULL
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('my-schema');
expect(table.name).toBe('user-profiles');
});
it('should handle tables with spaces in schema and table names', async () => {
const sql = `
CREATE TABLE "user schema"."profile table" (
"user id" integer PRIMARY KEY,
"full name" varchar(255)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('user schema');
expect(table.name).toBe('profile table');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
// Note: Column names with spaces might be parsed differently
});
it('should handle mixed quoted and unquoted identifiers', async () => {
const sql = `
CREATE TABLE "special-schema".users (
id serial PRIMARY KEY
);
CREATE TABLE public."special-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.tables[0].schema).toBe('special-schema');
expect(result.tables[0].name).toBe('users');
expect(result.tables[1].schema).toBe('public');
expect(result.tables[1].name).toBe('special-table');
});
it('should handle tables with dots in names', async () => {
const sql = `
CREATE TABLE "schema.with.dots"."table.with.dots" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema.with.dots');
expect(table.name).toBe('table.with.dots');
});
});
describe('FOREIGN KEY with quoted identifiers', () => {
it('should handle inline REFERENCES with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth-schema"."users" (
"user-id" serial PRIMARY KEY,
email text UNIQUE
);
CREATE TABLE "app-schema"."user-profiles" (
id serial PRIMARY KEY,
"user-id" integer REFERENCES "auth-schema"."users"("user-id"),
bio text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('user-profiles');
expect(relationship.targetTable).toBe('users');
expect(relationship.sourceColumn).toBe('user-id');
expect(relationship.targetColumn).toBe('user-id');
});
it('should handle FOREIGN KEY constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema one"."table one" (
"id field" serial PRIMARY KEY,
"data field" text
);
CREATE TABLE "schema two"."table two" (
id serial PRIMARY KEY,
"ref id" integer,
FOREIGN KEY ("ref id") REFERENCES "schema one"."table one"("id field")
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('table two');
expect(relationship.targetTable).toBe('table one');
expect(relationship.sourceColumn).toBe('ref id');
expect(relationship.targetColumn).toBe('id field');
});
it('should handle named constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth"."users" (
id serial PRIMARY KEY
);
CREATE TABLE "app"."profiles" (
id serial PRIMARY KEY,
user_id integer,
CONSTRAINT "fk-user-profile" FOREIGN KEY (user_id) REFERENCES "auth"."users"(id)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
// Note: Constraint names with special characters might be normalized
expect(relationship.name).toBeDefined();
});
it('should handle ALTER TABLE ADD CONSTRAINT with quoted identifiers', async () => {
const sql = `
CREATE TABLE "user-schema"."user-accounts" (
"account-id" serial PRIMARY KEY,
username text
);
CREATE TABLE "order-schema"."user-orders" (
"order-id" serial PRIMARY KEY,
"account-id" integer
);
ALTER TABLE "order-schema"."user-orders"
ADD CONSTRAINT "fk_orders_accounts"
FOREIGN KEY ("account-id")
REFERENCES "user-schema"."user-accounts"("account-id");
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.name).toBe('fk_orders_accounts');
expect(relationship.sourceTable).toBe('user-orders');
expect(relationship.targetTable).toBe('user-accounts');
expect(relationship.sourceColumn).toBe('account-id');
expect(relationship.targetColumn).toBe('account-id');
});
it('should handle complex mixed quoting scenarios', async () => {
const sql = `
CREATE TABLE auth.users (
id serial PRIMARY KEY
);
CREATE TABLE "app-data"."user_profiles" (
profile_id serial PRIMARY KEY,
"user-id" integer REFERENCES auth.users(id)
);
CREATE TABLE "app-data".posts (
id serial PRIMARY KEY,
profile_id integer
);
ALTER TABLE "app-data".posts
ADD CONSTRAINT fk_posts_profiles
FOREIGN KEY (profile_id)
REFERENCES "app-data"."user_profiles"(profile_id);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(3);
expect(result.relationships).toHaveLength(2);
// Verify the relationships were correctly identified
const profilesTable = result.tables.find(
(t) => t.name === 'user_profiles'
);
expect(profilesTable?.schema).toBe('app-data');
const postsTable = result.tables.find((t) => t.name === 'posts');
expect(postsTable?.schema).toBe('app-data');
});
});
describe('Edge cases and special scenarios', () => {
it('should handle Unicode characters in quoted identifiers', async () => {
const sql = `
CREATE TABLE "схема"."таблица" (
"идентификатор" serial PRIMARY KEY,
"данные" text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('схема');
expect(table.name).toBe('таблица');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
});
it('should handle parentheses in quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema(prod)"."users(archived)" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema(prod)');
expect(table.name).toBe('users(archived)');
});
it('should handle forward slashes in quoted identifiers', async () => {
const sql = `
CREATE TABLE "api/v1"."users/profiles" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('api/v1');
expect(table.name).toBe('users/profiles');
});
it('should handle IF NOT EXISTS with quoted identifiers', async () => {
const sql = `
CREATE TABLE IF NOT EXISTS "test-schema"."test-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('test-schema');
expect(table.name).toBe('test-table');
});
it('should handle ONLY keyword with quoted identifiers', async () => {
const sql = `
CREATE TABLE ONLY "parent-schema"."parent-table" (
id serial PRIMARY KEY
);
ALTER TABLE ONLY "parent-schema"."parent-table"
ADD CONSTRAINT "unique-constraint" UNIQUE (id);
`;
const result = await fromPostgres(sql);
// ONLY keyword might trigger warnings
expect(result.warnings).toBeDefined();
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('parent-schema');
expect(table.name).toBe('parent-table');
});
it('should handle self-referencing foreign keys with quoted identifiers', async () => {
const sql = `
CREATE TABLE "org-schema"."departments" (
"dept-id" serial PRIMARY KEY,
"parent-dept-id" integer REFERENCES "org-schema"."departments"("dept-id"),
name text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('departments');
expect(relationship.targetTable).toBe('departments'); // Self-reference
expect(relationship.sourceColumn).toBe('parent-dept-id');
expect(relationship.targetColumn).toBe('dept-id');
});
});
});

View File

@@ -373,6 +373,13 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
'SMALLSERIAL',
].includes(upperType.split('(')[0]);
// Check if it's an array type
let isArrayType = false;
if (columnType.endsWith('[]')) {
isArrayType = true;
columnType = columnType.slice(0, -2);
}
// Normalize the type
columnType = normalizePostgreSQLType(columnType);
@@ -395,6 +402,7 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] {
trimmedLine.includes('uuid_generate_v4()') ||
trimmedLine.includes('GENERATED ALWAYS AS IDENTITY') ||
trimmedLine.includes('GENERATED BY DEFAULT AS IDENTITY'),
array: isArrayType,
});
}
}
@@ -490,21 +498,16 @@ function extractForeignKeysFromCreateTable(
const tableBody = tableBodyMatch[1];
// Pattern for inline REFERENCES - handles quoted and unquoted identifiers
// Pattern for inline REFERENCES - more flexible to handle various formats
const inlineRefPattern =
/(?:"([^"]+)"|([^"\s,()]+))\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi;
/["']?(\w+)["']?\s+(?:\w+(?:\([^)]*\))?(?:\[[^\]]*\])?(?:\s+\w+)*\s+)?REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi;
let match;
while ((match = inlineRefPattern.exec(tableBody)) !== null) {
// Extract values from appropriate match groups
// Groups: 1=quoted source col, 2=unquoted source col,
// 3=quoted schema, 4=unquoted schema,
// 5=quoted target table, 6=unquoted target table,
// 7=quoted target col, 8=unquoted target col
const sourceColumn = match[1] || match[2];
const targetSchema = match[3] || match[4] || 'public';
const targetTable = match[5] || match[6];
const targetColumn = match[7] || match[8];
const sourceColumn = match[1];
const targetSchema = match[2] || 'public';
const targetTable = match[3];
const targetColumn = match[4];
const targetTableKey = `${targetSchema}.${targetTable}`;
const targetTableId = tableMap[targetTableKey];
@@ -526,16 +529,15 @@ function extractForeignKeysFromCreateTable(
}
}
// Pattern for FOREIGN KEY constraints - handles quoted and unquoted identifiers
// Pattern for FOREIGN KEY constraints
const fkConstraintPattern =
/FOREIGN\s+KEY\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)\s*REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\(\s*(?:"([^"]+)"|([^"\s,)]+))\s*\)/gi;
/FOREIGN\s+KEY\s*\(\s*["']?(\w+)["']?\s*\)\s*REFERENCES\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s*\(\s*["']?(\w+)["']?\s*\)/gi;
while ((match = fkConstraintPattern.exec(tableBody)) !== null) {
// Extract values from appropriate match groups
const sourceColumn = match[1] || match[2];
const targetSchema = match[3] || match[4] || 'public';
const targetTable = match[5] || match[6];
const targetColumn = match[7] || match[8];
const sourceColumn = match[1];
const targetSchema = match[2] || 'public';
const targetTable = match[3];
const targetColumn = match[4];
const targetTableKey = `${targetSchema}.${targetTable}`;
const targetTableId = tableMap[targetTableKey];
@@ -591,16 +593,12 @@ export async function fromPostgres(
? stmt.sql.substring(createTableIndex)
: stmt.sql;
// Updated regex to properly handle quoted identifiers with special characters
// Matches: schema.table, "schema"."table", "schema".table, schema."table"
const tableMatch = sqlFromCreate.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i
);
if (tableMatch) {
// Extract schema and table names from the appropriate match groups
// Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table
const schemaName = tableMatch[1] || tableMatch[2] || 'public';
const tableName = tableMatch[3] || tableMatch[4];
const schemaName = tableMatch[1] || 'public';
const tableName = tableMatch[2];
const tableKey = `${schemaName}.${tableName}`;
tableMap[tableKey] = generateId();
}
@@ -792,6 +790,16 @@ export async function fromPostgres(
normalizePostgreSQLType(rawDataType);
}
// Check if it's an array type
let isArrayType = false;
if (normalizedBaseType.endsWith('[]')) {
isArrayType = true;
normalizedBaseType = normalizedBaseType.slice(
0,
-2
);
}
// Now handle parameters - but skip for integer types that shouldn't have them
let finalDataType = normalizedBaseType;
@@ -884,6 +892,7 @@ export async function fromPostgres(
stmt.sql
.toUpperCase()
.includes('IDENTITY')),
array: isArrayType,
});
}
} else if (def.resource === 'constraint') {
@@ -948,16 +957,12 @@ export async function fromPostgres(
? stmt.sql.substring(createTableIndex)
: stmt.sql;
// Updated regex to properly handle quoted identifiers with special characters
// Matches: schema.table, "schema"."table", "schema".table, schema."table"
const tableMatch = sqlFromCreate.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))/i
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i
);
if (tableMatch) {
// Extract schema and table names from the appropriate match groups
// Groups: 1=quoted schema, 2=unquoted schema, 3=quoted table, 4=unquoted table
const schemaName = tableMatch[1] || tableMatch[2] || 'public';
const tableName = tableMatch[3] || tableMatch[4];
const schemaName = tableMatch[1] || 'public';
const tableName = tableMatch[2];
const tableKey = `${schemaName}.${tableName}`;
const tableId = tableMap[tableKey];
@@ -1144,22 +1149,18 @@ export async function fromPostgres(
} else if (stmt.type === 'alter' && !stmt.parsed) {
// Handle ALTER TABLE statements that failed to parse
// Extract foreign keys using regex as fallback
// Updated regex to handle quoted identifiers properly
const alterFKMatch = stmt.sql.match(
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s+ADD\s+CONSTRAINT\s+(?:"([^"]+)"|([^"\s]+))\s+FOREIGN\s+KEY\s*\((?:"([^"]+)"|([^"\s)]+))\)\s+REFERENCES\s+(?:(?:"([^"]+)"|([^"\s.]+))\.)?(?:"([^"]+)"|([^"\s.(]+))\s*\((?:"([^"]+)"|([^"\s)]+))\)/i
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s+ADD\s+CONSTRAINT\s+["']?([^"'\s]+)["']?\s+FOREIGN\s+KEY\s*\(["']?([^"'\s)]+)["']?\)\s+REFERENCES\s+(?:"?([^"\s.]+)"?\.)?["']?([^"'\s.(]+)["']?\s*\(["']?([^"'\s)]+)["']?\)/i
);
if (alterFKMatch) {
// Extract values from appropriate match groups
const sourceSchema =
alterFKMatch[1] || alterFKMatch[2] || 'public';
const sourceTable = alterFKMatch[3] || alterFKMatch[4];
const constraintName = alterFKMatch[5] || alterFKMatch[6];
const sourceColumn = alterFKMatch[7] || alterFKMatch[8];
const targetSchema =
alterFKMatch[9] || alterFKMatch[10] || 'public';
const targetTable = alterFKMatch[11] || alterFKMatch[12];
const targetColumn = alterFKMatch[13] || alterFKMatch[14];
const sourceSchema = alterFKMatch[1] || 'public';
const sourceTable = alterFKMatch[2];
const constraintName = alterFKMatch[3];
const sourceColumn = alterFKMatch[4];
const targetSchema = alterFKMatch[5] || 'public';
const targetTable = alterFKMatch[6];
const targetColumn = alterFKMatch[7];
const sourceTableId = getTableIdWithSchemaSupport(
tableMap,

View File

@@ -1,438 +0,0 @@
import { describe, it, expect } from 'vitest';
import { generateDBMLFromDiagram } from '../dbml-export';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
describe('DBML Export - Fix Multiline Table Names', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
const testId = () => `test-id-${++idCounter}`;
const testTime = Date.now();
// Helper to create a field
const createField = (overrides: Partial<DBField>): DBField =>
({
id: testId(),
name: 'field',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
createdAt: testTime,
...overrides,
}) as DBField;
// Helper to create a table
const createTable = (overrides: Partial<DBTable>): DBTable =>
({
id: testId(),
name: 'table',
fields: [],
indexes: [],
createdAt: testTime,
x: 0,
y: 0,
width: 200,
...overrides,
}) as DBTable;
// Helper to create a diagram
const createDiagram = (overrides: Partial<Diagram>): Diagram =>
({
id: testId(),
name: 'diagram',
databaseType: DatabaseType.POSTGRESQL,
tables: [],
relationships: [],
createdAt: testTime,
updatedAt: testTime,
...overrides,
}) as Diagram;
describe('DBML Generation with Special Characters', () => {
it('should handle table names with special characters', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'user-profiles',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
createField({
name: 'user-name',
type: { id: 'varchar', name: 'varchar' },
nullable: true,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with special characters
expect(result.standardDbml).toContain('Table "user-profiles"');
// Field names with special characters should also be quoted
expect(result.standardDbml).toContain('"user-name"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle schema-qualified table names', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'my-schema',
name: 'my-table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote schema and table names
expect(result.standardDbml).toContain(
'Table "my-schema"."my-table"'
);
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle table names with spaces', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'user profiles',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with spaces
expect(result.standardDbml).toContain('Table "user profiles"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle schema names with spaces', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'my schema',
name: 'my_table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote schema with spaces
expect(result.standardDbml).toContain(
'Table "my schema"."my_table"'
);
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should handle table names with dots', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'app.config',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should properly quote table names with dots
expect(result.standardDbml).toContain('Table "app.config"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should not have line breaks in table declarations', () => {
const diagram = createDiagram({
tables: [
createTable({
schema: 'very-long-schema-name-with-dashes',
name: 'very-long-table-name-with-special-characters',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Table declaration should be on a single line
const tableDeclarations =
result.standardDbml.match(/Table\s+[^{]+\{/g) || [];
tableDeclarations.forEach((decl) => {
// Should not contain newlines before the opening brace
expect(decl).not.toContain('\n');
});
// The full qualified name should be on one line
expect(result.standardDbml).toMatch(
/Table\s+"very-long-schema-name-with-dashes"\."very-long-table-name-with-special-characters"\s*\{/
);
});
});
describe('Multiple tables and relationships', () => {
it('should handle multiple tables with special characters', () => {
const parentTableId = testId();
const childTableId = testId();
const parentIdField = testId();
const childParentIdField = testId();
const diagram = createDiagram({
tables: [
createTable({
id: parentTableId,
schema: 'auth-schema',
name: 'user-accounts',
fields: [
createField({
id: parentIdField,
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
}),
],
}),
createTable({
id: childTableId,
schema: 'app-schema',
name: 'user-profiles',
fields: [
createField({
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
}),
createField({
id: childParentIdField,
name: 'account-id',
type: { id: 'uuid', name: 'uuid' },
nullable: false,
}),
],
}),
],
relationships: [
{
id: testId(),
name: 'fk_profiles_accounts',
sourceTableId: childTableId,
targetTableId: parentTableId,
sourceFieldId: childParentIdField,
targetFieldId: parentIdField,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const result = generateDBMLFromDiagram(diagram);
// Should contain both tables properly quoted
expect(result.standardDbml).toContain(
'Table "auth-schema"."user-accounts"'
);
expect(result.standardDbml).toContain(
'Table "app-schema"."user-profiles"'
);
// Should contain the relationship reference
expect(result.standardDbml).toContain('Ref');
// Should contain field names with dashes properly quoted
expect(result.standardDbml).toContain('"account-id"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
it('should work correctly with inline DBML format', () => {
const parentTableId = testId();
const childTableId = testId();
const parentIdField = testId();
const childParentIdField = testId();
const diagram = createDiagram({
tables: [
createTable({
id: parentTableId,
name: 'parent-table',
fields: [
createField({
id: parentIdField,
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
createTable({
id: childTableId,
name: 'child-table',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
createField({
id: childParentIdField,
name: 'parent-id',
type: { id: 'integer', name: 'integer' },
nullable: false,
}),
],
}),
],
relationships: [
{
id: testId(),
name: 'fk_child_parent',
sourceTableId: childTableId,
targetTableId: parentTableId,
sourceFieldId: childParentIdField,
targetFieldId: parentIdField,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const result = generateDBMLFromDiagram(diagram);
// Both standard and inline should be generated
expect(result.standardDbml).toBeDefined();
expect(result.inlineDbml).toBeDefined();
// Inline version should contain inline references
expect(result.inlineDbml).toContain('ref:');
// Both should properly quote table names
expect(result.standardDbml).toContain('Table "parent-table"');
expect(result.inlineDbml).toContain('Table "parent-table"');
expect(result.standardDbml).toContain('Table "child-table"');
expect(result.inlineDbml).toContain('Table "child-table"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
});
describe('Edge cases', () => {
it('should handle empty table names gracefully', () => {
const diagram = createDiagram({
tables: [
createTable({
name: '',
fields: [
createField({
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should not throw error
expect(result.error).toBeUndefined();
});
it('should handle Unicode characters in names', () => {
const diagram = createDiagram({
tables: [
createTable({
name: 'użytkownik',
fields: [
createField({
name: 'identyfikator',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
}),
],
}),
],
});
const result = generateDBMLFromDiagram(diagram);
// Should handle Unicode characters
expect(result.standardDbml).toContain('Table "użytkownik"');
expect(result.standardDbml).toContain('"identyfikator"');
// Should not have any errors
expect(result.error).toBeUndefined();
});
});
});

View File

@@ -605,23 +605,6 @@ const fixTableBracketSyntax = (dbml: string): string => {
);
};
// Fix table names that have been broken across multiple lines
const fixMultilineTableNames = (dbml: string): string => {
// Match Table declarations that might have line breaks in the table name
// This regex captures:
// - Table keyword
// - Optional quoted schema with dot
// - Table name that might be broken across lines (until the opening brace)
return dbml.replace(
/Table\s+((?:"[^"]*"\.)?"[^"]*(?:\n[^"]*)*")\s*\{/g,
(_, tableName) => {
// Remove line breaks within the table name
const fixedTableName = tableName.replace(/\n\s*/g, '');
return `Table ${fixedTableName} {`;
}
);
};
// Restore composite primary key names in the DBML
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
@@ -986,12 +969,10 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
}
standard = normalizeCharTypeFormat(
fixMultilineTableNames(
fixTableBracketSyntax(
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
)
fixTableBracketSyntax(
importer.import(
baseScript,
databaseTypeToImportFormat(diagram.databaseType)
)
)
);

View File

@@ -15,12 +15,12 @@ export interface DBCustomTypeField {
export interface DBCustomType {
id: string;
schema?: string | null;
schema?: string;
name: string;
kind: DBCustomTypeKind;
values?: string[] | null; // For enum types
fields?: DBCustomTypeField[] | null; // For composite types
order?: number | null;
values?: string[]; // For enum types
fields?: DBCustomTypeField[]; // For composite types
order?: number;
}
export const dbCustomTypeFieldSchema = z.object({
@@ -30,12 +30,11 @@ export const dbCustomTypeFieldSchema = z.object({
export const dbCustomTypeSchema: z.ZodType<DBCustomType> = z.object({
id: z.string(),
schema: z.string().or(z.null()).optional(),
schema: z.string(),
name: z.string(),
kind: z.nativeEnum(DBCustomTypeKind),
values: z.array(z.string()).or(z.null()).optional(),
fields: z.array(dbCustomTypeFieldSchema).or(z.null()).optional(),
order: z.number().or(z.null()).optional(),
values: z.array(z.string()).optional(),
fields: z.array(dbCustomTypeFieldSchema).optional(),
});
export const createCustomTypesFromMetadata = ({

View File

@@ -19,6 +19,7 @@ export interface DBField {
unique: boolean;
nullable: boolean;
increment?: boolean | null;
array?: boolean | null;
createdAt: number;
characterMaximumLength?: string | null;
precision?: number | null;
@@ -36,6 +37,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
unique: z.boolean(),
nullable: z.boolean(),
increment: z.boolean().or(z.null()).optional(),
array: z.boolean().or(z.null()).optional(),
createdAt: z.number(),
characterMaximumLength: z.string().or(z.null()).optional(),
precision: z.number().or(z.null()).optional(),
@@ -71,13 +73,48 @@ export const createFieldsFromMetadata = ({
pk.column.trim()
);
return sortedColumns.map(
(col: ColumnInfo): DBField => ({
return sortedColumns.map((col: ColumnInfo): DBField => {
// Check if type is an array (ends with [])
const isArrayType = col.type.endsWith('[]');
let baseType = col.type;
// Extract base type and any parameters if it's an array
if (isArrayType) {
baseType = col.type.slice(0, -2); // Remove the [] suffix
}
// Extract parameters from types like "character varying(100)" or "numeric(10,2)"
let charMaxLength = col.character_maximum_length;
let precision = col.precision?.precision;
let scale = col.precision?.scale;
// Handle types with single parameter like varchar(100)
const singleParamMatch = baseType.match(/^(.+?)\((\d+)\)$/);
if (singleParamMatch) {
baseType = singleParamMatch[1];
if (!charMaxLength || charMaxLength === 'null') {
charMaxLength = singleParamMatch[2];
}
}
// Handle types with two parameters like numeric(10,2)
const twoParamMatch = baseType.match(/^(.+?)\((\d+),\s*(\d+)\)$/);
if (twoParamMatch) {
baseType = twoParamMatch[1];
if (!precision) {
precision = parseInt(twoParamMatch[2]);
}
if (!scale) {
scale = parseInt(twoParamMatch[3]);
}
}
return {
id: generateId(),
name: col.name,
type: {
id: col.type.split(' ').join('_').toLowerCase(),
name: col.type.toLowerCase(),
id: baseType.split(' ').join('_').toLowerCase(),
name: baseType.toLowerCase(),
},
primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some(
@@ -87,20 +124,18 @@ export const createFieldsFromMetadata = ({
idx.columns[0].name === col.name
),
nullable: Boolean(col.nullable),
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { characterMaximumLength: col.character_maximum_length }
...(isArrayType ? { array: true } : {}),
...(charMaxLength && charMaxLength !== 'null'
? { characterMaximumLength: charMaxLength }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }
: {}),
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(precision ? { precision } : {}),
...(scale ? { scale } : {}),
...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}),
createdAt: Date.now(),
comments: col.comment ? col.comment : undefined,
})
);
};
});
};
export const generateDBFieldSuffix = (

View File

@@ -7,10 +7,6 @@ import {
useUpdateNodeInternals,
} from '@xyflow/react';
import React, { useEffect, useMemo, useRef } from 'react';
import {
LEFT_HANDLE_ID_PREFIX,
RIGHT_HANDLE_ID_PREFIX,
} from './table-node-field';
export const TOP_SOURCE_HANDLE_ID_PREFIX = 'top_dep_';
export const BOTTOM_SOURCE_HANDLE_ID_PREFIX = 'bottom_dep_';
@@ -40,22 +36,6 @@ export const TableNodeDependencyIndicator: React.FC<TableNodeDependencyIndicator
[connection, table.id]
);
const isTargetFromTable = useMemo(
() =>
connection.inProgress &&
connection.fromNode.id !== table.id &&
(connection.fromHandle.id?.startsWith(RIGHT_HANDLE_ID_PREFIX) ||
connection.fromHandle.id?.startsWith(
LEFT_HANDLE_ID_PREFIX
)),
[
connection.inProgress,
connection.fromNode?.id,
connection.fromHandle?.id,
table.id,
]
);
const numberOfEdgesToTable = useMemo(
() =>
dependencies.filter(
@@ -78,20 +58,12 @@ export const TableNodeDependencyIndicator: React.FC<TableNodeDependencyIndicator
return (
<>
{table.isView || table.isMaterializedView ? (
<>
<Handle
id={`${TOP_SOURCE_HANDLE_ID_PREFIX}${table.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || isTargetFromTable ? '!invisible' : ''}`}
position={Position.Top}
type="source"
/>
<Handle
id={`${BOTTOM_SOURCE_HANDLE_ID_PREFIX}${table.id}`}
className={`!z-10 !h-4 !w-4 !border-2 !bg-pink-600 ${!focused || isTargetFromTable ? '!invisible' : ''}`}
position={Position.Bottom}
type="source"
/>
</>
<Handle
id={`${TOP_SOURCE_HANDLE_ID_PREFIX}${table.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused ? '!invisible' : ''}`}
position={Position.Top}
type="source"
/>
) : null}
{Array.from(
{ length: numberOfEdgesToTable },
@@ -110,7 +82,7 @@ export const TableNodeDependencyIndicator: React.FC<TableNodeDependencyIndicator
id={`${TARGET_DEP_PREFIX}${numberOfEdgesToTable}_${table.id}`}
className={
isTarget
? '!absolute !inset-0 !z-10 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
: `!invisible`
}
position={Position.Top}

View File

@@ -33,10 +33,6 @@ import { useClickAway, useKeyPressEvent } from 'react-use';
import { Input } from '@/components/input/input';
import { useDiff } from '@/context/diff-context/use-diff';
import { useLocalConfig } from '@/hooks/use-local-config';
import {
BOTTOM_SOURCE_HANDLE_ID_PREFIX,
TOP_SOURCE_HANDLE_ID_PREFIX,
} from './table-node-dependency-indicator';
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
@@ -106,24 +102,6 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
tableNodeId,
]
);
const isTargetFromView = useMemo(
() =>
connection.inProgress &&
connection.fromNode.id !== tableNodeId &&
(connection.fromHandle.id?.startsWith(
TOP_SOURCE_HANDLE_ID_PREFIX
) ||
connection.fromHandle.id?.startsWith(
BOTTOM_SOURCE_HANDLE_ID_PREFIX
)),
[
connection.inProgress,
connection.fromNode?.id,
connection.fromHandle?.id,
tableNodeId,
]
);
const numberOfEdgesToField = useMemo(() => {
let count = 0;
for (const rel of relationships) {
@@ -311,13 +289,13 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
<>
<Handle
id={`${RIGHT_HANDLE_ID_PREFIX}${field.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly || isTargetFromView ? '!invisible' : ''}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly ? '!invisible' : ''}`}
position={Position.Right}
type="source"
/>
<Handle
id={`${LEFT_HANDLE_ID_PREFIX}${field.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly || isTargetFromView ? '!invisible' : ''}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly ? '!invisible' : ''}`}
position={Position.Left}
type="source"
/>

View File

@@ -6,7 +6,7 @@ import React, {
useEffect,
} from 'react';
import type { NodeProps, Node } from '@xyflow/react';
import { NodeResizer, useConnection, useStore } from '@xyflow/react';
import { NodeResizer, useStore } from '@xyflow/react';
import { Button } from '@/components/button/button';
import {
ChevronsLeftRight,
@@ -80,14 +80,6 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const inputRef = React.useRef<HTMLInputElement>(null);
const [isHovering, setIsHovering] = useState(false);
const connection = useConnection();
const isTarget = useMemo(() => {
if (!isHovering) return false;
return connection.inProgress && connection.fromNode.id !== table.id;
}, [connection, table.id, isHovering]);
const {
getTableNewName,
getTableNewColor,
@@ -306,7 +298,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
() =>
cn(
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
selected || isTarget
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700',
isOverlapping
@@ -343,7 +335,6 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
isDiffTableChanged,
isDiffNewTable,
isDiffTableRemoved,
isTarget,
]
);

View File

@@ -128,8 +128,8 @@ export const CustomTypeList: React.FC<CustomTypeProps> = ({ customTypes }) => {
// if both have order, sort by order
if (
customType1.order != null &&
customType2.order != null
customType1.order !== undefined &&
customType2.order !== undefined
) {
return (
customType1.order - customType2.order

View File

@@ -8,6 +8,7 @@ import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
import {
findDataTypeDataById,
supportsAutoIncrementDataType,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import {
Popover,
@@ -87,6 +88,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
unique: localField.unique,
default: localField.default,
increment: localField.increment,
array: localField.array,
});
}
prevFieldRef.current = localField;
@@ -102,6 +104,13 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
[field.type.name]
);
const supportsArray = useMemo(
() =>
databaseType === 'postgresql' &&
supportsArrayDataType(field.type.name),
[field.type.name, databaseType]
);
return (
<Popover
open={isOpen}
@@ -168,6 +177,27 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
/>
</div>
) : null}
{supportsArray ? (
<div className="flex items-center justify-between">
<Label
htmlFor="array"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.array'
)}
</Label>
<Checkbox
checked={localField.array ?? false}
onCheckedChange={(value) =>
setLocalField((current) => ({
...current,
array: !!value,
}))
}
/>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="default" className="text-subtitle">
{t(

View File

@@ -7,6 +7,7 @@ import type { DataTypeData } from '@/lib/data/data-types/data-types';
import {
dataTypeDataToDataType,
sortedDataTypeMap,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import {
Tooltip,
@@ -175,6 +176,13 @@ export const TableField: React.FC<TableFieldProps> = ({
}
}
// Check if the new type supports arrays - if not, clear the array property
const newTypeName = dataType?.name ?? (value as string);
const shouldClearArray =
databaseType === 'postgresql' &&
!supportsArrayDataType(newTypeName) &&
field.array;
updateField({
characterMaximumLength,
precision,
@@ -185,6 +193,7 @@ export const TableField: React.FC<TableFieldProps> = ({
name: value as string,
}
),
...(shouldClearArray ? { array: false } : {}),
});
},
[
@@ -193,6 +202,7 @@ export const TableField: React.FC<TableFieldProps> = ({
field.characterMaximumLength,
field.precision,
field.scale,
field.array,
]
);

View File

@@ -151,13 +151,13 @@ export const Menu: React.FC<MenuProps> = () => {
return (
<Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
<MenubarMenu>
<MenubarTrigger>{t('menu.actions.actions')}</MenubarTrigger>
<MenubarTrigger>{t('menu.databases.databases')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
{t('menu.actions.new')}
{t('menu.databases.new')}
</MenubarItem>
<MenubarItem onClick={openDiagram}>
{t('menu.actions.browse')}
{t('menu.databases.browse')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
@@ -167,7 +167,7 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={updateDiagramUpdatedAt}>
{t('menu.actions.save')}
{t('menu.databases.save')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
@@ -179,7 +179,7 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.actions.import')}
{t('menu.databases.import')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={openImportDiagramDialog}>
@@ -248,7 +248,7 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.actions.export_sql')}
{t('menu.databases.export_sql')}
</MenubarSubTrigger>
<MenubarSubContent>
{databaseType === DatabaseType.GENERIC ? (
@@ -331,7 +331,7 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
{t('menu.actions.export_as')}
{t('menu.databases.export_as')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>PNG</MenubarItem>
@@ -357,7 +357,7 @@ export const Menu: React.FC<MenuProps> = () => {
})
}
>
{t('menu.actions.delete_diagram')}
{t('menu.databases.delete_diagram')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>