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

Commit 33f46913 by Mick Hansen

Merge branch 'master' of github.com:sequelize/sequelize

2 parents 28461682 559ccac6
......@@ -37,17 +37,9 @@ matrix:
fast_finish: true
include:
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=mysql
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=postgres
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=sqlite
- node_js: "0.10"
env: COVERAGE=true
allow_failures:
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=mysql
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=postgres
- node_js: "0.8"
env: COVERAGE=true DB=mysql DIALECT=sqlite
\ No newline at end of file
- node_js: "0.10"
env: COVERAGE=true
\ No newline at end of file
......@@ -11,18 +11,20 @@ teaser:
node -pe "Array(20 + '$(DIALECT)'.length + 3).join('#')" && \
echo ''
ifeq (true,$(COVERAGE))
test: coveralls
else
test:
@if [ -n "$$COVERAGE" ]; then \
make cover && make coveralls-send; \
@elif [ "$$GREP" ]; \ then \
@if [ "$$GREP" ]; then \
make teaser && ./node_modules/mocha/bin/mocha --globals setImmediate,clearImmediate --check-leaks --colors -t 10000 --reporter $(REPORTER) -g "$$GREP" $(TESTS); \
else \
make teaser && ./node_modules/mocha/bin/mocha --globals setImmediate,clearImmediate --check-leaks --colors -t 10000 --reporter $(REPORTER) $(TESTS); \
fi
endif
cover:
rm -rf coverage \
make teaser && ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -t 10000 -R spec $(TESTS); \
make teaser && ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -t 10000 $(TESTS); \
mariadb:
@DIALECT=mariadb make test
......@@ -40,18 +42,23 @@ binary:
mariadb-cover:
rm -rf coverage
@DIALECT=mariadb make cover
mv coverage coverage-mariadb
sqlite-cover:
rm -rf coverage
@DIALECT=sqlite make cover
mv coverage coverage-sqlite
mysql-cover:
rm -rf coverage
@DIALECT=mysql make cover
mv coverage coverage-mysql
postgres-cover:
rm -rf coverage
@DIALECT=postgres make cover
mv coverage coverage-postgres
postgres-native-cover:
rm -rf coverage
@DIALECT=postgres-native make cover
mv coverage coverage-postgresnative
binary-cover:
rm -rf coverage
@./test/binary/sequelize.test.bats
......@@ -62,7 +69,7 @@ merge-coverage:
./node_modules/.bin/lcov-result-merger 'coverage-*/lcov.info' 'coverage/lcov.info'
coveralls-send:
cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage
cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage*
# test aliases
......
# Sequelize [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/sequelize/sequelize/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Build Status](https://secure.travis-ci.org/sequelize/sequelize.png)](http://travis-ci.org/sequelize/sequelize) [![Dependency Status](https://david-dm.org/sequelize/sequelize.png)](https://david-dm.org/sequelize/sequelize) [![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](http://flattr.com/thing/1259407/Sequelize) #
# Sequelize
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/sequelize/sequelize/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Build Status](https://secure.travis-ci.org/sequelize/sequelize.png)](http://travis-ci.org/sequelize/sequelize) [![Dependency Status](https://david-dm.org/sequelize/sequelize.png)](https://david-dm.org/sequelize/sequelize) [![Coverage Status](https://img.shields.io/coveralls/sequelize/sequelize.svg)](https://coveralls.io/r/sequelize/sequelize?branch=master)[![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](http://flattr.com/thing/1259407/Sequelize) #
MySQL, MariaDB, PostgresSQL, and SQLite Object Relational Mapper (ORM) for [node](http://nodejs.org).
......
......@@ -6,6 +6,8 @@ Notice: All 1.7.x changes are present in 2.0.x aswell
- [FEATURE] It's now possible to add multiple relations to a hasMany association, modelInstance.addRelations([otherInstanceA, otherInstanceB])
- [FEATURE] `define()` stores models in `sequelize.models` Object e.g. `sequelize.models.MyModel`
- [FEATURE] The `set` / `add` / `has` methods for associations now allow you to pass the value of a primary key, instead of a full Instance object, like so: `user.addTask(15);`.
- [FEATURE] Support for FOR UPDATE and FOR SHARE statements [#1777](https://github.com/sequelize/sequelize/pull/1777)
- [FEATURE] n:m createAssocation now returns the target model instance instead of the join model instance
- [BUG] An error is now thrown if an association would create a naming conflict between the association and the foreign key when doing eager loading. Closes [#1272](https://github.com/sequelize/sequelize/issues/1272)
- [INTERNALS] `bulkDeleteQuery` was removed from the MySQL / abstract query generator, since it was never used internally. Please use `deleteQuery` instead.
......
......@@ -49,17 +49,25 @@ module.exports = (function() {
*/
this.associationAccessor = this.as
if (!this.associationAccessor && (typeof this.through === "string" || Object(this.through) === this.through)) {
this.associationAccessor = this.through.tableName || this.through
}
else if (!this.associationAccessor) {
this.associationAccessor = this.combinedTableName
if (!this.associationAccessor) {
if (typeof this.through === 'string') {
this.associationAccessor = this.through
} else if (Object(this.through) === this.through) {
this.associationAccessor = this.through.tableName
} else {
this.associationAccessor = this.combinedTableName
}
}
/*
* If self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
// check 'as' is defined for many-to-many self-association
if (this.through && this.through !== true && !this.as) {
throw new Error('\'as\' must be defined for many-to-many self-associations')
}
this.targetAssociation = this
}
......@@ -69,15 +77,15 @@ module.exports = (function() {
if (this.through) {
_.each(this.target.associations, function (association, accessor) {
if (self.source === association.target) {
var paired = false
var paired
// If through is default, we determine pairing by the accesor value (i.e. DAOFactory's using as won't pair, but regular ones will)
if (self.through === true && accessor === self.associationAccessor) {
paired = true
if (self.through === true) {
paired = accessor === self.associationAccessor
}
// If through is not default, determine pairing by through value (model/string)
if (self.through !== true && self.options.through === association.options.through) {
paired = true
else {
paired = self.options.through === association.options.through
}
// If paired, set properties identifying both associations as double linked, and allow them to each eachtoerh
if (paired) {
......@@ -90,13 +98,12 @@ module.exports = (function() {
}
})
}
/*
* If we are double linked, and through is either default or a string, we create the through model and set it on both associations
*/
if (this.doubleLinked) {
if (this.through === true) {
this.through = this.combinedTableName
}
if (this.doubleLinked && this.through === true) {
this.through = this.combinedTableName
}
if (typeof this.through === "string") {
......@@ -150,10 +157,12 @@ module.exports = (function() {
if ((this.isSelfAssociation && Object(this.through) === this.through) || doubleLinked) {
// We need to remove the keys that 1:M have added
if (this.isSelfAssociation && doubleLinked) {
if (self.through.rawAttributes[this.targetAssociation.identifier]._autoGenerated) {
if (self.through.rawAttributes[this.targetAssociation.identifier]
&& self.through.rawAttributes[this.targetAssociation.identifier]._autoGenerated) {
delete self.through.rawAttributes[this.targetAssociation.identifier];
}
if (self.through.rawAttributes[this.targetAssociation.foreignIdentifier]._autoGenerated) {
if (self.through.rawAttributes[this.targetAssociation.foreignIdentifier]
&& self.through.rawAttributes[this.targetAssociation.foreignIdentifier]._autoGenerated) {
delete self.through.rawAttributes[this.targetAssociation.foreignIdentifier];
}
}
......@@ -161,19 +170,19 @@ module.exports = (function() {
this.foreignIdentifier = this.targetAssociation.identifier
this.targetAssociation.foreignIdentifier = this.identifier
if (isForeignKeyDeletionAllowedFor.call(this, this.source, this.foreignIdentifier)) {
delete this.source.rawAttributes[this.foreignIdentifier]
}
if (this.isSelfAssociation && this.foreignIdentifier === this.identifier) {
this.foreignIdentifier = Utils._.camelizeIf(
[Utils.singularize(this.as, this.source.options.language), this.source.primaryKeyAttribute].join("_"),
!this.source.options.underscored
);
}
if (isForeignKeyDeletionAllowedFor.call(this, this.source, this.foreignIdentifier)) {
delete this.source.rawAttributes[this.foreignIdentifier]
}
)
if (isForeignKeyDeletionAllowedFor.call(this, this.target, this.identifier)) {
delete this.targetAssociation.source.rawAttributes[this.identifier]
if (doubleLinked) {
this.targetAssociation.identifier = this.foreignIdentifier
}
}
// remove any PKs previously defined by sequelize
......@@ -185,8 +194,7 @@ module.exports = (function() {
})
// define a new model, which connects the models
var combinedTableAttributes = {}
, sourceKeyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type
var sourceKeyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type
, targetKeyType = this.target.rawAttributes[this.target.primaryKeyAttribute].type
, sourceAttribute = { type: sourceKeyType }
, targetAttribute = { type: targetKeyType }
......@@ -206,15 +214,11 @@ module.exports = (function() {
if (primaryKeyDeleted) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true
} else {
var uniqueKey = [this.through.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_')
targetAttribute.unique = sourceAttribute.unique = uniqueKey
}
combinedTableAttributes[this.identifier] = sourceAttribute
combinedTableAttributes[this.foreignIdentifier] = targetAttribute
if (!this.through.rawAttributes[this.identifier]) {
this.through.rawAttributes[this.identifier] = {
_autoGenerated: true
......@@ -227,8 +231,8 @@ module.exports = (function() {
}
}
this.through.rawAttributes[this.identifier] = Utils._.merge(this.through.rawAttributes[this.identifier], sourceAttribute);
this.through.rawAttributes[this.foreignIdentifier] = Utils._.merge(this.through.rawAttributes[this.foreignIdentifier], targetAttribute);
this.through.rawAttributes[this.identifier] = Utils._.extend(this.through.rawAttributes[this.identifier], sourceAttribute);
this.through.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.rawAttributes[this.foreignIdentifier], targetAttribute);
this.through.init(this.through.daoFactoryManager)
} else {
......@@ -427,6 +431,10 @@ module.exports = (function() {
var instance = this
, options = {}
if (values === undefined) {
values = {}
}
if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction
}
......@@ -434,7 +442,7 @@ module.exports = (function() {
if (Object(association.through) === association.through) {
// Create the related model instance
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, options)
return instance[association.accessors.add](newAssociatedObject, options).return(newAssociatedObject)
})
} else {
values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
......
......@@ -3,10 +3,10 @@ var Utils = require("./../utils")
module.exports = {
addForeignKeyConstraints: function(newAttribute, source, target, options) {
// FK constraints are opt-in: users must either rset `foreignKeyConstraints`
// FK constraints are opt-in: users must either set `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behaviour
if(options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
var primaryKeys = Utils._.filter(Utils._.keys(source.rawAttributes), function(key) {
......
......@@ -78,7 +78,7 @@ var Mixin = module.exports = function(){}
*
* The following methods are injected on the source:
*
* * get[AS] - for example getProfile()
* * get[AS] - for example getProfile(finder). The finder object is passed to `target.find`.
* * set[AS] - for example setProfile(instance, options). Options are passed to `target.save`
* * create[AS] - for example createProfile(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to `target.create`
*
......@@ -119,7 +119,7 @@ Mixin.hasOne = function(targetModel, options) {
*
* The following methods are injected on the source:
*
* * get[AS] - for example getUser()
* * get[AS] - for example getUser(finder). The finder object is passed to `target.find`.
* * set[AS] - for example setUser(instance, options). Options are passed to this.save
* * create[AS] - for example createUser(value, options). Builds and saves a new instance of the associated model. Values and options are passed on to target.create
*
......@@ -169,7 +169,7 @@ Mixin.belongsTo = function(targetModel, options) {
* The following methods are injected on the source:
*
* * get[AS] - for example getPictures().
* * get[AS] - for example getPictures(finder). The finder object is passed to `target.find`.
* * set[AS] - for example setPictures(instances, defaultAttributes|options). Update the associations. All currently associated models that are not in instances will be removed.
* * add[AS] - for example addPicture(instance, defaultAttributes|options). Add another associated object.
* * add[AS] [plural] - for example addPictures([instance1, instance2], defaultAttributes|options). Add some more associated objects.
......@@ -211,7 +211,7 @@ Mixin.belongsTo = function(targetModel, options) {
* })
* ```
*
* @param {Model} target
* @param {Model} target
* @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {Model|string} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it.
......
......@@ -956,6 +956,14 @@ module.exports = (function() {
query = mainQueryItems.join('')
}
if (options.lock && this._dialect.supports.lock) {
if (options.lock === 'SHARE') {
query += ' ' + this._dialect.supports.forShare
} else {
query += ' FOR UPDATE'
}
}
query += ";";
return query
......
......@@ -13,7 +13,7 @@ module.exports = (function() {
mysql = require('mysql')
}
} catch (err) {
console.log('You need to install mysql package manually')
throw new Error('Please install mysql package manually')
}
this.sequelize = sequelize
......
......@@ -7,7 +7,9 @@ var MysqlDialect = function(sequelize) {
MysqlDialect.prototype.supports = _.defaults({
'VALUES ()': true,
'LIMIT ON UPDATE':true
'LIMIT ON UPDATE':true,
lock: true,
forShare: 'LOCK IN SHARE MODE'
}, Abstract.prototype.supports)
module.exports = MysqlDialect
......@@ -8,7 +8,9 @@ var PostgresDialect = function(sequelize) {
PostgresDialect.prototype.supports = _.defaults({
'RETURNING': true,
'DEFAULT VALUES': true,
schemas: true
schemas: true,
lock: true,
forShare: 'FOR SHARE',
}, Abstract.prototype.supports)
module.exports = PostgresDialect
......@@ -400,7 +400,7 @@ module.exports = (function() {
* @param {Boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* @param {Transaction} [options.transaction]
*
* @return {Promise}
* @return {Promise<this>}
*/
Instance.prototype.save = function(fieldsOrOptions, options) {
if (fieldsOrOptions instanceof Array) {
......@@ -571,7 +571,7 @@ module.exports = (function() {
*
* @see {Model#find}
* @param {Object} [options] Options that are passed on to `Model.find`
* @return {Promise}
* @return {Promise<this>}
*/
Instance.prototype.reload = function(options) {
var self = this
......@@ -598,7 +598,7 @@ module.exports = (function() {
* @param {Array} [options.skip] An array of strings. All properties that are in this array will not be validated
* @see {InstanceValidator}
*
* @return {Promise}
* @return {Promise<undefined|Error}
*/
Instance.prototype.validate = function(options) {
return new InstanceValidator(this, options).validate()
......@@ -618,7 +618,7 @@ module.exports = (function() {
* @param {Object} updates See `setAttributes`
* @param {Object} options See `save`
*
* @return {Promise}
* @return {Promise<this>}
*/
Instance.prototype.updateAttributes = function(updates, options) {
if (options instanceof Array) {
......@@ -639,7 +639,7 @@ module.exports = (function() {
* @param {Object} [options={}]
* @param {Boolean} [options.force=false] If set to true, paranoid models will actually be deleted
*
* @return {Promise}
* @return {Promise<undefined>}
*/
Instance.prototype.destroy = function(options) {
options = options || {}
......
......@@ -382,7 +382,7 @@ module.exports = (function() {
/**
* Sync this Model to the DB, that is create the table. Upon success, the callback will be called with the model instance (this)
* @see {Sequelize#sync} for options
* @return {Promise}
* @return {Promise<this>}
*/
Model.prototype.sync = function(options) {
options = Utils._.extend({}, this.options, options || {})
......@@ -487,7 +487,7 @@ module.exports = (function() {
* ```
*
* @param {Array|Object|String|null} options* The scope(s) to apply. Scopes can either be passed as consecutive arguments, or as an array of arguments. To apply simple scopes, pass them as strings. For scope function, pass an object, with a `method` property. The value can either be a string, if the method does not take any arguments, or an array, where the first element is the name of the method, and consecutive elements are arguments to that method. Pass null to remove all scopes, including the default.
* @return {Model} A reference to the model, with the scope(s) applied. Calling scope again on the returned model will clear the previous scope.
* @return {Model} A reference to the model, with the scope(s) applied. Calling scope again on the returned model will clear the previous scope.
*/
Model.prototype.scope = function(option) {
var self = Object.create(this)
......@@ -643,9 +643,10 @@ module.exports = (function() {
* @param {Number} [options.offset]
* @param {Object} [queryOptions] Set the query options, e.g. raw, specifying that you want raw data instead of built Instances. See sequelize.query for options
* @param {Transaction} [queryOptions.transaction]
* @param {String} [queryOptions.lock] Lock the selected rows in either share or update mode. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. See [transaction.LOCK for an example](https://github.com/sequelize/sequelize/wiki/API-Reference-Transaction#LOCK)
*
* @see {Sequelize#query}
* @return {Promise}
* @return {Promise<Array<Instance>>}
* @alias all
*/
Model.prototype.findAll = function(options, queryOptions) {
......@@ -706,7 +707,7 @@ module.exports = (function() {
* @param {Object} [queryOptions]
*
* @see {Model#findAll} for an explanation of options and queryOptions
* @return {Promise}
* @return {Promise<Instance>}
*/
Model.prototype.find = function(options, queryOptions) {
var hasJoin = false
......@@ -800,9 +801,9 @@ module.exports = (function() {
* @param {String} field The field to aggregate over. Can be a field name or *
* @param {String} aggregateFunction The function to use for aggregation, e.g. sum, max etc.
* @param {Object} [options] Query options. See sequelize.query for full options
* @param {DataType|String} [options.dataType] The type of the result. If field is a field in the Instance, the default will be the type of that field, otherwise defaults to float.
* @param {DataType|String} [options.dataType] The type of the result. If `field` is a field in this Model, the default will be the type of that field, otherwise defaults to float.
*
* @return {Promise}
* @return {Promise<options.dataType>}
*/
Model.prototype.aggregate = function(field, aggregateFunction, options) {
options = Utils._.extend({ attributes: [] }, options || {})
......@@ -831,7 +832,7 @@ module.exports = (function() {
* @param {Object} [options]
* @param {Object} [options.include] Include options. See `find` for details
*
* @return {Promise}
* @return {Promise<Integer>}
*/
Model.prototype.count = function(options) {
options = Utils._.clone(options || {})
......@@ -866,7 +867,7 @@ module.exports = (function() {
* @param {Object} [queryOptions] See Sequelize.query
*
* @see {Model#findAll} for a specification of find and query options
* @return {Promise}
* @return {Promise<Object>}
*/
Model.prototype.findAndCountAll = function(findOptions, queryOptions) {
var self = this
......@@ -896,7 +897,7 @@ module.exports = (function() {
* @param {Object} [options] See aggregate
* @see {Model#aggregate} for options
*
* @return {Promise}
* @return {Promise<Any>}
*/
Model.prototype.max = function(field, options) {
return this.aggregate(field, 'max', options)
......@@ -909,7 +910,7 @@ module.exports = (function() {
* @param {Object} [options] See aggregate
* @see {Model#aggregate} for options
*
* @return {Promise}
* @return {Promise<Any>}
*/
Model.prototype.min = function(field, options) {
return this.aggregate(field, 'min', options)
......@@ -922,7 +923,7 @@ module.exports = (function() {
* @param {Object} [options] See aggregate
* @see {Model#aggregate} for options
*
* @return {Promise}
* @return {Promise<Number>}
*/
Model.prototype.sum = function(field, options) {
return this.aggregate(field, 'sum', options)
......@@ -993,7 +994,7 @@ module.exports = (function() {
* @param {Array} [options.include] an array of include options - Used to build prefetched/included model instances
* @param {Transaction} [options.transaction]
*
* @return {Promise}
* @return {Promise<Instance>}
*/
Model.prototype.create = function(values, options) {
Utils.validateParameter(values, Object, { optional: true })
......@@ -1021,8 +1022,7 @@ module.exports = (function() {
* @param {Object} [options] Options passed to the find call
* @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API
*
* @return {Promise}
* @return {EventEmitter}
* @return {Promise<Instance>}
* @method
* @alias findOrBuild
*/
......@@ -1069,7 +1069,7 @@ module.exports = (function() {
* @param {Object} [options] Options passed to the find and create calls
* @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API
*
* @return {Promise}
* @return {Promise<Instance>}
*/
Model.prototype.findOrCreate = function (where, defaults, options) {
var self = this
......@@ -1118,7 +1118,7 @@ module.exports = (function() {
* @param {Boolean} [options.hooks=false] Run before / after create hooks for each individual Instance? BulkCreate hooks will still be run.
* @param {Boolean} [options.ignoreDuplicates=false] Ignore duplicate values for primary keys? (not supported by postgres)
*
* @return {Promise}
* @return {Promise<Array<Instance>>}
*/
Model.prototype.bulkCreate = function(records, fieldsOrOptions, options) {
Utils.validateParameter(fieldsOrOptions, Object, { deprecated: Array, optional: true, index: 2, method: 'Model#bulkCreate' })
......@@ -1256,7 +1256,7 @@ module.exports = (function() {
* @param {Number} [options.limit] How many rows to delete
* @param {Boolean} [options.truncate] If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
*
* @return {Promise}
* @return {Promise<undefined>}
*/
Model.prototype.destroy = function(where, options) {
options = options || {}
......@@ -1339,7 +1339,7 @@ module.exports = (function() {
* @param {Number} [options.limit] How many rows to update (only for mysql and mariadb)
* @deprecated The syntax is due for change, in order to make `where` more consistent with the rest of the API
*
* @return {EventEmitter}
* @return {Promise}
*/
Model.prototype.update = function(attrValueHash, where, options) {
var self = this
......@@ -1432,7 +1432,7 @@ module.exports = (function() {
/**
* Run a describe query on the table. The result will be return to the listener as a hash of attributes and their types.
*
* @return {EventEmitter}
* @return {Promise}
*/
Model.prototype.describe = function(schema) {
return this.QueryInterface.describeTable(this.tableName, schema || this.options.schema || undefined)
......
......@@ -552,6 +552,8 @@ module.exports = (function() {
})
}
options.lock = queryOptions.lock
var sql = this.QueryGenerator.selectQuery(tableName, options, factory)
queryOptions = Utils._.extend({}, queryOptions, {
include: options.include,
......
......@@ -40,6 +40,26 @@ Transaction.ISOLATION_LEVELS = {
}
/**
* Possible options for row locking. Used in conjuction with `find` calls:
*
* ```js
* t1 // is a transaction
* Model.findAll({
* where: ...
* }, {
* transaction: t1,
* lock: t1.LOCK.UPDATE,
* lock: t1.LOCK.SHARE
* })
* ```
* @property LOCK
*/
Transaction.LOCK = Transaction.prototype.LOCK = {
UPDATE: 'UPDATE',
SHARE: 'SHARE'
}
/**
* Commit the transaction
*
* @return {this}
......
......@@ -998,7 +998,9 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
this.sequelize.sync({ force: true }).success(function() {
Task.create({ title: 'task' }).success(function(task) {
task.createUser({ username: 'foo' }).success(function() {
task.createUser({ username: 'foo' }).success(function(createdUser) {
expect(createdUser.Model).to.equal(User)
expect(createdUser.username).to.equal('foo')
task.getUsers().success(function(_users) {
expect(_users).to.have.length(1)
......
......@@ -57,7 +57,41 @@ describe(Support.getTestDialectTeaser("Self"), function() {
});
});
it('can handle n:m associations', function(done) {
it('can handle n:m associations', function() {
var self = this
var Person = this.sequelize.define('Person', { name: DataTypes.STRING })
Person.hasMany(Person, { as: 'Parents', through: 'Family' })
Person.hasMany(Person, { as: 'Childs', through: 'Family' })
var foreignIdentifiers = _.map(_.values(Person.associations), 'foreignIdentifier')
var rawAttributes = _.keys(this.sequelize.models.Family.rawAttributes)
expect(foreignIdentifiers.length).to.equal(2)
expect(rawAttributes.length).to.equal(4)
expect(foreignIdentifiers).to.have.members([ 'PersonId', 'ChildId' ])
expect(rawAttributes).to.have.members([ 'createdAt', 'updatedAt', 'PersonId', 'ChildId' ])
return this.sequelize.sync({ force: true }).then(function() {
return self.sequelize.Promise.all([
Person.create({ name: 'Mary' }),
Person.create({ name: 'John' }),
Person.create({ name: 'Chris' })
]).spread(function (mary, john, chris) {
return mary.setParents([john]).then(function() {
return chris.addParent(john)
}).then(function() {
return john.getChilds()
}).then(function(children) {
expect(_.map(children, 'id')).to.have.members([mary.id, chris.id])
})
})
})
})
it('can handle n:m associations with pre-defined through table', function(done) {
var Person = this.sequelize.define('Person', { name: DataTypes.STRING });
var Family = this.sequelize.define('Family', {
preexisting_child: {
......
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, dialect = Support.getTestDialect()
, Transaction = require(__dirname + '/../lib/transaction')
, sinon = require('sinon')
describe(Support.getTestDialectTeaser("Transaction"), function () {
this.timeout(4000);
......@@ -71,4 +74,122 @@ describe(Support.getTestDialectTeaser("Transaction"), function () {
})
})
})
if (dialect !== 'sqlite') {
describe('row locking', function () {
this.timeout(10000);
it('supports for update', function (done) {
var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING,
awesome: Support.Sequelize.BOOLEAN
})
, self = this
, t1Spy = sinon.spy()
, t2Spy = sinon.spy()
this.sequelize.sync({ force: true }).then(function () {
return User.create({ username: 'jan'})
}).then(function () {
self.sequelize.transaction(function (t1) {
return User.find({
where: {
username: 'jan'
}
}, {
lock: t1.LOCK.UPDATE,
transaction: t1
}).then(function (t1Jan) {
self.sequelize.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED
}, function (t2) {
User.find({
where: {
username: 'jan'
},
}, {
lock: t2.LOCK.UPDATE,
transaction: t2
}).then(function () {
t2Spy()
t2.commit().then(function () {
expect(t2Spy).to.have.been.calledAfter(t1Spy) // Find should not succeed before t1 has comitted
done()
})
})
t1Jan.updateAttributes({
awesome: true
}, { transaction: t1}).then(function () {
t1Spy()
setTimeout(t1.commit.bind(t1), 2000)
})
})
})
})
})
})
it('supports for share', function (done) {
var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING,
awesome: Support.Sequelize.BOOLEAN
})
, self = this
, t1Spy = sinon.spy()
, t2FindSpy = sinon.spy()
, t2UpdateSpy = sinon.spy()
this.sequelize.sync({ force: true }).then(function () {
return User.create({ username: 'jan'})
}).then(function () {
self.sequelize.transaction(function (t1) {
return User.find({
where: {
username: 'jan'
}
}, {
lock: t1.LOCK.SHARE,
transaction: t1
}).then(function (t1Jan) {
self.sequelize.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED
}, function (t2) {
User.find({
where: {
username: 'jan'
}
}, { transaction: t2}).then(function (t2Jan) {
t2FindSpy()
t2Jan.updateAttributes({
awesome: false
}, {
transaction: t2
}).then(function () {
t2UpdateSpy()
t2.commit().then(function () {
expect(t2FindSpy).to.have.been.calledBefore(t1Spy) // The find call should have returned
expect(t2UpdateSpy).to.have.been.calledAfter(t1Spy) // But the update call should not happen before the first transaction has committed
done()
})
})
})
t1Jan.updateAttributes({
awesome: true
}, {
transaction: t1
}).then(function () {
setTimeout(function () {
t1Spy()
t1.commit()
}, 2000)
})
})
})
})
})
})
})
}
})
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!