mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Add Oracle & MSSQL Support
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataTypes, ForeignKeyActions, Modifiers, MYSQL_MAX_VAR_LENGTH, TimeDefaultValues } from "@/lib/field";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { DBDiffOperation, mapDiffToDBDiffOperation, normalizeDatabase } from "@/utils/database";
|
||||
import { optimizeOps, prepareForMigration } from "@/utils/migration";
|
||||
import { compare } from "fast-json-patch";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { Statement } from "pgsql-ast-parser";
|
||||
import { getPostgresEnumName } from "../render-uttils";
|
||||
import { emptyDb } from "@/lib/database";
|
||||
|
||||
export default abstract class BaseDatabaseRenderer {
|
||||
|
||||
protected dialect: DatabaseDialect;
|
||||
protected data_types: DataType[];
|
||||
protected schema?: DatabaseType | undefined;
|
||||
protected parser: any;
|
||||
protected readyPromise: Promise<void>;
|
||||
|
||||
public constructor(dialect: DatabaseDialect, data_types: DataType[]) {
|
||||
this.dialect = dialect;
|
||||
this.data_types = data_types;
|
||||
|
||||
this.readyPromise = import("node-sql-parser").then((sqlParser) => {
|
||||
this.parser = new sqlParser.Parser();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public getDialect(): DatabaseDialect {
|
||||
return this.dialect;
|
||||
}
|
||||
|
||||
public async renderDDL(database: DatabaseType): Promise<string> {
|
||||
|
||||
const previousDatabase = emptyDb(database);
|
||||
|
||||
// we prepare both databases by enriching them and removing some UI elements that can interfear with json fast path
|
||||
const preparedCurrentDatabase = prepareForMigration(database);
|
||||
const preparedPreviousDatabse = prepareForMigration(previousDatabase);
|
||||
|
||||
// notmalize both just ignore array elements order diff
|
||||
const normalizedPreviousDatabase = normalizeDatabase(preparedPreviousDatabse);
|
||||
const normalizedCurrentDatabase = normalizeDatabase(preparedCurrentDatabase);
|
||||
|
||||
// perform a comparison to extract the diff between the previous database and the current one .
|
||||
const diff_json = compare(normalizedPreviousDatabase, normalizedCurrentDatabase);
|
||||
// map the diff to DB Diff operations to facilate the rendering
|
||||
// optimize the operations by enriching each operation
|
||||
let operations: DBDiffOperation[] = mapDiffToDBDiffOperation(diff_json)
|
||||
operations = optimizeOps(operations, preparedCurrentDatabase, preparedPreviousDatabse, this.data_types);
|
||||
|
||||
// extract the ast based on the dialect
|
||||
const ast: ASTStatment[] = this.operationsToAst(operations, database);
|
||||
// render ast from sql
|
||||
const sql = await this.astToSQL(ast);
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||
const ast: ASTStatment[] = [];
|
||||
// we basiclly loop over all operations , and turn them into ast .
|
||||
// some operation can return multiple statments .
|
||||
// so for that we need to check our input if its one AST or an object AST
|
||||
for (const operation of operations) {
|
||||
const statmentAst = this.operationToAst(operation);
|
||||
if (!statmentAst)
|
||||
continue;
|
||||
else if (Array.isArray(statmentAst))
|
||||
ast.push(...statmentAst);
|
||||
else
|
||||
ast.push(statmentAst);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected operationToAst(operation: DBDiffOperation): ASTStatment | ASTStatment[] | null {
|
||||
switch (operation.type) {
|
||||
case "CREATE_TABLE":
|
||||
return this.createTableAst(
|
||||
operation.table
|
||||
);
|
||||
|
||||
case "CREATE_RELATIONSHIP":
|
||||
if (operation.relationship.cardinality == Cardinality.many_to_many)
|
||||
break;
|
||||
return this.createRelationshipAst(
|
||||
operation.relationship,
|
||||
);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract astToSQL(ast: ASTStatment[]): Promise<string>;
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
let indices_ast: ASTStatment[] = [];
|
||||
// get primary keys and check if we hame more than one
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
||||
// check if we have multiple primary keys if so we need to create a contraint
|
||||
const constraints: ASTStatment[] = multiPrimaryKeys ? [this.getPrimaryKeyContraint(primaryKeys)] : [];
|
||||
// get field dification
|
||||
const field_definitions = table.fields.map((field: FieldType) => this.getFieldDefinition(field, table, multiPrimaryKeys))
|
||||
// the base table ast contain only the field dification and the contraints , then child renderer turn it to ast
|
||||
// check if the table have indices
|
||||
if (table.indices && table.indices.length > 0) {
|
||||
// if so loop over it indices and ignore the index with no columns since that will cause an SQl Syntax error .
|
||||
// finally get the ast of the index and push it to the indices_ast
|
||||
for (const index of table.indices) {
|
||||
if (index.fieldIndices.length == 0)
|
||||
continue;
|
||||
indices_ast.push(this.createIndexAst(
|
||||
table,
|
||||
index
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
field_definitions,
|
||||
constraints,
|
||||
indices_ast
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
protected updateFieldAst(table: TableType, field: FieldType, changes: FieldType): ASTStatment | ASTStatment[] {
|
||||
// get the previous type
|
||||
let type = field.type;
|
||||
|
||||
const ast: any = {
|
||||
length: null,
|
||||
scale: null,
|
||||
dataType: null,
|
||||
auto_increment: null,
|
||||
}
|
||||
// if there is changes in the type , max length , scale or precision or auto incrrement .
|
||||
// in this case SQL Database treat this as a type changes
|
||||
if (changes.typeId || changes.maxLength || changes.scale || changes.precision || changes.autoIncrement !== undefined || changes.values) {
|
||||
// if the type changes , get the new type
|
||||
if (changes.typeId)
|
||||
type = changes.type;
|
||||
|
||||
// get the new or the old type name
|
||||
ast.dataType = (this.dialect == DatabaseDialect.POSTGRES && type.name == "enum") ? getPostgresEnumName(table, field) : type.name?.toLocaleUpperCase();
|
||||
// get the new or old type modifiers
|
||||
const modifiers: string[] = type.modifiers ? JSON.parse(type.modifiers) : [];
|
||||
// if the type support length as VARCHAR(length) and we either have an old max length , or new max length , then set it in the ast
|
||||
if (modifiers.includes(Modifiers.LENGTH) && (changes.maxLength || field.maxLength)) {
|
||||
if (changes.maxLength)
|
||||
ast.length = changes.maxLength
|
||||
else
|
||||
ast.length = field.maxLength;
|
||||
}
|
||||
// if the field support precision and we either have a previous precision or the user add new one .
|
||||
// then set the precision in the AST .
|
||||
if (modifiers.includes(Modifiers.PRECISION) && (changes.precision || field.precision)) {
|
||||
if (changes.precision) {
|
||||
ast.length = changes.precision;
|
||||
} else {
|
||||
ast.length = field.precision;
|
||||
}
|
||||
if (modifiers.includes(Modifiers.SCALE) && !(changes.scale || field.scale)) {
|
||||
// if the scale is not set , we have to set it as O
|
||||
ast.scale = "0";
|
||||
}
|
||||
}
|
||||
// if the type support scale , and scale is set then add it to the ast
|
||||
if (modifiers.includes(Modifiers.SCALE) && (changes.scale || field.scale)) {
|
||||
if (changes.scale) {
|
||||
ast.scale = changes.scale;
|
||||
} else {
|
||||
ast.scale = field.scale;
|
||||
}
|
||||
}
|
||||
// okay if the field is auto increment in postgres we need to switch the type ro serial type .
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && (field.autoIncrement || changes.autoIncrement) && this.dialect == DatabaseDialect.POSTGRES) {
|
||||
if (changes.autoIncrement !== false) {
|
||||
if (ast.dataType == "INTEGER")
|
||||
ast.dataType = "SERIAL";
|
||||
else
|
||||
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||
}
|
||||
}
|
||||
// if there changes in the values
|
||||
if (modifiers.includes(Modifiers.VALUES) && changes.values && this.dialect != DatabaseDialect.POSTGRES) {
|
||||
try {
|
||||
ast.values = JSON.parse(changes.values);
|
||||
} catch (error) {
|
||||
ast.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast as AST;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
|
||||
let primaryKey: FieldType = relationship.sourceField;
|
||||
let foreignKey: FieldType = relationship.targetField;
|
||||
|
||||
let sourceTable: TableType = relationship.sourceTable;
|
||||
let targetTable: TableType = relationship.targetTable;
|
||||
|
||||
if (relationship.cardinality == Cardinality.many_to_one) {
|
||||
primaryKey = relationship.targetField;
|
||||
foreignKey = relationship.sourceField;
|
||||
sourceTable = relationship.targetTable;
|
||||
targetTable = relationship.sourceTable;
|
||||
}
|
||||
return {
|
||||
primaryKey,
|
||||
foreignKey,
|
||||
sourceTable,
|
||||
targetTable
|
||||
} as any
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract createIndexAst(table: TableType, index: IndexType): ASTStatment;
|
||||
|
||||
protected abstract getPrimaryKeyContraint(field: FieldType[], table?: TableType): ASTStatment;
|
||||
|
||||
protected abstract startTransaction(): string;
|
||||
|
||||
protected abstract commit(): string;
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): ASTStatment {
|
||||
// get the modifiers based on the field data type
|
||||
const modifiers: string[] = field?.type?.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||
|
||||
const ast: any = {
|
||||
length: null,
|
||||
scale: null,
|
||||
default_value: null,
|
||||
dataType: null,
|
||||
values: null,
|
||||
default_value_type: null,
|
||||
auto_increment: null,
|
||||
primary_key: ignorePkContraint ? false : field.isPrimary,
|
||||
modifiers: null
|
||||
}
|
||||
|
||||
|
||||
|
||||
// data types by the defaut the field data type name , except for Postgres types needs to be handled diffrenet
|
||||
ast.dataType = field.type?.name?.toLocaleUpperCase();
|
||||
|
||||
// if the field support PRECISION & SCALE modifiers set the length to precision
|
||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
||||
ast.length = field.precision;
|
||||
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
||||
ast.scale = "0";
|
||||
}
|
||||
// if the field support sclae and sclae is set then set the scale
|
||||
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
||||
ast.scale = field.scale;
|
||||
|
||||
// the field support dynamic length , set the length ast
|
||||
// but in case the max length is not set, and the dielect (Mysql , MariaDb ) require a length , then set the default length
|
||||
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
||||
ast.length = field.maxLength;
|
||||
|
||||
else if (
|
||||
modifiers.includes(Modifiers.LENGTH) &&
|
||||
(field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) &&
|
||||
field.type.name?.startsWith('var') &&
|
||||
!field.maxLength
|
||||
)
|
||||
ast.length = MYSQL_MAX_VAR_LENGTH;
|
||||
|
||||
// if the field is an enum type just parse the enum values
|
||||
if (modifiers.includes(Modifiers.VALUES) && field.values) {
|
||||
try {
|
||||
ast.values = JSON.parse(field.values);
|
||||
} catch (error) {
|
||||
ast.value = [];
|
||||
}
|
||||
}
|
||||
const default_value_defintition = this.processDefaultValue(field) as any
|
||||
|
||||
if (default_value_defintition) {
|
||||
const { default_value_type, default_value } = default_value_defintition;
|
||||
ast.default_value_type = default_value_type;
|
||||
ast.default_value = default_value;
|
||||
|
||||
}
|
||||
|
||||
// if the field support auto increment and if the field it is , then set aut increment ast to true .
|
||||
// but if the dialect is Postgres , then we have to change the type from INTEGERS to SERIALS
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) {
|
||||
ast.auto_increment = true
|
||||
} else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) {
|
||||
ast.auto_increment = true;
|
||||
if (ast.dataType == "INTEGER")
|
||||
ast.dataType = "SERIAL";
|
||||
else
|
||||
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||
} else {
|
||||
ast.auto_increment = false;
|
||||
}
|
||||
|
||||
ast.modifiers = modifiers;
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
// if the field have a default value then we need to proccess it based on it type
|
||||
if (field.defaultValue && field.defaultValue.trim().length > 0) {
|
||||
|
||||
const ast: any = {
|
||||
default_value_type: null,
|
||||
default_value: null
|
||||
}
|
||||
|
||||
if ((field.type.type == DataTypes.INTEGER || field.type.type == DataTypes.NUMERIC) && !isNaN(Number(field.defaultValue))) {
|
||||
ast.default_value_type = "number"
|
||||
if (field.type.type == DataTypes.NUMERIC)
|
||||
ast.default_value = parseFloat(field.defaultValue);
|
||||
else
|
||||
ast.default_value = parseInt(field.defaultValue);
|
||||
}
|
||||
else if (field.type.type == DataTypes.TEXT || field.type.type == DataTypes.ENUM) {
|
||||
if (field.type.type == DataTypes.ENUM) {
|
||||
const values = field.values ? JSON.parse(field.values) : [];
|
||||
|
||||
if (field.type.name == "set") {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
|
||||
}
|
||||
else if (values.includes(field.defaultValue)) {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
// if we have a field of type time and the default value is Now , then the default value is the fucntion CURRENT_TIME or NOW()
|
||||
else if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW) {
|
||||
ast.default_value_type = "function";
|
||||
ast.default_value = "CURRENT_TIMESTAMP";
|
||||
}
|
||||
|
||||
else if ((field.type.name == "uuid" || field.type.name == "uniqueidentifier") && field.defaultValue == "random") {
|
||||
ast.default_value_type = "function";
|
||||
ast.default_value = "random";
|
||||
|
||||
}
|
||||
else if (field.type.type == DataTypes.TIME) {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
// the field have a boolean value
|
||||
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||
ast.default_value_type = "bool"
|
||||
ast.default_value = field.defaultValue == "true" ? true : false;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DBRenderOutput {
|
||||
schema: DatabaseType,
|
||||
diff_json: any[],
|
||||
sql: string;
|
||||
operations: DBDiffOperation[];
|
||||
}
|
||||
|
||||
|
||||
export type ASTStatment = AST | Statement | string | null
|
||||
@@ -0,0 +1,306 @@
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { format } from "sql-formatter";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
|
||||
|
||||
|
||||
export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
// in the base SQL Render we use node-sql-parser .
|
||||
// by simly passing the AST and formating the code .
|
||||
// other renderers can override this method to handle the parsing based on the dielact
|
||||
await this.readyPromise ;
|
||||
|
||||
let sql: string = this.parser.sqlify(ast as AST[], {
|
||||
database: getDatabaseByDialect(this.dialect).name
|
||||
});
|
||||
return format(sql, { language: 'sql' });
|
||||
}
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
// get the tale defintion from parent render that contain only the field defintion and primary keys contraints
|
||||
// this definition is commun amount (Mysql , MariaDB , Sqlite) , for postgres we need to override it and handle enums
|
||||
const { field_definitions, constraints, indices_ast } = super.createTableAst(table) as any
|
||||
// wrape those defintion with create table AST
|
||||
return [{
|
||||
keyword: "table",
|
||||
type: "create",
|
||||
table: [{
|
||||
table: table.name
|
||||
}],
|
||||
create_definitions: [...field_definitions, ...constraints]
|
||||
}, ...indices_ast] as any
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||
// also we need default value expression that it is compatible with node-sql-parser
|
||||
let valuesExpr: any | null;
|
||||
// if the definition extracted values os this field is an enum .
|
||||
// and for that we generate the values expression that's compatible with out parser
|
||||
if (definition.values && definition.values.length > 0) {
|
||||
const values = definition.values.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}));
|
||||
|
||||
valuesExpr = {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: values
|
||||
}
|
||||
}
|
||||
|
||||
const default_val = !definition.modifiers.includes(Modifiers.NO_DEFAULT) ? this.processDefaultValue(field) : undefined;
|
||||
const unique: string | null = !definition.modifiers.includes(Modifiers.NO_UNIQUE) ? (field.unique ? "unique" : null) : null;
|
||||
// return an AST statment for a field definition
|
||||
return {
|
||||
column: {
|
||||
type: "column_ref",
|
||||
column: {
|
||||
expr: {
|
||||
type: "default", value: field.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
default_val,
|
||||
unique,
|
||||
auto_increment: definition.auto_increment ? "auto_increment" : undefined,
|
||||
nullable: {
|
||||
type: field.nullable ? "null" : "not null",
|
||||
value: field.nullable ? "null" : "not null",
|
||||
},
|
||||
definition: {
|
||||
dataType: definition.dataType,
|
||||
length: definition.length,
|
||||
scale: definition.scale,
|
||||
expr: valuesExpr
|
||||
},
|
||||
primary_key: definition.primary_key ? "primary key" : null,
|
||||
resource: "column"
|
||||
} as any
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
// get the foriegn key , primary key source table and target table from the parent renderer
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
let on_action: any[] = [];
|
||||
// get the foriegn key actions for on delete and on cascade
|
||||
if (relationship.onDelete) {
|
||||
const value: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on delete",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (relationship.onUpdate) {
|
||||
const value: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on update",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
// return create foriegn key contraint for Mysql Dialect
|
||||
return {
|
||||
type: "alter",
|
||||
keyword: "table",
|
||||
table: [
|
||||
{
|
||||
|
||||
table: targetTable.name
|
||||
}
|
||||
],
|
||||
expr: [{
|
||||
action: "add",
|
||||
create_definitions: {
|
||||
constraint: relationship.name,
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: foreignKey.name
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
constraint_type: "FOREIGN KEY",
|
||||
keyword: "constraint",
|
||||
resource: "constraint",
|
||||
reference_definition: {
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: primaryKey.name
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
table: [
|
||||
{
|
||||
db: null,
|
||||
table: sourceTable.name
|
||||
}
|
||||
],
|
||||
keyword: "references",
|
||||
on_action
|
||||
}
|
||||
},
|
||||
|
||||
resource: "constraint",
|
||||
type: "alter"
|
||||
}]
|
||||
} as any
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "cascade";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "set null";
|
||||
|
||||
case ForeignKeyActions.RESTRICT:
|
||||
return "restrict";
|
||||
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "set default";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(primaryKeys: FieldType[]): AST {
|
||||
// primary key contraint
|
||||
return {
|
||||
constraint_type: "primary key",
|
||||
resource: "constraint",
|
||||
definition: primaryKeys.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
} as any
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): AST {
|
||||
// create index AST for Mysql Dielect
|
||||
return {
|
||||
index: index.name,
|
||||
type: "create",
|
||||
table: { table: table.name },
|
||||
keyword: "index",
|
||||
on_kw: "on",
|
||||
index_type: index.unique ? "unique" : null,
|
||||
index_columns: index.fields.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
} as AST
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
const definition = super.processDefaultValue(field) as any;
|
||||
// if the field have a default value , then we generate an expression based on the type of the default value
|
||||
if (definition && definition.default_value !== null) {
|
||||
let default_val: any | null = null
|
||||
if (definition.default_value_type == "string")
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "single_quote_string",
|
||||
value: field.defaultValue
|
||||
}
|
||||
}
|
||||
else if (definition.default_value_type == "function" && field.defaultValue == TimeDefaultValues.NOW)
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "origin",
|
||||
value: "CURRENT_TIMESTAMP"
|
||||
}
|
||||
]
|
||||
},
|
||||
over: null
|
||||
}
|
||||
}
|
||||
else if (definition.default_value_type == "function" && field.defaultValue == "random") {
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "default",
|
||||
value: this.dialect == DatabaseDialect.POSTGRES ? "gen_random_uuid" : "UUID"
|
||||
}
|
||||
]
|
||||
},
|
||||
args: {
|
||||
type: "expr_list",
|
||||
value: []
|
||||
},
|
||||
parentheses: this.dialect == DatabaseDialect.MARIADB
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: definition.default_value_type,
|
||||
value: definition.default_value
|
||||
}
|
||||
}
|
||||
}
|
||||
return default_val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN TRANSACTION;" ;
|
||||
}
|
||||
|
||||
protected commit(): string {
|
||||
return "COMMIT;"
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import MysqlRenderer from "./mysql-renderer";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
export default class MariaDbRenderer extends MysqlRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super( data_types , DatabaseDialect.MARIADB)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { format } from 'sql-formatter';
|
||||
import { AST } from "node-sql-parser";
|
||||
|
||||
export default class MSSqlRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.MSSQL, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
let sql = ast.join("\n");
|
||||
sql = format(sql, { language: 'sql' });
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql}
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
const ast: any = super.createTableAst(table);
|
||||
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
|
||||
// get primary keys and check if we hame more than one
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
let constraints: ASTStatment = "";
|
||||
if (primaryKeys.length > 0) {
|
||||
constraints = this.getPrimaryKeyContraint(primaryKeys, table)
|
||||
}
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${constraints ? ',\n' + constraints : ""}
|
||||
);
|
||||
${indexes}
|
||||
` ;
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||
|
||||
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "IDENTITY(1,1)" : "") : ""
|
||||
|
||||
let nullable: string = ""
|
||||
let unique: string = "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||
|
||||
let options: number[] | string = [];
|
||||
|
||||
if (ast.length)
|
||||
options.push(ast.length);
|
||||
|
||||
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||
options.push(DEFAULT_LENGTH_PARAM);
|
||||
|
||||
if (ast.scale && ast.scale != "0")
|
||||
options.push(ast.scale);
|
||||
|
||||
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||
|
||||
let defaultValue: any = "";
|
||||
|
||||
if (ast.default_value !== undefined && ast.default_value !== null && !dropDefaultValue) {
|
||||
defaultValue = `CONSTRAINT DF_${table.name}_${field.name} DEFAULT ${ast.default_value}`;
|
||||
}
|
||||
if (!ast.primary_key) {
|
||||
unique = field.unique ? `CONSTRAINT UQ_${table.name}_${field.name} UNIQUE` : "";
|
||||
}
|
||||
|
||||
const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
const ast: any = super.processDefaultValue(field);
|
||||
|
||||
if (ast && ast.default_value_type) {
|
||||
if (ast.default_value_type == "single_quote_string") {
|
||||
ast.default_value = `'${ast.default_value}'`
|
||||
}
|
||||
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||
ast.default_value = ast.default_value;
|
||||
|
||||
else if (ast.default_value_type == "function") {
|
||||
|
||||
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||
|
||||
switch (field.type.name?.toUpperCase()) {
|
||||
case "DATE":
|
||||
ast.default_value = "CAST(GETDATE() AS DATE)"
|
||||
break;
|
||||
case "DATETIME":
|
||||
ast.default_value = "GETDATE()"
|
||||
break;
|
||||
case "SMALLDATETIME":
|
||||
ast.default_value = "GETDATE()"
|
||||
break;
|
||||
case "DATETIME2":
|
||||
ast.default_value = "SYSDATETIME()"
|
||||
break;
|
||||
case "DATETIMEOFFSET":
|
||||
ast.default_value = "SYSDATETIMEOFFSET()"
|
||||
break;
|
||||
}
|
||||
} else if (ast.default_value == "random") {
|
||||
if (field.isPrimary)
|
||||
ast.default_value = "NEWSEQUENTIALID()"
|
||||
else
|
||||
ast.default_value = "NEWID()"
|
||||
}
|
||||
|
||||
}
|
||||
if ( ast.default_value_type == "bool") {
|
||||
if (ast.default_value) {
|
||||
ast.default_value = 1 ;
|
||||
}else {
|
||||
ast.default_value = 0 ;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
|
||||
const onDeleteFKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onUpdateFKAction: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
|
||||
const onDeleteAction: string = onDeleteFKAction ? `ON DELETE ${onDeleteFKAction}` : ""
|
||||
const onUpdateAction: string = onUpdateFKAction ? `ON UPDATE ${onUpdateFKAction}` : ""
|
||||
|
||||
const FKActions: string = [onDeleteAction, onUpdateAction].join(" ");
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${FKActions};
|
||||
`
|
||||
}
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "CASCADE";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "SET NULL";
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "SET DEFAULT";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[], table?: TableType): ASTStatment {
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
return `CONSTRAINT PK_${table?.name} PRIMARY KEY (${pks.join(",")})`
|
||||
}
|
||||
|
||||
protected startTransaction(): string {
|
||||
return `BEGIN TRANSACTION;`
|
||||
}
|
||||
protected commit(): string {
|
||||
return "COMMIT;"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { fixCharsetPlacement } from "../render-uttils";
|
||||
import { format } from 'sql-formatter';
|
||||
import { Modifiers } from "@/lib/field";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
export default class MysqlRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[], dialect: DatabaseDialect = DatabaseDialect.MYSQL) {
|
||||
super(dialect, data_types)
|
||||
}
|
||||
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||
// for Mysql , MariaDB , we use node-sql-parser to turn AST to SQL ,
|
||||
// the package have a bug which is CHARSET and COLLATION missplacementts
|
||||
// so we patch it using fixCharsetPlacement function
|
||||
// and finally we format the code
|
||||
try {
|
||||
let sql: string = await super.astToSQL(ast);
|
||||
sql = fixCharsetPlacement(format(sql, { language: "sql" }));
|
||||
sql = format(sql, { language: 'sql' });
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql};
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// get the modifiers based on the field data type
|
||||
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||
// also we need default value expression that it is compatible with node-sql-parser
|
||||
let suffix: string[] = [];
|
||||
|
||||
// check if the field support ZEROFILL and if it set , and the same thing for unsigned
|
||||
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
|
||||
suffix.push(Modifiers.ZEROFILL.toUpperCase());
|
||||
}
|
||||
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
|
||||
suffix.push(Modifiers.UNSIGNED.toUpperCase());
|
||||
}
|
||||
// if it support charset and charset is set , then generate it expression
|
||||
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
|
||||
|
||||
definition.character_set = {
|
||||
type: "CHARACTER SET",
|
||||
value: {
|
||||
type: "default",
|
||||
value: field.charset
|
||||
}
|
||||
}
|
||||
// if it support collate and collate is set , then generate it expression
|
||||
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
|
||||
|
||||
definition.collate = {
|
||||
keyword: "collate",
|
||||
type: "collate",
|
||||
collate: {
|
||||
name: field.collate,
|
||||
}
|
||||
}
|
||||
|
||||
if (suffix.length > 0) {
|
||||
definition.definition.suffix = suffix
|
||||
}
|
||||
return definition;
|
||||
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { AST } from "node-sql-parser";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { format } from 'sql-formatter';
|
||||
|
||||
export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.ORACLE, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
let sql = ast.join("\n");
|
||||
sql = format(sql, { language: 'sql' });
|
||||
return sql;
|
||||
}
|
||||
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
const ast: any = super.createTableAst(table);
|
||||
let pk_constraint: string = ast.constraints?.pop();
|
||||
if (pk_constraint)
|
||||
pk_constraint = "," + pk_constraint
|
||||
else
|
||||
pk_constraint = "";
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${pk_constraint}
|
||||
);
|
||||
${indexes}
|
||||
` ;
|
||||
}
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||
|
||||
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
let primaryKey: string = ast.primary_key ? "PRIMARY KEY" : "";
|
||||
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "GENERATED BY DEFAULT AS IDENTITY" : "") : ""
|
||||
|
||||
let nullable: string = ""
|
||||
let unique: string = "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
unique = field.unique ? "UNIQUE" : "";
|
||||
|
||||
let options: number[] | string = [];
|
||||
|
||||
if (ast.length)
|
||||
options.push(ast.length);
|
||||
|
||||
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||
options.push(DEFAULT_LENGTH_PARAM);
|
||||
|
||||
if (ast.scale && ast.scale != "0")
|
||||
options.push(ast.scale);
|
||||
|
||||
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||
|
||||
let defaultValue: any = "";
|
||||
|
||||
if (ast.default_value !== undefined && ast.default_value !== null) {
|
||||
defaultValue = "DEFAULT " + ast.default_value
|
||||
}
|
||||
if (dropDefaultValue) {
|
||||
defaultValue = "DEFAULT NULL"
|
||||
}
|
||||
|
||||
let dataType = ast.dataType;
|
||||
if (dataType === "TIMESTAMP WITH LOCAL TIME ZONE" && options) {
|
||||
dataType = `TIMESTAMP${options} WITH LOCAL TIME ZONE`
|
||||
}
|
||||
else if (dataType === "TIMESTAMP WITH TIME ZONE" && options) {
|
||||
dataType = `TIMESTAMP${options} WITH TIME ZONE`
|
||||
}
|
||||
else if (dataType === "INTERVAL DAY TO SECOND" && options) {
|
||||
dataType = `INTERVAL DAY${options} TO SECOND`
|
||||
}
|
||||
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
|
||||
dataType = `INTERVAL YEAR${options} TO MONTH`
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
dataType = dataType + options
|
||||
|
||||
|
||||
//const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
|
||||
}
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
|
||||
const ast: any = super.processDefaultValue(field);
|
||||
|
||||
if (ast && ast.default_value !== undefined && ast.default_value !== null) {
|
||||
|
||||
|
||||
if (ast.default_value_type == "single_quote_string") {
|
||||
ast.default_value = `'${ast.default_value}'`
|
||||
}
|
||||
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||
ast.default_value = ast.default_value;
|
||||
|
||||
else if (ast.default_value_type == "function") {
|
||||
|
||||
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||
|
||||
switch (field.type.name?.toUpperCase()) {
|
||||
case "DATE":
|
||||
ast.default_value = "SYSDATE"
|
||||
break;
|
||||
case "TIMESTAMP":
|
||||
ast.default_value = "CURRENT_TIMESTAMP"
|
||||
break;
|
||||
case "TIMESTAMP WITH TIME ZONE":
|
||||
ast.default_value = "SYSTIMESTAMP"
|
||||
break;
|
||||
case "TIMESTAMP WITH LOCAL TIME ZONE":
|
||||
ast.default_value = "SYSTIMESTAMP"
|
||||
break;
|
||||
}
|
||||
} else if (ast.default_value == "random") {
|
||||
ast.default_value = "SYS_GUID()"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
|
||||
const FKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onDeleteAction: string = FKAction ? `ON DELETE ${FKAction}` : ""
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${onDeleteAction};
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "CASCADE";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "SET NULL";
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
|
||||
//throw new Error("Method not implemented.");
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
return `
|
||||
PRIMARY KEY (${pks.join(",")})
|
||||
`
|
||||
}
|
||||
protected startTransaction(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected commit(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { getPostgresEnumName } from "../render-uttils";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Statement, toSql } from "pgsql-ast-parser";
|
||||
import { ASTStatment } from "./base-database-renderer";
|
||||
import { format } from "sql-formatter";
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { DataTypes } from "@/lib/field";
|
||||
|
||||
|
||||
export default class PostgresqlRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.POSTGRES, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
// postgres renderer is different , we use a different parser pgsql-ast-parser for index , enum renaming .
|
||||
// those kind of operations return Statment not AST .
|
||||
// so the we pass AST to the default parser and Statment to Postgres parser
|
||||
|
||||
await this.readyPromise ;
|
||||
let sql: string[] = [];
|
||||
|
||||
for (const statment of ast) {
|
||||
if ((statment as Statement)?.type == "alter index" || (statment as Statement)?.type == "alter enum") {
|
||||
// index renaming
|
||||
sql.push(toSql.statement(statment as Statement) + ";");
|
||||
}
|
||||
else {
|
||||
const statment_sql = this.parser.sqlify(statment as AST, {
|
||||
database: getDatabaseByDialect(this.dialect).name
|
||||
});
|
||||
sql.push(statment_sql + ";");
|
||||
}
|
||||
}
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = [this.startTransaction( ) , ...sql , this.commit()]
|
||||
}
|
||||
return format(sql.join(""), { language: 'postgresql' });
|
||||
}
|
||||
|
||||
|
||||
protected createTableAst(table: TableType): AST[] | AST {
|
||||
// the only difference in create table statment in postgres is we need to get declare all enums before creating the table
|
||||
const definition: AST[] = super.createTableAst(table) as any
|
||||
// so we need an enum ast
|
||||
const enumsAst: AST[] = [];
|
||||
// get fields to type enum
|
||||
let postgresEnums: FieldType[] = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||
if (postgresEnums.length > 0) {
|
||||
// loop over them and intiat them one by one
|
||||
for (const postgresEnum of postgresEnums) {
|
||||
(postgresEnum as any).table = table;
|
||||
enumsAst.push(this.createEnumAst(postgresEnum));
|
||||
}
|
||||
}
|
||||
definition.splice(0, 0, ...enumsAst);
|
||||
return definition;
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
// since postrges uses the serial data types , then there is not need for auto incrmenet attribute
|
||||
definition.auto_increment = undefined;
|
||||
// postgres handle enums diffrently , we need to create the enum first then reference to it in the column line
|
||||
if (definition.definition.dataType == "ENUM") {
|
||||
definition.definition.dataType = getPostgresEnumName(table, field);
|
||||
definition.definition.expr = undefined;
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private createEnumAst(field: FieldType): AST {
|
||||
// get the enum values , and cast it into an array
|
||||
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
||||
// return the ast of creating a table
|
||||
return {
|
||||
as: "as",
|
||||
type: "create",
|
||||
resource: "enum",
|
||||
name: {
|
||||
schema: null,
|
||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||
},
|
||||
keyword: "type",
|
||||
create_definitions: {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: jsonValues.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}))
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, newDataType: DataType): AST | null {
|
||||
|
||||
const oldType: DataTypes = field.type.type as DataTypes;
|
||||
const newType: DataTypes = newDataType.type as DataTypes;
|
||||
|
||||
if (oldType === newType && oldType != DataTypes.ENUM) return null;
|
||||
|
||||
if (
|
||||
(oldType === DataTypes.INTEGER && newType === DataTypes.NUMERIC) ||
|
||||
(oldType === DataTypes.NUMERIC && newType === DataTypes.INTEGER)
|
||||
) return null;
|
||||
|
||||
|
||||
let dataType = newDataType.name?.toUpperCase();
|
||||
|
||||
if (newDataType.name == "enum")
|
||||
dataType = "TEXT::" + getPostgresEnumName(table, field);
|
||||
else if (newDataType.name != "text")
|
||||
dataType = "TEXT::" + newDataType.name?.toUpperCase();
|
||||
|
||||
return {
|
||||
as: null,
|
||||
symbol: "::",
|
||||
target: [
|
||||
{
|
||||
dataType
|
||||
}
|
||||
],
|
||||
type: "cast",
|
||||
keyword: "cast",
|
||||
expr: {
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: field.name
|
||||
}
|
||||
},
|
||||
collate: null
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN;" ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { fixSQLiteColumnOrder } from "../render-uttils";
|
||||
import { format } from 'sql-formatter';
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { ASTStatment } from "./base-database-renderer";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { orderTables } from "@/utils/tables";
|
||||
|
||||
export default class SqliteRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.SQLITE, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||
try {
|
||||
let sql: string = await super.astToSQL(ast);
|
||||
|
||||
sql = fixSQLiteColumnOrder(format(sql, { language: "sql" }));
|
||||
sql = format(sql, { language: 'sql' });
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql};
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||
|
||||
const ast: ASTStatment[] = [];
|
||||
|
||||
let sortedTables: TableType[] | RenderableTable = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_TABLE").map((operation: DBDiffOperation) => (operation as any).table)
|
||||
|
||||
if (database) {
|
||||
let renderableTables: RenderableTable[] = sortedTables.map((table: TableType) => toRenderableTable(table, database));
|
||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||
|
||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||
|
||||
sortedTables = sortedTablesIds.map((id: string) =>
|
||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||
);
|
||||
}
|
||||
|
||||
const relationshipOperations: DBDiffOperation[] = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_RELATIONSHIP");
|
||||
|
||||
// we basiclly loop over all operations , and turn them into ast .
|
||||
// some operation can return multiple statments .
|
||||
// so for that we need to check our input if its one AST or an object AST
|
||||
for (const table of sortedTables) {
|
||||
const statmentAst = this.createTableAst(table);
|
||||
|
||||
if (!statmentAst)
|
||||
continue;
|
||||
|
||||
|
||||
if ((table as RenderableTable).foreignRelationships?.length > 0) {
|
||||
const relationshipsAst: ASTStatment[] = (table as RenderableTable).foreignRelationships.map((relationship: RelationshipType) => {
|
||||
const operation: DBDiffOperation = relationshipOperations.find((operation: DBDiffOperation) => (operation as any).relationship?.id == relationship.id) as DBDiffOperation;
|
||||
return this.createRelationshipAst((operation as any).relationship)
|
||||
})
|
||||
|
||||
if (Array.isArray(statmentAst)) {
|
||||
|
||||
const index = statmentAst.findIndex((statment: any) => statment.type == "create" && statment.keyword == "table");
|
||||
|
||||
if (index >= 0) {
|
||||
(statmentAst[index] as any).create_definitions.push(...relationshipsAst)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(statmentAst))
|
||||
ast.push(...statmentAst);
|
||||
else
|
||||
ast.push(statmentAst);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const relationshipAst: any = super.createRelationshipAst(relationship) as any;
|
||||
return relationshipAst.expr[0].create_definitions;
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected dropPrimaryKeyConstraintExpr(table: TableType): ASTStatment {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected createPrimaryKeyConstraintExpr(table: TableType, fields: FieldType[]): ASTStatment {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
|
||||
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataTypes, ForeignKeyActions, Modifiers, MYSQL_MAX_VAR_LENGTH, TimeDefaultValues } from "@/lib/field";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
||||
import { getForeignRelationships } from "@/utils/relationship";
|
||||
import { orderTables } from "@/utils/tables";
|
||||
import _ from "lodash" ;
|
||||
|
||||
export const DatabaseToAst = (db: DatabaseType, data_types: DataType[]) => {
|
||||
let dbAst: any = [];
|
||||
if (!data_types || data_types.length == 0)
|
||||
return dbAst;
|
||||
const database = _.cloneDeep(db) ;
|
||||
let renderableTables: RenderableTable[] = database.tables.map((table: TableType) => toRenderableTable(table, database));
|
||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||
|
||||
try {
|
||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||
|
||||
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
|
||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||
);
|
||||
for (const table of sortedTables) {
|
||||
let postgresEnums: FieldType[];
|
||||
if (database.dialect == DatabaseDialect.POSTGRES) {
|
||||
postgresEnums = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||
if (postgresEnums.length > 0) {
|
||||
for (const postgresEnum of postgresEnums) {
|
||||
(postgresEnum as any).table = table;
|
||||
dbAst.push(PotgresEnumToAst(postgresEnum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbAst.push(TableToAst(table, data_types, database.dialect == DatabaseDialect.POSTGRES));
|
||||
|
||||
if (table.indices && table.indices.length > 0)
|
||||
for (const index of table.indices)
|
||||
if (index.fieldIndices?.length > 0)
|
||||
dbAst.push(IndexToAst({
|
||||
...index,
|
||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
|
||||
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
|
||||
) as FieldType[],
|
||||
}, table));
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return dbAst;
|
||||
}
|
||||
|
||||
|
||||
export const TableToAst = (table: TableType, data_types: DataType[], isPostgresDialect: boolean = false) => {
|
||||
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
||||
|
||||
const primaryKeysConstraints: any[] | undefined = multiPrimaryKeys ? [{
|
||||
constraint_type: "primary key",
|
||||
resource: "constraint",
|
||||
definition: primaryKeys.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
}] : [];
|
||||
|
||||
let foreignRelationships = getForeignRelationships(table);
|
||||
const foreignKeysConstraints = foreignRelationships.map((relationship: RelationshipType) => relationshipToAst(relationship));
|
||||
const constraints: any[] = [
|
||||
...primaryKeysConstraints,
|
||||
...foreignKeysConstraints
|
||||
];
|
||||
|
||||
const filed_definitions = table.fields.filter((field: FieldType) => field.type).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: `${table.name}_${field.name.toLowerCase()}_enum` } as DataType,
|
||||
}, multiPrimaryKeys, !isPostgresEnum)
|
||||
})
|
||||
|
||||
return {
|
||||
keyword: "table",
|
||||
type: "create",
|
||||
table: [{
|
||||
table: table.name
|
||||
}],
|
||||
create_definitions: [...filed_definitions, ...constraints]
|
||||
}
|
||||
}
|
||||
export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, upperCaseType: boolean = true) => {
|
||||
|
||||
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||
let suffix: string[] = [];
|
||||
|
||||
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
|
||||
suffix.push(Modifiers.ZEROFILL.toUpperCase());
|
||||
}
|
||||
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
|
||||
suffix.push(Modifiers.UNSIGNED.toUpperCase());
|
||||
}
|
||||
|
||||
|
||||
let length: number | null = null;
|
||||
let scale: number | string | null = null;
|
||||
|
||||
let character_set: any | null = null;
|
||||
let collate: any | null = null;
|
||||
|
||||
let values: any[] = [];
|
||||
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;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
||||
length = field.precision;
|
||||
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
||||
scale = "0";
|
||||
}
|
||||
|
||||
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
||||
scale = field.scale;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
||||
length = field.maxLength;
|
||||
|
||||
else if (modifiers.includes(Modifiers.LENGTH) && (field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) && field.type.name?.startsWith('var') && !field.maxLength)
|
||||
length = MYSQL_MAX_VAR_LENGTH;
|
||||
|
||||
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
|
||||
character_set = {
|
||||
type: "CHARACTER SET",
|
||||
value: {
|
||||
type: "default",
|
||||
value: field.charset
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
|
||||
collate = {
|
||||
keyword: "collate",
|
||||
type: "collate",
|
||||
collate: {
|
||||
name: field.collate,
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers.includes(Modifiers.VALUES) && field.values) {
|
||||
|
||||
const jsonValues = JSON.parse(field.values);
|
||||
if (jsonValues.length > 0) {
|
||||
values = jsonValues.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}));
|
||||
|
||||
valuesExpr = {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.defaultValue && field.defaultValue.trim().length > 0) {
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "single_quote_string",
|
||||
value: field.defaultValue
|
||||
}
|
||||
}
|
||||
if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW)
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "origin",
|
||||
value: "CURRENT_TIMESTAMP"
|
||||
}
|
||||
]
|
||||
},
|
||||
over: null
|
||||
}
|
||||
}
|
||||
|
||||
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||
default_val.value.type = "bool"
|
||||
default_val.value.value = field.defaultValue == "true" ? true : false;
|
||||
}
|
||||
|
||||
// Check if it's a number (but not empty string or just whitespace)
|
||||
else if (!isNaN(Number(field.defaultValue))) {
|
||||
default_val.value.type = "number"
|
||||
default_val.value.value = Number(field.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
column: {
|
||||
expr: {
|
||||
type: "default", value: field.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
collate,
|
||||
character_set,
|
||||
default_val,
|
||||
unique: field.unique ? "unique" : null,
|
||||
auto_increment,
|
||||
nullable: {
|
||||
type: field.nullable ? "null" : "not null",
|
||||
value: field.nullable ? "null" : "not null",
|
||||
},
|
||||
definition: {
|
||||
dataType,
|
||||
length,
|
||||
scale,
|
||||
suffix,
|
||||
expr: valuesExpr
|
||||
},
|
||||
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
|
||||
resource: "column"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const IndexToAst = (index: IndexType, table: TableType) => {
|
||||
|
||||
return {
|
||||
index: index.name,
|
||||
type: "create",
|
||||
table: { table: table.name },
|
||||
keyword: "index",
|
||||
on_kw: "on",
|
||||
index_type: index.unique ? "unique" : null,
|
||||
index_columns: index.fields.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const PotgresEnumToAst = (field: FieldType) => {
|
||||
|
||||
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
||||
|
||||
return {
|
||||
as: "as",
|
||||
type: "create",
|
||||
resource: "enum",
|
||||
name: {
|
||||
schema: null,
|
||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||
},
|
||||
keyword: "type",
|
||||
create_definitions: {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: jsonValues.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const foreignKeyActionToAst = (action: ForeignKeyActions): string | null => {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "cascade";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "set null";
|
||||
|
||||
case ForeignKeyActions.RESTRICT:
|
||||
return "restrict";
|
||||
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "set default";
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const relationshipToAst = (relationship: RelationshipType) => {
|
||||
|
||||
let primaryKey: FieldType = relationship.sourceField;
|
||||
let foreignKey: FieldType = relationship.targetField;
|
||||
|
||||
let sourceTable: TableType = relationship.sourceTable;
|
||||
let targetTable: TableType = relationship.targetTable;
|
||||
|
||||
if (relationship.cardinality == Cardinality.many_to_one) {
|
||||
primaryKey = relationship.targetField;
|
||||
foreignKey = relationship.sourceField;
|
||||
sourceTable = relationship.targetTable;
|
||||
targetTable = relationship.sourceTable;
|
||||
}
|
||||
|
||||
let on_action: any[] = [];
|
||||
|
||||
if (relationship.onDelete) {
|
||||
const value: string | null = foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on delete",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (relationship.onUpdate) {
|
||||
const value: string | null = foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on update",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
constraint: null,
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
column: foreignKey.name,
|
||||
|
||||
}
|
||||
],
|
||||
constraint_type: "FOREIGN KEY",
|
||||
resource: "constraint",
|
||||
reference_definition: {
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
column: primaryKey.name,
|
||||
}
|
||||
],
|
||||
table: [
|
||||
{
|
||||
table: sourceTable.name,
|
||||
}
|
||||
],
|
||||
keyword: "references",
|
||||
on_action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
import { DataTypes, ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { Parser } from "node-sql-parser";
|
||||
import { v4 } from "uuid";
|
||||
import { parse } from 'pgsql-ast-parser';
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
|
||||
const parser = new Parser();
|
||||
let errors: Error[] = [];
|
||||
|
||||
const createTableStatements: string[] = [];
|
||||
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
|
||||
.replace(/--.*$/gm, '') // remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
||||
.replace(/\s+/g, ' ') // normalize whitespace
|
||||
.replace(/;\s*/g, ';\n'); // separate statements
|
||||
|
||||
// Split into individual statements
|
||||
const statements = cleanedSql
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const stmt of statements) {
|
||||
const upper = stmt.toUpperCase();
|
||||
|
||||
if (upper.startsWith('CREATE TABLE')) {
|
||||
createTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('ALTER TABLE')) {
|
||||
alterTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) {
|
||||
createIndexStatements.push(stmt);
|
||||
} else if (upper.startsWith('CREATE TYPE')) {
|
||||
createPostgresTypesStatements.push(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (dialect == DatabaseDialect.POSTGRES) {
|
||||
for (const postgresType of createPostgresTypesStatements) {
|
||||
try {
|
||||
const instructionAst = parse(postgresType);
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
postgresTypes.push(instructionAst[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const createTable of createTableStatements) {
|
||||
try {
|
||||
const instructionAst = parse(createTable);
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
const table: TableInsertType = postgresAstToTable(instructionAst[0], data_types, postgresTypes);
|
||||
tables.push(table);
|
||||
|
||||
for (const column of (instructionAst[0] as any).columns)
|
||||
if (column.constraints?.length > 0) {
|
||||
let referenceColumn: any | undefined = column.constraints.find((contraint: any) => contraint.type == "reference")
|
||||
|
||||
if (!referenceColumn)
|
||||
continue
|
||||
|
||||
referenceColumn = {
|
||||
...referenceColumn, localColumns: [
|
||||
{ name: column.name.name }
|
||||
]
|
||||
}
|
||||
|
||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst([referenceColumn], table);
|
||||
|
||||
try {
|
||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
||||
relationships = relationships.concat(newRelationships);
|
||||
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
|
||||
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 = postgresAstToRelationship(relationshipAst, tables);
|
||||
relationships = relationships.concat(newRelationships);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
|
||||
}
|
||||
}
|
||||
for (const createIndex of createIndexStatements) {
|
||||
try {
|
||||
const instructionAst = parse(createIndex);
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
indices.push(postgresAstToIndex(instructionAst[0], tables));
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
for (const alterTable of alterTableStatements) {
|
||||
try {
|
||||
const instructionAst = parse(alterTable);
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
const extractedRelationships: RelationshipInsertType[] = postgresAstToRelationship((instructionAst[0] as any), tables);
|
||||
relationships = relationships.concat(extractedRelationships);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let foreignKeyConstraints: any[] = [];
|
||||
let referenceDefinitions: any[] = [];
|
||||
|
||||
for (let createTable of createTableStatements) {
|
||||
|
||||
try {
|
||||
|
||||
if (dialect == DatabaseDialect.SQLITE)
|
||||
createTable = createTable.replace(/\btext\s*\(\s*\d+\s*\)/gi, 'TEXT');
|
||||
|
||||
|
||||
let instructionAst = parser.astify(createTable, {
|
||||
database: dialect == DatabaseDialect.MARIADB ? getDatabaseByDialect(DatabaseDialect.MYSQL).name : getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
if (instructionAst) {
|
||||
const table: TableInsertType = astToTable(instructionAst, data_types);
|
||||
tables.push(table);
|
||||
|
||||
const tableForeignKeyConstraints = (instructionAst as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
|
||||
const tableReferenceDefinitions = (instructionAst as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
|
||||
|
||||
foreignKeyConstraints = foreignKeyConstraints.concat(
|
||||
tableForeignKeyConstraints.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
referenceDefinitions = referenceDefinitions.concat(
|
||||
tableReferenceDefinitions.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const referenceDefinition of referenceDefinitions) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const alterTable of alterTableStatements) {
|
||||
try {
|
||||
let instructionAst: any = parser.astify(alterTable, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (instructionAst) {
|
||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
|
||||
...instructionAst[0],
|
||||
table: instructionAst[0].table?.[0].table
|
||||
}) as RelationshipInsertType[];
|
||||
|
||||
relationships = relationships.concat(extractedRelationships)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (const createIndex of createIndexStatements) {
|
||||
try {
|
||||
let instructionAst = parser.astify(createIndex, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
indices.push(astToIndex(instructionAst, tables));
|
||||
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tables.length == 0)
|
||||
throw Error("Error while Parsing")
|
||||
|
||||
|
||||
return { tables, relationships, indices, errors };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const astToForiegnKeyAction = (ast: string): ForeignKeyActions => {
|
||||
switch (ast) {
|
||||
case "cascade":
|
||||
return ForeignKeyActions.CASCADE;
|
||||
case "set null":
|
||||
return ForeignKeyActions.SET_NULL;
|
||||
case 'restrict':
|
||||
return ForeignKeyActions.RESTRICT;
|
||||
case "set default":
|
||||
return ForeignKeyActions.SET_DEFAULT;
|
||||
}
|
||||
return ForeignKeyActions.NO_ACTION
|
||||
}
|
||||
|
||||
export const astToRelationship = (tables: TableInsertType[], constraintAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
|
||||
|
||||
let targetField: FieldInsertType | undefined;
|
||||
let sourceTable: TableInsertType | undefined;
|
||||
let sourceField: FieldInsertType | undefined;
|
||||
let targetTable: TableInsertType | undefined;
|
||||
|
||||
let relationships: RelationshipInsertType[] = [];
|
||||
|
||||
|
||||
let onDelete: ForeignKeyActions | undefined;
|
||||
let onUpdate: ForeignKeyActions | undefined;
|
||||
let on_action: any | undefined;
|
||||
|
||||
|
||||
|
||||
if (constraintAst) {
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == constraintAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.definition?.[0].column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == constraintAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.reference_definition?.definition?.[0].column);
|
||||
on_action = constraintAst.reference_definition.on_action;
|
||||
|
||||
}
|
||||
|
||||
if (columnAst) {
|
||||
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == columnAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.column?.column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == columnAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.reference_definition?.definition?.[0].column);
|
||||
on_action = columnAst.reference_definition.on_action;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (on_action) {
|
||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
||||
|
||||
if (onDeleteAst)
|
||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
||||
if (onUpdateAst)
|
||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
||||
}
|
||||
|
||||
if (alterTableAst) {
|
||||
|
||||
const expressions = alterTableAst.expr;
|
||||
const foreignKeyExpressions = expressions.filter((expression: any) => expression.resource == "constraint" && expression.create_definitions?.constraint_type == "FOREIGN KEY")
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == alterTableAst.table);
|
||||
|
||||
|
||||
for (const expression of foreignKeyExpressions) {
|
||||
|
||||
const targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.definition?.[0].column);
|
||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == expression.create_definitions?.reference_definition?.table?.[0].table)
|
||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.reference_definition?.definition?.[0].column);
|
||||
|
||||
const on_action: any | undefined = expression.create_definitions?.reference_definition?.on_action;
|
||||
|
||||
let onDelete: ForeignKeyActions | undefined;
|
||||
let onUpdate: ForeignKeyActions | undefined;
|
||||
if (on_action) {
|
||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
||||
|
||||
if (onDeleteAst)
|
||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
||||
if (onUpdateAst)
|
||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
||||
}
|
||||
|
||||
|
||||
if (!targetField || !sourceField || !sourceTable)
|
||||
continue;
|
||||
|
||||
relationships.push({
|
||||
id: v4(),
|
||||
targetTableId: targetTable?.id,
|
||||
targetFieldId: targetField.id,
|
||||
sourceTableId: sourceTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete,
|
||||
onUpdate
|
||||
} as RelationshipInsertType)
|
||||
}
|
||||
|
||||
if (relationships.length == foreignKeyExpressions.length)
|
||||
return relationships;
|
||||
else
|
||||
throw Error({
|
||||
success: false,
|
||||
message: "Failed to Extract all relationships",
|
||||
relationships
|
||||
} as any)
|
||||
}
|
||||
|
||||
if (!targetField || !sourceField || !sourceTable)
|
||||
throw Error("Failed to extract relationship");
|
||||
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
targetTableId: targetTable?.id,
|
||||
targetFieldId: targetField.id,
|
||||
sourceTableId: sourceTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete,
|
||||
onUpdate
|
||||
} as RelationshipInsertType;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.table[0]?.table,
|
||||
fields: ast.create_definitions.filter((column: any) => column.resource == "column")
|
||||
.map((fieldAst: any, index: number) => astToField(fieldAst, data_types, index)),
|
||||
color: randomColor()
|
||||
} as TableInsertType;
|
||||
}
|
||||
|
||||
|
||||
export const astToField = (ast: any, data_types: DataType[], sequence: number): FieldInsertType => {
|
||||
|
||||
|
||||
|
||||
const dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
return dataType.name == ast.definition.dataType?.toLowerCase() || synonyms.includes(ast.definition.dataType?.toLowerCase())
|
||||
});
|
||||
|
||||
let values: string | undefined = undefined;
|
||||
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
const { length, scale } = ast.definition;
|
||||
const { character_set, collate: collation } = ast;
|
||||
|
||||
let charset: string | undefined;
|
||||
let collate: string | undefined;
|
||||
|
||||
let defaultValue: string | undefined = ast.default_val?.value?.value ? ast.default_val?.value?.value : undefined;
|
||||
|
||||
let maxLength: number | null = null;
|
||||
let precision: number | null = null;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
||||
maxLength = length;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
||||
precision = length;
|
||||
|
||||
|
||||
if (modifiers.includes(Modifiers.CHARSET) && character_set)
|
||||
charset = character_set.value?.value;
|
||||
|
||||
if (modifiers.includes(Modifiers.COLLATE) && collation)
|
||||
collate = collation.collate?.name;
|
||||
|
||||
if (modifiers.includes(Modifiers.VALUES)) {
|
||||
|
||||
|
||||
const value = ast.definition?.expr?.value.map((value: any) => value.value);
|
||||
if (value)
|
||||
values = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (dataType?.type == DataTypes.TIME &&
|
||||
ast.default_val?.value?.type == "function" &&
|
||||
ast.default_val?.value?.name?.name?.length > 0 &&
|
||||
ast.default_val?.value?.name?.name[0].value == "CURRENT_TIMESTAMP")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
const isPrimary: boolean = ast.primary_key == "primary key";
|
||||
const nullable: boolean = ast.nullable?.value ? ast.nullable?.value != "not null" : !isPrimary;
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.column.column,
|
||||
defaultValue,
|
||||
typeId: dataType?.id,
|
||||
nullable,
|
||||
unique: ast.unique == "unique",
|
||||
maxLength,
|
||||
precision,
|
||||
scale,
|
||||
sequence,
|
||||
autoIncrement: ast.auto_increment == "auto_increment",
|
||||
isPrimary,
|
||||
values,
|
||||
charset,
|
||||
collate,
|
||||
} as FieldInsertType;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const postgresAstToTable = (ast: any, data_types: DataType[], postgresTypes: any[]): TableInsertType => {
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.name.name,
|
||||
fields: ast.columns.filter((column: any) => column.kind == "column")
|
||||
.map((fieldAst: any, index: number) => postgresAstToField(fieldAst, data_types, index, postgresTypes)),
|
||||
color: randomColor(),
|
||||
} as TableInsertType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const postgresAstToField = (ast: any, data_types: DataType[], sequence: number, postgresTypes: any[]): FieldInsertType => {
|
||||
|
||||
let dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
return dataType.name == ast.dataType.name?.toLowerCase() || synonyms.includes(ast.dataType.name?.toLowerCase())
|
||||
});
|
||||
|
||||
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;
|
||||
values = JSON.stringify(postgresType.values.map((value: any) => value.value));
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
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];
|
||||
|
||||
if (ast.dataType.config.length == 2)
|
||||
scale = ast.dataType.config[1];
|
||||
}
|
||||
|
||||
|
||||
let maxLength: number | null = null;
|
||||
let precision: number | null = null;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
||||
maxLength = length;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
||||
precision = length;
|
||||
|
||||
let defaultValue: string | undefined;
|
||||
let nullable: boolean = true;
|
||||
|
||||
const constraints: any[] | undefined = ast.constraints;
|
||||
if (constraints && constraints.length > 0) {
|
||||
|
||||
const nullableConstraints: any | undefined = constraints.find((c: any) => c.type == "not null");
|
||||
const defaultValueConstraints: any | undefined = constraints.find((c: any) => c.type == "default");
|
||||
|
||||
if (defaultValueConstraints) {
|
||||
if (defaultValueConstraints.default.type == "keyword" && defaultValueConstraints.default.keyword == "current_timestamp")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "call" && defaultValueConstraints.default.function?.name == "now")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "cast" && defaultValueConstraints.default.operand)
|
||||
defaultValue = String(defaultValueConstraints.default.operand.value)
|
||||
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;
|
||||
|
||||
if (nullableConstraints || primaryKey)
|
||||
nullable = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.name.name,
|
||||
defaultValue,
|
||||
typeId: dataType?.id,
|
||||
nullable,
|
||||
unique,
|
||||
maxLength,
|
||||
precision,
|
||||
scale,
|
||||
isPrimary: primaryKey,
|
||||
sequence,
|
||||
values,
|
||||
autoIncrement
|
||||
} as FieldInsertType;
|
||||
}
|
||||
|
||||
export const postgresAstToRelationship = (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)
|
||||
throw Error("source table not found");
|
||||
|
||||
const foreignKeyConstraints = changes.filter((change: any) => change.type == 'add constraint' && change.constraint && change.constraint.type == "foreign key").map((change: any) => change.constraint);
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
|
||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == foreignKeyConstraint.foreignTable.name);
|
||||
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 onDelete = foreignKeyConstraint.onDelete ? astToForiegnKeyAction(foreignKeyConstraint.onDelete) : undefined;
|
||||
const onUpdate = foreignKeyConstraint.onUpdate ? astToForiegnKeyAction(foreignKeyConstraint.onUpdate) : undefined;
|
||||
|
||||
if (!sourceField || !targetField || !sourceTable)
|
||||
continue;
|
||||
|
||||
relationships.push({
|
||||
id: v4(),
|
||||
sourceTableId: sourceTable.id,
|
||||
targetTableId: targetTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
targetFieldId: targetField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete, onUpdate
|
||||
} as RelationshipInsertType)
|
||||
}
|
||||
|
||||
if (relationships.length == foreignKeyConstraints.length)
|
||||
return relationships;
|
||||
else
|
||||
throw Error({
|
||||
success: false,
|
||||
message: "Failed to Extract all relationships",
|
||||
relationships
|
||||
} as any)
|
||||
}
|
||||
|
||||
|
||||
const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableInsertType) => {
|
||||
|
||||
const changes: any[] = constraints.filter((constraint: any) => constraint.type == "foreign key" || constraint.type == "reference").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
|
||||
}
|
||||
],
|
||||
onDelete: constraint.onDelete,
|
||||
onUpdate: constraint.onUpdate,
|
||||
}
|
||||
}))
|
||||
|
||||
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.table);
|
||||
|
||||
if (!table)
|
||||
throw Error("table not found");
|
||||
|
||||
const fieldNames: string[] = ast.index_columns.map((column: any) => column.column);
|
||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
||||
.map((field: FieldInsertType) => field.id);
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.index,
|
||||
tableId: table.id,
|
||||
unique: ast.index_type == "unique",
|
||||
fieldIndices: fieldIds?.map((id: string) => ({
|
||||
id: v4(),
|
||||
fieldId: id
|
||||
}))
|
||||
} as IndexInsertType
|
||||
}
|
||||
|
||||
export const postgresAstToIndex = (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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import MysqlRenderer from "./database/mysql-renderer";
|
||||
import PostgresqlRenderer from "./database/postgresql-renderer";
|
||||
import MariaDbRenderer from "./database/mariadb-renderer";
|
||||
import OracleRenderer from "./database/oracle-renderer";
|
||||
import MSSqlRenderer from "./database/mssql-database-renderer";
|
||||
import SqliteRenderer from "./database/sqlite-database-renderer";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
/**
|
||||
* Fixes charset/collation placement in SQL CREATE TABLE statements
|
||||
* @param sql The SQL string to process
|
||||
@@ -151,4 +162,32 @@ export interface CircularDependencyError {
|
||||
cycle: string[];
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const getPostgresEnumName = (table: TableType, field: FieldType): string => {
|
||||
|
||||
return `${table.name}_${field.name.toLowerCase()}_enum`
|
||||
}
|
||||
|
||||
export const getRenderer = (dialect: DatabaseDialect, data_types: DataType[]) => {
|
||||
|
||||
switch (dialect) {
|
||||
case DatabaseDialect.MYSQL:
|
||||
return new MysqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.POSTGRES:
|
||||
return new PostgresqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.MARIADB:
|
||||
return new MariaDbRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.ORACLE:
|
||||
return new OracleRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.MSSQL:
|
||||
return new MSSqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.SQLITE:
|
||||
return new SqliteRenderer(data_types);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user