Skip to content

Commit

Permalink
feat: add options.rawErrors to Sequelize#query method (#13881)
Browse files Browse the repository at this point in the history
Co-authored-by: Zoé <zoe@ephys.dev>
Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 25, 2022
1 parent c0e6350 commit 4c8fa9a
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 10 deletions.
23 changes: 23 additions & 0 deletions lib/dialects/abstract/query.js
Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
24 changes: 14 additions & 10 deletions lib/dialects/db2/query.js
Expand Up @@ -35,40 +35,42 @@ 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());
}
});
} 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());
}
});
} 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());
}
});
} 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());
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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 = [];
Expand Down Expand Up @@ -355,7 +357,7 @@ class Query extends AbstractQuery {
});
}

formatError(err, conn, parameters) {
formatError(err, errStack, conn, parameters) {
let match;

if (!(err && err.message)) {
Expand Down Expand Up @@ -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/)
Expand All @@ -422,6 +424,7 @@ class Query extends AbstractQuery {
fields: null,
index: match[1],
parent: err,
stack: errStack,
});
}

Expand All @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions lib/sequelize.js
Expand Up @@ -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}
*
Expand Down
11 changes: 11 additions & 0 deletions test/integration/sequelize/query.test.js
Expand Up @@ -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 {
Expand Down

0 comments on commit 4c8fa9a

Please sign in to comment.