Import Tables and relationships from Postgres SQL code is implemented successfully

This commit is contained in:
KarimTamani
2025-06-21 21:44:59 +01:00
parent 26c1406f1b
commit f33d6f5123
18 changed files with 1094 additions and 51 deletions
@@ -17,7 +17,6 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
className="database-checkbox"
classNames={{
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative",
wrapper: "absolute top-2 left-2 " ,
label : "flex items-center justify-center w-full h-full p-2 "
}}
@@ -25,7 +24,6 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
<div className="min-w-full h-full flex items-center justify-center ">
<Image
src={database.logo}
className="w-full"
/>
</div>
@@ -0,0 +1,46 @@
import { ImportDatabaseMethod, ImportDatabaseOption } from "@/lib/database"
import { Avatar, Checkbox, Chip } from "@heroui/react";
interface OptionCheckboxProps {
value: string;
label: string ;
icon?: React.ReactNode;
logo?: string
}
const OptionCheckbox: React.FC<OptionCheckboxProps> = ({ value, icon, logo , label }) => {
return (
<Checkbox
aria-label={value}
value={value}
size="sm"
className="option-checkbox"
classNames={{
wrapper: "hidden",
label: "flex items-center justify-center w-full h-full"
}}
>
<Chip radius="sm" variant="bordered" color="default" className="option-span px-2 border-1 transition-all duration-300 border-divider"
avatar={logo ? <Avatar src={logo} /> : undefined}
startContent={
icon
}
>
<span className="text-sm text-font font-semibold">
{label}
</span>
</Chip>
</Checkbox>
)
}
export default OptionCheckbox;
+8 -7
View File
@@ -1,7 +1,7 @@
import { useTheme } from "next-themes";
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
import React, { useMemo } from "react";
import React, { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
@@ -23,6 +23,10 @@ const Menu: React.FC<MenuProps> = ({ }) => {
const { t } = useTranslation();
const { open } = useModal();
useEffect(() => {
open(Modals.IMPORT_DATABASE)
} , [])
const menu: MenuDropdownProps[] = useMemo(() => [
{
title: t("menu.file"),
@@ -43,12 +47,9 @@ const Menu: React.FC<MenuProps> = ({ }) => {
{
title: t("menu.import"),
divide: true,
children: [
{ title: t("menu.json") },
{ title: t("menu.dbml") },
{ title: t("menu.mysql") },
{ title: t("menu.postgresql") },
],
clickHandler: () => {
open(Modals.IMPORT_DATABASE)
}
},
{
title: t("menu.export_sql"),
+2 -2
View File
@@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next";
export interface ModalProps {
isOpen?: boolean,
onOpenChange: (open: boolean) => void,
onOpenChange? : (open: boolean) => void,
className?: string,
backdrop?: "blur" | "transparent" | "opaque",
title: string,
@@ -45,7 +45,7 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
<HeroUiModal
ref={targetRef}
isOpen={isOpen}
onOpenChange={onOpenChange}
onOpenChange={onOpenChange ? onOpenChange : undefined}
className={className}
backdrop={backdrop}
radius="sm"
+15 -11
View File
@@ -8,7 +8,7 @@ import { Parser } from "node-sql-parser";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { format } from 'sql-formatter';
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
import { CircularDependencyError, fixCharsetPlacement, fixSQLiteColumnOrder } from "@/utils/render/parsers/render-uttils";
import { CircularDependencyError, fixCharsetPlacement, fixSQLiteColumnOrder } from "@/utils/render/render-uttils";
import { areArraysEqual } from "@/utils/utils";
const parser = new Parser();
@@ -17,7 +17,7 @@ const parser = new Parser();
export const useRenderSql = (database: DatabaseType) => {
const [sql, setSql] = useState<string>("");
const { data_types } = useDatabaseOperations();
const [circularDependency , setCircularDependency] = useState<CircularDependencyError | undefined>(undefined)
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined)
useEffect(() => {
try {
const dbAst: any = DatabaseToAst(database, data_types);
@@ -34,18 +34,22 @@ export const useRenderSql = (database: DatabaseType) => {
setSql(
(formattedSqlCode)
);
setCircularDependency( undefined ) ;
setCircularDependency(undefined);
} catch (error) {
setCircularDependency((previousError) => {
if ( !previousError || !areArraysEqual( previousError.cycle , (error as CircularDependencyError).cycle) )
return error as CircularDependencyError ;
return previousError ;
})
if ((error as CircularDependencyError)?.cycle)
setCircularDependency((previousError) => {
if (!previousError)
return error as CircularDependencyError;
else if (Array.isArray(previousError.cycle) && Array.isArray((error as CircularDependencyError).cycle) && !(areArraysEqual(previousError.cycle, (error as CircularDependencyError).cycle)))
return error as CircularDependencyError;
return previousError;
})
}
}, [database]);
return {sql , circularDependency};
return { sql, circularDependency };
}
+7 -1
View File
@@ -180,7 +180,13 @@ export const en = {
open_database_header: "Open a database by selecting one from the list.",
delete_database: "Delete Database",
delete_database_content: "This action is irreversible and will permanently remove the diagram.",
delete: "Delete"
delete: "Delete" ,
import_database : {
title : "Import your Database" ,
import : "Import" ,
import_options : "Would you like to import using :"
}
}
}
+21
View File
@@ -38,4 +38,25 @@ export const DBTypes: DatabaseType[] = [
export const getDatabaseByDialect = (dialect: DatabaseDialect): DatabaseType => {
const dbType : DatabaseType | undefined = DBTypes.find((dbType : DatabaseType) => dbType.dialect == dialect) ;
return dbType ? dbType : DBTypes[0] ;
}
export enum ImportMethodType {
DUMP = "DUMP" ,
DB_CLIENT = "DB_CLIENT" ,
}
export interface ImportDatabaseMethod {
id : string ;
name : string ;
logo? : string ;
icon? : React.ReactNode ;
instructions? : string ;
type : ImportMethodType ;
}
export interface ImportDatabaseOption {
dialect : DatabaseDialect ,
methods : ImportDatabaseMethod []
}
+451
View File
@@ -0,0 +1,451 @@
export const PostgresSqlExample = `
--
-- PostgreSQL database dump
--
-- Dumped from database version 17.4
-- Dumped by pg_dump version 17.4
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: postgres
--
-- *not* creating schema, since initdb creates it
ALTER SCHEMA public OWNER TO postgres;
--
-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: postgres
--
COMMENT ON SCHEMA public IS '';
--
-- Name: citext; Type: EXTENSION; Schema: -; Owner: -
--
CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;
--
-- Name: EXTENSION citext; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION citext IS 'data type for case-insensitive character strings';
--
-- Name: type_enum; Type: TYPE; Schema: public; Owner: postgres
--
CREATE TYPE public.type_enum AS ENUM (
'user',
'admin',
'manager'
);
ALTER TYPE public.type_enum OWNER TO postgres;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: category; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.category (
id bigint NOT NULL,
name character varying
);
ALTER TABLE public.category OWNER TO postgres;
--
-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.category_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.category_id_seq OWNER TO postgres;
--
-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id;
--
-- Name: field_2_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.field_2_seq
START WITH 12
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.field_2_seq OWNER TO postgres;
--
-- Name: products; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.products (
id bigint NOT NULL,
name character varying NOT NULL,
descripttion text,
sale_price money NOT NULL,
cost_price money DEFAULT 0 NOT NULL,
quantity integer DEFAULT 0,
store_id bigint NOT NULL
);
ALTER TABLE public.products OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.products_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.products_id_seq OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id;
--
-- Name: stores; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.stores (
id bigint NOT NULL,
title character varying NOT NULL,
bio text,
logo bytea,
rating numeric(3,2) DEFAULT 0 NOT NULL,
user_id bigint NOT NULL,
category_id bigint NOT NULL
);
ALTER TABLE public.stores OWNER TO postgres;
--
-- Name: stores_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.stores_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.stores_id_seq OWNER TO postgres;
--
-- Name: stores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.stores_id_seq OWNED BY public.stores.id;
--
-- Name: stores_user_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.stores_user_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.stores_user_id_seq OWNER TO postgres;
--
-- Name: stores_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.stores_user_id_seq OWNED BY public.stores.user_id;
--
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.users (
id bigint NOT NULL,
name character varying NOT NULL,
lastname character varying NOT NULL,
email character varying(100) NOT NULL,
password character varying(100) NOT NULL,
type public.type_enum DEFAULT 'user'::public.type_enum NOT NULL,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE public.users OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.users_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.users_id_seq OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- Name: category id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass);
--
-- Name: products id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass);
--
-- Name: stores id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.stores ALTER COLUMN id SET DEFAULT nextval('public.stores_id_seq'::regclass);
--
-- Name: stores user_id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.stores ALTER COLUMN user_id SET DEFAULT nextval('public.stores_user_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- Data for Name: category; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.category (id, name) FROM stdin;
\.
--
-- Data for Name: products; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.products (id, name, descripttion, sale_price, cost_price, quantity, store_id) FROM stdin;
\.
--
-- Data for Name: stores; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.stores (id, title, bio, logo, rating, user_id, category_id) FROM stdin;
\.
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.users (id, name, lastname, email, password, type, created_at) FROM stdin;
\.
--
-- Name: category_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.category_id_seq', 1, false);
--
-- Name: field_2_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.field_2_seq', 12, false);
--
-- Name: products_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.products_id_seq', 1, false);
--
-- Name: stores_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.stores_id_seq', 1, false);
--
-- Name: stores_user_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.stores_user_id_seq', 1, false);
--
-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.users_id_seq', 1, false);
--
-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.category
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
--
-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT products_pkey PRIMARY KEY (id);
--
-- Name: stores stores_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.stores
ADD CONSTRAINT stores_pkey PRIMARY KEY (id);
--
-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_email_key UNIQUE (email);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: email_index; Type: INDEX; Schema: public; Owner: postgres
--
CREATE UNIQUE INDEX email_index ON public.users USING btree (email);
--
-- Name: products products_store_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT products_store_id_fkey FOREIGN KEY (store_id) REFERENCES public.stores(id);
--
-- Name: stores stores_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.stores
ADD CONSTRAINT stores_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id);
--
-- Name: stores stores_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.stores
ADD CONSTRAINT stores_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE USAGE ON SCHEMA public FROM PUBLIC;
--
-- PostgreSQL database dump complete
--
`
+4 -2
View File
@@ -12,7 +12,8 @@ export const data_types = sqliteTable('data_types', {
}).notNull().default("postgres"),
type: text('type').notNull(),
modifiers: text("modifiers")
modifiers: text("modifiers"),
synonyms: text("synonyms")
});
@@ -24,6 +25,7 @@ export const dataTypeRelations = relations(fields, ({ many }) => ({
export interface DataType extends InferSelectModel<typeof data_types> {
modifiers: string | null | any ;
modifiers: string | null | any;
synonyms: string | null | any;
};
@@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { CircularDependencyError } from "@/utils/render/parsers/render-uttils";
import { CircularDependencyError } from "@/utils/render/render-uttils";
import { addToast, Alert, Button, Listbox, ListboxItem, toast } from "@heroui/react";
import { AlertTriangle, Trash } from "lucide-react";
import React, { useCallback, useEffect, useMemo } from "react";
@@ -40,7 +40,7 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
const focus = (key: any) => {
console.log(key)
focusOnRelationship(
key, true, false
)
@@ -1,6 +1,6 @@
import { useRenderSql } from "@/hooks/user-render-sql";
import { useDatabase } from "@/providers/database-provider/database-provider";
import React, { useEffect, useMemo } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { sql } from '@codemirror/lang-sql';
@@ -11,7 +11,7 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
import CircularDependencyAlert from "./circular-dependecy-alert";
import { addToast } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
@@ -19,20 +19,20 @@ import { useTranslation } from "react-i18next";
const SqlPreview: React.FC = ({ }) => {
const { database } = useDatabase();
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
const { resolvedTheme } = useTheme();
const { t } = useTranslation() ;
const { t } = useTranslation();
useEffect(() => {
useEffect(() => {
if (circularDependency)
addToast({
title: t("db_controller.circular_dependency.title"),
description: t("db_controller.circular_dependency.description") ,
description: t("db_controller.circular_dependency.description"),
color: "danger",
variant: "solid"
});
}, [circularDependency])
}, [circularDependency]);
if (circularDependency)
return <CircularDependencyAlert error={circularDependency} />
@@ -40,7 +40,8 @@ const SqlPreview: React.FC = ({ }) => {
return (
<div className="flex w-full h-full ">
{
<CodeMirror
<CodeMirror
defaultValue={sqlCode}
value={sqlCode}
className="flex flex-1 w-full"
extensions={[sql()]}
@@ -0,0 +1,205 @@
import Modal, { ModalProps } from "@/components/modal/modal"
import ReactCodeMirror, { oneDark } from "@uiw/react-codemirror";
import { useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { sql } from '@codemirror/lang-sql';
import { useTheme } from "next-themes";
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType } from "@/lib/database";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { CheckboxGroup } from "@heroui/react";
import OptionCheckbox from "@/components/checkbox/option-checkbox";
import { Code } from "lucide-react";
import { Code as CodeSection } from "@heroui/react";
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
import { adjustTablesPositions } from "@/utils/tables";
const options: ImportDatabaseOption[] = [{
dialect: DatabaseDialect.POSTGRES,
methods: [{
id: "pg_dump",
name: "pg_dump",
icon: <Code className="size-4 text-font/90" />,
type: ImportMethodType.DUMP
}, {
id: "pg_admin",
name: "pg Admin",
logo: "/postgresql_logo.png",
type: ImportMethodType.DB_CLIENT
}]
}]
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
const { database } = useDatabase();
const { data_types, createTable , createRelationship} = useDatabaseOperations();
const [sqlCode, setSqlCode] = useState<string>("");
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
}, [database]);
const [selectedMethodId, setSelectedMethodId] = useState<string[]>(currentOption ? [currentOption.methods[0].id] : []);
if (!currentOption) {
onOpenChange && onOpenChange(false);
}
const onImportMethodChange = (types: string[]) => {
const selectedType: string | undefined = types.pop();
if (selectedType && selectedType != selectedMethodId?.[0])
setSelectedMethodId([selectedType]);
}
const selectedImportMethod: ImportDatabaseMethod = useMemo(() => {
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId?.[0]) as ImportDatabaseMethod;
}, [selectedMethodId, currentOption])
const importDatabase = useCallback( async () => {
const {tables , relationships } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect);
for (const table of tables) {
await createTable(table)
}
for ( const relationship of relationships ) {
await createRelationship(relationship) ;
}
}, [sqlCode, database?.dialect, data_types]);
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.import_database.title")}
actionName={t("modals.import_database.import")}
className="min-w-[860px] max-w-[860px]"
actionHandler={importDatabase}
>
<div className="flex flex-col gap-4 ">
<div className="w-full ">
<p className="text-sm text-font/90">
{t("modals.import_database.import_options")}
</p>
<CheckboxGroup
classNames={{
base: "w-full p-0 ",
wrapper: "flex flex-row p-2 gap-2 px-0 "
}}
aria-label="Select Database"
value={selectedMethodId}
onChange={onImportMethodChange}
>
{
currentOption?.methods.map((method: ImportDatabaseMethod) => (
<OptionCheckbox
label={method.name}
value={method.id}
icon={method.icon}
logo={method.logo}
/>
))
}
</CheckboxGroup>
{
selectedImportMethod.type == ImportMethodType.DUMP &&
<div className="space-y-2">
<label className="text-sm font-semibold">
Instructions :
</label>
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-2 marker:font-semibold ">
<li>
install <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
</li>
<li>
Run the following command in your terminal :
<ReactCodeMirror
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
editable={false}
value={`pg_dump -h <host> -p <port> -d <database_name>
-U <username> -s -F p -E UTF-8
-f <output_file_path>`}
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
/>
Example :
<ReactCodeMirror
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
value={`pg_dump -h localhost -p 5432 -d my_db
-U postgres -s -F p -E UTF-8
-f schema_export.sql`}
editable={false}
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
/>
</li>
<li>
Drag and drop the output .sql file in code section or copy it content
</li>
</ul>
</div>
}
{
selectedImportMethod.type == ImportMethodType.DB_CLIENT &&
<div className="space-y-2">
<label className="text-sm font-semibold">
Instructions :
</label>
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-3 marker:font-semibold ">
<li>
Open <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
</li>
<li>
Right-click your database and select <span className="font-semibold text-font/90">Backup</span> from the context menu.
</li>
<li>
Name your <CodeSection className="p-1" color="default" size="sm"> .sql </CodeSection> file, set Format to <span className="font-semibold text-font/90">Plain</span>, and choose <span className="font-semibold text-font/90">Encoding: UTF8.</span>
</li>
<li>
Make sure <span className="font-semibold text-font/90">Only schema</span> is checked and <span className="font-semibold text-font/90">Only data</span> is unchecked in the <span className="font-semibold text-font/90">Data Options tab</span>.
</li>
<li>
Click <span className="font-semibold text-font/90">Backup</span> to export the file, then copy its content into the code editor section.
</li>
</ul>
</div>
}
</div>
<div className="flex flex-1 ">
{
<ReactCodeMirror
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
extensions={[sql()]}
value={sqlCode}
onChange={setSqlCode}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
}
</div>
</div>
</Modal>
)
}
export default ImportDatabaseModal
@@ -5,7 +5,8 @@ export enum Modals {
CREATE_RELATIONSHIP = "CREATE_RELATIONSHIP",
CREATE_DATABASE = "CREATE_DATABASE" ,
OPEN_DATABASE = "OPEN_DATABASE" ,
DELETE_DATABASE = "DELETE_DATABASE"
DELETE_DATABASE = "DELETE_DATABASE" ,
IMPORT_DATABASE = "IMPOR_DATABASE"
}
interface ModalContextType {
@@ -7,6 +7,7 @@ import CreateRelationshipModal from "@/pages/database/modals/create-relationship
import { CreateDatabaseModal } from "@/pages/database/modals/create-database-modal";
import OpenDatabaseModal from "@/pages/database/modals/open-database-modal";
import DeleteDatabaseModal from "@/pages/database/modals/delete-database-modal";
import ImportDatabaseModal from "@/pages/database/modals/import-database";
interface Props { children: React.ReactNode }
@@ -54,8 +55,12 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
||
currentModal.modal == Modals.DELETE_DATABASE &&
<DeleteDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
(currentModal.modal == Modals.IMPORT_DATABASE ) &&
<ImportDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
) : undefined
}
}
{children}
</ModalContext.Provider>
)
+28 -13
View File
@@ -187,29 +187,44 @@ tbody[role="rowgroup"] tr td:first-child {
button[data-testid="remove"] {
top : 1px ;
top: 1px;
position: relative;
}
button[data-testid="remove"]:after {
content: '×' ;
color : white;
font-size : 11px ;
content: '×';
color: white;
font-size: 11px;
position: relative;
top: -2px ;
top: -2px;
}
button[data-testid="remove"] svg {
display: none ;
display: none;
}
[data-invalid = "true"] div[data-slot="input-wrapper"] {
border : 1px solid red !important ;
background-color: transparent ;
outline: none ;
}
[data-invalid="true"] div[data-slot="input-wrapper"] {
border: 1px solid red !important;
background-color: transparent;
outline: none;
}
.option-checkbox[data-selected="true"] .option-span {
border-color: hsl(var(--heroui-divider));
background-color: hsl(var(--heroui-default));
}
+286
View File
@@ -0,0 +1,286 @@
import { Modifiers, TimeDefaultValues } from "@/lib/field";
import { DataType } from "@/lib/schemas/data-type-schema";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { TableInsertType } from "@/lib/schemas/table-schema";
import { Parser } from "node-sql-parser";
import { v4 } from "uuid";
import { parse } from 'pgsql-ast-parser';
import { DatabaseDialect } from "@/lib/database";
import { randomColor } from "@/lib/colors";
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
const parser = new Parser();
const createTableStatements: string[] = [];
const alterTableStatements: string[] = [];
const createIndexStatements: string[] = [];
const createPostgresTypesStatements: string[] = [];
const tables: TableInsertType[] = [];
let relationships: RelationshipInsertType[] = [];
const postgresTypes: any[] = [];
// Clean up SQL: remove comments and normalize
const cleanedSql = sql
.replace(/--.*$/gm, '') // remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
.replace(/\s+/g, ' ') // normalize whitespace
.replace(/;\s*/g, ';\n'); // separate statements
// Split into individual statements
const statements = cleanedSql
.split('\n')
.map(s => s.trim())
.filter(Boolean);
for (const stmt of statements) {
const upper = stmt.toUpperCase();
if (upper.startsWith('CREATE TABLE')) {
createTableStatements.push(stmt);
} else if (upper.startsWith('ALTER TABLE')) {
alterTableStatements.push(stmt);
} else if (upper.startsWith('CREATE INDEX')) {
createIndexStatements.push(stmt);
} else if (upper.startsWith('CREATE TYPE')) {
createPostgresTypesStatements.push(stmt);
}
}
if (dialect == DatabaseDialect.POSTGRES)
for (const postgresType of createPostgresTypesStatements) {
try {
const instructionAst = parse(postgresType);
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
postgresTypes.push(instructionAst[0]);
}
} catch (error) {
console.log(error);
}
}
if (dialect == DatabaseDialect.POSTGRES)
for (const createTable of createTableStatements) {
try {
const instructionAst = parse(createTable);
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
tables.push(postgresAstToTable(instructionAst[0], data_types, postgresTypes));
}
/*
const instructionAst = parser.astify(createTable, {
database: "MySql"
});
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
tables.push(astToTable(instructionAst[0], data_types));
}*/
} catch (error) {
console.log(error);
}
}
for (const alterTable of alterTableStatements) {
try {
const instructionAst = parse(alterTable);
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
const extractedRelationships: RelationshipInsertType[] = astToRelationship((instructionAst[0] as any), tables);
relationships = relationships.concat(extractedRelationships);
}
} catch (error) {
if ((error as any).relationships && (error as any).relationships.length > 0)
relationships = relationships.concat((error as any).relationships);
}
}
return {tables , relationships};
}
export const postgresAstToTable = (ast: any, data_types: DataType[], postgresTypes: any[]): TableInsertType => {
// console.log(ast);
return {
id: v4(),
name: ast.name.name,
fields: ast.columns.filter((column: any) => column.kind == "column")
.map((fieldAst: any, index: number) => postgresAstToField(fieldAst, data_types, index, postgresTypes)),
color: randomColor(),
} as TableInsertType;
}
export const postgresAstToField = (ast: any, data_types: DataType[], sequence: number, postgresTypes: any[]): FieldInsertType => {
let dataType: DataType | undefined = data_types.find((dataType: DataType) => {
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
return dataType.name == ast.dataType.name?.toLowerCase() || synonyms.includes(ast.dataType.name?.toLowerCase())
});
let values: string | undefined;
if (!dataType && postgresTypes && postgresTypes.length > 0) {
const postgresType: any = postgresTypes.find((type: any) => type.name.name == ast.dataType.name);
if (postgresType) {
dataType = data_types.find((dataType: DataType) => dataType.name == "enum") as DataType;
values = JSON.stringify(postgresType.values.map((value: any) => value.value));
};
}
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
let length: number | undefined;
let scale: number | undefined;
if (ast.dataType.config && ast.dataType.config.length > 0)
if (ast.dataType.config.length == 1)
length = ast.dataType.config[0];
else if (ast.dataType.config.length == 2)
scale = ast.dataType.config[1];
let maxLength: number | null = null;
let precision: number | null = null;
if (modifiers.includes(Modifiers.LENGTH) && length)
maxLength = length;
if (modifiers.includes(Modifiers.PRECISION) && length)
precision = length;
let defaultValue: string | undefined;
let nullable: boolean = true;;
const constraints: any[] | undefined = ast.constraints;
if (constraints && constraints.length > 0) {
const nullableConstraints: any | undefined = constraints.find((c: any) => c.type == "not null");
if (nullableConstraints)
nullable = false;
const defaultValueConstraints: any | undefined = constraints.find((c: any) => c.type == "default");
if (defaultValueConstraints) {
if (defaultValueConstraints.default.type == "keyword" && defaultValueConstraints.default.keyword == "current_timestamp")
defaultValue = TimeDefaultValues.NOW;
else if (defaultValueConstraints.default.type == "cast" && defaultValueConstraints.default.operand)
defaultValue = String(defaultValueConstraints.default.operand.value)
else
defaultValue = String(defaultValueConstraints.default.value);
}
}
return {
id: v4(),
name: ast.name.name,
defaultValue,
typeId: dataType?.id,
nullable,
unique: ast.unique,
maxLength,
precision,
scale,
sequence,
values
} as FieldInsertType;
}
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
return {
id: v4(),
name: ast.table[0]?.table,
fields: ast.create_definitions.filter((column: any) => column.resource == "column")
.map((fieldAst: any) => astToField(fieldAst, data_types))
} as TableInsertType;
}
export const astToField = (ast: any, data_types: DataType[]): FieldInsertType => {
const dataType: DataType | undefined = data_types.find((dataType: DataType) => dataType.name == ast.definition.dataType?.toLowerCase());
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
const { length, scale } = ast.definition;
let maxLength: number | null = null;
let precision: number | null = null;
if (modifiers.includes(Modifiers.LENGTH) && length)
maxLength = length;
if (modifiers.includes(Modifiers.PRECISION) && length)
precision = length;
return {
id: v4(),
name: ast.column.column,
defaultValue: ast.default_val,
typeId: dataType?.id,
nullable: ast.nullable?.value == "not null",
unique: ast.unique,
maxLength,
precision,
scale: scale,
} as FieldInsertType;
}
export const astToRelationship = (ast: any, tables: TableInsertType[]): RelationshipInsertType[] => {
const relationships: RelationshipInsertType[] = [];
const changes = ast.changes;
const targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name);
if (!targetTable)
throw Error("source table not found");
const foreignKeyConstraints = changes.filter((change: any) => change.type == 'add constraint' && change.constraint && change.constraint.type == "foreign key").map((change: any) => change.constraint);
for (const foreignKeyConstraint of foreignKeyConstraints) {
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == foreignKeyConstraint.foreignTable.name);
const targetField: FieldInsertType | undefined = targetTable.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.localColumns[0]?.name)
const sourceField : FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.foreignColumns[0]?.name)
if (!sourceField || !targetField || !sourceTable)
continue;
relationships.push({
id: v4(),
sourceTableId: sourceTable.id,
targetTableId: targetTable.id,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many
} as RelationshipInsertType)
}
if (relationships.length == foreignKeyConstraints.length)
return relationships;
else
throw Error({
success: false,
message: "Failed to Extract all relationships",
relationships
} as any)
}