import { Attribute, DB, Table } from "./db" import { Aggregation, checkConstraint, Constraint, Datatype, Modifier, uniqueConstraint } from "./dbStructure" import { Query, QueryBuilder, selectQuery } from "./query" import { allModifierInput, primaryData, serializeReturn } from "./types" export class Handler { async syncDB(db: DB, handler: Handler, deleteInDB: boolean = false) { console.log("start sync"); let gd = new QueryBuilder(); gd.addCode(`SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where CONSTRAINT_SCHEMA = `); gd.addInjection(db.name); gd.addCode(` or TABLE_SCHEMA = `); gd.addInjection(db.name); gd.addCode(`;select * from information_schema.table_constraints where CONSTRAINT_SCHEMA = `); gd.addInjection(db.name) gd.addCode(` or TABLE_SCHEMA = `); gd.addInjection(db.name); let [key, constraints] = await db.query(handler.builders.query(gd)); let tableData = []; for (let i = 0; i < db.tables.length; i++) { const table = db.tables[i]; try { const tableDataBuilder = new QueryBuilder(); tableDataBuilder.addCode("DESCRIBE "); tableDataBuilder.addCode(table.serialize(handler)); tableData[i] = Object.fromEntries((await db.query(handler.builders.query(tableDataBuilder))).map((d: any) => [d.Field, d])); } catch (_) { tableData[i] = null; } } const del = new QueryBuilder(); const create = new QueryBuilder(); for (let i = 0; i < constraints.length; i++) { const c = constraints[i]; if (c.CONSTRAINT_TYPE == "CHECK") { del.appendEnding(handler.querys.removeCheck(handler, c.TABLE_NAME, c.CONSTRAINT_NAME)); } } // TODO: delete old constraints/tables function freeForUpdate(a: Attribute) { for (let i = 0; i < key.length; i++) { const k = key[i]; if ( k.REFERENCED_TABLE_NAME == a.table.dbLangTableName && k.REFERENCED_TABLE_NAME == a.name ) { del.appendEnding(handler.querys.removeForeignKey(handler, k.TABLE_NAME, k.CONSTRAINT_NAME)); } } } //create tables for (let i = 0; i < db.tables.length; i++) { const table = db.tables[i]; const tableD = tableData[i]; let changePrimary = false; if (tableD == null) { create.appendEnding(handler.querys.create(handler, table)); changePrimary = true; } else { let keys = Object.keys(table.dbLangTableAttributes); for (let j = 0; j < keys.length; j++) { const a = table.dbLangTableAttributes[keys[j]]; const attrData = tableD[keys[j]]; if (attrData == null) { create.appendEnding(handler.querys.addColumn(handler, a)); changePrimary = true; } else if ( !handler.builders.compareDatatypes(handler, a.type, attrData.Type) || a.ops.default != attrData.Default || (!!a.ops.notNull || !!a.ops.autoIncrement || !!a.ops.primaryKey) != (attrData.Null == "NO") || (!!a.ops.autoIncrement) != (attrData.Extra == "auto_increment") ) { console.log(!handler.builders.compareDatatypes(handler, a.type, attrData.Type), "|", a.ops.default, attrData.Default, "|", (!!a.ops.notNull || !!a.ops.autoIncrement || !!a.ops.primaryKey), (attrData.Null == "NO"), "|", (!!a.ops.autoIncrement), (attrData.Extra == "auto_increment")); freeForUpdate(a); create.appendEnding(handler.querys.changeColumn(handler, a)); } if (attrData == null) { changePrimary = true; } else { if ((attrData.Key == "PRI") != (!!a.ops.primaryKey)) { freeForUpdate(a); changePrimary = true; } } } } if (changePrimary) { create.appendEnding(handler.querys.removePrimaryKey(handler, table)); create.appendEnding(handler.querys.addPrimaryKey(handler, table)); } for (let j = 0; j < table.dbLangConstrains.length; j++) { const c = table.dbLangConstrains[j]; if (c instanceof checkConstraint) { create.appendEnding(handler.querys.addCheck(handler, table, c)); } } // TODO: unique } if (!create.isEmpty()) del.append(create); if (!del.isEmpty()) await db.query(handler.builders.query(del)); } querys = { select: (handler: Handler, q: selectQuery): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode("select "); builder.append(joinArg(", ")(handler, q.attr)); builder.addCode(` from ${q.from == null ? 'DUAL' : q.from.serialize(handler)}`); if (q.whereD) { builder.addCode(" where "); builder.append(q.whereD.serialize(handler)); } if (q.groupByD.length > 0) { builder.addCode(" group by "); builder.append(joinArg(",")(handler, q.groupByD)); if (q.havingD) { builder.addCode(" having "); builder.append(q.havingD.serialize(handler)); } } if (q.havingD) { builder.addCode(" limit "); builder.addInjection(q.limitD); } /*builder.setHandler((json)=>{ return json; });*/ return builder; }, create: (handler: Handler, table: Table): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode(`create table if not exists ${table.toString(handler)}(`); let keys = Object.keys(table.dbLangTableAttributes); for (let i = 0; i < keys.length; i++) { const a = table.dbLangTableAttributes[keys[i]]; builder.addCode(a.serialize(handler) + " "); builder.append(a.serializeSettings(handler)); if (i + 1 < keys.length) builder.addCode(", "); } builder.addCode(")"); return builder; }, addColumn: (handler: Handler, attr: Attribute): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode(`alter table `); builder.addCode(attr.table.toString(handler)); builder.addCode(` add if not exists `); builder.addCode(attr.serialize(handler) + " "); builder.append(attr.serializeSettings(handler)); return builder; }, changeColumn: (handler: Handler, attr: Attribute): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode(`alter table `); builder.addCode(attr.table.toString(handler)); builder.addCode(` change `); builder.addCode(attr.serialize(handler) + " "); builder.addCode(attr.serialize(handler) + " "); builder.append(attr.serializeSettings(handler)); return builder; }, addForeignKey: (handler: Handler, table: Table) => { const builder = new QueryBuilder(); return builder; }, removeForeignKey: (handler: Handler, tablenName: string, name: string) => { const builder = new QueryBuilder(); builder.addCode("ALTER TABLE "); builder.addCode(handler.builders.escapeID(tablenName)); builder.addCode(" DROP FOREIGN KEY IF EXISTS "); builder.addCode(handler.builders.escapeID(name)); builder.addCode(";ALTER TABLE "); builder.addCode(handler.builders.escapeID(tablenName)); builder.addCode(" DROP INDEX IF EXISTS "); builder.addCode(handler.builders.escapeID(name)); return builder; }, addPrimaryKey: (handler: Handler, table: Table) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(table.serialize(handler)); qb.addCode(" add PRIMARY KEY if not exists ("); qb.addCodeCommaSeperated( Object.entries(table.dbLangTableAttributes) .filter(([n, attr]) => !!attr.ops.primaryKey) .map(([n, attr]) => handler.builders.escapeID(n)) ); qb.addCode(")"); return qb; }, removePrimaryKey: (handler: Handler, table: Table) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(table.serialize(handler)); qb.addCode(" DROP INDEX IF EXISTS `PRIMARY`"); return qb; }, removeCheck: (handler: Handler, table: string, name: string) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(handler.builders.escapeID(table)); qb.addCode(" DROP CONSTRAINT IF EXISTS "); qb.addCode(handler.builders.escapeID(name)); return qb; }, addCheck: (handler: Handler, table: Table, c: checkConstraint) => { const qb = new QueryBuilder(); qb.addCode(`ALTER TABLE `); qb.addCode(table.serialize(handler)); qb.addCode(` ADD CONSTRAINT `); qb.addCode(handler.builders.escapeID(c.name)); qb.addCode(` CHECK (`); qb.append(c.checkQuery.serialize(handler)); qb.addCode(")"); return qb; }, removeUnique: (handler: Handler, table: string, name: string) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(handler.builders.escapeID(table)); qb.addCode(" DROP INDEX IF EXISTS "); qb.addCode(handler.builders.escapeID(name)); return qb; }, addUnique: (handler: Handler, table: Table, u: uniqueConstraint) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(table.serialize(handler)); qb.addCode(" ADD CONSTRAINT "); qb.addCode(handler.builders.escapeID(u.name)); qb.addCode(" UNIQUE ("); qb.addCodeCommaSeperated(u.attrs.map(a => a.serialize(handler))); qb.addCode(")"); return qb; }, /*// add constraints appPrimaryKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), appForeignKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), addUniqueKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), addChecks: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), //should drop all keys to be able to recreate them dropPrimaryKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), dropForeignKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), dropUniqueKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), dropChecks: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), // check constraints listPrimaryKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), listForeignKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), listUniqueKeys: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(), listChecks: (handler: Handler, table: Table): QueryBuilder => new QueryBuilder(),*/ } builders = { query: (qb: QueryBuilder): Query => { let args: primaryData[] = []; let sql = ""; for (let i = 0; i < qb.list.length; i++) { const [inject, data] = qb.list[i]; if (inject) { sql += "?"; args.push(data); } else { sql += data; } } return new Query([sql, args]); }, attributeSettings: (handler: Handler, a: Attribute): QueryBuilder => { const builder = new QueryBuilder(); builder.append(a.type.serialize(handler)); if (a.ops.autoIncrement) { builder.addCode(" auto_increment"); } if (a.ops.default != null) { if (a.ops.autoIncrement || a.ops.primaryKey || a.ops.notNull) throw new Error(`Can not set default when autoIncrement, primaryKey or notNull ist set on Attribute: ${a.toStringFunc(handler)}`); builder.addCode(" default "); builder.addInjection(a.ops.default); } if (a.ops.notNull != null) { if (!a.ops.autoIncrement && !a.ops.primaryKey && a.ops.default == null) { builder.addCode(" not null"); } else builder.addCode(" null"); } else builder.addCode(" null"); return builder; }, escapeID: (key: string): string => { if (!key || key === "" || key.includes('\u0000')) throw new Error("Can not escape empty key or with null unicode!"); if (key.match(/^`.+`$/g)) return key; return `\`${key.replace(/`/g, '``')}\``; }, compareDatatypes: (handler: Handler, attr: Datatype, curr: String) => { let qb = attr.serialize(handler); let sql = ""; for (let i = 0; i < qb.list.length; i++) { const [inject, data] = qb.list[i]; sql += data; } return curr.split(" ").join("").startsWith(sql.split(" ").join("")); }, } aggregations = { count: (handler: Handler, a: Attribute): QueryBuilder => new QueryBuilder([{ data: "count(" + a.toString(handler) + ")" }]), sum: (handler: Handler, a: Attribute): QueryBuilder => new QueryBuilder([{ data: "sum(" + a.toString(handler) + ")" }]), avg: (handler: Handler, a: Attribute): QueryBuilder => new QueryBuilder([{ data: "avg(" + a.toString(handler) + ")" }]), min: (handler: Handler, a: Attribute): QueryBuilder => new QueryBuilder([{ data: "min(" + a.toString(handler) + ")" }]), max: (handler: Handler, a: Attribute): QueryBuilder => new QueryBuilder([{ data: "max(" + a.toString(handler) + ")" }]), } modifiers = { and: joinArg("and"), or: joinArg("or"), le: joinArg("<"), leq: joinArg("<="), eq: joinArg("="), geq: joinArg(">="), ge: joinArg(">"), plus: joinArg("+"), minus: joinArg("-"), not: (handler: Handler, a: allModifierInput[]): QueryBuilder => { let e = a[0]; if (e instanceof Attribute) return new QueryBuilder([{ data: "not (" + e.toString(handler) + ")" }]) if (e instanceof Modifier || e instanceof selectQuery || e instanceof Aggregation) { const builder = new QueryBuilder(); builder.addCode("not ("); builder.append(e.serialize(handler)); builder.addCode(")"); return builder; } return new QueryBuilder([ { data: "not(" }, { inject: true, data: e }, { data: ")" } ]) } } datatypes = { char: dataTypeSingleNum("char"), varchar: dataTypeSingleNum("varchar"), binary: dataTypeSingleNum("binary"), varbinary: dataTypeSingleNum("varbinary"), tinyblob: dataTypeNoArg("tinyblob"), blob: dataTypeNoArg("blob"), mediumblob: dataTypeNoArg("mediumblob"), longblob: dataTypeNoArg("longblob"), tinytext: dataTypeNoArg("tinytext"), text: dataTypeNoArg("text"), mediumtext: dataTypeNoArg("mediumtext"), longtext: dataTypeNoArg("longtext"), enum: (a: primaryData[]): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode("enum("); builder.addInjectionCommaSeperated(a); builder.addCode(")"); return builder; }, set: (a: primaryData[]): QueryBuilder => { const builder = new QueryBuilder(); builder.addCode("set("); builder.addInjectionCommaSeperated(a); builder.addCode(")"); return builder; }, bool: dataTypeNoArg("bool"), bit: dataTypeNoArg("bit"), tinyint: dataTypeNoArg("tinyint"), smallint: dataTypeNoArg("smallint"), mediumint: dataTypeNoArg("mediumint"), int: dataTypeNoArg("int"), bigint: dataTypeNoArg("bigint"), float: dataTypeDblNum("float"), double: dataTypeDblNum("double"), decimal: dataTypeDblNum("decimal"), date: dataTypeNoArg("date"), datetime: dataTypeNoArg("datatime"), timestamp: dataTypeNoArg("timestamp"), time: dataTypeNoArg("time"), year: dataTypeNoArg("year"), } }; function dataTypeNoArg(type: string) { return (a: primaryData[]): QueryBuilder => { return new QueryBuilder([{ inject: false, data: type }]); } } function dataTypeSingleNum(type: string) { return (a: primaryData[]): QueryBuilder => { return new QueryBuilder([ { data: type + "(" }, { inject: true, data: a[0] }, { data: ")" } ]); } } function dataTypeDblNum(type: string) { return (a: primaryData[]): QueryBuilder => { return new QueryBuilder([ { data: type + "(" }, { inject: true, data: a[0] }, { data: ", " }, { inject: true, data: a[1] }, { data: ")" } ]); } } function joinArg(type: string) { return (handler: Handler, a: (allModifierInput)[]): QueryBuilder => { const builder = new QueryBuilder(); for (let i = 0; i < a.length; i++) { const d = a[i]; if (d instanceof Attribute) builder.addCode(d.toString(handler)); else if (d instanceof Modifier || d instanceof selectQuery || d instanceof Aggregation) { builder.addCode("("); builder.append(d.serialize(handler)); builder.addCode(")"); } else { builder.addInjection(d); } if (i + 1 < a.length) builder.addCode(" " + type + " "); } return builder; } }