imoprt Postgres database features implemented

This commit is contained in:
KarimTamani
2025-06-22 18:17:12 +01:00
parent f33d6f5123
commit ea275f0a92
6 changed files with 204 additions and 52 deletions
+1
View File
@@ -26,6 +26,7 @@ export const databases = sqliteTable('databases', {
export const databaseRelations = relations(databases, ({ many }) => ({
tables: many(tables),
relationships: many(relationships),
}));
+5 -15
View File
@@ -42,7 +42,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ 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<string>("");
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
@@ -61,23 +61,13 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ 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<ModalProps> = ({ 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}
>
<div className="flex flex-col gap-4 ">
<div className="w-full ">
@@ -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<QueryResult>,
// 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<void> ;
}
export const DatabaseDataContext = createContext<DatabaseDataContextType>({} as DatabaseDataContextType);
@@ -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<Props> = ({ 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<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
importDatabase,
data_types,
grouped_data_types
}), [
@@ -430,6 +476,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
importDatabase,
data_types,
grouped_data_types
]);
+22 -9
View File
@@ -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: {
+123 -27
View File
@@ -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)
}
}
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
}