Render sql from Diagram initialized

This commit is contained in:
KarimTamani
2025-06-05 23:27:12 +01:00
parent 3ab346efd9
commit b9a996ba7a
11 changed files with 219 additions and 70 deletions
+3 -2
View File
@@ -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"
}
}
}
+1 -2
View File
@@ -11,7 +11,7 @@ import { Save } from "lucide-react";
const RenameDatabase: React.FC = ({ }) => {
const { database } = useDatabase();
const { editDatabase } = useDatabaseOperations();
const [editMode, setEditMode] = useState<boolean>(true);
const [editMode, setEditMode] = useState<boolean>(false);
const [dbName, setDbName] = useState<string>(database.name);
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(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)}
+28
View File
@@ -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<string>("");
const { data_types } = useDatabaseOperations();
useEffect(() => {
const dbAst: any = DatabaseToAst(database, data_types);
setSql(
parser.sqlify(dbAst)
);
}, [database]) ;
return sql ;
}
+7 -3
View File
@@ -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),
}));
+1 -1
View File
@@ -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")
+7 -8
View File
@@ -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<any> = useCallback((changes: EdgeChange<any>[]) => {
@@ -160,7 +160,6 @@ const DatabasePage: React.FC = () => {
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
return (
<div className="w-full h-screen flex relative overflow-hidden">
@@ -205,7 +204,7 @@ const DatabasePage: React.FC = () => {
/>
</Controls >
<Background className="bg-default/10 dark:bg-background-100" />
<Background className=" dark:bg-background-100" />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
@@ -0,0 +1,27 @@
import { useRenderSql } from "@/hooks/user-render-sql";
import { useDatabase } from "@/providers/database-provider/database-provider";
import React from "react";
const SqlPreview : React.FC = ({}) => {
const {database} = useDatabase() ;
const sql = useRenderSql(database) ;
return(
<p>
{sql
}
</p>
)
}
export default React.memo(SqlPreview) ;
@@ -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<Props> = ({ }) => {
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<TableType[]>(allTables);
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState(new Set([]));
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId } = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
@@ -46,17 +49,19 @@ const TablesController: React.FC<Props> = ({ }) => {
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<Props> = ({ }) => {
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 (
<div className="w-full h-full flex flex-col gap-2">
@@ -84,12 +99,9 @@ const TablesController: React.FC<Props> = ({ }) => {
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 ? (
<List className="size-4" />
) : (
<Code className="size-4" />
@@ -108,15 +120,15 @@ const TablesController: React.FC<Props> = ({ }) => {
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}
/>
</div>
<Button
@@ -132,37 +144,43 @@ const TablesController: React.FC<Props> = ({ }) => {
> {t("db_controller.add_table")}
</Button>
</div>
<div className=" flex-1 overflow-auto">
{
!showSqlPreview &&
<div className=" flex-1 overflow-auto">
<Accordion
hideIndicator
selectedKeys={selectedTable}
onSelectionChange={setSelectedTable as any}
isCompact
<Accordion
hideIndicator
>
{tables.map((table: TableType) => (
<AccordionItem
key={table.id}
aria-label={table.name}
classNames={{
trigger: "w-full h-12 hover:bg-default transition-all duration-200 dark:hover:bg-background",
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
content : "bg-transparent"
}}
subtitle={
<TableAccordionHeader
isOpen={selectedTableId == table.id}
table={table}
/>
}
>
<TableAccordionBody table={table} />
</AccordionItem>
))}
</Accordion>
</div>
selectedKeys={selectedTable}
onSelectionChange={setSelectedTable as any}
isCompact
>
{tables.map((table: TableType) => (
<AccordionItem
key={table.id}
aria-label={table.name}
classNames={{
trigger: "w-full h-12 hover:bg-default transition-all duration-200 dark:hover:bg-background",
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
content: "bg-transparent"
}}
subtitle={
<TableAccordionHeader
isOpen={selectedTableId == table.id}
table={table}
/>
}
>
<TableAccordionBody table={table} />
</AccordionItem>
))}
</Accordion>
</div>
}
{
showSqlPreview && <SqlPreview/>
}
</div>
)
}
@@ -174,3 +192,4 @@ const TablesController: React.FC<Props> = ({ }) => {
export default TablesController
@@ -20,6 +20,7 @@ interface DatabaseDataContextType {
isLoading: boolean,
isSwitchingDatabase: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
getDefaultPrimaryKeyType : () => DataType | undefined
}
@@ -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<Props> = ({ 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<Props> = ({ children }) => {
})
)
);
const switchDatabase = useCallback((databaseId: string | undefined) => {
setCurrentDatabaseId(databaseId);
@@ -83,6 +81,13 @@ const DatabaseProvider: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ children }) => {
databases: databases as DatabaseType[],
isLoading,
isSwitchingDatabase,
getDefaultPrimaryKeyType ,
getField,
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
@@ -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"
}
}