diff --git a/lib/mariadb.class.js b/lib/mariadb.class.js index 8f64cc2..85ef34d 100644 --- a/lib/mariadb.class.js +++ b/lib/mariadb.class.js @@ -8,38 +8,44 @@ const crypto = require('crypto'); * @returns {string} Hashed message * */ function SHA256(msg) { - return crypto.createHash('sha256').update(msg).digest('hex'); + const hash = crypto.createHash('sha256'); + hash.update(msg); + return hash.digest('hex') } class Database { - constructor(config, rejectEmpty = false, limitQueryExecution = true) { - this._config = config; - this._rejectEmpty = config.rejectEmpty || rejectEmpty; - this._limitQueryExecution = limitQueryExecution; - // hashed connection info for connection id - this._connectionHash = SHA256(`${config.host}${config.user}${config.database}`); - - // check if __sqlPools already declared, if not declare it as object - if (global.__sqlPools === undefined) { - global.__sqlPools = {}; - } - }; + constructor() {}; - /** - * Expose mariadb package - * @returns {Object} current connection - * */ - connection() { - return __sqlPools[this._connectionHash] - ? __sqlPools[this._connectionHash].getConnection() - : undefined; - }; + lastCon = {} + + setConfig(config, rejectEmpty = false, limitQueryExecution = true){ + this._config = config; + this._rejectEmpty = config.rejectEmpty || rejectEmpty; + this._limitQueryExecution = limitQueryExecution; + // hashed connection info for connection id + this._connectionHash = SHA256(`${config.host}${config.user}${config.database}`); + + // check if __sqlPools already declared, if not declare it as object + if (global.__sqlPools === undefined) { + global.__sqlPools = {}; + } + } + + /** + * Expose mariadb package + * @returns {Object} current connection + * */ + connection() { + return __sqlPools[this._connectionHash] + ? __sqlPools[this._connectionHash].getConnection() + : undefined; + }; - /** - * Escape undefined in args as null. Preventing query execution from throwing error - * @param {string[]} args - arguments to be passed into query - * @returns {string[]} escaped args - * */ + /** + * Escape undefined in args as null. Preventing query execution from throwing error + * @param {string[]} args - arguments to be passed into query + * @returns {string[]} escaped args + * */ escapeUndefined(args){ if(!args instanceof Object){ return args === undefined ? null : args; @@ -54,106 +60,159 @@ class Database { return args; }; - /** - * Execute query with arguments - * @param {string} sql - sql query - * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) - * @param {boolean} [dateStrings=true] - if false datetime columns will be returned as js Date object - * @returns {Object[]} sql query result - * */ - query(sql, args =[], stripMeta = false, dateStrings = true) { - return new Promise(async (resolve, reject) => { - //create pool and add it to global to minimize number of same connection in mysql - if (!__sqlPools[this._connectionHash]) { - __sqlPools[this._connectionHash] = await mariadb.createPool(this._config); - } + /** + * Execute query with arguments + * @param {string} sql - sql query + * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) + * @param {boolean} [dateStrings=true] - if false datetime columns will be returned as js Date object + * @returns {Object[]} sql query result + * */ + query(sql, args, stripMeta = false, dateStrings = true, strict = true) { + const self = this - //just in case. Limit query executed only for data manipulation only - if (this._limitQueryExecution && sql.match(/(GRANT|SHUTDOWN)($|[\s\;])/i)) { - reject({ - errno: 0, - msg: "SQL Query contains forbidden words : CREATE,TRUNCATE,GRANT,DROP,ALTER,SHUTDOWN", - }); + return new Promise(async (resolve, reject) => { + //create pool and add it to global to minimize number of same connection in mysql + + const isSelectQuery = sql.trim().match(/^(SELECT)/i) ? true: false - return; - } + if (!__sqlPools[self._connectionHash]) { + + // if it select query then + if(isSelectQuery){ + __sqlPools[self._connectionHash] = await mariadb.createPool(self._config); + }else{ + reject({ + errno: 1, + msg: "Use 'beginTransaction' before every query", + }); + + return + } + + } + + //just in case. Limit query executed only for data manipulation only + if (self._limitQueryExecution) { + if(strict && sql.match(/(GRANT|SHUTDOWN)($|[\s\;])/i)){ + reject({ + errno: 0, + msg: "SQL Query contains forbidden words : CREATE,TRUNCATE,GRANT,DROP,ALTER,SHUTDOWN", + }); + + return; + } + } + + let con + try { - let con; - try { - con = await this.connection(); - const res = await con.query({ sql, dateStrings }, this.escapeUndefined(args)); + let res + if(isSelectQuery){ + con = await self.connection() + res = await con.query({ sql, dateStrings }, self.escapeUndefined(args)); + }else{ + res = await self.lastCon.query({ sql, dateStrings }, self.escapeUndefined(args)); + } - if (Array.isArray(res) && res.length == 0 && this._rejectEmpty) { - reject({ code: 'EMPTY_RESULT' }); - } else { - if(typeof(res.insertId) === 'bigint'){ - res.insertId = Number(res.insertId); + if (Array.isArray(res) && res.length == 0 && self._rejectEmpty) { + reject({ code: 'EMPTY_RESULT' }); + } else { + if(stripMeta){ + delete res.meta; } - resolve(res); - } - } catch (error) { - reject(error); - } finally { - if (con) { - con.release(); - } - } - }); - }; + resolve(res); + } + } + catch (error) { + reject(error); + } + finally { + if (isSelectQuery) { + con?.release(); + } + } + }) + }; - /** - * Execute query batch with arguments - * @param {string} sql - sql query - * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) - * @param {boolean} [dateStrings=true] - if false datetime columns will be returned as js Date object - * @returns {Object[]} sql query result - * */ - batch(sql, args, dateStrings = true) { - return new Promise(async (resolve, reject) => { - //create pool and add it to global to minimize number of same connection in mysql - if (!__sqlPools[this._connectionHash]) { - __sqlPools[this._connectionHash] = await mariadb.createPool(this._config); - } + /** + * Execute query batch with arguments + * @param {string} sql - sql query + * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) + * @param {boolean} [dateStrings=true] - if false datetime columns will be returned as js Date object + * @returns {Object[]} sql query result + * */ + batch(sql, args, stripMeta = true, dateStrings = true, strict = true) { + const self = this - //just in case. Limit query executed only for data manipulation only - if (this._limitQueryExecution && sql.match(/(CREATE|TRUNCATE|GRANT|DROP|ALTER|SHUTDOWN)($|[\s\;])/i)) { - reject({ - errno: 0, - msg: "SQL Query contains forbidden words : CREATE,TRUNCATE,GRANT,DROP,ALTER,SHUTDOWN", - }); + return new Promise(async (resolve, reject) => { + //create pool and add it to global to minimize number of same connection in mysql + + const isSelectQuery = sql.trim().match(/^(SELECT)/i) ? true: false - return; - } + if (!__sqlPools[self._connectionHash]) { - let con; - try { - con = await this.connection(); - const res = await con.batch({ sql, dateStrings }, args); + // if it select query then + if(isSelectQuery){ + __sqlPools[self._connectionHash] = await mariadb.createPool(self._config); + }else{ + reject({ + errno: 1, + msg: "Use 'beginTransaction' before every query", + }); + + return + } - if (Array.isArray(res) && res.length == 0 && this._rejectEmpty) { - reject({ code: 'EMPTY_RESULT' }); - } else { - resolve(res); - } - } - catch (error) { - reject(error); - } - finally { - if (con) { - con.release(); - } - } - }); - }; + } + + //just in case. Limit query executed only for data manipulation only + if (self._limitQueryExecution) { + if(strict && sql.match(/(CREATE|TRUNCATE|GRANT|DROP|ALTER|SHUTDOWN)($|[\s\;])/i)){ + reject({ + errno: 0, + msg: "SQL Query contains forbidden words : CREATE,TRUNCATE,GRANT,DROP,ALTER,SHUTDOWN", + }); + + return; + } + } + + try { + + if(isSelectQuery){ + self.lastCon = await self.connection() + } - /** - * Debug SQL query with arguments - * @param {string} sql - sql query - * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) - * @returns {Object[]} sql query with arguments - * */ + const res = await self.lastCon.batch({ sql, dateStrings }, self.escapeUndefined(args)); + + if (Array.isArray(res) && res.length == 0 && self._rejectEmpty) { + reject({ code: 'EMPTY_RESULT' }); + } else { + if(stripMeta){ + delete res.meta; + } + + resolve(res); + } + } + catch (error) { + reject(error); + } + finally { + if ("release" in self.lastCon && isSelectQuery) { + self.lastCon.release(); + } + } + }) + }; + + /** + * Debug SQL query with arguments + * @param {string} sql - sql query + * @param {string[]} args - escaped arguments to be passed into query (avoiding injection) + * @returns {Object[]} sql query with arguments + * */ debug(sql, args) { if(!Array.isArray(args)){ args = [args]; @@ -170,13 +229,116 @@ class Database { return sql.replace(/[\t\n\ ]+/g,' '); } - /** - * End current connection and delete it from global object - * */ - async end() { - await __sqlPools[this._connectionHash].end(); - delete __sqlPools[this._connectionHash]; - }; + + /** + * Begin transaction, + * transaction is important to rollback all change if there are any error + * in queries. Set it on every beginning of process. + * */ + async beginTransaction(){ + const self = this + + //create pool and add it to global to minimize number of same connection in mysql + if (!__sqlPools[self._connectionHash]) { + __sqlPools[self._connectionHash] = await mariadb.createPool(self._config); + } + + try { + this.lastCon = await self.connection(); + return await this.lastCon.beginTransaction(); + } + catch (error) { + if (this.lastCon) { + this.lastCon.release(); + } + + return error + } + } + + /** + * Commit + * If there are no error, then execute it normally. + * Set it on every end of process. + * */ + async commit(){ + const self = this + + return new Promise(async (resolve, reject) => { + if (!__sqlPools[self._connectionHash]) { + return reject({ + errno: 1, + msg: "Use 'beginTransaction' before every query", + }); + } + + const result = await self.lastCon.commit() + + if ("release" in self.lastCon) { + self.lastCon.release(); + } + + return resolve(result) + }) + } + + /** + * Rollback + * If there are error, then execute undo every queries. + * Set it on every try "catch" of process. + * */ + async rollback(){ + const self = this + + return new Promise(async (resolve, reject) => { + if (!__sqlPools[self._connectionHash]) { + return reject({ + errno: 1, + msg: "Use 'beginTransaction' before every query", + }); + + } + + const result = await self.lastCon.rollback() + + if ("release" in self.lastCon) { + self.lastCon.release(); + } + + return resolve(result) + }) + } + + /** + * escape string as parameter on query to check if string contains sql injection + * @param {string} string - string that will be part of query + * @returns {string} - string that already be escaped + * */ + escape(string) { + return this.connection().escape(string); + } + + /** + * End current connection and delete it from global object + * */ + async end() { + await __sqlPools[this._connectionHash].end(); + delete __sqlPools[this._connectionHash]; + }; + + /** + * End all connection and delete it from global object + * */ + async endAll(){ + try{ + for(let connection in __sqlPools){ + await __sqlPools[connection].end(); + delete __sqlPools[connection]; + } + }catch(err){} + } + + } -module.exports = Database; +module.exports = new Database(); diff --git a/lib/sqlImporter.class.js b/lib/sqlImporter.class.js index d63ed77..f9c2e1d 100644 --- a/lib/sqlImporter.class.js +++ b/lib/sqlImporter.class.js @@ -500,15 +500,25 @@ class IMPORTER { const self = this; if(!self._mysql){ - self._mysql = new _mariadb({ - ...self._config, - multipleStatements: true - }); + self._mysql = _mariadb + self._mysql.setConfig({ + ...self._config, + multipleStatements: true + }) } - // drop all tables and routines first - await self.dropAllTables(self._config.database); - await self.dropRoutines(self._config.database); + + try{ + await self._mysql.beginTransaction() + + // drop all tables and routines first + await self.dropAllTables(self._config.database); + await self.dropRoutines(self._config.database); + + await self._mysql.commit() + }catch(err){ + await self._mysql.rollback() + } if(closeConnection){ self.close(); @@ -718,14 +728,19 @@ class IMPORTER { async importFile(options = {}){ const self = this; - if(!self._mysql){ - self._mysql = new _mariadb({ - ...self._config, - multipleStatements: true - }); + if(!self._mysql || !self._mysql?._config){ + self._mysql = _mariadb + self._mysql.setConfig({ + ...self._config, + multipleStatements: true + }) } - let { withData, dropFirst, closeConnection, compareExisting } = options; + // start transaction + // although transaction only work for DML (https://github.com/iqrok/sql-importer/issues/5) + await self._mysql.beginTransaction() + + let { withData, dropFirst, closeConnection, compareExisting, schemaUpdateClearData } = options; if(withData === undefined){ withData = true; @@ -739,6 +754,12 @@ class IMPORTER { compareExisting = false; } + // if true then it will clear data before update schema + // if false then it will ignore error + if(schemaUpdateClearData === undefined){ + schemaUpdateClearData = false; + } + const execAll = async function(queries, verbose){ const _failedQueries= [] @@ -803,9 +824,14 @@ class IMPORTER { // get existing table & columns const getExistTables = await self._mysql.query(` - SELECT table_name, column_name - FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = ? + SELECT table_name, column_name, + CONCAT( + IF(column_type IS NULL, "", CONCAT(column_type, " ")), + IF(is_nullable = "YES", "NULL", "NOT NULL"), + IF(column_default IS NULL, "", CONCAT(" DEFAULT ", column_default)) + ) as columnDetail + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = ? `,[self._config.database]) .then(res => { @@ -822,7 +848,7 @@ class IMPORTER { } const newObj = {} - newObj[curCol] = true + newObj[curCol] = res[i].columnDetail Object.assign(tables[curTbl], newObj) @@ -845,10 +871,23 @@ class IMPORTER { // compare every columns in each tables const columns = self._getColumnsFromCreateTable(queries.table[tbl][0]) for(let column in columns){ + + + // if column not found on existing table then if(!(column in getExistTables[tbl])){ sameTables[tbl].push({ name: column, - detail: columns[column] + detail: columns[column], + isUpdate: false + }) + + // if column found on existing table, but has different detail, then + } else if((column in getExistTables[tbl]) && getExistTables[tbl][column] != columns[column]){ + sameTables[tbl].push({ + name: column, + detail: columns[column], + isUpdate: true, + previousDetail: getExistTables[tbl][column] }) } } @@ -949,9 +988,22 @@ class IMPORTER { if(sameTables[table].length > 0){ + // console.log("========UPDATE SCHEMA==========") for(let column of sameTables[table]){ - queries.alter.unshift("ALTER TABLE `"+table+"` ADD `"+column.name+"` "+column.detail) + if(column.isUpdate){ + // console.log(table, column) + if(schemaUpdateClearData){ + await execAll([ + "UPDATE `"+table+"` SET `"+column.name+"` = NULL" + ], verbose) + } + + queries.alter.unshift("ALTER TABLE `"+table+"` CHANGE `"+column.name+"` `"+column.name+"` "+column.detail) + }else{ + queries.alter.unshift("ALTER TABLE `"+table+"` ADD `"+column.name+"` "+column.detail) + } } + // console.log("==================") } } @@ -1025,6 +1077,12 @@ class IMPORTER { } } } + + if(_failed.length > 0){ + await self._mysql.rollback() + }else{ + await self._mysql.commit() + } if(closeConnection === true) await self.close(); diff --git a/sample/from_pma_update_column.sql b/sample/from_pma_update_column.sql new file mode 100644 index 0000000..adf2ee3 --- /dev/null +++ b/sample/from_pma_update_column.sql @@ -0,0 +1,96 @@ +-- phpMyAdmin SQL Dump +-- version 5.2.1 +-- https://www.phpmyadmin.net/ +-- +-- Host: localhost +-- Generation Time: Dec 13, 2023 at 08:52 PM +-- Server version: 10.6.12-MariaDB-0ubuntu0.22.04.1 +-- PHP Version: 8.1.2-1ubuntu2.14 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `test_db` +-- + +DELIMITER $$ +-- +-- Procedures +-- +CREATE DEFINER=`root`@`localhost` PROCEDURE `p_test` () BEGIN +SELECT aa.mahasiswaUsername FROM d_access_proposal aa; +END$$ + +-- +-- Functions +-- +CREATE DEFINER=`root`@`localhost` FUNCTION `f_test` () RETURNS INT(11) BEGIN +RETURN 1; +END$$ + +DELIMITER ; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `d_access_proposal` +-- + +CREATE TABLE `d_access_proposal` ( + `mahasiswaUsername` varchar(64) NOT NULL, + `labCode` varchar(64) NOT NULL, + `itemCode` varchar(64) DEFAULT NULL, + `proposedDate` date NOT NULL, + `proposedTime` time DEFAULT NULL, + `statusCode` text NULL DEFAULT "NO NO", + `confirmedBy` varchar(64) DEFAULT NULL, + `createdDate` timestamp NOT NULL DEFAULT current_timestamp(), + `deletedDate` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- +-- Dumping data for table `d_access_proposal` +-- + +INSERT INTO `d_access_proposal` (`mahasiswaUsername`, `labCode`, `itemCode`, `proposedDate`, `proposedTime`, `statusCode`, `confirmedBy`, `createdDate`, `deletedDate`) VALUES +('are@gmail.com', 'SKJ', 'itemB', '2022-08-14', '09:46:52', 'ACCEPT', NULL, '2022-08-12 15:47:22', NULL), +('are@gmail.com', 'SKJ', 'itemA', '2022-08-19', '12:00:00', 'REJECT', NULL, '2022-08-13 01:07:03', NULL), +('eieieie@gmail.com', 'SKJ', 'itemC', '2022-08-13', '09:46:52', 'REJECT', NULL, '2022-08-12 15:47:22', NULL), +('esesese@gmail.com', 'SKJ', 'itemD', '2022-08-10', '13:00:00', 'REJECT', NULL, '2022-08-13 01:20:20', NULL), +('muham@mail.ugm.ac.id', 'SKJ', 'itemE', '2022-08-09', '14:00:00', 'IDLE', NULL, '2022-08-13 00:18:01', '2022-08-13 00:43:57'), +('muham@mail.ugm.ac.id', 'SKJ', 'itemF', '2022-08-08', '13:00:00', 'ACCEPT', NULL, '2022-08-13 04:31:05', NULL), +('nande@gmail.com', 'SKJ', 'itemG', '2022-08-05', '12:00:00', 'ACCEPT', NULL, '2022-08-13 01:19:56', NULL); + +-- +-- Triggers `d_access_proposal` +-- +DELIMITER $$ +CREATE TRIGGER `t_test` BEFORE INSERT ON `d_access_proposal` FOR EACH ROW SET new.confirmedBy = NULL +$$ +DELIMITER ; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `d_access_proposal` +-- +ALTER TABLE `d_access_proposal` + ADD PRIMARY KEY (`mahasiswaUsername`,`labCode`,`createdDate`) USING BTREE, + ADD KEY `labCode` (`labCode`), + ADD KEY `confirmedBy` (`confirmedBy`), + ADD KEY `statusCode` (`statusCode`); +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/sample/from_pma_update_column_nulling.sql b/sample/from_pma_update_column_nulling.sql new file mode 100644 index 0000000..3037dfa --- /dev/null +++ b/sample/from_pma_update_column_nulling.sql @@ -0,0 +1,96 @@ +-- phpMyAdmin SQL Dump +-- version 5.2.1 +-- https://www.phpmyadmin.net/ +-- +-- Host: localhost +-- Generation Time: Dec 13, 2023 at 08:52 PM +-- Server version: 10.6.12-MariaDB-0ubuntu0.22.04.1 +-- PHP Version: 8.1.2-1ubuntu2.14 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `test_db` +-- + +DELIMITER $$ +-- +-- Procedures +-- +CREATE DEFINER=`root`@`localhost` PROCEDURE `p_test` () BEGIN +SELECT aa.mahasiswaUsername FROM d_access_proposal aa; +END$$ + +-- +-- Functions +-- +CREATE DEFINER=`root`@`localhost` FUNCTION `f_test` () RETURNS INT(11) BEGIN +RETURN 1; +END$$ + +DELIMITER ; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `d_access_proposal` +-- + +CREATE TABLE `d_access_proposal` ( + `mahasiswaUsername` varchar(64) NOT NULL, + `labCode` varchar(64) NOT NULL, + `itemCode` varchar(64) DEFAULT NULL, + `proposedDate` date NOT NULL, + `proposedTime` time DEFAULT NULL, + `statusCode` text NULL DEFAULT NULL, + `confirmedBy` varchar(64) DEFAULT NULL, + `createdDate` timestamp NOT NULL DEFAULT current_timestamp(), + `deletedDate` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- +-- Dumping data for table `d_access_proposal` +-- + +INSERT INTO `d_access_proposal` (`mahasiswaUsername`, `labCode`, `itemCode`, `proposedDate`, `proposedTime`, `statusCode`, `confirmedBy`, `createdDate`, `deletedDate`) VALUES +('are@gmail.com', 'SKJ', 'itemB', '2022-08-14', '09:46:52', 'ACCEPT', NULL, '2022-08-12 15:47:22', NULL), +('are@gmail.com', 'SKJ', 'itemA', '2022-08-19', '12:00:00', 'REJECT', NULL, '2022-08-13 01:07:03', NULL), +('eieieie@gmail.com', 'SKJ', 'itemC', '2022-08-13', '09:46:52', 'REJECT', NULL, '2022-08-12 15:47:22', NULL), +('esesese@gmail.com', 'SKJ', 'itemD', '2022-08-10', '13:00:00', 'REJECT', NULL, '2022-08-13 01:20:20', NULL), +('muham@mail.ugm.ac.id', 'SKJ', 'itemE', '2022-08-09', '14:00:00', 'IDLE', NULL, '2022-08-13 00:18:01', '2022-08-13 00:43:57'), +('muham@mail.ugm.ac.id', 'SKJ', 'itemF', '2022-08-08', '13:00:00', 'ACCEPT', NULL, '2022-08-13 04:31:05', NULL), +('nande@gmail.com', 'SKJ', 'itemG', '2022-08-05', '12:00:00', 'ACCEPT', NULL, '2022-08-13 01:19:56', NULL); + +-- +-- Triggers `d_access_proposal` +-- +DELIMITER $$ +CREATE TRIGGER `t_test` BEFORE INSERT ON `d_access_proposal` FOR EACH ROW SET new.confirmedBy = NULL +$$ +DELIMITER ; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `d_access_proposal` +-- +ALTER TABLE `d_access_proposal` + ADD PRIMARY KEY (`mahasiswaUsername`,`labCode`,`createdDate`) USING BTREE, + ADD KEY `labCode` (`labCode`), + ADD KEY `confirmedBy` (`confirmedBy`), + ADD KEY `statusCode` (`statusCode`); +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/test/module/general.js b/test/module/general.js index 07cd95d..ae0f8a5 100644 --- a/test/module/general.js +++ b/test/module/general.js @@ -1,4 +1,6 @@ -const mysql = new (require(`${__basedir}/lib/mariadb.class.js`))(__config.db); +const mysql = require(`${__basedir}/lib/mariadb.class.js`); +mysql.setConfig(__config.db) + class generalTest{ @@ -104,10 +106,39 @@ class generalTest{ .catch(err => false) } + async isStatusCodeChanged(){ + return mysql.query(` + SELECT + CONCAT( + IF(column_type IS NULL, "", CONCAT(column_type, " ")), + IF(is_nullable = "YES", "NULL", "NOT NULL"), + IF(column_default IS NULL, "", CONCAT(" DEFAULT ", column_default)) + ) as curDetail + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = "d_access_proposal" AND COLUMN_NAME = "statusCode"; + `,[__config.db.database]) + .then(res => { + return res[0].curDetail == `text NULL DEFAULT 'NO NO'` ? true : false + }) + .catch(err => false) + } + + async isStatusCodeHasData(){ + return mysql.query(` + SELECT SUM(IF(statusCode IS NOT NULL, 1, 0)) as total FROM d_access_proposal + `) + .then(res => { + return res[0].total > 0 ? true: false + }) + .catch(err => false) + + } + + async close(){ try{ - await mysql.end(); + await mysql.endAll(); }catch(err){ console.log(err) } diff --git a/test/module/all.test.js b/test/module/pma.test.js similarity index 65% rename from test/module/all.test.js rename to test/module/pma.test.js index 999df6f..db3984d 100644 --- a/test/module/all.test.js +++ b/test/module/pma.test.js @@ -151,6 +151,100 @@ describe("From PMA's export", () => { }) + test("Import partially (Update Column)", async () => { + // import db from sql file + const filepath = __basedir + '/sample/from_pma_update_column.sql'; + + const proc = await importer.init(__config.db) + .read(filepath) + .importFile({ + dropFirst: false, + compareExisting: true + }); + + + // check if routines success imported + const isRoutineExist = await gen.checkRoutine() + expect(isRoutineExist).toBe(true) + + + // check if table success imported + const isTableExist = await gen.checkTable() + expect(isTableExist).toBe(true) + + + // check if table has no content + const isTableHasContent = await gen.checkTableHasContent() + expect(isTableHasContent).toBe(true) + + + // check last update + // check if table has column "itemCode" + const isTableHasItemCode = await gen.checkTableHasItemCode() + expect(isTableHasItemCode).toBe(true) + + // check if table has column "itemCode" and has content + const isTableHasItemCodeContent = await gen.checkTableHasItemCodeContent() + expect(isTableHasItemCodeContent).toBe(false) + + + + // check new update + // check if column "statusCode" has type "text" and has default "NO NO" + const isStatusCodeChanged = await gen.isStatusCodeChanged() + expect(isStatusCodeChanged).toBe(true) + + // check if column "statusCode" still has it's data + const isStatusCodeHasData = await gen.isStatusCodeHasData() + expect(isStatusCodeHasData).toBe(true) + }) + + test("Import partially (Update Column, but clear data)", async () => { + // import db from sql file + const filepath = __basedir + '/sample/from_pma_update_column_nulling.sql'; + + const proc = await importer.init(__config.db) + .read(filepath) + .importFile({ + dropFirst: false, + compareExisting: true, + schemaUpdateClearData: true + }); + + + // check if routines success imported + const isRoutineExist = await gen.checkRoutine() + expect(isRoutineExist).toBe(true) + + + // check if table success imported + const isTableExist = await gen.checkTable() + expect(isTableExist).toBe(true) + + + // check if table has no content + const isTableHasContent = await gen.checkTableHasContent() + expect(isTableHasContent).toBe(true) + + + // check last update + // check if table has column "itemCode" + const isTableHasItemCode = await gen.checkTableHasItemCode() + expect(isTableHasItemCode).toBe(true) + + // check if table has column "itemCode" and has content + const isTableHasItemCodeContent = await gen.checkTableHasItemCodeContent() + expect(isTableHasItemCodeContent).toBe(false) + + + + // check new update + // check if column "statusCode" has no data + const isStatusCodeHasData = await gen.isStatusCodeHasData() + expect(isStatusCodeHasData).toBe(false) + }) + + test("Import partially (Add Empty Table)", async () => { // import db from sql file const filepath = __basedir + '/sample/from_pma_add_table.sql'; @@ -203,6 +297,7 @@ describe("From PMA's export", () => { const isTableItemContent = await gen.checkTableItemHasContent() expect(isTableItemContent).toBe(true) }) + })