diff --git a/src/main/ipc-handlers/schema.js b/src/main/ipc-handlers/schema.js index 7380823e..207a8fd7 100644 --- a/src/main/ipc-handlers/schema.js +++ b/src/main/ipc-handlers/schema.js @@ -167,14 +167,12 @@ export default connections => { } }); - ipcMain.handle('export', async (event, { uid, ...rest }) => { + ipcMain.handle('export', async (event, { uid, type, tables, ...rest }) => { if (exporter !== null) return; - const type = connections[uid]._client; - switch (type) { case 'mysql': - exporter = new MysqlExporter(connections[uid], rest); + exporter = new MysqlExporter(connections[uid], tables, rest); break; default: return { diff --git a/src/main/libs/exporters/BaseExporter.js b/src/main/libs/exporters/BaseExporter.js index fe5e5970..fa5ce605 100644 --- a/src/main/libs/exporters/BaseExporter.js +++ b/src/main/libs/exporters/BaseExporter.js @@ -3,8 +3,9 @@ import path from 'path'; import EventEmitter from 'events'; export class BaseExporter extends EventEmitter { - constructor (options) { + constructor (tables, options) { super(); + this._tables = tables; this._options = options; this._isCancelled = false; this._outputStream = fs.createWriteStream(this._options.outputFile, { diff --git a/src/main/libs/exporters/sql/MysqlExporter.js b/src/main/libs/exporters/sql/MysqlExporter.js index 875cd0b7..49fd5a38 100644 --- a/src/main/libs/exporters/sql/MysqlExporter.js +++ b/src/main/libs/exporters/sql/MysqlExporter.js @@ -13,28 +13,36 @@ export default class MysqlExporter extends SqlExporter { /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; SET NAMES utf8mb4; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`; return dump; } async getFooter () { + const footer = await super.getFooter(); + return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!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 */;`; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; + +${footer} +`; } async getCreateTable (tableName) { - const { rows } = await this._client.raw(`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``); + const { rows } = await this._client.raw( + `SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\`` + ); - if (rows.length !== 1) - return ''; + if (rows.length !== 1) return ''; - return rows[0]['Create Table'] + ';'; + const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table'; + + return rows[0][col] + ';'; } getDropTable (tableName) { @@ -45,54 +53,82 @@ SET NAMES utf8mb4; let rowCount = 0; let sqlStr = ''; - const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``); - if (countResults.rows.length === 1) - rowCount = countResults.rows[0].count; + const countResults = await this._client.raw( + `SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\`` + ); + if (countResults.rows.length === 1) rowCount = countResults.rows[0].count; if (rowCount > 0) { - const columns = await this._client.getTableColumns({ table: tableName, schema: this.schemaName }); + let queryLength = 0; + let rowsWritten = 0; + const { sqlInsertDivider, sqlInsertAfter } = this._options; + const columns = await this._client.getTableColumns({ + table: tableName, + schema: this.schemaName + }); const columnNames = columns.map(col => '`' + col.name + '`'); - const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(', ')}) VALUES`; + const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join( + ', ' + )}) VALUES`; - const tableResult = await this._client.raw(`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``); + const tableResult = await this._client.raw( + `SELECT ${columnNames.join(', ')} FROM \`${ + this.schemaName + }\`.\`${tableName}\`` + ); sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`; sqlStr += '\n\n'; sqlStr += insertStmt; - sqlStr += '\n'; - for (const row of tableResult.rows) { - sqlStr += '\t('; + for (const rowIndex in tableResult.rows) { + const row = tableResult.rows[rowIndex]; + let sqlInsertString = ''; + + if ( + (sqlInsertDivider === 'bytes' && + queryLength >= sqlInsertAfter * 1024) || + (sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter) + ) { + sqlInsertString += `;\n${insertStmt}\n\t(`; + + queryLength = 0; + rowsWritten = 0; + } + else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t('; + else sqlInsertString += ',\n\t('; for (const i in columns) { const column = columns[i]; const val = row[column.name]; - if (val === null) - sqlStr += 'NULL'; - - else if (BIT.includes(column.type)) - sqlStr += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; - + if (val === null) sqlInsertString += 'NULL'; + else if (BIT.includes(column.type)) { + sqlInsertString += `b'${hexToBinary( + Buffer.from(val).toString('hex') + )}'`; + } else if (BLOB.includes(column.type)) - sqlStr += `X'${val.toString('hex').toUpperCase()}'`; - - else if (val === '') - sqlStr += '\'\''; + sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`; + else if (val === '') sqlInsertString += '\'\''; + else { + sqlInsertString += + typeof val === 'string' ? this.escapeAndQuote(val) : val; + } - else - sqlStr += typeof val === 'string' ? this.escapeAndQuote(val) : val; - - if (parseInt(i) !== columns.length - 1) - sqlStr += ', '; + if (parseInt(i) !== columns.length - 1) sqlInsertString += ', '; } - sqlStr += '),\n'; + sqlInsertString += ')'; + sqlStr += sqlInsertString; + + queryLength += sqlInsertString.length; + rowsWritten++; } - sqlStr += '\n'; + sqlStr += ';\n\n'; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`; sqlStr += 'UNLOCK TABLES;'; @@ -101,6 +137,173 @@ SET NAMES utf8mb4; return sqlStr; } + async getViews () { + const { rows: views } = await this._client.raw( + `SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'` + ); + let sqlString = ''; + + for (const view of views) { + sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`; + const viewSyntax = await this.getCreateTable(view.Name); + sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', ''); + sqlString += '\n'; + } + + return sqlString; + } + + async getTriggers () { + const { rows: triggers } = await this._client.raw( + `SHOW TRIGGERS FROM \`${this.schemaName}\`` + ); + const generatedTables = this._tables + .filter(t => t.includeStructure) + .map(t => t.table); + + let sqlString = ''; + + for (const trigger of triggers) { + const { + Trigger: name, + Timing: timing, + Event: event, + Table: table, + Statement: statement, + sql_mode: sqlMode + } = trigger; + + if (!generatedTables.includes(table)) continue; + + const definer = this.getEscapedDefiner(trigger.Definer); + sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n'; + sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`; + sqlString += 'DELIMITER ;;\n'; + sqlString += '/*!50003 CREATE*/ '; + sqlString += `/*!50017 DEFINER=${definer}*/ `; + sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON ${table} FOR EACH ROW ${statement}*/;;\n`; + sqlString += 'DELIMITER ;\n'; + sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n'; + } + + return sqlString; + } + + async getSchedulers () { + const { rows: schedulers } = await this._client.raw( + `SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'` + ); + let sqlString = ''; + + for (const scheduler of schedulers) { + const { + EVENT_NAME: name, + SQL_MODE: sqlMode, + EVENT_TYPE: type, + INTERVAL_VALUE: intervalValue, + INTERVAL_FIELD: intervalField, + STARTS: starts, + ENDS: ends, + EXECUTE_AT: at, + ON_COMPLETION: onCompletion, + STATUS: status, + EVENT_DEFINITION: definition + } = scheduler; + + const definer = this.getEscapedDefiner(scheduler.DEFINER); + const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT); + + sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`; + sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n'; + sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`; + sqlString += 'DELIMITER ;;\n'; + sqlString += '/*!50106 CREATE*/ '; + sqlString += `/*!50117 DEFINER=${definer}*/ `; + sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `; + if (type === 'RECURRING') { + sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `; + + if (ends) sqlString += `ENDS '${ends}' `; + } + else sqlString += `AT '${at}' `; + sqlString += `ON COMPLETION ${onCompletion} ${ + status === 'disabled' ? 'DISABLE' : 'ENABLE' + } COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`; + sqlString += 'DELIMITER ;\n'; + sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n'; + } + + return sqlString; + } + + async getFunctions () { + const { rows: functions } = await this._client.raw( + `SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';` + ); + + let sqlString = ''; + + for (const func of functions) { + sqlString += await this.getRoutineSyntax( + func.Name, + func.Type, + func.Definer + ); + } + + return sqlString; + } + + async getRoutines () { + const { rows: routines } = await this._client.raw( + `SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';` + ); + + let sqlString = ''; + + for (const routine of routines) { + sqlString += await this.getRoutineSyntax( + routine.Name, + routine.Type, + routine.Definer + ); + } + + return sqlString; + } + + async getRoutineSyntax (name, type, definer) { + const { rows: routines } = await this._client.raw( + `SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\`` + ); + + if (routines.length === 0) return ''; + + const routine = routines[0]; + + const { sql_mode: sqlMode, 'Create Function': createProcedure } = routine; + + const startOffset = createProcedure.indexOf(type); + const procedureBody = createProcedure.substring(startOffset); + + let sqlString = 'DELIMITER ;;\n'; + sqlString = `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`; + sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n'; + sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`; + sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`; + sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n'; + sqlString += 'DELIMITER ;\n'; + + return sqlString; + } + + getEscapedDefiner (definer) { + return definer + .split('@') + .map(part => '`' + part + '`') + .join('@'); + } + escapeAndQuote (value) { if (!value) return null; return `'${value.replaceAll(/'/g, '\'\'')}'`; diff --git a/src/main/libs/exporters/sql/SqlExporter.js b/src/main/libs/exporters/sql/SqlExporter.js index 6d488dd5..e8aed7fd 100644 --- a/src/main/libs/exporters/sql/SqlExporter.js +++ b/src/main/libs/exporters/sql/SqlExporter.js @@ -3,8 +3,8 @@ import moment from 'moment'; import { BaseExporter } from '../BaseExporter'; export class SqlExporter extends BaseExporter { - constructor (client, options) { - super(options); + constructor (client, tables, options) { + super(tables, options); this._client = client; this._commentChar = '#'; } @@ -23,8 +23,15 @@ export class SqlExporter extends BaseExporter { } async dump () { + const { includes } = this._options; + const extraItems = Object.keys(includes).filter(key => includes[key]); + const totalTableToProcess = this._tables.filter( + t => t.includeStructure || t.includeContent || t.includeDropStatement + ).length; + const processingItemCount = totalTableToProcess + extraItems.length; + const exportState = { - totalItems: this._options.items.length, + totalItems: processingItemCount, currentItemIndex: 0, currentItem: '', op: '' @@ -34,13 +41,16 @@ export class SqlExporter extends BaseExporter { this.writeString(header); this.writeString('\n\n\n'); - for (const item of this._options.items) { + for (const item of this._tables) { // user abort operation - if (this.isCancelled) - return; + if (this.isCancelled) return; // skip item if not set to output any detail for them - if (!item.includeStructure && !item.includeContent && !item.includeDropStatement) + if ( + !item.includeStructure && + !item.includeContent && + !item.includeDropStatement + ) continue; exportState.currentItemIndex++; @@ -49,7 +59,9 @@ export class SqlExporter extends BaseExporter { this.emitUpdate(exportState); - const tableHeader = this.buildComment(`Dump of table ${item.table}\n------------------------------------------------------------`); + const tableHeader = this.buildComment( + `Dump of table ${item.table}\n------------------------------------------------------------` + ); this.writeString(tableHeader); this.writeString('\n\n'); @@ -79,12 +91,38 @@ export class SqlExporter extends BaseExporter { this.writeString('\n\n'); } + for (const item of extraItems) { + const processingMethod = `get${item.charAt(0).toUpperCase() + + item.slice(1)}`; + exportState.currentItemIndex++; + exportState.currentItem = item; + exportState.op = 'PROCESSING'; + this.emitUpdate(exportState); + + if (this[processingMethod]) { + const data = await this[processingMethod](); + if (data !== '') { + const header = + this.buildComment( + `Dump of ${item}\n------------------------------------------------------------` + ) + '\n\n'; + + this.writeString(header); + this.writeString(data); + this.writeString('\n\n'); + } + } + } + const footer = await this.getFooter(); this.writeString(footer); } buildComment (text) { - return text.split('\n').map(txt => `${this._commentChar} ${txt}`).join('\n'); + return text + .split('\n') + .map(txt => `${this._commentChar} ${txt}`) + .join('\n'); } async getSqlHeader () { @@ -105,11 +143,13 @@ Generation time: ${moment().format()} } async getFooter () { - return ''; + return this.buildComment(`Dump completed on ${moment().format()}`); } getCreateTable (tableName) { - throw new Error('Sql Exporter must implement the "getCreateTable" method'); + throw new Error( + 'Sql Exporter must implement the "getCreateTable" method' + ); } getDropTable (tableName) { @@ -117,6 +157,8 @@ Generation time: ${moment().format()} } getTableInsert (tableName) { - throw new Error('Sql Exporter must implement the "getTableInsert" method'); + throw new Error( + 'Sql Exporter must implement the "getTableInsert" method' + ); } } diff --git a/src/renderer/components/ModalExportSchema.vue b/src/renderer/components/ModalExportSchema.vue index 5ed537ff..b3aedd22 100644 --- a/src/renderer/components/ModalExportSchema.vue +++ b/src/renderer/components/ModalExportSchema.vue @@ -37,111 +37,168 @@ + -