diff --git a/package.json b/package.json index 6efb23c..e2a250a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix", - "preview": "vite preview" + "preview": "vite preview" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -29,6 +29,7 @@ "i18next-browser-languagedetector": "^8.0.5", "lucide-react": "^0.501.0", "next-themes": "^0.4.6", + "node-sql-parser": "^5.3.9", "object-hash": "^3.0.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -65,4 +66,4 @@ "vite-plugin-top-level-await": "^1.5.0", "vite-tsconfig-paths": "^4.3.2" } -} \ No newline at end of file +} diff --git a/src/components/navbar/rename-database.tsx b/src/components/navbar/rename-database.tsx index 89745a4..31e87d9 100644 --- a/src/components/navbar/rename-database.tsx +++ b/src/components/navbar/rename-database.tsx @@ -11,7 +11,7 @@ import { Save } from "lucide-react"; const RenameDatabase: React.FC = ({ }) => { const { database } = useDatabase(); const { editDatabase } = useDatabaseOperations(); - const [editMode, setEditMode] = useState(true); + const [editMode, setEditMode] = useState(false); const [dbName, setDbName] = useState(database.name); const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); @@ -72,7 +72,6 @@ const RenameDatabase: React.FC = ({ }) => { placeholder={database.name} type="text" size="sm" - autoFocus radius="sm" variant="faded" onChange={(event: any) => setDbName(event.target.value)} diff --git a/src/hooks/user-render-sql.tsx b/src/hooks/user-render-sql.tsx new file mode 100644 index 0000000..b666727 --- /dev/null +++ b/src/hooks/user-render-sql.tsx @@ -0,0 +1,28 @@ + + +import { useDatabaseOperations } from "@/providers/database-provider/database-provider"; +import { DatabaseToAst } from "@/utils/render/parsers/database_to_ast"; +import { useEffect, useState } from "react"; + +import { Parser } from "node-sql-parser"; +import { DatabaseType } from "@/lib/schemas/database-schema"; + +const parser = new Parser(); + + +export const useRenderSql = (database: DatabaseType) => { + const [sql, setSql] = useState(""); + const { data_types } = useDatabaseOperations(); + + useEffect(() => { + const dbAst: any = DatabaseToAst(database, data_types); + setSql( + parser.sqlify(dbAst) + ); + + }, [database]) ; + + + return sql ; + +} \ No newline at end of file diff --git a/src/lib/schemas/data-type-schema.ts b/src/lib/schemas/data-type-schema.ts index f704cec..ec2ccad 100644 --- a/src/lib/schemas/data-type-schema.ts +++ b/src/lib/schemas/data-type-schema.ts @@ -1,16 +1,20 @@ import { InferSelectModel, relations } from 'drizzle-orm'; -import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { fields } from './field-schema'; export const data_types = sqliteTable('data_types', { - id : text("id") , + id: text("id"), name: text('name'), + + dialect: text("dialect", { + enum: ["postgres", "mysql", "sqlite", "mariadb"], + }).notNull().default("postgres"), }); export const dataTypeRelations = relations(fields, ({ many }) => ({ - fields : many(fields) , + fields: many(fields), })); diff --git a/src/lib/schemas/database-schema.ts b/src/lib/schemas/database-schema.ts index 3ad9238..b1eaa06 100644 --- a/src/lib/schemas/database-schema.ts +++ b/src/lib/schemas/database-schema.ts @@ -11,7 +11,7 @@ export const databases = sqliteTable('databases', { .unique(), name: text('name').notNull(), dialect: text("dialect", { - enum: ["postgres", "mysql", "sqlite"], + enum: ["postgres", "mysql", "sqlite" , "mariadb"], }).notNull().default("postgres"), numOfTables: integer("numOfTables") diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index dd185b6..b329087 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -37,8 +37,8 @@ import useHighlightedEdges from "@/hooks/use-highlighted-edges"; import { useTranslation } from "react-i18next"; import useOverlappingTables from "@/hooks/use-overlapping-tables"; import { useTheme } from "next-themes"; - - +import { Parser } from "node-sql-parser"; +const parser = new Parser(); const DatabasePage: React.FC = () => { const { t } = useTranslation(); @@ -55,7 +55,7 @@ const DatabasePage: React.FC = () => { const { setIsConnectionInProgress } = useDiagramOps(); // Destructure tables and relationships from database - const { tables, relationships } = database; + const { tables, relationships } = database; // Hook to allow zooming and centering the diagram const { fitView } = useReactFlow(); @@ -63,7 +63,7 @@ const DatabasePage: React.FC = () => { // Define custom node and edge types const nodeTypes = useMemo(() => ({ table: Table }), []); const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []); - + // Called when a connection is made between fields const onConnect = useCallback((connection: Connection) => { const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop(); @@ -113,11 +113,11 @@ const DatabasePage: React.FC = () => { } as TableInsertType))); // Delete tables if removed - if (nodeRemoveChanges.length > 0 ) { + if (nodeRemoveChanges.length > 0) { deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id)); } return onNodesChange(changes); - }, [onNodesChange ]); + }, [onNodesChange]); // Called when edges (relationships) change const handleEdgeChanges: OnEdgesChange = useCallback((changes: EdgeChange[]) => { @@ -160,7 +160,6 @@ const DatabasePage: React.FC = () => { useHighlightedEdges(nodes, relationships, edges); const { isOverlapping, puls } = useOverlappingTables(tables); - return (
@@ -205,7 +204,7 @@ const DatabasePage: React.FC = () => { /> - +
{ + + const {database} = useDatabase() ; + const sql = useRenderSql(database) ; + + + return( +

+ {sql + } +

+ ) + +} + + + +export default React.memo(SqlPreview) ; \ No newline at end of file diff --git a/src/pages/database/db-controller/tables-controller/tables-controller.tsx b/src/pages/database/db-controller/tables-controller/tables-controller.tsx index f03dbf8..937a5f2 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -1,6 +1,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip" import { Accordion, AccordionItem, Button, Input } from "@heroui/react" -import { Code, List, Table } from "lucide-react" +import { Code, List, Table } from "lucide-react" import { useTranslation } from "react-i18next"; import { Ref, useCallback, useEffect, useRef, useState } from "react"; import TableAccordionHeader from "./table-accordion-item/table-accordion-header"; @@ -9,8 +9,10 @@ import { useDatabase, useDatabaseOperations } from "@/providers/database-provide import { TableInsertType, TableType } from "@/lib/schemas/table-schema"; import { v4 } from "uuid"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; -import { useReactFlow } from "@xyflow/react"; -import { randomColor } from "@/lib/colors"; +import { useReactFlow } from "@xyflow/react"; +import SqlPreview from "../sql-preview"; + + interface Props { } @@ -19,14 +21,15 @@ const PADDING_Y = 80; const TablesController: React.FC = ({ }) => { - const { database } = useDatabase(); - const { createTable } = useDatabaseOperations(); + const { database, getDefaultPrimaryKeyType } = useDatabase(); + const { createTable, data_types } = useDatabaseOperations(); const { getViewport } = useReactFlow(); const { tables: allTables } = database; const [tables, setTables] = useState(allTables); const { t } = useTranslation(); const [selectedTable, setSelectedTable] = useState(new Set([])); + const [showSqlPreview, setShowSqlPreview] = useState(false); const { focusedTableId } = useDiagram(); const nameRef: Ref = useRef(null); @@ -46,17 +49,19 @@ const TablesController: React.FC = ({ }) => { name: `table_${tables.length + 1}`, posX, posY, - + fields: [{ id: v4(), name: "id", isPrimary: true, - unique: true + unique: true, + typeId: getDefaultPrimaryKeyType()?.id + }] } as TableInsertType); setSelectedTable(new Set([newTableId]) as any); - }, [tables, getViewport]); + }, [tables, getViewport, getDefaultPrimaryKeyType]); useEffect(() => { @@ -71,7 +76,17 @@ const TablesController: React.FC = ({ }) => { const keyword = nameRef.current?.value; if (keyword !== undefined) setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim()))) - }, [nameRef, allTables]) + }, [nameRef, allTables]); + + + + const toggleSqlPreview = useCallback(() => { + setShowSqlPreview(preview => !preview); + }, []) + + + + return (
@@ -84,12 +99,9 @@ const TablesController: React.FC = ({ }) => { variant="light" className="size-8 p-0 text-icon hover:text-font/90" isIconOnly - onPress={() => - //setShowDBML((value) => !value) - console.log("hello world ") - } + onPressEnd={toggleSqlPreview} > - {false ? ( + {showSqlPreview ? ( ) : ( @@ -108,15 +120,15 @@ const TablesController: React.FC = ({ }) => { type="text" size="sm" autoFocus - radius="sm" + radius="sm" variant="faded" placeholder={t("db_controller.filter")} className="h-8 w-full focus-visible:ring-0 shadow-none " classNames={{ - inputWrapper : "dark:bg-default border-divider group-hover:border-primary " , + inputWrapper: "dark:bg-default border-divider group-hover:border-primary ", }} onKeyUp={searchTables} - + />
-
+ { + !showSqlPreview && +
- - {tables.map((table: TableType) => ( - - } - > - - - ))} - -
+ selectedKeys={selectedTable} + onSelectionChange={setSelectedTable as any} + isCompact + + > + {tables.map((table: TableType) => ( + + } + > + + + ))} + +
+ } + { + showSqlPreview && + }
) } @@ -174,3 +192,4 @@ const TablesController: React.FC = ({ }) => { export default TablesController + diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index 96ca316..2914541 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -20,6 +20,7 @@ interface DatabaseDataContextType { isLoading: boolean, isSwitchingDatabase: boolean, getField: (tableId: string, id: string) => FieldType | undefined, + getDefaultPrimaryKeyType : () => DataType | undefined } diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 13e379f..922a4a3 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -15,6 +15,7 @@ import { getTimestamp } from "@/utils/utils"; import { IndexInsertType, indices } from "@/lib/schemas/index-schema"; import { field_indices } from "@/lib/schemas/field_index-schema"; import { v4 } from "uuid"; +import { DataType } from "@/lib/schemas/data-type-schema"; interface Props { children: React.ReactNode } @@ -28,10 +29,7 @@ const DatabaseProvider: React.FC = ({ children }) => { db.query.databases.findMany() )); - // Fetch all data types - const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery( - db.query.data_types.findMany() - )); + // Fetch the current database with nested tables, fields, and relationships let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery( @@ -69,7 +67,7 @@ const DatabaseProvider: React.FC = ({ children }) => { }) ) ); - + const switchDatabase = useCallback((databaseId: string | undefined) => { setCurrentDatabaseId(databaseId); @@ -83,6 +81,13 @@ const DatabaseProvider: React.FC = ({ children }) => { if (database.length == 1) database = database[0] as any; + // Fetch all data types + const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery( + db.query.data_types.findMany({ + where : (data_types , { eq })=> eq(data_types.dialect, (database as any).dialect) + }) + )); + // Auto-select first database if none is selected useEffect(() => { if (databases.length > 0 && !currentDatabaseId) { @@ -119,7 +124,7 @@ const DatabaseProvider: React.FC = ({ children }) => { .from(tables) .where(eq(tables.databaseId, databaseId)) - console.log(numOfTables) + await tx.update(databaseModel).set({ numOfTables }).where(eq(databaseModel.id, databaseId)) @@ -346,6 +351,11 @@ const DatabaseProvider: React.FC = ({ children }) => { } }, [db, currentDatabaseId]); + + const getDefaultPrimaryKeyType = useCallback(() => { + return data_types.find((dataType : DataType) => dataType.name == "bigint" ) + } , [data_types ]) ; + const databaseOpsValue = useMemo(() => ({ createDatabase, @@ -404,8 +414,7 @@ const DatabaseProvider: React.FC = ({ children }) => { databases: databases as DatabaseType[], isLoading, isSwitchingDatabase, - - + getDefaultPrimaryKeyType , getField, }}> diff --git a/src/utils/render/parsers/database_to_ast.ts b/src/utils/render/parsers/database_to_ast.ts new file mode 100644 index 0000000..bbd9c1b --- /dev/null +++ b/src/utils/render/parsers/database_to_ast.ts @@ -0,0 +1,62 @@ + + +import { DataType } from "@/lib/schemas/data-type-schema"; +import { DatabaseType } from "@/lib/schemas/database-schema"; +import { FieldType } from "@/lib/schemas/field-schema"; +import { TableType } from "@/lib/schemas/table-schema"; + + +export const DatabaseToAst = (database: DatabaseType , data_types : DataType[]) => { + let dbAst: any = []; + if ( ! data_types || data_types.length == 0) + return dbAst ; + for (const table of database.tables) { + dbAst.push(TableToAst(table , data_types )) + }; + + return dbAst; +} + + +export const TableToAst = (table: TableType , data_types : DataType[]) => { + return { + keyword: "table", + type: "create", + table: [{ + table: table.name + }], + create_definitions: table.fields.map((field: FieldType) => FieldToAst({ + ...field , + type : data_types.find((dataType : DataType) => dataType.id == field.typeId) as DataType + } )) + } + +} + + +export const FieldToAst = (field: FieldType ) => { + return { + column: { + type: "column_ref", + column: { + expr: { + type: "default", value: field.name + } + }, + }, + default_val: null, + unique: field.unique ? "unique" : null, + nullable: { + type: field.nullable ? "null" : "not null", + value: field.nullable ? "null" : "not null", + }, + definition: { + dataType: field.type?.name?.toLocaleUpperCase() , + + }, + primary_key: field.isPrimary ? "primary key" : null, + resource: "column" + } +} + +