import { Attribute, DB, Table } from "./db" import { Aggregation, checkConstraint, Constraint, Datatype, foreignConstraint, Modifier, uniqueConstraint } from "./dbStructure" import { Query, QueryBuilder, selectQuery } from "./query" import { allModifierInput, onAction, 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)); //Table List let allTables = (await db.query(handler.builders.query(handler.querys.listTables(handler, db)))).map((d: any) => d.TABLE_NAME); // gether Table Schematics let tableData: { [key: string]: any; } = {}; 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[table.dbLangTableName.toLowerCase()] = Object.fromEntries((await db.query(handler.builders.query(tableDataBuilder))).map((d: any) => [d.Field.toLowerCase(), d])); } catch (_) { tableData[table.dbLangTableName.toLowerCase()] = null; } } const del = new QueryBuilder(); const create = new QueryBuilder(); function checkUniqueIsVaild(n: string, table: string) { let t = db.getTable(table); if (t == null) return false; for (let i = 0; i < t.dbLangConstrains.length; i++) { const c = t.dbLangConstrains[i]; if (c.name == n.toLowerCase() && c instanceof uniqueConstraint) { let attrs = c.attrs.map(a => a.name); let found = 0; for (let j = 0; j < key.length; j++) { if ( key[j].CONSTRAINT_NAME == n && key[j].TABLE_NAME == table ) { if (attrs.includes(key[j].COLUMN_NAME.toLowerCase())) found++; else return false; } } return found == attrs.length; } } return false; } function checkForeignIsVaild(n: string, table: string) { let t = db.getTable(table); if (t == null) return false; for (let i = 0; i < t.dbLangConstrains.length; i++) { const c = t.dbLangConstrains[i]; if (c.name == n.toLowerCase() && c instanceof foreignConstraint) { let fromAttrs = c.fromAttrs.map(a => a.name); let toAttrs = c.toAttrs.map(a => a.name); let refTable = c.toAttrs[0].table.dbLangTableName; let found = 0; for (let j = 0; j < key.length; j++) { if ( key[j].CONSTRAINT_NAME == n && key[j].TABLE_NAME == table && key[j].REFERENCED_TABLE_NAME == refTable ) { let inF = fromAttrs.indexOf(key[j].COLUMN_NAME.toLowerCase()); let inT = toAttrs.indexOf(key[j].REFERENCED_COLUMN_NAME.toLowerCase()); if (inF != -1 && inT == inF) found++; else return false; } } return found == fromAttrs.length && found == toAttrs.length; } } return false; } function checkUniqueExists(tabel: Table, c: uniqueConstraint) { let attrs = c.attrs.map(a => a.name); let found = 0; for (let j = 0; j < key.length; j++) { if ( key[j].CONSTRAINT_NAME.toLowerCase() == c.name && key[j].TABLE_NAME == tabel.dbLangTableName ) { if (attrs.includes(key[j].COLUMN_NAME.toLowerCase())) found++; else return false; } } return found == attrs.length; } function checkForeignExists(tabel: Table, c: foreignConstraint) { let fromAttrs = c.fromAttrs.map(a => a.name); let toAttrs = c.toAttrs.map(a => a.name); let refTable = c.toAttrs[0].table.dbLangTableName; let found = 0; for (let j = 0; j < key.length; j++) { if ( key[j].CONSTRAINT_NAME == c.name && key[j].TABLE_NAME == tabel.dbLangTableName && key[j].REFERENCED_TABLE_NAME == refTable ) { let inF = fromAttrs.indexOf(key[j].COLUMN_NAME.toLowerCase()); let inT = toAttrs.indexOf(key[j].REFERENCED_COLUMN_NAME.toLowerCase()); if (inF != -1 && inT == inF) found++; else return false; } } return found == fromAttrs.length && found == toAttrs.length; } //delete check and unused/false(unique, foreign) constraints 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)); } else if (c.CONSTRAINT_TYPE == "UNIQUE") { if ( (typeof tableData[c.TABLE_NAME.toLowerCase()] == "undefined" && deleteInDB) || !checkUniqueIsVaild(c.CONSTRAINT_NAME, c.TABLE_NAME) ) del.appendEnding(handler.querys.removeUnique(handler, c.TABLE_NAME, c.CONSTRAINT_NAME)); } else if (c.CONSTRAINT_TYPE == "FOREIGN KEY") { if ( (typeof tableData[c.TABLE_NAME.toLowerCase()] == "undefined" && deleteInDB) || !checkForeignIsVaild(c.CONSTRAINT_NAME, c.TABLE_NAME) ) del.appendEnding(handler.querys.removeForeignKey(handler, c.TABLE_NAME, c.CONSTRAINT_NAME)); } } //delete unused Tables if (deleteInDB) for (let i = 0; i < allTables.length; i++) { if (typeof tableData[allTables[i].toLowerCase()] == "undefined") del.appendEnding(handler.querys.deleteTable(handler, allTables[i])); } function freeForUpdate(attr: string, table: string) { for (let i = 0; i < key.length; i++) { const k = key[i]; if ( k.REFERENCED_TABLE_NAME == table && k.REFERENCED_COLUMN_NAME.toLowerCase() == attr.toLowerCase() ) { 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[table.dbLangTableName.toLowerCase()]; //delete unused Columns if (deleteInDB && tableD != null) { let keys = Object.keys(tableD); for (let i = 0; i < keys.length; i++) { if (table.dbLangTableAttributes[keys[i]] == null) { freeForUpdate(keys[i].toLowerCase(), table.dbLangTableName); create.appendEnding(handler.querys.removeColumn(handler, table, keys[i])); } } } //add/mofify columns 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.name, a.table.dbLangTableName); create.appendEnding(handler.querys.changeColumn(handler, a)); } if (attrData == null) { changePrimary = true; } else { if ((attrData.Key == "PRI") != (!!a.ops.primaryKey)) { freeForUpdate(a.name, a.table.dbLangTableName); changePrimary = true; } } } } if (changePrimary) { create.appendEnding(handler.querys.removePrimaryKey(handler, table.dbLangTableName)); 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)); } else if (c instanceof uniqueConstraint) { if (!checkUniqueExists(table, c)) { create.appendEnding(handler.querys.addUnique(handler, table, c)); } } else if (c instanceof foreignConstraint) { if (!checkForeignExists(table, c)) { create.appendEnding(handler.querys.addForeignKey(handler, table, c)); } } } } 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; }, listTables: (handler: Handler, db: DB) => { const qb = new QueryBuilder(); qb.addCode(`SELECT * FROM information_schema.tables where TABLE_SCHEMA = `); qb.addInjection(db.name); qb.addCode(` and TABLE_TYPE = "BASE TABLE"`); return qb; }, 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; }, deleteTable: (handler: Handler, table: string): QueryBuilder => { const qb = new QueryBuilder(); qb.addCode(`DROP TABLE `); qb.addCode(handler.builders.escapeID(table)); qb.addCode(` CASCADE`); return qb; }, 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; }, removeColumn: (handler: Handler, tabel: Table, attr: string) => { const builder = new QueryBuilder(); builder.addCode("ALTER TABLE "); builder.addCode(tabel.serialize(handler)); builder.addCode(" DROP COLUMN "); builder.addCode(handler.builders.escapeID(attr)); return builder; }, addForeignKey: (handler: Handler, table: Table, c: foreignConstraint) => { const builder = new QueryBuilder(); builder.addCode("ALTER TABLE "); builder.addCode(table.serialize(handler)); builder.addCode(" ADD CONSTRAINT "); builder.addCode(handler.builders.escapeID(c.name)); builder.addCode(" FOREIGN KEY ("); builder.addCodeCommaSeperated(c.fromAttrs.map(a => a.serialize(handler))); builder.addCode(") REFERENCES "); builder.addCode(c.toAttrs[0].table.serialize(handler)); builder.addCode("("); builder.addCodeCommaSeperated(c.toAttrs.map(a => a.serialize(handler))); builder.addCode(")"); if (c.onUpdate == onAction.cascade) builder.addCode(" ON UPDATE CASCADE"); if (c.onUpdate == onAction.noAction) builder.addCode(" ON UPDATE NO ACTION"); if (c.onUpdate == onAction.setDefault) builder.addCode(" ON UPDATE SET DEFUALT"); if (c.onUpdate == onAction.setNull) builder.addCode(" ON UPDATE SET NULL"); if (c.onDelete == onAction.cascade) builder.addCode(" ON DELETE CASCADE"); if (c.onDelete == onAction.noAction) builder.addCode(" ON DELETE NO ACTION"); if (c.onDelete == onAction.setDefault) builder.addCode(" ON DELETE SET DEFUALT"); if (c.onDelete == onAction.setNull) builder.addCode(" ON DELETE SET NULL"); 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: string) => { const qb = new QueryBuilder(); qb.addCode("ALTER TABLE "); qb.addCode(handler.builders.escapeID(table)); 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; } }