import * as exporter from 'common/interfaces/exporter'; import { valueToSqlString } from 'common/libs/sqlUtils'; import { MySQLClient } from '../../clients/MySQLClient'; import { SqlExporter } from './SqlExporter'; export default class MysqlExporter extends SqlExporter { protected _client: MySQLClient; constructor (client: MySQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) { super(tables, options); this._client = client; this._commentChar = '#'; } async getSqlHeader () { let dump = await super.getSqlHeader(); dump += ` /*!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 */; SET NAMES utf8mb4; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!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 */; ${footer} `; } async getCreateTable (tableName: string) { const { rows } = await this._client.raw( `SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\`` ); if (rows.length !== 1) return ''; const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table'; return rows[0][col] + ';'; } getDropTable (tableName: string) { return `DROP TABLE IF EXISTS \`${tableName}\`;`; } async * getTableInsert (tableName: string) { 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; if (rowCount > 0) { let queryLength = 0; let rowsWritten = 0; let rowIndex = 0; const { sqlInsertDivider, sqlInsertAfter } = this._options; const columns = await this._client.getTableColumns({ table: tableName, schema: this.schemaName }); const notGeneratedColumns = columns.filter(col => !col.generated); const columnNames = notGeneratedColumns.map(col => '`' + col.name + '`'); const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join( ', ' )}) VALUES`; sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`; sqlStr += '\n\n'; yield sqlStr; yield insertStmt; const stream = await this._queryStream( `SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\`` ); for await (const row of stream) { if (this.isCancelled) { stream.destroy(); yield null; return; } let sqlInsertString = ''; if ( (sqlInsertDivider === 'bytes' && queryLength >= sqlInsertAfter * 1024) || (sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter) ) { sqlInsertString += `;\n${insertStmt}\n\t(`; queryLength = 0; rowsWritten = 0; } else if (rowIndex === 0) sqlInsertString += '\n\t('; else sqlInsertString += ',\n\t('; for (const i in notGeneratedColumns) { const column = notGeneratedColumns[i]; const val = row[column.name]; sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column }); if (parseInt(i) !== notGeneratedColumns.length - 1) sqlInsertString += ', '; } sqlInsertString += ')'; queryLength += sqlInsertString.length; rowsWritten++; rowIndex++; yield sqlInsertString; } sqlStr = ';\n\n'; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`; sqlStr += 'UNLOCK TABLES;'; yield sqlStr; } } async getViews () { const { rows: views } = await this._client.raw( `SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'` ); let sqlString = ''; sqlString += this.buildComment('Creating temporary tables to overcome VIEW dependency errors'); // Temporary tables for (const view of views) { const viewFields = await this._client.getTableColumns({ schema: this.schemaName, table: view.Name }); const tableFields: string[] = []; for (const field of viewFields) { const typeInfo = this._client.getTypeInfo(field.type); const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; tableFields.push(`\`${field.name}\` ${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''} ${field.unsigned ? 'UNSIGNED' : ''} ${field.zerofill ? 'ZEROFILL' : ''} ${field.nullable ? 'NULL' : 'NOT NULL'} ${field.autoIncrement ? 'AUTO_INCREMENT' : ''} ${field.collation ? `COLLATE ${field.collation}` : ''}`); } sqlString += ` CREATE TABLE \`${view.Name}\`( ${tableFields.join(',\n\t')} );`; sqlString += '\n'; } sqlString += '\n'; for (const view of views) { sqlString += `DROP TABLE IF EXISTS \`${view.Name}\`;\n`; const viewSyntax = await this.getCreateTable(view.Name); sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', ''); sqlString += '\n\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;// Commented to avoid issues if export contains triggers without tables 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) { const definer = this.getEscapedDefiner(func.Definer); sqlString += await this.getRoutineSyntax( func.Name, func.Type, 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) { const definer = this.getEscapedDefiner(routine.Definer); sqlString += await this.getRoutineSyntax( routine.Name, routine.Type, definer ); } return sqlString; } async getRoutineSyntax (name: string, type: string, definer: string) { const { rows: routines } = await this._client.raw( `SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\`` ); if (routines.length === 0) return ''; const routine = routines[0]; const fieldName = `Create ${type === 'PROCEDURE' ? 'Procedure' : 'Function'}`; const sqlMode = routine.sql_mode; const createProcedure = routine[fieldName]; let sqlString = ''; if (createProcedure) { // If procedure body not empty const startOffset = createProcedure.indexOf(type); const procedureBody = createProcedure.substring(startOffset); 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 += 'DELIMITER ;;\n'; sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`; sqlString += 'DELIMITER ;\n'; sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;\n'; } return sqlString; } async _queryStream (sql: string) { const connection = await this._client.getConnection(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const stream = (connection as any).connection.query(sql).stream(); const dispose = () => connection.end(); stream.on('end', dispose); stream.on('error', dispose); stream.on('close', dispose); return stream; } getEscapedDefiner (definer: string) { return definer .split('@') .map(part => '`' + part + '`') .join('@'); } escapeAndQuote (val: string) { // eslint-disable-next-line no-control-regex const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_ESCAPE_MAP: Record = { '\0': '\\0', '\b': '\\b', '\t': '\\t', '\n': '\\n', '\r': '\\r', '\x1a': '\\Z', '"': '\\"', '\'': '\\\'', '\\': '\\\\' }; let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0; let escapedVal = ''; let match; while ((match = CHARS_TO_ESCAPE.exec(val))) { escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]]; chunkIndex = CHARS_TO_ESCAPE.lastIndex; } if (chunkIndex === 0) return `'${val}'`; if (chunkIndex < val.length) return `'${escapedVal + val.slice(chunkIndex)}'`; return `'${escapedVal}'`; } }