Skip to content

Commit

Permalink
feat: add options.rawErrors to Sequelize#query method (sequelize#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 authored and aliatsis committed Jun 2, 2022
1 parent 8d32df5 commit a02546d
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 12 deletions.
23 changes: 23 additions & 0 deletions lib/dialects/abstract/query.js
Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
32 changes: 20 additions & 12 deletions lib/dialects/db2/query.js
Expand Up @@ -31,40 +31,43 @@ 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());
}
});
} 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 All @@ -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) : ''}`);

Expand All @@ -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 = [];
Expand Down Expand Up @@ -333,7 +339,7 @@ class Query extends AbstractQuery {
});
}

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

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

Expand All @@ -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 });
}


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

0 comments on commit a02546d

Please sign in to comment.