不要怂,就是干,撸起袖子干!

Commit 04a778d2 by Mick Hansen

Merge pull request #2173 from janmeier/findOrCreateTransaction

Use a transaction internally in findOrCreate
2 parents b10a8ffd d51cc90e
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
"globals": { "globals": {
"Promise": true "Promise": true
}, },
"predef": [ "predef": [
"alert", "alert",
"describe", "describe",
......
...@@ -19,6 +19,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell ...@@ -19,6 +19,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell
+ moment 2.5.0 -> 2.7.0 + moment 2.5.0 -> 2.7.0
+ generic-pool 2.0.4 -> 2.1.1 + generic-pool 2.0.4 -> 2.1.1
+ sql 0.35.0 -> 0.39.0 + sql 0.35.0 -> 0.39.0
- [INTERNALS] Use a transaction inside `findOrCreate`, and handle unique constraint errors if multiple calls are issues concurrently on the same transaction
#### Backwards compatability changes #### Backwards compatability changes
- We are using a new inflection library, which should make pluralization and singularization in general more robust. However, a couple of pluralizations have changed as a result: - We are using a new inflection library, which should make pluralization and singularization in general more robust. However, a couple of pluralizations have changed as a result:
...@@ -35,6 +36,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell ...@@ -35,6 +36,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell
Old: `err.validateCustom[0]` Old: `err.validateCustom[0]`
New: `err.get('validateCustom')[0]` New: `err.get('validateCustom')[0]`
- The syntax for findOrCreate has changed, to be more in line with the rest of the library. `Model.findOrCreate(where, defaults);` becomes `Model.findOrCreate({ where: where, defaults: defaults });`.
# v2.0.0-dev12 # v2.0.0-dev12
......
...@@ -45,12 +45,13 @@ var options = { ...@@ -45,12 +45,13 @@ var options = {
docfile.members = []; docfile.members = [];
docfile.javadoc.forEach(function(javadoc){ docfile.javadoc.forEach(function(javadoc){
// Find constructor tags
javadoc.isConstructor = getTag(javadoc.raw.tags, 'constructor') !== undefined; javadoc.isConstructor = getTag(javadoc.raw.tags, 'constructor') !== undefined;
javadoc.isMixin = getTag(javadoc.raw.tags, 'mixin') !== undefined; javadoc.isMixin = getTag(javadoc.raw.tags, 'mixin') !== undefined;
javadoc.isProperty = getTag(javadoc.raw.tags, 'property') !== undefined; javadoc.isProperty = getTag(javadoc.raw.tags, 'property') !== undefined;
javadoc.private = getTag(javadoc.raw.tags, 'private') !== undefined; javadoc.private = getTag(javadoc.raw.tags, 'private') !== undefined;
javadoc.since = getTag(javadoc.raw.tags, 'since'); javadoc.since = getTag(javadoc.raw.tags, 'since');
javadoc.extends = getTag(javadoc.raw.tags, 'extends');
// Find constructor tags
// Only show params without a dot in them (dots means attributes of object, so no need to clutter the signature too much) // Only show params without a dot in them (dots means attributes of object, so no need to clutter the signature too much)
var params = [] ; var params = [] ;
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<? }) -?> <? }) -?>
<? if (comment.paramTags.length > 0) { ?> <? if (comment.paramTags.length > 0) { ?>
##### Params: ##### Params:
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
<? comment.paramTags.forEach(function(paramTag) { -?> <? comment.paramTags.forEach(function(paramTag) { -?>
...@@ -66,6 +66,9 @@ ...@@ -66,6 +66,9 @@
<? if (comment.since) { ?> <? if (comment.since) { ?>
__Since:__ *<?= comment.since.string ?>* __Since:__ *<?= comment.since.string ?>*
<? } ?> <? } ?>
<? if (comment.extends) { ?>
__Extends:__ *<?= comment.extends.otherClass ?>*
<? } ?>
====== ======
<? } ?> <? } ?>
<? }) ?> <? }) ?>
...@@ -74,4 +77,4 @@ ...@@ -74,4 +77,4 @@
_This documentation was automagically created on <?= new Date().toString() ?>_ _This documentation was automagically created on <?= new Date().toString() ?>_
<? }) ?> <? }) ?>
\ No newline at end of file
...@@ -182,6 +182,12 @@ module.exports = (function() { ...@@ -182,6 +182,12 @@ module.exports = (function() {
emptyQuery += ' RETURNING *'; emptyQuery += ' RETURNING *';
} }
if (this._dialect.supports['EXCEPTION'] && options.exception) {
// Mostly for internal use, so we expect the user to know what he's doing!
// pg_temp functions are private per connection, so we never risk this function interfering with another one.
valueQuery = 'CREATE OR REPLACE FUNCTION pg_temp.testfunc() RETURNS SETOF <%= table %> AS $body$ BEGIN RETURN QUERY ' + valueQuery + '; EXCEPTION ' + options.exception + ' END; $body$ LANGUAGE plpgsql; SELECT * FROM pg_temp.testfunc()';
}
valueHash = Utils.removeNullValuesFromHash(valueHash, this.options.omitNull); valueHash = Utils.removeNullValuesFromHash(valueHash, this.options.omitNull);
for (key in valueHash) { for (key in valueHash) {
......
...@@ -138,6 +138,10 @@ module.exports = (function() { ...@@ -138,6 +138,10 @@ module.exports = (function() {
AbstractQuery.prototype.isInsertQuery = function(results, metaData) { AbstractQuery.prototype.isInsertQuery = function(results, metaData) {
var result = true; var result = true;
if (this.options.type === QueryTypes.INSERT) {
return true;
}
// is insert query if sql contains insert into // is insert query if sql contains insert into
result = result && (this.sql.toLowerCase().indexOf('insert into') === 0); result = result && (this.sql.toLowerCase().indexOf('insert into') === 0);
......
...@@ -96,7 +96,7 @@ module.exports = (function() { ...@@ -96,7 +96,7 @@ module.exports = (function() {
errorDetected = true; errorDetected = true;
self.promise.emit('sql', self.sql); self.promise.emit('sql', self.sql);
err.sql = sql; err.sql = sql;
self.reject(err); self.reject(self.formatError(err));
}) })
.on('end', function(info) { .on('end', function(info) {
if (alreadyEnded || errorDetected) { if (alreadyEnded || errorDetected) {
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
var Utils = require('../../utils') var Utils = require('../../utils')
, AbstractQuery = require('../abstract/query') , AbstractQuery = require('../abstract/query')
, uuid = require('node-uuid'); , uuid = require('node-uuid')
, sequelizeErrors = require('../../errors.js');
module.exports = (function() { module.exports = (function() {
var Query = function(connection, sequelize, callee, options) { var Query = function(connection, sequelize, callee, options) {
...@@ -35,7 +36,8 @@ module.exports = (function() { ...@@ -35,7 +36,8 @@ module.exports = (function() {
if (err) { if (err) {
err.sql = sql; err.sql = sql;
reject(err);
reject(self.formatError(err));
} else { } else {
resolve(self.formatResults(results)); resolve(self.formatResults(results));
} }
...@@ -46,6 +48,25 @@ module.exports = (function() { ...@@ -46,6 +48,25 @@ module.exports = (function() {
return promise; return promise;
}; };
Query.prototype.formatError = function (err) {
var match;
switch (err.errno || err.code) {
case 1062:
match = err.message.match(/Duplicate entry '(.*)' for key '(.*?)'$/);
return new sequelizeErrors.UniqueConstraintError({
fields: null,
index: match[2],
value: match[1],
parent: err
});
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.isShowIndexesQuery = function () { Query.prototype.isShowIndexesQuery = function () {
return this.sql.toLowerCase().indexOf('show index from') === 0; return this.sql.toLowerCase().indexOf('show index from') === 0;
}; };
......
...@@ -14,6 +14,7 @@ var PostgresDialect = function(sequelize) { ...@@ -14,6 +14,7 @@ var PostgresDialect = function(sequelize) {
PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), { PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), {
'RETURNING': true, 'RETURNING': true,
'DEFAULT VALUES': true, 'DEFAULT VALUES': true,
'EXCEPTION': true,
schemas: true, schemas: true,
lock: true, lock: true,
forShare: 'FOR SHARE', forShare: 'FOR SHARE',
......
...@@ -5,7 +5,8 @@ var Utils = require('../../utils') ...@@ -5,7 +5,8 @@ var Utils = require('../../utils')
, DataTypes = require('../../data-types') , DataTypes = require('../../data-types')
, hstore = require('./hstore') , hstore = require('./hstore')
, QueryTypes = require('../../query-types') , QueryTypes = require('../../query-types')
, Promise = require('../../promise'); , Promise = require('../../promise')
, sequelizeErrors = require('../../errors.js');
// Parses hstore fields if the model has any hstore fields. // Parses hstore fields if the model has any hstore fields.
// This cannot be done in the 'pg' lib because hstore is a UDT. // This cannot be done in the 'pg' lib because hstore is a UDT.
...@@ -61,7 +62,7 @@ module.exports = (function() { ...@@ -61,7 +62,7 @@ module.exports = (function() {
receivedError = true; receivedError = true;
err.sql = sql; err.sql = sql;
promise.emit('sql', sql, self.client.uuid); promise.emit('sql', sql, self.client.uuid);
reject(err); reject(self.formatError(err));
}); });
query.on('end', function(result) { query.on('end', function(result) {
...@@ -221,6 +222,25 @@ module.exports = (function() { ...@@ -221,6 +222,25 @@ module.exports = (function() {
return this; return this;
}; };
Query.prototype.formatError = function (err) {
var match;
switch (err.code) {
case '23505':
match = err.detail.match(/Key \((.*?)\)=\((.*?)\) already exists/);
return new sequelizeErrors.UniqueConstraintError({
fields: match[1].split(', '),
value: match[2].split(', '),
index: null,
parent: err
});
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.isShowIndexesQuery = function () { Query.prototype.isShowIndexesQuery = function () {
return this.sql.indexOf('pg_get_indexdef') !== -1; return this.sql.indexOf('pg_get_indexdef') !== -1;
}; };
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
var Utils = require('../../utils') var Utils = require('../../utils')
, AbstractQuery = require('../abstract/query') , AbstractQuery = require('../abstract/query')
, QueryTypes = require('../../query-types'); , QueryTypes = require('../../query-types')
, sequelizeErrors = require('../../errors.js');
module.exports = (function() { module.exports = (function() {
var Query = function(database, sequelize, callee, options) { var Query = function(database, sequelize, callee, options) {
...@@ -48,10 +49,9 @@ module.exports = (function() { ...@@ -48,10 +49,9 @@ module.exports = (function() {
self.database[self.getDatabaseMethod()](self.sql, function(err, results) { self.database[self.getDatabaseMethod()](self.sql, function(err, results) {
// allow clients to listen to sql to do their own logging or whatnot // allow clients to listen to sql to do their own logging or whatnot
promise.emit('sql', self.sql, self.options.uuid); promise.emit('sql', self.sql, self.options.uuid);
if (err) { if (err) {
err.sql = self.sql; err.sql = self.sql;
reject(err); reject(self.formatError(err));
} else { } else {
var metaData = this; var metaData = this;
metaData.columnTypes = columnTypes; metaData.columnTypes = columnTypes;
...@@ -173,6 +173,39 @@ module.exports = (function() { ...@@ -173,6 +173,39 @@ module.exports = (function() {
}); });
}; };
Query.prototype.formatError = function (err) {
var match;
switch (err.code) {
case 'SQLITE_CONSTRAINT':
match = err.message.match(/columns (.*?) are/); // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
if (match !== null && match.length >= 2) {
return new sequelizeErrors.UniqueConstraintError(match[1].split(', '),null, err);
}
match = err.message.match(/UNIQUE constraint failed: (.*)/); // Sqlite 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
if (match !== null && match.length >= 2) {
var fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1];
});
return new sequelizeErrors.UniqueConstraintError({
fields: fields,
index: null,
value: null,
parent: err
});
}
return err;
case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err);
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.handleShowIndexesQuery = function (data) { Query.prototype.handleShowIndexesQuery = function (data) {
var self = this; var self = this;
......
...@@ -33,6 +33,8 @@ util.inherits(error.BaseError, Error); ...@@ -33,6 +33,8 @@ util.inherits(error.BaseError, Error);
* *
* @param {string} message Error message * @param {string} message Error message
* @param {Array} [errors] Array of ValidationErrorItem objects describing the validation errors * @param {Array} [errors] Array of ValidationErrorItem objects describing the validation errors
*
* @extends BaseError
* @constructor * @constructor
*/ */
error.ValidationError = function(message, errors) { error.ValidationError = function(message, errors) {
...@@ -65,6 +67,87 @@ error.ValidationError.prototype.get = function(path) { ...@@ -65,6 +67,87 @@ error.ValidationError.prototype.get = function(path) {
error.ValidationError.prototype.errors; error.ValidationError.prototype.errors;
/** /**
* A base class for all database related errors.
* @extends BaseError
* @constructor
*/
error.DatabaseError = function (parent) {
error.BaseError.apply(this, arguments);
this.name = 'SequelizeDatabaseError';
this.parent = parent;
this.sql = parent.sql;
};
util.inherits(error.DatabaseError, error.BaseError);
/**
* The database specific error which triggered this one
* @property parent
* @name parent
*/
error.DatabaseError.prototype.parent;
/**
* The SQL that triggered the error
* @property sql
* @name sql
*/
error.DatabaseError.prototype.sql;
/**
* Thrown when a database query times out because of a deadlock
* @extends DatabaseError
* @constructor
*/
error.TimeoutError = function (parent) {
error.DatabaseError.call(this, parent);
this.name = 'SequelizeTimeoutError';
};
util.inherits(error.TimeoutError, error.BaseError);
/**
* Thrown when a unique constraint is violated in the database
* @extends DatabaseError
* @constructor
*/
error.UniqueConstraintError = function (options) {
options = options || {};
options.parent = options.parent || { sql: '' };
error.DatabaseError.call(this, options.parent);
this.name = 'SequelizeUniqueConstraintError';
this.message = options.message;
this.fields = options.fields;
this.value = options.value;
this.index = options.index;
};
util.inherits(error.UniqueConstraintError, error.DatabaseError);
/**
* The message from the DB.
* @property message
* @name message
*/
error.DatabaseError.prototype.message;
/**
* The fields of the unique constraint
* @property fields
* @name fields
*/
error.DatabaseError.prototype.fields;
/**
* The value(s) which triggered the error
* @property value
* @name value
*/
error.DatabaseError.prototype.value;
/**
* The name of the index that triggered the error
* @property index
* @name index
*/
error.DatabaseError.prototype.index;
/**
* Validation Error Item * Validation Error Item
* Instances of this class are included in the `ValidationError.errors` property. * Instances of this class are included in the `ValidationError.errors` property.
* *
......
...@@ -572,21 +572,25 @@ module.exports = (function() { ...@@ -572,21 +572,25 @@ module.exports = (function() {
}); });
} }
}).then(function() { }).then(function() {
return self.QueryInterface[query].apply(self.QueryInterface, args).catch(function(err) { return self.QueryInterface[query].apply(self.QueryInterface, args).catch(self.sequelize.UniqueConstraintError, function(err) {
if (!!self.__options.uniqueKeys && err.code && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.code) { if (!!self.__options.uniqueKeys && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.parent.code) {
var index = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.toString()); var index = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.parent.toString());
if (index !== false) { if (index !== false) {
var fields = index.fields.filter(function(f) { return f !== self.Model.tableName; }); var fields = index.fields.filter(function(f) { return f !== self.Model.tableName; });
Utils._.each(self.__options.uniqueKeys, function(uniqueKey) { Utils._.each(self.__options.uniqueKeys, function(uniqueKey) {
if (!!uniqueKey.msg && (Utils._.isEqual(uniqueKey.fields, fields)) || uniqueKey.name === index.indexName) { if (!!uniqueKey.msg && (Utils._.isEqual(uniqueKey.fields, fields)) || uniqueKey.name === index.indexName) {
err = new Error(uniqueKey.msg); err = new self.sequelize.UniqueConstraintError({
message: uniqueKey.msg,
fields: fields,
index: index.indexName,
parent: err.parent
});
} }
}); });
} }
} }
throw err; throw err;
}).tap(function(result) { }).tap(function(result) {
// Transfer database generated values (defaults, autoincrement, etc) // Transfer database generated values (defaults, autoincrement, etc)
......
...@@ -1081,43 +1081,71 @@ module.exports = (function() { ...@@ -1081,43 +1081,71 @@ module.exports = (function() {
* Find a row that matches the query, or build and save the row if none is found * Find a row that matches the query, or build and save the row if none is found
* The successfull result of the promise will be (instance, created) - Make sure to use .spread() * The successfull result of the promise will be (instance, created) - Make sure to use .spread()
* *
* @param {Object} where A hash of search attributes. Note that this method differs from finders, in that the syntax is `{ attr1: 42 }` and NOT `{ where: { attr1: 42}}`. This is subject to change in 2.0 * If no transaction is passed in the `queryOptions` object, a new transaction will be created internally, to prevent the race condition where a matching row is created by another connection after the find but before the insert call.
* @param {Object} [defaults] Default values to use if creating a new instance * However, it is not always possible to handle this case in SQLite, specifically if one transaction inserts and another tries to select before the first one has comitted. In this case, an instance of sequelize.TimeoutError will be thrown instead.
* @param {Object} [options] Options passed to the find and create calls * If a transaction is created, a savepoint will be created instead, and any unique constraint violation will be handled internally.
* @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API
* *
* @return {Promise<Instance>} * @param {Object} options
* @param {Object} options.where where A hash of search attributes.
* @param {Object} [options.defaults] Default values to use if creating a new instance
* @param {Object} [queryOptions] Options passed to the find and create calls
*
* @return {Promise<Instance,created>}
*/ */
Model.prototype.findOrCreate = function(where, defaults, options) { Model.prototype.findOrCreate = function(options, queryOptions) {
var self = this var self = this
, values = {}; , internalTransaction = !(queryOptions && queryOptions.transaction)
, values;
options = Utils._.extend({ queryOptions = queryOptions ? Utils._.clone(queryOptions) : {};
transaction: null
}, options || {});
if (!(where instanceof Utils.or) && !(where instanceof Utils.and) && !Array.isArray(where)) { if (!options || !options.where) {
for (var attrname in where) { throw new Error('Missing where attribute in the options parameter passed to findOrCreate. Please note that the API has changed, and is now options (an object with where and defaults keys), queryOptions (transaction etc.)');
values[attrname] = where[attrname];
}
} }
return self.find({ // Create a transaction or a savepoint, depending on whether a transaction was passed in
where: where return self.sequelize.transaction(queryOptions).bind({}).then(function (transaction) {
}, { this.transaction = transaction;
transaction: options.transaction queryOptions.transaction = transaction;
return self.find(options, {
transaction: transaction,
});
}).then(function(instance) { }).then(function(instance) {
if (instance === null) { if (instance !== null) {
for (var attrname in defaults) { return [instance, false];
values[attrname] = defaults[attrname]; }
values = Utils._.clone(options.defaults) || {};
if (Utils._.isPlainObject(options.where)) {
values = Utils._.defaults(values, options.where);
}
// VERY PG specific
// If a unique constraint is triggered inside a transaction, we cannot execute further queries inside that transaction, short of rolling back or committing.
// To circumwent this, we add an EXCEPTION WHEN unique_violation clause, which always returns an empty result set
queryOptions.exception = 'WHEN unique_violation THEN RETURN QUERY SELECT * FROM <%= table %> WHERE 1 <> 1;';
return self.create(values, queryOptions).bind(this).then(function(instance) {
if (instance.get(self.primaryKeyAttribute, { raw: true }) === null) {
// If the query returned an empty result for the primary key, we know that this was actually a unique constraint violation
throw new self.sequelize.UniqueConstraintError();
} }
return self.create(values, options).then(function(instance) { return [instance, true];
return Promise.resolve([instance, true]); }).catch(self.sequelize.UniqueConstraintError, function () {
// Someone must have created a matching instance inside the same transaction since we last did a find. Let's find it!
return self.find(options, {
transaction: internalTransaction ? null : this.transaction,
}).then(function(instance) {
return [instance, false];
}); });
});
}).tap(function () {
if (internalTransaction) {
// If we created a transaction internally, we should clean it up
return this.transaction.commit();
} }
return Promise.resolve([instance, false]);
}); });
}; };
......
...@@ -434,6 +434,7 @@ module.exports = (function() { ...@@ -434,6 +434,7 @@ module.exports = (function() {
QueryInterface.prototype.insert = function(dao, tableName, values, options) { QueryInterface.prototype.insert = function(dao, tableName, values, options) {
var sql = this.QueryGenerator.insertQuery(tableName, values, dao.Model.rawAttributes, options); var sql = this.QueryGenerator.insertQuery(tableName, values, dao.Model.rawAttributes, options);
options.type = 'INSERT';
return this.sequelize.query(sql, dao, options).then(function(result) { return this.sequelize.query(sql, dao, options).then(function(result) {
result.isNewRecord = false; result.isNewRecord = false;
return result; return result;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module.exports = { module.exports = {
SELECT: 'SELECT', SELECT: 'SELECT',
INSERT: 'INSERT',
BULKUPDATE: 'BULKUPDATE', BULKUPDATE: 'BULKUPDATE',
BULKDELETE: 'BULKDELETE' BULKDELETE: 'BULKDELETE'
}; };
...@@ -248,6 +248,27 @@ module.exports = (function() { ...@@ -248,6 +248,27 @@ module.exports = (function() {
sequelizeErrors.ValidationErrorItem; sequelizeErrors.ValidationErrorItem;
/** /**
* A base class for all database related errors.
* @see {Errors#DatabaseError}
*/
Sequelize.prototype.DatabaseError = Sequelize.DatabaseError =
sequelizeErrors.DatabaseError;
/**
* Thrown when a database query times out because of a deadlock
* @see {Errors#TimeoutError}
*/
Sequelize.prototype.TimeoutError = Sequelize.TimeoutError =
sequelizeErrors.TimeoutError;
/**
* Thrown when a unique constraint is violated in the database
* @see {Errors#UniqueConstraintError}
*/
Sequelize.prototype.UniqueConstraintError = Sequelize.UniqueConstraintError =
sequelizeErrors.UniqueConstraintError;
/**
* Returns the specified dialect. * Returns the specified dialect.
* *
* @return {String} The specified dialect. * @return {String} The specified dialect.
......
...@@ -22,6 +22,7 @@ var Transaction = module.exports = function(sequelize, options) { ...@@ -22,6 +22,7 @@ var Transaction = module.exports = function(sequelize, options) {
this.id = this.options.transaction.id; this.id = this.options.transaction.id;
this.options.transaction.savepoints.push(this); this.options.transaction.savepoints.push(this);
this.name = this.id + '-savepoint-' + this.options.transaction.savepoints.length; this.name = this.id + '-savepoint-' + this.options.transaction.savepoints.length;
this.parent = this.options.transaction;
} else { } else {
this.id = this.name = Utils.generateUUID(); this.id = this.name = Utils.generateUUID();
} }
...@@ -146,5 +147,6 @@ Transaction.prototype.setIsolationLevel = function() { ...@@ -146,5 +147,6 @@ Transaction.prototype.setIsolationLevel = function() {
}; };
Transaction.prototype.cleanup = function() { Transaction.prototype.cleanup = function() {
this.connection.uuid = undefined;
return this.sequelize.connectionManager.releaseConnection(this.connection); return this.sequelize.connectionManager.releaseConnection(this.connection);
}; };
...@@ -312,16 +312,17 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -312,16 +312,17 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
it('allows us to customize the error message for unique constraint', function(done) { it('allows us to customize the error message for unique constraint', function(done) {
var User = this.sequelize.define('UserWithUniqueUsername', { var self = this
username: { type: Sequelize.STRING, unique: { name: 'user_and_email', msg: 'User and email must be unique' }}, , User = this.sequelize.define('UserWithUniqueUsername', {
email: { type: Sequelize.STRING, unique: 'user_and_email' }, username: { type: Sequelize.STRING, unique: { name: 'user_and_email', msg: 'User and email must be unique' }},
aCol: { type: Sequelize.STRING, unique: 'a_and_b' }, email: { type: Sequelize.STRING, unique: 'user_and_email' },
bCol: { type: Sequelize.STRING, unique: 'a_and_b' } aCol: { type: Sequelize.STRING, unique: 'a_and_b' },
}) bCol: { type: Sequelize.STRING, unique: 'a_and_b' }
})
User.sync({ force: true }).success(function() { User.sync({ force: true }).success(function() {
User.create({username: 'tobi', email: 'tobi@tobi.me'}).success(function() { User.create({username: 'tobi', email: 'tobi@tobi.me'}).success(function() {
User.create({username: 'tobi', email: 'tobi@tobi.me'}).error(function(err) { User.create({username: 'tobi', email: 'tobi@tobi.me'}).catch(self.sequelize.UniqueConstraintError, function(err) {
expect(err.message).to.equal('User and email must be unique') expect(err.message).to.equal('User and email must be unique')
done() done()
}) })
......
...@@ -14,16 +14,19 @@ chai.config.includeStack = true; ...@@ -14,16 +14,19 @@ chai.config.includeStack = true;
describe(Support.getTestDialectTeaser("DAOFactory"), function () { describe(Support.getTestDialectTeaser("DAOFactory"), function () {
beforeEach(function() { beforeEach(function() {
this.User = this.sequelize.define('User', { return Support.prepareTransactionTest(this.sequelize).bind(this).then(function(sequelize) {
username: DataTypes.STRING, this.sequelize = sequelize;
secretValue: DataTypes.STRING, this.User = this.sequelize.define('User', {
data: DataTypes.STRING, username: DataTypes.STRING,
intVal: DataTypes.INTEGER, secretValue: DataTypes.STRING,
theDate: DataTypes.DATE, data: DataTypes.STRING,
aBool: DataTypes.BOOLEAN intVal: DataTypes.INTEGER,
}); theDate: DataTypes.DATE,
aBool: DataTypes.BOOLEAN
});
return this.User.sync({ force: true }); return this.User.sync({ force: true });
});
}); });
describe('scopes', function() { describe('scopes', function() {
...@@ -267,7 +270,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -267,7 +270,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}); });
it("should have no problem performing findOrCreate", function() { it("should have no problem performing findOrCreate", function() {
return this.ScopeMe.findOrCreate({username: 'fake'}).spread(function(user) { return this.ScopeMe.findOrCreate({ where: {username: 'fake'}}).spread(function(user) {
expect(user.username).to.equal('fake'); expect(user.username).to.equal('fake');
}); });
}); });
......
"use strict";
/* jshint camelcase: false */ /* jshint camelcase: false */
var chai = require('chai') var chai = require('chai')
, sinon = require('sinon')
, expect = chai.expect , expect = chai.expect
, Support = require(__dirname + '/support') , Support = require(__dirname + '/support')
, Sequelize = Support.Sequelize; , Sequelize = Support.Sequelize
, Promise = Sequelize.Promise;
chai.config.includeStack = true; chai.config.includeStack = true;
...@@ -49,7 +53,31 @@ describe(Support.getTestDialectTeaser("Sequelize Errors"), function () { ...@@ -49,7 +53,31 @@ describe(Support.getTestDialectTeaser("Sequelize Errors"), function () {
var matches = validationError.get('first_name'); var matches = validationError.get('first_name');
expect(matches).to.be.instanceOf(Array); expect(matches).to.be.instanceOf(Array);
expect(matches).to.have.lengthOf(1); expect(matches).to.have.lengthOf(1);
expect(matches[0]).to.have.property('message', 'invalid') expect(matches[0]).to.have.property('message', 'invalid');
});
});
describe('Constraint error', function () {
it('Can be intercepted using .catch', function () {
var spy = sinon.spy()
, User = this.sequelize.define('user', {
first_name: {
type: Sequelize.STRING,
unique: 'unique_name'
},
last_name: {
type: Sequelize.STRING,
unique: 'unique_name'
}
});
return this.sequelize.sync({ force: true }).bind(this).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' });
}).then(function () {
return User.create({ first_name: 'jan', last_name: 'meier' }).catch(this.sequelize.UniqueConstraintError, spy);
}).then(function () {
expect(spy).to.have.been.calledOnce;
});
}); });
}) });
}); });
...@@ -10,32 +10,35 @@ var chai = require('chai') ...@@ -10,32 +10,35 @@ var chai = require('chai')
chai.config.includeStack = true chai.config.includeStack = true
describe(Support.getTestDialectTeaser("Promise"), function () { describe(Support.getTestDialectTeaser("Promise"), function () {
beforeEach(function(done) { beforeEach(function() {
this.User = this.sequelize.define('User', { return Support.prepareTransactionTest(this.sequelize).bind(this).then(function(sequelize) {
username: { type: DataTypes.STRING }, this.sequelize = sequelize;
touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, this.User = this.sequelize.define('User', {
aNumber: { type: DataTypes.INTEGER }, username: { type: DataTypes.STRING },
bNumber: { type: DataTypes.INTEGER }, touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
aNumber: { type: DataTypes.INTEGER },
validateTest: { bNumber: { type: DataTypes.INTEGER },
type: DataTypes.INTEGER,
allowNull: true, validateTest: {
validate: {isInt: true} type: DataTypes.INTEGER,
}, allowNull: true,
validateCustom: { validate: {isInt: true}
type: DataTypes.STRING, },
allowNull: true, validateCustom: {
validate: {len: {msg: 'Length failed.', args: [1, 20]}} type: DataTypes.STRING,
}, allowNull: true,
validate: {len: {msg: 'Length failed.', args: [1, 20]}}
dateAllowNullTrue: { },
type: DataTypes.DATE,
allowNull: true dateAllowNullTrue: {
} type: DataTypes.DATE,
}) allowNull: true
}
})
this.User.sync({ force: true }).then(function() { done() }) return this.User.sync({ force: true })
}) })
});
describe('increment', function () { describe('increment', function () {
beforeEach(function(done) { beforeEach(function(done) {
...@@ -321,7 +324,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { ...@@ -321,7 +324,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () {
describe('with spread', function () { describe('with spread', function () {
it('user not created', function (done) { it('user not created', function (done) {
this.User this.User
.findOrCreate({ id: 1}) .findOrCreate({ where: { id: 1}})
.spread(function(user, created) { .spread(function(user, created) {
expect(user.id).to.equal(1) expect(user.id).to.equal(1)
expect(created).to.equal(false) expect(created).to.equal(false)
...@@ -331,7 +334,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () { ...@@ -331,7 +334,7 @@ describe(Support.getTestDialectTeaser("Promise"), function () {
}) })
it('user created', function (done) { it('user created', function (done) {
this.User this.User
.findOrCreate({ id: 2}) .findOrCreate({ where: { id: 2}})
.spread(function(user, created) { .spread(function(user, created) {
expect(user.id).to.equal(2) expect(user.id).to.equal(2)
expect(created).to.equal(true) expect(created).to.equal(true)
......
...@@ -188,7 +188,7 @@ var Support = { ...@@ -188,7 +188,7 @@ var Support = {
}; };
var sequelize = Support.createSequelizeInstance(); var sequelize = Support.createSequelizeInstance();
//
// For Postgres' HSTORE functionality and to properly execute it's commands we'll need this... // For Postgres' HSTORE functionality and to properly execute it's commands we'll need this...
before(function() { before(function() {
var dialect = Support.getTestDialect(); var dialect = Support.getTestDialect();
...@@ -205,4 +205,5 @@ beforeEach(function() { ...@@ -205,4 +205,5 @@ beforeEach(function() {
return Support.clearDatabase(this.sequelize); return Support.clearDatabase(this.sequelize);
}); });
module.exports = Support; module.exports = Support;
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!