From f055cc5a7e5cbd69208b09a8cd979e8e826ebf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Eiras?= Date: Sat, 1 Jan 2022 22:06:07 +0100 Subject: [PATCH 1/2] feat(query): added options.rawErrors to query() method This option causes all errors from the underlying connection/database library to be propagated unformatted and unmodified. Use this for lesser common errors sequelize can't parse or in case you perfer to see the full raw errors. --- lib/dialects/abstract/query.js | 23 +++++++++++++++++++++++ lib/sequelize.js | 1 + test/integration/sequelize/query.test.js | 11 +++++++++++ 3 files changed, 35 insertions(+) diff --git a/lib/dialects/abstract/query.js b/lib/dialects/abstract/query.js index 6aa63ba55629..1f3c0bf4ec18 100644 --- a/lib/dialects/abstract/query.js +++ b/lib/dialects/abstract/query.js @@ -23,6 +23,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; + } } /** @@ -109,6 +117,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/sequelize.js b/lib/sequelize.js index c8ad42189220..8f1df057b164 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -518,6 +518,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 79ab8ffe6562..9cb6199d1b75 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -365,6 +365,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 { From 05c5c84949ffff9bcf8a28c9d8d7bbb5c3e7bb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Eiras?= Date: Mon, 10 Jan 2022 00:06:12 +0100 Subject: [PATCH 2/2] feat(db2/formatError): added support for reporting stack that triggered the query --- lib/dialects/db2/query.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/dialects/db2/query.js b/lib/dialects/db2/query.js index 32cfe171f5d8..8a14ddcaab50 100644 --- a/lib/dialects/db2/query.js +++ b/lib/dialects/db2/query.js @@ -35,12 +35,14 @@ class Query extends AbstractQuery { 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()); } @@ -48,7 +50,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()); } @@ -56,7 +58,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()); } @@ -64,11 +66,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()); } @@ -97,7 +99,7 @@ class Query extends AbstractQuery { connection.prepare(newSql, (err, stmt) => { if (err) { - reject(this.formatError(err)); + reject(this.formatError(err, errStack)); } stmt.execute(params, (err, result, outparams) => { @@ -118,7 +120,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 = []; @@ -355,7 +357,7 @@ class Query extends AbstractQuery { }); } - formatError(err, conn, parameters) { + formatError(err, errStack, conn, parameters) { let match; if (!(err && err.message)) { @@ -411,7 +413,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/) @@ -422,6 +424,7 @@ class Query extends AbstractQuery { fields: null, index: match[1], parent: err, + stack: errStack, }); } @@ -436,10 +439,11 @@ class Query extends AbstractQuery { constraint, table, parent: err, + stack: errStack, }); } - return new sequelizeErrors.DatabaseError(err); + return new sequelizeErrors.DatabaseError(err, { stack: errStack }); } isDropSchemaQuery() {