'use strict'; const AbstractQuery = require('../abstract/query'); const sequelizeErrors = require('../../errors'); const parserStore = require('../parserStore')('db2'); const _ = require('lodash'); const { logger } = require('../../utils/logger'); const moment = require('moment'); const debug = logger.debugContext('sql:db2'); class Query extends AbstractQuery { getInsertIdField() { return 'id'; } getSQLTypeFromJsType(value) { const param = { ParamType: 'INPUT', Data: value }; if (Buffer.isBuffer(value)) { param.DataType = 'BLOB'; return param; } return value; } async _run(connection, sql, parameters) { this.sql = sql; const benchmark = this.sequelize.options.benchmark || this.options.benchmark; let queryBegin; if (benchmark) { queryBegin = Date.now(); } else { this.sequelize.log(`Executing (${ this.connection.uuid || 'default' }): ${ this.sql}`, this.options); } return new Promise((resolve, reject) => { // TRANSACTION SUPPORT if (_.startsWith(this.sql, 'BEGIN TRANSACTION')) { connection.beginTransaction(err => { if (err) { reject(this.formatError(err)); } else { resolve(this.formatResults()); } }); } else if (_.startsWith(this.sql, 'COMMIT TRANSACTION')) { connection.commitTransaction(err => { if (err) { reject(this.formatError(err)); } else { resolve(this.formatResults()); } }); } else if (_.startsWith(this.sql, 'ROLLBACK TRANSACTION')) { connection.rollbackTransaction(err => { if (err) { reject(this.formatError(err)); } else { resolve(this.formatResults()); } }); } else if (_.startsWith(this.sql, 'SAVE TRANSACTION')) { connection.commitTransaction(err => { if (err) { reject(this.formatError(err)); } else { connection.beginTransaction(err => { if (err) { reject(this.formatError(err)); } else { resolve(this.formatResults()); } }); } }, this.options.transaction.name); } else { const params = []; if (parameters) { _.forOwn(parameters, (value, key) => { const param = this.getSQLTypeFromJsType(value, key); params.push(param); }); } const SQL = this.sql.toUpperCase(); let newSql = this.sql; if ((this.isSelectQuery() || _.startsWith(SQL, 'SELECT ')) && SQL.indexOf(' FROM ', 8) === -1 ) { if (this.sql.charAt(this.sql.length - 1) === ';') { newSql = this.sql.slice(0, this.sql.length - 1); } newSql += ' FROM SYSIBM.SYSDUMMY1;'; } connection.prepare(newSql, (err, stmt) => { if (err) { reject(this.formatError(err)); } stmt.execute(params, (err, result, outparams) => { debug(`executed(${this.connection.uuid || 'default'}):${newSql} ${parameters ? JSON.stringify(parameters) : ''}`); if (benchmark) { this.sequelize.log(`Executed (${ this.connection.uuid || 'default' }): ${ newSql} ${parameters ? JSON.stringify(parameters) : ''}`, Date.now() - queryBegin, this.options); } if (err && err.message) { err = this.filterSQLError(err, this.sql, connection); if (err === null) { stmt.closeSync(); resolve(this.formatResults([], 0)); } } if (err) { err.sql = sql; stmt.closeSync(); reject(this.formatError(err, connection, parameters)); } else { let data = []; let metadata = []; let affectedRows = 0; if (typeof result === 'object') { if (_.startsWith(this.sql, 'DELETE FROM ')) { affectedRows = result.getAffectedRowsSync(); } else { data = result.fetchAllSync(); metadata = result.getColumnMetadataSync(); } result.closeSync(); } stmt.closeSync(); const datalen = data.length; if (datalen > 0) { const coltypes = {}; for (let i = 0; i < metadata.length; i++) { coltypes[metadata[i].SQL_DESC_NAME] = metadata[i].SQL_DESC_TYPE_NAME; } for (let i = 0; i < datalen; i++) { for (const column in data[i]) { const parse = parserStore.get(coltypes[column]); const value = data[i][column]; if (value !== null) { if (parse) { data[i][column] = parse(value); } else if (coltypes[column] === 'TIMESTAMP') { data[i][column] = new Date(moment.utc(value)); } else if (coltypes[column] === 'BLOB') { data[i][column] = new Buffer.from(value); } else if (coltypes[column].indexOf('FOR BIT DATA') > 0) { data[i][column] = new Buffer.from(value, 'hex'); } } } } if (outparams && outparams.length) { data.unshift(outparams); } resolve(this.formatResults(data, datalen, metadata, connection)); } else { resolve(this.formatResults(data, affectedRows)); } } }); }); } }); } async run(sql, parameters) { return await this._run(this.connection, sql, parameters); } static formatBindParameters(sql, values, dialect) { let bindParam = {}; const replacementFunc = (match, key, values) => { if (values[key] !== undefined) { bindParam[key] = values[key]; return '?'; } return undefined; }; sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0]; if (Array.isArray(values) && typeof values[0] === 'object') { bindParam = values; } return [sql, bindParam]; } filterSQLError(err, sql, connection) { if (err.message.search('SQL0204N') != -1 && _.startsWith(sql, 'DROP ')) { err = null; // Ignore table not found error for drop table. } else if (err.message.search('SQL0443N') != -1) { if (this.isDropSchemaQuery()) { // Delete ERRORSCHEMA.ERRORTABLE if it exist. connection.querySync('DROP TABLE ERRORSCHEMA.ERRORTABLE;'); // Retry deleting the schema connection.querySync(this.sql); } err = null; // Ignore drop schema error. } else if (err.message.search('SQL0601N') != -1) { const match = err.message.match(/SQL0601N {2}The name of the object to be created is identical to the existing name "(.*)" of type "(.*)"./); if (match && match.length > 1 && match[2] === 'TABLE') { let table; const mtarray = match[1].split('.'); if (mtarray[1]) { table = `"${mtarray[0]}"."${mtarray[1]}"`; } else { table = `"${mtarray[0]}"`; } if (connection.dropTable !== false) { connection.querySync(`DROP TABLE ${table}`); err = connection.querySync(sql); } else { err = null; } } else { err = null; // Ignore create schema error. } } else if (err.message.search('SQL0911N') != -1) { if (err.message.search('Reason code "2"') != -1) { err = null; // Ignore deadlock error due to program logic. } } else if (err.message.search('SQL0605W') != -1) { err = null; // Ignore warning. } else if (err.message.search('SQL0668N') != -1 && _.startsWith(sql, 'ALTER TABLE ')) { connection.querySync(`CALL SYSPROC.ADMIN_CMD('REORG TABLE ${sql.substring(12).split(' ')[0]}')`); err = connection.querySync(sql); } if (err && err.length === 0) { err = null; } return err; } /** * High level function that handles the results of a query execution. * * * Example: * query.formatResults([ * { * id: 1, // this is from the main table * attr2: 'snafu', // this is from the main table * Tasks.id: 1, // this is from the associated table * Tasks.title: 'task' // this is from the associated table * } * ]) * * @param {Array} data - The result of the query execution. * @param {Integer} rowCount - The number of affected rows. * @param {Array} metadata - Metadata of the returned result set. * @param {object} conn - The connection object. * @private */ formatResults(data, rowCount, metadata, conn) { let result = this.instance; if (this.isInsertQuery(data, metadata)) { this.handleInsertQuery(data, metadata); if (!this.instance) { if (this.options.plain) { const record = data[0]; result = record[Object.keys(record)[0]]; } else { result = data; } } } if (this.isShowTablesQuery()) { result = data; } else if (this.isDescribeQuery()) { result = {}; for (const _result of data) { if (_result.Default) { _result.Default = _result.Default.replace("('", '').replace("')", '').replace(/'/g, ''); } result[_result.Name] = { type: _result.Type.toUpperCase(), allowNull: _result.IsNull === 'Y' ? true : false, defaultValue: _result.Default, primaryKey: _result.KeySeq > 0, autoIncrement: _result.IsIdentity === 'Y' ? true : false, comment: _result.Comment }; } } else if (this.isShowIndexesQuery()) { result = this.handleShowIndexesQuery(data); } else if (this.isSelectQuery()) { result = this.handleSelectQuery(data); } else if (this.isUpsertQuery()) { result = data; } else if (this.isDropSchemaQuery()) { result = data[0]; if (conn) { const query = 'DROP TABLE ERRORSCHEMA.ERRORTABLE'; conn.querySync(query); } } else if (this.isCallQuery()) { result = data; } else if (this.isBulkUpdateQuery()) { result = data.length; } else if (this.isBulkDeleteQuery()) { result = rowCount; } else if (this.isVersionQuery()) { result = data[0].VERSION; } else if (this.isForeignKeysQuery()) { result = data; } else if (this.isInsertQuery() || this.isUpdateQuery()) { result = [result, rowCount]; } else if (this.isShowConstraintsQuery()) { result = this.handleShowConstraintsQuery(data); } else if (this.isRawQuery()) { // Db2 returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta result = [data, metadata]; } else { result = data; } return result; } handleShowTablesQuery(results) { return results.map(resultSet => { return { tableName: resultSet.TABLE_NAME, schema: resultSet.TABLE_SCHEMA }; }); } handleShowConstraintsQuery(data) { // Remove SQL Contraints from constraints list. return _.remove(data, constraint => { return !_.startsWith(constraint.constraintName, 'SQL'); }); } formatError(err, conn, parameters) { let match; if (!(err && err.message)) { err['message'] = 'No error message found.'; } match = err.message.match(/SQL0803N {2}One or more values in the INSERT statement, UPDATE statement, or foreign key update caused by a DELETE statement are not valid because the primary key, unique constraint or unique index identified by "(\d)+" constrains table "(.*)\.(.*)" from having duplicate values for the index key./); if (match && match.length > 0) { let uniqueIndexName = ''; let uniqueKey = ''; const fields = {}; let message = err.message; const query = `SELECT INDNAME FROM SYSCAT.INDEXES WHERE IID = ${match[1]} AND TABSCHEMA = '${match[2]}' AND TABNAME = '${match[3]}'`; if (!!conn && match.length > 3) { uniqueIndexName = conn.querySync(query); uniqueIndexName = uniqueIndexName[0]['INDNAME']; } if (this.model && !!uniqueIndexName) { uniqueKey = this.model.uniqueKeys[uniqueIndexName]; } if (!uniqueKey && this.options.fields) { uniqueKey = this.options.fields[match[1] - 1]; } if (uniqueKey) { if (this.options.where && this.options.where[uniqueKey.column] !== undefined) { fields[uniqueKey.column] = this.options.where[uniqueKey.column]; } else if (this.options.instance && this.options.instance.dataValues && this.options.instance.dataValues[uniqueKey.column]) { fields[uniqueKey.column] = this.options.instance.dataValues[uniqueKey.column]; } else if (parameters) { fields[uniqueKey.column] = parameters['0']; } } if (uniqueKey && !!uniqueKey.msg) { message = uniqueKey.msg; } const errors = []; _.forOwn(fields, (value, field) => { errors.push(new sequelizeErrors.ValidationErrorItem( this.getUniqueConstraintErrorMessage(field), 'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB, field, value, this.instance, 'not_unique' )); }); return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields }); } match = err.message.match(/SQL0532N {2}A parent row cannot be deleted because the relationship "(.*)" restricts the deletion/) || err.message.match(/SQL0530N/) || err.message.match(/SQL0531N/); if (match && match.length > 0) { return new sequelizeErrors.ForeignKeyConstraintError({ fields: null, index: match[1], parent: err }); } match = err.message.match(/SQL0204N {2}"(.*)" is an undefined name./); if (match && match.length > 1) { const constraint = match[1]; let table = err.sql.match(/table "(.+?)"/i); table = table ? table[1] : undefined; return new sequelizeErrors.UnknownConstraintError({ message: match[0], constraint, table, parent: err }); } return new sequelizeErrors.DatabaseError(err); } isDropSchemaQuery() { let result = false; if (_.startsWith(this.sql, 'CALL SYSPROC.ADMIN_DROP_SCHEMA')) { result = true; } return result; } isShowOrDescribeQuery() { let result = false; result = result || this.sql.toLowerCase().startsWith("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'"); result = result || this.sql.toLowerCase().startsWith('select tablename = t.name, name = ind.name,'); result = result || this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname'); return result; } isShowIndexesQuery() { let result = false; result = result || this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname'); result = result || this.sql.startsWith('SELECT NAME AS "name", TBNAME AS "tableName", UNIQUERULE AS "keyType", COLNAMES, INDEXTYPE AS "type" FROM SYSIBM.SYSINDEXES'); return result; } handleShowIndexesQuery(data) { let currItem; const result = []; data.forEach(item => { if (!currItem || currItem.name !== item.Key_name) { currItem = { primary: item.keyType === 'P', fields: [], name: item.name, tableName: item.tableName, unique: item.keyType === 'U', type: item.type }; _.forEach(item.COLNAMES.replace(/\+|-/g, x => { return ` ${ x}`; }).split(' '), column => { let columnName = column.trim(); if ( columnName ) { columnName = columnName.replace(/\+|-/, ''); currItem.fields.push({ attribute: columnName, length: undefined, order: column.indexOf('-') === -1 ? 'ASC' : 'DESC', collate: undefined }); } }); result.push(currItem); } }); return result; } handleInsertQuery(results, metaData) { if (this.instance) { // add the inserted row id to the instance const autoIncrementAttribute = this.model.autoIncrementAttribute; let id = null; let autoIncrementAttributeAlias = null; if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) && this.model.rawAttributes[autoIncrementAttribute].field !== undefined) autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field; id = id || results && results[0][this.getInsertIdField()]; id = id || metaData && metaData[this.getInsertIdField()]; id = id || results && results[0][autoIncrementAttribute]; id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias]; this.instance[autoIncrementAttribute] = id; } } } module.exports = Query; module.exports.Query = Query; module.exports.default = Query;