Files
stackrender/src/utils/import/postgresql-importer.ts
T
2026-06-10 18:09:54 +01:00

284 lines
8.9 KiB
TypeScript

import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseDialect } from "@/lib/database";
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
import { init, parse, format, validate, Statement } from '@guanmingchiu/sqlparser-ts';
import { TableInsertType } from "@/lib/schemas/table-schema";
import { FieldInsertType } from "@/lib/schemas/field-schema";
interface PostgreSQLType {
name: string;
values: string[]
}
export class PostgreSqlImporter extends BaseSqlImporter {
private enums: PostgreSQLType[] = [];
public constructor(data_types: DataType[]) {
super(data_types);
this.dialect = DatabaseDialect.POSTGRES;
}
public parseSql(sql: string) {
try {
this.enums = [];
const cleaned: string = this.cleanSql(sql);
const { createPostgresTypesStatements, alterSequenceStatments } = this.extractStatments(cleaned);
if (createPostgresTypesStatements)
for (const statment of createPostgresTypesStatements) {
const astStatment: any = parse(statment, this.dialect as any)?.pop();
if (astStatment?.CreateType) {
this.enums.push(this.astToEnum(astStatment.CreateType));
}
}
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
if (alterSequenceStatments) {
for (const statment of alterSequenceStatments) {
try {
this.processAlterSequenceStatements(statment, parsedDatabaase.tables);
} catch (error) {
parsedDatabaase.errors.push(error as Error);
}
}
}
return parsedDatabaase;
} catch (error) {
throw error;
}
}
protected processAlterSequenceStatements(statment: string, tables: TableInsertType[]) {
const regex =
/ALTER\s+SEQUENCE\s+[\w"]+\.[\w"]+\s+OWNED\s+BY\s+("?[\w]+"?)\.("?[\w]+"?)\.("?[\w]+"?)/i;
const match = statment.match(regex);
if (!match) {
return;
}
const schema = match[1].replace(/"/g, '');
const tableName = match[2].replace(/"/g, '');
const columnName = match[3].replace(/"/g, '');
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
if (!table) {
throw Error(`table ${tableName} not found in : ${statment}`);
}
const index = table.fields?.findIndex((field: FieldInsertType) => field.name == columnName) ?? -1;
if (index < 0) {
throw Error(`field ${columnName} not found in : ${statment}`);
}
(table.fields as FieldInsertType[])[index].autoIncrement = true ;
}
protected astToField(ast: any, sequence: number): ParsedField {
const { field, fk_constraint } = super.astToField(ast, sequence);
if (typeof ast.data_type === "object" && ast.data_type.Custom) {
const typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
// it's a serial type
if (typeName?.toLowerCase().includes("serial")) {
field.autoIncrement = true;
}
else {
const postgreSqlType: PostgreSQLType | undefined = this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName);
if (postgreSqlType && postgreSqlType.values.length > 0) {
try {
field.values = JSON.stringify(postgreSqlType.values);
} catch (error) {
console.error("failed to parse postgresql type values")
}
}
}
}
return { field, fk_constraint };
}
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
if (!defaultValue) {
if (ast.Function) {
if (ast.Function.name?.[0]?.Identifier?.value == "gen_random_uuid") {
return "random";
}
}
}
return defaultValue
}
protected processDataType(typeName: string): DataType | undefined {
let dataType: DataType | undefined = super.processDataType(typeName);
if (!dataType) {
if (typeName?.toLowerCase().includes("serial")) {
// if it's a serial type , then get the base integer type (integer , smallint , bigint)
let baseType: string = typeName.toLowerCase() == "serial" ? "integer" : typeName.toLowerCase().replace("serial", "int");
return super.processDataType(baseType);
}
else {
const isEnum: boolean = Boolean(this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName));
if (isEnum) {
return this.data_types.find((dataType: DataType) => dataType.name == "enum")
}
}
}
return dataType
}
private astToEnum(ast: any): PostgreSQLType {
const values: string[] | undefined = ast.representation?.Enum?.labels?.map((label: any) => label.value);
return {
name: ast.name?.[0]?.Identifier?.value,
values
} as PostgreSQLType;
}
protected extractStatments(sql: string): ExtractedStatments {
const createPostgresTypesStatements: string[] = [];
const alterSequenceStatments: string[] = [];
const { createIndexStatements, createTableStatements, alterTableStatements } = super.extractStatments(sql);
// Split into individual statements
const statements = sql
.split('\n')
.map(s => s.trim())
.filter(Boolean);
for (const stmt of statements) {
const upper = stmt.toUpperCase();
if (upper.startsWith('CREATE TYPE')) {
createPostgresTypesStatements.push(stmt);
}
if (upper.startsWith("ALTER SEQUENCE")) {
alterSequenceStatments.push(stmt);
}
}
return {
createIndexStatements,
createTableStatements,
alterTableStatements,
createPostgresTypesStatements,
alterSequenceStatments
};
}
}
/*
CREATE TYPE "users_user_type_enum" AS ENUM (
'individual',
'agency',
'developer',
'admin',
'employee'
);
CREATE TYPE "users_provider_enum" AS ENUM (
'google.com',
'facebook.com',
'password',
'anonymous'
);
CREATE TABLE "users" (
id INTEGER NOT NULL PRIMARY KEY,
full_name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
picture_url VARCHAR NULL,
phone_number VARCHAR NULL,
password_hash VARCHAR NOT NULL,
user_type users_user_type_enum NOT NULL DEFAULT 'individual',
provider users_provider_enum NOT NULL DEFAULT 'password',
birthday TIMESTAMP(3) NULL,
gender BOOLEAN NULL,
created_at TIMESTAMPTZ (6) NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ (6) NULL,
is_email_verified BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE all_postgres_types (
id BIGSERIAL PRIMARY KEY,
-- Numeric types
col_smallint SMALLINT NOT NULL DEFAULT 1,
col_integer INTEGER NOT NULL UNIQUE DEFAULT 1000,
col_bigint BIGINT NULL DEFAULT 10000,
col_decimal DECIMAL(10,2) NOT NULL DEFAULT 99.99,
col_numeric NUMERIC(8,3) NULL DEFAULT 123.456,
col_real REAL NULL DEFAULT 1.23,
col_double DOUBLE PRECISION NOT NULL DEFAULT 2.3456,
col_boolean BOOLEAN NOT NULL DEFAULT TRUE,
-- Date & time types
col_date DATE NOT NULL DEFAULT DATE '2024-01-01',
col_timestamp TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
col_timestamptz TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
col_time TIME(6) NULL DEFAULT TIME '12:34:56.123456',
-- String types
col_char CHAR(10) NOT NULL DEFAULT 'charval',
col_varchar VARCHAR(255) NULL UNIQUE DEFAULT 'varchar value',
col_text TEXT NULL DEFAULT 'some text',
-- Binary
col_bytea BYTEA NULL,
-- Enum (PostgreSQL requires type creation first)
col_enum TEXT NOT NULL DEFAULT 'A',
-- JSON
col_json JSON NULL,
col_jsonb JSONB NULL,
-- UUID
col_uuid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
-- Array
col_text_array TEXT[] NULL DEFAULT ARRAY['a','b','c'],
-- Range types
col_int_range INT4RANGE NULL,
col_ts_range TSRANGE NULL,
-- XML
col_xml XML NULL,
-- Some extra UNIQUE inline
col_unique_text TEXT UNIQUE
);
*/