From 7c588511a37af5a5ab8c483bffa39a4060122d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Eiras?= Date: Tue, 25 Jan 2022 19:58:28 +0100 Subject: [PATCH] feat: add options.rawErrors to `Sequelize#query` method (#13881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ZoƩ Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com> --- lib/dialects/abstract/query.js | 23 +++++++++++++++++ lib/dialects/db2/query.js | 32 +++++++++++++++--------- lib/sequelize.js | 1 + test/integration/sequelize/query.test.js | 11 ++++++++ 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/lib/dialects/abstract/query.js b/lib/dialects/abstract/query.js index 2404a48fe0f8..5e141c66a582 100644 --- a/lib/dialects/abstract/query.js +++ b/lib/dialects/abstract/query.js @@ -24,6 +24,14 @@ class AbstractQuery { ...options }; this.checkLoggingOption(); + + if (options.rawErrors) { + // The default implementation in AbstractQuery just returns the same + // error object. By overidding this.formatError, this saves every dialect + // having to check for options.rawErrors in their own formatError + // implementations. + this.formatError = AbstractQuery.prototype.formatError; + } } /** @@ -107,6 +115,21 @@ class AbstractQuery { return [sql, []]; } + /** + * Formats a raw database error from the database library into a common Sequelize exception. + * + * @param {Error} error The exception object. + * @param {object} errStack The stack trace that started the database query. + * @returns {BaseError} the new formatted error object. + */ + formatError(error, errStack) { + // Default implementation, no formatting. + // Each dialect overrides this method to parse errors from their respective the database engines. + error.stack = errStack; + + return error; + } + /** * Execute the passed sql query. * diff --git a/lib/dialects/db2/query.js b/lib/dialects/db2/query.js index 1e9383aab1d2..9c6ffe32644d 100644 --- a/lib/dialects/db2/query.js +++ b/lib/dialects/db2/query.js @@ -31,12 +31,15 @@ class Query extends AbstractQuery { } else { this.sequelize.log(`Executing (${ this.connection.uuid || 'default' }): ${ this.sql}`, this.options); } + + const errStack = new Error().stack; + return new Promise((resolve, reject) => { // TRANSACTION SUPPORT if (_.startsWith(this.sql, 'BEGIN TRANSACTION')) { connection.beginTransaction(err => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } else { resolve(this.formatResults()); } @@ -44,7 +47,7 @@ class Query extends AbstractQuery { } else if (_.startsWith(this.sql, 'COMMIT TRANSACTION')) { connection.commitTransaction(err => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } else { resolve(this.formatResults()); } @@ -52,7 +55,7 @@ class Query extends AbstractQuery { } else if (_.startsWith(this.sql, 'ROLLBACK TRANSACTION')) { connection.rollbackTransaction(err => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } else { resolve(this.formatResults()); } @@ -60,11 +63,11 @@ class Query extends AbstractQuery { } else if (_.startsWith(this.sql, 'SAVE TRANSACTION')) { connection.commitTransaction(err => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } else { connection.beginTransaction(err => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } else { resolve(this.formatResults()); } @@ -90,7 +93,10 @@ class Query extends AbstractQuery { } connection.prepare(newSql, (err, stmt) => { - if (err) { reject(this.formatError(err)); } + if (err) { + reject(this.formatError(err, errStack)); + } + stmt.execute(params, (err, result, outparams) => { debug(`executed(${this.connection.uuid || 'default'}):${newSql} ${parameters ? JSON.stringify(parameters) : ''}`); @@ -108,7 +114,7 @@ class Query extends AbstractQuery { if (err) { err.sql = sql; stmt.closeSync(); - reject(this.formatError(err, connection, parameters)); + reject(this.formatError(err, errStack, connection, parameters)); } else { let data = []; let metadata = []; @@ -333,7 +339,7 @@ class Query extends AbstractQuery { }); } - formatError(err, conn, parameters) { + formatError(err, errStack, conn, parameters) { let match; if (!(err && err.message)) { @@ -389,7 +395,7 @@ class Query extends AbstractQuery { )); }); - return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields }); + return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields, stack: errStack }); } match = err.message.match(/SQL0532N {2}A parent row cannot be deleted because the relationship "(.*)" restricts the deletion/) || @@ -399,7 +405,8 @@ class Query extends AbstractQuery { return new sequelizeErrors.ForeignKeyConstraintError({ fields: null, index: match[1], - parent: err + parent: err, + stack: errStack }); } @@ -413,11 +420,12 @@ class Query extends AbstractQuery { message: match[0], constraint, table, - parent: err + parent: err, + stack: errStack }); } - return new sequelizeErrors.DatabaseError(err); + return new sequelizeErrors.DatabaseError(err, { stack: errStack }); } diff --git a/lib/sequelize.js b/lib/sequelize.js index a68facc8ecf1..cca1660837c7 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -519,6 +519,7 @@ class Sequelize { * @param {boolean} [options.supportsSearchPath] If false do not prepend the query with the search_path (Postgres only) * @param {boolean} [options.mapToModel=false] Map returned fields to model's fields if `options.model` or `options.instance` is present. Mapping will occur before building the model instance. * @param {object} [options.fieldMap] Map returned fields to arbitrary names for `SELECT` query type. + * @param {boolean} [options.rawErrors=false] Set to `true` to cause errors coming from the underlying connection/database library to be propagated unmodified and unformatted. Else, the default behavior (=false) is to reinterpret errors as sequelize.errors.BaseError objects. * * @returns {Promise} * diff --git a/test/integration/sequelize/query.test.js b/test/integration/sequelize/query.test.js index aeaff34c8a67..1cc9ad76af09 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -342,6 +342,17 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { await this.UserVisit.sync({ force: true }); }); + it('emits raw errors if requested', async function() { + const sql = 'SELECT 1 FROM NotFoundTable'; + + await expect(this.sequelize.query(sql, { rawErrors: false })) + .to.eventually.be.rejectedWith(DatabaseError); + + await expect(this.sequelize.query(sql, { rawErrors: true })) + .to.eventually.be.rejected + .and.not.be.an.instanceOf(DatabaseError); + }); + it('emits full stacktraces for generic database error', async function() { let error = null; try {