From f33d6f51237edcd5a975fbffd206435cc32c402c Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Sat, 21 Jun 2025 21:44:59 +0100 Subject: [PATCH] Import Tables and relationships from Postgres SQL code is implemented successfully --- package.json | 1 + src/components/checkbox/database-checkbox.tsx | 2 - src/components/checkbox/option-checkbox.tsx | 46 ++ src/components/menu/menu.tsx | 15 +- src/components/modal/modal.tsx | 4 +- src/hooks/user-render-sql.tsx | 26 +- src/i18/languages/en.ts | 8 +- src/lib/database.ts | 21 + src/lib/import/import_db.ts | 451 ++++++++++++++++++ src/lib/schemas/data-type-schema.ts | 6 +- .../circular-dependecy-alert.tsx | 4 +- .../database/db-controller/sql-preview.tsx | 19 +- src/pages/database/modals/import-database.tsx | 205 ++++++++ .../modal-provider/modal-contxet.tsx | 3 +- .../modal-provider/modal-provider.tsx | 7 +- src/styles/globals.css | 41 +- src/utils/render/parsers/sql_to_database.ts | 286 +++++++++++ .../render/{parsers => }/render-uttils.ts | 0 18 files changed, 1094 insertions(+), 51 deletions(-) create mode 100644 src/components/checkbox/option-checkbox.tsx create mode 100644 src/lib/import/import_db.ts create mode 100644 src/pages/database/modals/import-database.tsx create mode 100644 src/utils/render/parsers/sql_to_database.ts rename src/utils/render/{parsers => }/render-uttils.ts (100%) diff --git a/package.json b/package.json index d57e986..c875621 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "next-themes": "^0.4.6", "node-sql-parser": "^5.3.9", "object-hash": "^3.0.0", + "pgsql-ast-parser": "^12.0.1", "react": "18.3.1", "react-dom": "18.3.1", "react-i18next": "^15.5.1", diff --git a/src/components/checkbox/database-checkbox.tsx b/src/components/checkbox/database-checkbox.tsx index a577941..e5e2044 100644 --- a/src/components/checkbox/database-checkbox.tsx +++ b/src/components/checkbox/database-checkbox.tsx @@ -17,7 +17,6 @@ const DatabaseCheckbox: React.FC = ({ 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 = ({ database }) => {
diff --git a/src/components/checkbox/option-checkbox.tsx b/src/components/checkbox/option-checkbox.tsx new file mode 100644 index 0000000..0fa9c68 --- /dev/null +++ b/src/components/checkbox/option-checkbox.tsx @@ -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 = ({ value, icon, logo , label }) => { + + return ( + + : undefined} + startContent={ + icon + } + > + + {label} + + + + ) +} + + +export default OptionCheckbox; \ No newline at end of file diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 8e92316..d92755d 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -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 = ({ }) => { 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 = ({ }) => { { 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"), diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index e71c203..94684b0 100644 --- a/src/components/modal/modal.tsx +++ b/src/components/modal/modal.tsx @@ -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 = ({ isOpen, onOpenChange, className, backdrop { const [sql, setSql] = useState(""); const { data_types } = useDatabaseOperations(); - const [circularDependency , setCircularDependency] = useState(undefined) + const [circularDependency, setCircularDependency] = useState(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 }; } \ No newline at end of file diff --git a/src/i18/languages/en.ts b/src/i18/languages/en.ts index b75cf48..887fd15 100644 --- a/src/i18/languages/en.ts +++ b/src/i18/languages/en.ts @@ -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 :" + } } } diff --git a/src/lib/database.ts b/src/lib/database.ts index ebdf7ec..f351697 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -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 [] } \ No newline at end of file diff --git a/src/lib/import/import_db.ts b/src/lib/import/import_db.ts new file mode 100644 index 0000000..3075c0c --- /dev/null +++ b/src/lib/import/import_db.ts @@ -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 +-- + + + + +` + + diff --git a/src/lib/schemas/data-type-schema.ts b/src/lib/schemas/data-type-schema.ts index 58bebac..7794e55 100644 --- a/src/lib/schemas/data-type-schema.ts +++ b/src/lib/schemas/data-type-schema.ts @@ -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 { - modifiers: string | null | any ; + modifiers: string | null | any; + synonyms: string | null | any; }; \ No newline at end of file diff --git a/src/pages/database/db-controller/circular-dependecy-alert.tsx b/src/pages/database/db-controller/circular-dependecy-alert.tsx index 90eaf29..f984c36 100644 --- a/src/pages/database/db-controller/circular-dependecy-alert.tsx +++ b/src/pages/database/db-controller/circular-dependecy-alert.tsx @@ -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 = ({ error const focus = (key: any) => { - console.log(key) + focusOnRelationship( key, true, false ) diff --git a/src/pages/database/db-controller/sql-preview.tsx b/src/pages/database/db-controller/sql-preview.tsx index 71e7043..8673931 100644 --- a/src/pages/database/db-controller/sql-preview.tsx +++ b/src/pages/database/db-controller/sql-preview.tsx @@ -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 @@ -40,7 +40,8 @@ const SqlPreview: React.FC = ({ }) => { return (
{ - , + type: ImportMethodType.DUMP + + }, { + id: "pg_admin", + name: "pg Admin", + logo: "/postgresql_logo.png", + type: ImportMethodType.DB_CLIENT + }] +}] + +const ImportDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => { + + const { t } = useTranslation(); + const { resolvedTheme } = useTheme(); + const { database } = useDatabase(); + const { data_types, createTable , createRelationship} = useDatabaseOperations(); + const [sqlCode, setSqlCode] = useState(""); + let currentOption: ImportDatabaseOption | undefined = useMemo(() => { + return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect) + }, [database]); + + + const [selectedMethodId, setSelectedMethodId] = useState(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 ( + +
+
+

+ {t("modals.import_database.import_options")} +

+ + { + currentOption?.methods.map((method: ImportDatabaseMethod) => ( + + )) + } + + { + selectedImportMethod.type == ImportMethodType.DUMP && +
+ +
    +
  • + install {selectedImportMethod.name} . +
  • + +
  • + Run the following command in your terminal : + -p -d +-U -s -F p -E UTF-8 +-f `} + theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]} + /> + Example : + + +
  • +
  • + Drag and drop the output .sql file in code section or copy it content +
  • +
+
+ } + + { + selectedImportMethod.type == ImportMethodType.DB_CLIENT && +
+ +
    +
  • + Open {selectedImportMethod.name} . +
  • +
  • + Right-click your database and select Backup from the context menu. +
  • +
  • + Name your .sql file, set Format to Plain, and choose Encoding: UTF8. +
  • +
  • + Make sure Only schema is checked and Only data is unchecked in the Data Options tab. +
  • +
  • + Click Backup to export the file, then copy its content into the code editor section. +
  • +
+
+ } + +
+
+ { + + + + } +
+
+
+ ) +} + + +export default ImportDatabaseModal \ No newline at end of file diff --git a/src/providers/modal-provider/modal-contxet.tsx b/src/providers/modal-provider/modal-contxet.tsx index 8b2dd2d..b7fed01 100644 --- a/src/providers/modal-provider/modal-contxet.tsx +++ b/src/providers/modal-provider/modal-contxet.tsx @@ -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 { diff --git a/src/providers/modal-provider/modal-provider.tsx b/src/providers/modal-provider/modal-provider.tsx index 42e47d9..34fda7e 100644 --- a/src/providers/modal-provider/modal-provider.tsx +++ b/src/providers/modal-provider/modal-provider.tsx @@ -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 = ({ children }) => { || currentModal.modal == Modals.DELETE_DATABASE && + || + (currentModal.modal == Modals.IMPORT_DATABASE ) && + ) : undefined - } + +} {children} ) diff --git a/src/styles/globals.css b/src/styles/globals.css index f04944f..3b09a22 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -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 ; -} \ No newline at end of file +[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)); + + + +} + + \ No newline at end of file diff --git a/src/utils/render/parsers/sql_to_database.ts b/src/utils/render/parsers/sql_to_database.ts new file mode 100644 index 0000000..5199829 --- /dev/null +++ b/src/utils/render/parsers/sql_to_database.ts @@ -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) + + +} \ No newline at end of file diff --git a/src/utils/render/parsers/render-uttils.ts b/src/utils/render/render-uttils.ts similarity index 100% rename from src/utils/render/parsers/render-uttils.ts rename to src/utils/render/render-uttils.ts