Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add options.rawErrors to Sequelize#query method #13881

Merged
merged 4 commits into from Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
joaoe marked this conversation as resolved.
Show resolved Hide resolved
// 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