From ea275f0a9232bff8fe75ed681321d2689ffbeacc Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Sun, 22 Jun 2025 18:17:12 +0100 Subject: [PATCH] imoprt Postgres database features implemented --- src/lib/schemas/database-schema.ts | 1 + src/pages/database/modals/import-database.tsx | 20 +-- .../database-provider/database-context.ts | 5 + .../database-provider/database-provider.tsx | 49 +++++- src/utils/render/parsers/database_to_ast.ts | 31 ++-- src/utils/render/parsers/sql_to_database.ts | 150 ++++++++++++++---- 6 files changed, 204 insertions(+), 52 deletions(-) diff --git a/src/lib/schemas/database-schema.ts b/src/lib/schemas/database-schema.ts index 1a8f419..5a201e3 100644 --- a/src/lib/schemas/database-schema.ts +++ b/src/lib/schemas/database-schema.ts @@ -26,6 +26,7 @@ export const databases = sqliteTable('databases', { export const databaseRelations = relations(databases, ({ many }) => ({ tables: many(tables), relationships: many(relationships), + })); diff --git a/src/pages/database/modals/import-database.tsx b/src/pages/database/modals/import-database.tsx index e8693d1..8c84051 100644 --- a/src/pages/database/modals/import-database.tsx +++ b/src/pages/database/modals/import-database.tsx @@ -42,7 +42,7 @@ const ImportDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => const { t } = useTranslation(); const { resolvedTheme } = useTheme(); const { database } = useDatabase(); - const { data_types, createTable , createRelationship} = useDatabaseOperations(); + const { data_types, importDatabase } = useDatabaseOperations(); const [sqlCode, setSqlCode] = useState(""); let currentOption: ImportDatabaseOption | undefined = useMemo(() => { return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect) @@ -61,23 +61,13 @@ const ImportDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => 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) ; - } + const onImport = useCallback(async () => { + const { tables, relationships, indices } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect); + return await importDatabase(tables, relationships, indices) }, [sqlCode, database?.dialect, data_types]); @@ -88,7 +78,7 @@ const ImportDatabaseModal: React.FC = ({ isOpen, onOpenChange }) => title={t("modals.import_database.title")} actionName={t("modals.import_database.import")} className="min-w-[860px] max-w-[860px]" - actionHandler={importDatabase} + actionHandler={onImport} >
diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index da0f633..ff4435c 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -23,6 +23,7 @@ interface DatabaseDataContextType { getField: (tableId: string, id: string) => FieldType | undefined, getDefaultPrimaryKeyType : (dialect? : DatabaseDialect) => DataType | undefined + } @@ -58,6 +59,10 @@ interface DatabaseOperationsContextType { deleteMultiRelationships: (ids: string[]) => Promise, // execute the diff operation whenver user click in undo or redo executeDbDiffOps: (operations: DBDiffOperation[]) => void, + + // insert databse tables , relationships , indices in one operation + importDatabase : (tables : TableInsertType[] , relationships : RelationshipInsertType[] , indices : IndexInsertType[]) => Promise ; + } export const DatabaseDataContext = createContext({} as DatabaseDataContextType); diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 0d65610..a98d6a2 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -13,7 +13,7 @@ import { DBDiffOperation } from "@/utils/database"; import { DatabaseInsertType, DatabaseType, databases as databaseModel } from "@/lib/schemas/database-schema"; import { getTimestamp, groupBy } from "@/utils/utils"; import { IndexInsertType, indices } from "@/lib/schemas/index-schema"; -import { field_indices } from "@/lib/schemas/field_index-schema"; +import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-schema"; import { v4 } from "uuid"; import { DataType } from "@/lib/schemas/data-type-schema"; import { Modifiers } from "@/lib/field"; @@ -382,6 +382,51 @@ const DatabaseProvider: React.FC = ({ children }) => { }, [data_types]); + + + const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => { + if (currentDatabaseId) { + return await db.transaction(async (tx) => { + for (const table of importedTables) { + await tx.insert(tables).values({ + ...table, + databaseId: currentDatabaseId, + createdAt: table.createdAt || getTimestamp() + } as TableInsertType); + await updateDbNumTables(currentDatabaseId, tx); + if (table.fields) { + await tx.insert(fields).values( + table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id })) + ); + } + } + + for (const relationship of importedRelationships) { + await tx.insert(relationships).values({ + ...relationship, + databaseId: currentDatabaseId, + createdAt: relationship.createdAt || getTimestamp() + }); + } + + for (const index of importedIndices) { + await tx.insert(indices).values({ + ...index, + createdAt: index.createdAt ? index.createdAt : getTimestamp() + }); + + if (index.fieldIndices) { + await tx.insert(field_indices).values( + index.fieldIndices.map((fieldIndex: FieldIndexInsertType) => ({ ...fieldIndex, indexId: index.id })) + ) + } + } + }); + } else { + throw Error("no database selected") + } + }, [currentDatabaseId]) + const databaseOpsValue = useMemo(() => ({ createDatabase, @@ -406,6 +451,7 @@ const DatabaseProvider: React.FC = ({ children }) => { editIndex, deleteIndex, editFieldIndices, + importDatabase, data_types, grouped_data_types }), [ @@ -430,6 +476,7 @@ const DatabaseProvider: React.FC = ({ children }) => { editIndex, deleteIndex, editFieldIndices, + importDatabase, data_types, grouped_data_types ]); diff --git a/src/utils/render/parsers/database_to_ast.ts b/src/utils/render/parsers/database_to_ast.ts index ceb7db2..996078a 100644 --- a/src/utils/render/parsers/database_to_ast.ts +++ b/src/utils/render/parsers/database_to_ast.ts @@ -48,7 +48,7 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) => }, table)); }; } catch (error) { - throw error ; + throw error; } return dbAst; @@ -78,13 +78,13 @@ export const TableToAst = (table: TableType, data_types: DataType[], isPostgresD const filed_definitions = table.fields.map((field: FieldType) => { const isPostgresEnum: boolean = isPostgresDialect && field.type.name == "enum"; - + return FieldToAst({ ...field, type: !(isPostgresEnum) ? data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType : { name: `${field.name.toLowerCase()}_enum` } as DataType, - }, multiPrimaryKeys , !isPostgresEnum) + }, multiPrimaryKeys, !isPostgresEnum) }) @@ -122,7 +122,7 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, let valuesExpr: any | null; let default_val: any | null = null; - let dataType: string | undefined = upperCaseType ? field.type?.name?.toLocaleUpperCase() : field.type?.name as string | undefined ; + let dataType: string | undefined = upperCaseType ? field.type?.name?.toLocaleUpperCase() : field.type?.name as string | undefined; if (modifiers.includes(Modifiers.PRECISION) && field.precision) { length = field.precision; @@ -211,7 +211,21 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, } } - // field.type?.name == "varchar" && (field.type?.dialect == DatabaseDialect.MYSQL || field.type?.dialect == DatabaseDialect.MARIADB) ? 255 : null + let auto_increment: string | undefined; + if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) { + auto_increment = "auto_increment" + } else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) { + if (dataType == "INTEGER") + dataType = "SERIAL"; + else + dataType = dataType?.replace("INT", "SERIAL"); + + } else { + auto_increment = undefined; + } + + + return { column: { type: "column_ref", @@ -225,8 +239,7 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, character_set, default_val, unique: field.unique ? "unique" : null, - auto_increment: (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) ? "auto_increment" : null, - + auto_increment, nullable: { type: field.nullable ? "null" : "not null", value: field.nullable ? "null" : "not null", @@ -264,9 +277,9 @@ export const IndexToAst = (index: IndexType, table: TableType) => { export const PotgresEnumToAst = (field: FieldType) => { const jsonValues = field.values ? JSON.parse(field.values) : []; - + return { - as : "as" , + as: "as", type: "create", resource: "enum", name: { diff --git a/src/utils/render/parsers/sql_to_database.ts b/src/utils/render/parsers/sql_to_database.ts index 5199829..d3df880 100644 --- a/src/utils/render/parsers/sql_to_database.ts +++ b/src/utils/render/parsers/sql_to_database.ts @@ -8,6 +8,9 @@ import { parse } from 'pgsql-ast-parser'; import { DatabaseDialect } from "@/lib/database"; import { randomColor } from "@/lib/colors"; import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema"; +import { IndexInsertType } from "@/lib/schemas/index-schema"; +import { relationshipToAst } from "./database_to_ast"; +import { DatabaseInsertType } from "@/lib/schemas/database-schema"; export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => { @@ -16,8 +19,11 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data const alterTableStatements: string[] = []; const createIndexStatements: string[] = []; const createPostgresTypesStatements: string[] = []; + const tables: TableInsertType[] = []; let relationships: RelationshipInsertType[] = []; + const indices: IndexInsertType[] = [] + const postgresTypes: any[] = []; // Clean up SQL: remove comments and normalize const cleanedSql = sql @@ -39,7 +45,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data createTableStatements.push(stmt); } else if (upper.startsWith('ALTER TABLE')) { alterTableStatements.push(stmt); - } else if (upper.startsWith('CREATE INDEX')) { + } else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) { createIndexStatements.push(stmt); } else if (upper.startsWith('CREATE TYPE')) { createPostgresTypesStatements.push(stmt); @@ -60,27 +66,38 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data 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 table: TableInsertType = postgresAstToTable(instructionAst[0], data_types, postgresTypes); + tables.push(table); + if ((instructionAst[0] as any).constraints && (instructionAst[0] as any).constraints.length > 0) { + const relationshipAst: any = foreignKeyConstraintToAlterTableAst((instructionAst[0] as any).constraints, table) + try { + const newRelationships = astToRelationship(relationshipAst, tables); + relationships = relationships.concat(newRelationships); + } catch (error) { + if ((error as any).relationships && (error as any).relationships.length > 0) + relationships = relationships.concat((error as any).relationships); + } + } } - - /* - - 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 createIndex of createIndexStatements) { + try { + const instructionAst = parse(createIndex); + if (Array.isArray(instructionAst) && instructionAst.length > 0) { + indices.push(astToIndex(instructionAst[0], tables)); + } + } catch (error) { + continue; + } + } for (const alterTable of alterTableStatements) { try { const instructionAst = parse(alterTable); @@ -93,9 +110,9 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data relationships = relationships.concat((error as any).relationships); } } - - return {tables , relationships}; + + return { tables, relationships, indices }; } @@ -105,6 +122,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data export const postgresAstToTable = (ast: any, data_types: DataType[], postgresTypes: any[]): TableInsertType => { // console.log(ast); + return { id: v4(), name: ast.name.name, @@ -123,9 +141,9 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n }); let values: string | undefined; + let autoIncrement: boolean = false; 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; @@ -133,16 +151,27 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n }; } + if (ast.dataType.name?.toLowerCase().includes("serial")) { + + let baseType: string = ast.dataType.name?.toLowerCase() == "serial" ? "integer" : ast.dataType.name?.toLowerCase().replace("serial", "int"); + dataType = data_types.find((dataType: DataType) => { + return dataType.name == baseType; + }); + autoIncrement = true; + } 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) + let unique: boolean = false; + let primaryKey: boolean = false; + 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) + + if (ast.dataType.config.length == 2) scale = ast.dataType.config[1]; + } let maxLength: number | null = null; @@ -173,6 +202,14 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n else defaultValue = String(defaultValueConstraints.default.value); } + const uniqueConstraints: any | undefined = constraints.find((c: any) => c.type == "unique"); + const primryKeyConstraints: any | undefined = constraints.find((c: any) => c.type == "primary key"); + + if (uniqueConstraints) + unique = true; + if (primryKeyConstraints) + primaryKey = true; + } return { @@ -181,12 +218,14 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n defaultValue, typeId: dataType?.id, nullable, - unique: ast.unique, + unique, maxLength, precision, scale, + isPrimary: primaryKey, sequence, - values + values, + autoIncrement } as FieldInsertType; } @@ -204,6 +243,7 @@ const astToTable = (ast: any, data_types: DataType[]): 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) : []; @@ -237,11 +277,10 @@ export const astToField = (ast: any, data_types: DataType[]): 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) @@ -255,9 +294,7 @@ export const astToRelationship = (ast: any, tables: TableInsertType[]): Relation 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) - - + const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.foreignColumns[0]?.name) if (!sourceField || !targetField || !sourceTable) continue; @@ -283,4 +320,63 @@ export const astToRelationship = (ast: any, tables: TableInsertType[]): Relation } as any) -} \ No newline at end of file +} + + +const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableInsertType) => { + + const changes: any[] = constraints.map((constraint: any) => ({ + type: "add constraint", + constraint: { + type: "foreign key", + localColumns: [ + { + name: constraint.localColumns?.[0].name + } + ], + foreignTable: { + name: constraint.foreignTable.name, + }, + foreignColumns: [ + { + name: constraint.foreignColumns?.[0].name + } + ] + } + })) + + return { + type: "alter table", + only: true, + table: { + name: table.name + }, + changes + + } +} + +export const astToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType => { + const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name); + + if (!table) + throw Error("table not found"); + + const fieldNames: string[] = ast.expressions.map((expression: any) => expression.expression.name); + + + const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name)) + .map((field: FieldInsertType) => field.id); + + return { + id: v4(), + name: ast.indexName.name, + tableId: table.id, + unique: ast.unique, + fieldIndices: fieldIds?.map((id: string) => ({ + id: v4(), + fieldId: id + })) + } as IndexInsertType +} +