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

Commit 350980c8 by Jan Aagaard Meier

Merge branch 'master' into rewriteAssoc

Conflicts:
	lib/associations/belongs-to-many.js
	lib/associations/has-many.js
	test/unit/associations/belongs-to-many.test.js
2 parents c54d6ca0 52443ede
......@@ -3,8 +3,8 @@ language: node_js
node_js:
- "0.10"
- "0.12"
- "iojs-v1.8"
# - "iojs" # iojs 2.0 is failing because of pg-native and nan
- "iojs-v1"
- "iojs-v2"
sudo: false
......
# Next
# 3.2.0
- [FEATURE] Add support for new option `targetKey` in a belongs-to relationship for situations where the target key is not the id field.
- [FEATURE] Add support for keyword `after` in options of a field (useful for migrations), only for MySQL. [#3166](https://github.com/sequelize/sequelize/pull/3166)
- [FEATURE] There's a new sequelize.truncate function to truncate all tables defined through the sequelize models [#2671](https://github.com/sequelize/sequelize/pull/2671)
- [FEATURE] Add support for MySQLs TINYTEXT, MEDIUMTEXT and LONGTEXT. [#3836](https://github.com/sequelize/sequelize/pull/3836)
- [FEATURE] Provide warnings if you misuse data types. [#3839](https://github.com/sequelize/sequelize/pull/3839)
- [FIXED] Fix a case where Postgres arrays containing JSONB type was being generated as JSON type.
- [FIXED] Fix a case where `type` in `sequelize.query` was not being set to raw. [#3800](https://github.com/sequelize/sequelize/pull/3800)
- [FIXED] Fix an issue where include all was not being properly expanded for self-references [#3804](https://github.com/sequelize/sequelize/issues/3804)
- [FIXED] Fix instance.changed regression to not return false negatives for not changed null values [#3812](https://github.com/sequelize/sequelize/issues/3812)
- [FIXED] Fix isEmail validator to allow args: true [#3770](https://github.com/sequelize/sequelize/issues/3770)
- [FIXED] Fix some occasions where `options.logging` was not used correctly
- [FIXED] Fix all occasions where `options.logging` was not used correctly [#3834](https://github.com/sequelize/sequelize/issues/3834)
- [FIXED] Fix `Model#destroy()` to correctly use `options.transaction`
- [FIXED] Fix `QueryInterface#showIndex()` to correctly pass on `options.transaction`
......
......@@ -56,7 +56,7 @@ var sequelize = new Sequelize('mysql://localhost:3306/database', {})
| [options.query={}] | Object | Default options for sequelize.query |
| [options.set={}] | Object | Default options for sequelize.set |
| [options.sync={}] | Object | Default options for sequelize.sync |
| [options.timezone='+00:00'] | String | The timezone used when converting a date from the database into a javascript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM. |
| [options.timezone='+00:00'] | String | The timezone used when converting a date from the database into a JavaScript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM. |
| [options.logging=console.log] | Function | A function that gets executed everytime Sequelize would log something. |
| [options.omitNull=false] | Boolean | A flag that defines if null values should be passed to SQL queries or not. |
| [options.native=false] | Boolean | A flag that defines if native library shall be used or not. Currently only has an effect for postgres |
......@@ -446,7 +446,7 @@ For more about validation, see http://docs.sequelizejs.com/en/latest/docs/models
| attributes.column | String | DataType | Object | The description of a database column |
| attributes.column.type | String | DataType | A string or a data type |
| [attributes.column.allowNull=true] | Boolean | If false, the column will have a NOT NULL constraint, and a not null validation will be run before an instance is saved. |
| [attributes.column.defaultValue=null] | Any | A literal default value, a javascript function, or an SQL function (see `sequelize.fn`) |
| [attributes.column.defaultValue=null] | Any | A literal default value, a JavaScript function, or an SQL function (see `sequelize.fn`) |
| [attributes.column.unique=false] | String | Boolean | If true, the column will get a unique constraint. If a string is provided, the column will be part of a composite unique index. If multiple columns have the same string, they will be part of the same unique index |
| [attributes.column.primaryKey=false] | Boolean | |
| [attributes.column.field=null] | String | If set, sequelize will map the attribute name to a different name in the database |
......
......@@ -56,6 +56,17 @@ var User = this.sequelize.define('User', {/* attributes */})
User.belongsTo(Company, {foreignKey: 'fk_company'}); // Adds fk_company to User
```
#### Target keys
By default the target key for a belongsTo relation will be the target primary key. To override this behavior, use the `targetKey` option.
```js
var User = this.sequelize.define('User', {/* attributes */})
, Company = this.sequelize.define('Company', {/* attributes */});
User.belongsTo(Company, {foreignKey: 'fk_companyname', targetKey: 'name'}); // Adds fk_companyname to User
```
### HasOne
......@@ -254,7 +265,7 @@ UPDATE comments SET commentable_id = 42, commentable = 'image'
The `getItem` utility function on `Comment` completes the picture - it simply converts the `commentable` string into a call to etiher `getImage` or `getPost`, providing an abstraction over whether a comment belongs to a post or an image.
#### n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several item
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several items.
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
......
......@@ -273,25 +273,8 @@ migration.removeIndex('Person', ['firstname', 'lastname'])
```
## Programmatic use
Sequelize has a [sister library](https://github.com/sequelize/umzug) for programmatically handling execution and logging of migration tasks.
If you need to interact with the migrator within your code, you can easily achieve that via `sequelize.getMigrator`. You can specify the path to your migrations as well as a pattern which represents the files that contain the migrations.
```js
var migrator = sequelize.getMigrator({
path: process.cwd() + '/database/migrations',
filesFilter: /\.coffee$/
})
```
Once you have a migrator object, you can run its migration with `migrator.migrate`. By default, this will execute all the up methods within your pending migrations. If you want to rollback a migration, just call it like this:
```js
migrator
.migrate({ method: 'down' })
.then(function() {
// The migrations have been executed!
})
```
[0]: http://gulpjs.com/
[1]: https://github.com/sequelize/cli
......
......@@ -22,6 +22,24 @@ var BelongsToMany = function(source, target, options) {
this.isSelfAssociation = this.source === this.target;
this.doubleLinked = false;
this.as = this.options.as;
if (this.as) {
this.isAliased = true;
if (Utils._.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
......@@ -137,23 +155,6 @@ var BelongsToMany = function(source, target, options) {
this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);
if (this.as) {
this.isAliased = true;
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
......
......@@ -55,7 +55,7 @@ var BelongsTo = function(source, target, options) {
!this.target.options.underscored
);
this.targetIdentifier = this.target.primaryKeyAttribute;
this.targetIdentifier = this.options.targetKey || this.target.primaryKeyAttribute;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
......
......@@ -162,8 +162,9 @@ Mixin.hasOne = singleLinked(HasOne);
* @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 {string} [options.as] The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the source table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the colum. Defaults to the name of target + primary key of target
* @param {string} [options.as] The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting assocated models. Defaults to the singularized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the source table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of target + primary key of target
* @param {string} [options.targetKey] The name of the field to use as the key for the association in the target table. Defaults to the primary key of the target table
* @param {string} [options.onDelete='SET NULL']
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
......
'use strict';
var util = require('util')
, _ = require('lodash');
, _ = require('lodash')
, warnings = {};
/**
* A convenience class holding commonly used data types. The datatypes are used when definining a new model using `Sequelize.define`, like this:
......@@ -38,12 +39,20 @@ var ABSTRACT = function(options) {
};
ABSTRACT.prototype.dialectTypes = '';
ABSTRACT.prototype.toString = function() {
return this.toSql();
};
ABSTRACT.prototype.toSql = function() {
return this.key;
};
ABSTRACT.prototype.warn = function(text) {
if (!warnings[text]) {
warnings[text] = true;
console.warn('>> WARNING:', text, '\n>> Check:', this.dialectTypes);
}
};
/**
* A variable length string. Default length 255
......
......@@ -1194,7 +1194,7 @@ var QueryGenerator = {
, tableRight = as
, attrRight = association.associationType !== 'BelongsTo' ?
association.identifierField || association.identifier :
right.rawAttributes[primaryKeysRight[0]].field || primaryKeysRight[0]
right.rawAttributes[association.targetIdentifier || primaryKeysRight[0]].field
, joinOn
, subQueryJoinOn;
......
......@@ -4,6 +4,8 @@ var BaseTypes = require('../../data-types')
, util = require('util')
, _ = require('lodash');
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://msdn.microsoft.com/en-us/library/ms187752%28v=sql.110%29.aspx';
var STRING = function() {
if (!(this instanceof STRING)) return new STRING();
BaseTypes.STRING.apply(this, arguments);
......@@ -21,8 +23,12 @@ STRING.prototype.toSql = function() {
BaseTypes.TEXT.prototype.toSql = function() {
// TEXT is deprecated in mssql and it would normally be saved as a non-unicode string.
// Using unicode is just future proof
if (this._length.toLowerCase() === 'tiny') {
return 'NVARCHAR(256)'; // tiny text should be 2^8 < 8000
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
this.warn('MSSQL does not support TEXT with the `length` = `tiny` option. `NVARCHAR(256)` will be used instead.');
return 'NVARCHAR(256)';
}
this.warn('MSSQL does not support TEXT with the `length` option. `NVARCHAR(MAX)` will be used instead.');
}
return 'NVARCHAR(MAX)';
};
......@@ -38,8 +44,12 @@ BOOLEAN.prototype.toSql = function() {
};
BaseTypes.BLOB.prototype.toSql = function() {
if (this._length.toLowerCase() === 'tiny') {
return 'VARBINARY(256)'; // tiny blobs should be 2^8 < 8000
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
this.warn('MSSQL does not support BLOB with the `length` = `tiny` option. `VARBINARY(256)` will be used instead.');
return 'VARBINARY(256)';
}
this.warn('MSSQL does not support BLOB with the `length` option. `VARBINARY(MAX)` will be used instead.');
}
return 'VARBINARY(MAX)';
};
......@@ -78,11 +88,14 @@ var INTEGER = function() {
if (!(this instanceof INTEGER)) return new INTEGER();
BaseTypes.INTEGER.apply(this, arguments);
// MSSQL does not support any parameters for integer
// MSSQL does not support any options for integer
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('MSSQL does not support INTEGER with options. Plain `INTEGER` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(INTEGER, BaseTypes.INTEGER);
......@@ -90,11 +103,14 @@ var BIGINT = function() {
if (!(this instanceof BIGINT)) return new BIGINT();
BaseTypes.BIGINT.apply(this, arguments);
// MSSQL does not support any parameters for bigint
// MSSQL does not support any options for bigint
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('MSSQL does not support BIGINT with options. Plain `BIGINT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(BIGINT, BaseTypes.BIGINT);
......@@ -102,11 +118,14 @@ var REAL = function() {
if (!(this instanceof REAL)) return new REAL();
BaseTypes.REAL.apply(this, arguments);
// MSSQL does not support any parameters for real
// MSSQL does not support any options for real
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('MSSQL does not support REAL with options. Plain `REAL` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(REAL, BaseTypes.REAL);
......@@ -114,15 +133,23 @@ var FLOAT = function() {
if (!(this instanceof FLOAT)) return new FLOAT();
BaseTypes.FLOAT.apply(this, arguments);
// MSSQL does only support lengths as parameter.
// MSSQL does only support lengths as option.
// Values between 1-24 result in 7 digits precision (4 bytes storage size)
// Values between 25-53 result in 15 digits precision (8 bytes storage size)
// If decimals are provided remove these and print a warning
if (this._decimals) {
this.warn('MSSQL does not support Float with decimals. Plain `FLOAT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
}
if (this._unsigned) {
this.warn('MSSQL does not support Float unsigned. `UNSIGNED` was removed.');
this._unsigned = undefined;
}
if (this._zerofill) {
this.warn('MSSQL does not support Float zerofill. `ZEROFILL` was removed.');
this._zerofill = undefined;
}
};
util.inherits(FLOAT, BaseTypes.FLOAT);
......
......@@ -4,6 +4,8 @@ var BaseTypes = require('../../data-types')
, util = require('util')
, _ = require('lodash');
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://dev.mysql.com/doc/refman/5.7/en/data-types.html';
var UUID = function() {
if (!(this instanceof UUID)) return new UUID();
BaseTypes.UUID.apply(this, arguments);
......
......@@ -4,6 +4,8 @@ var BaseTypes = require('../../data-types')
, util = require('util')
, _ = require('lodash');
BaseTypes.ABSTRACT.prototype.dialectTypes = 'http://www.postgresql.org/docs/9.4/static/datatype.html';
var STRING = function() {
if (!(this instanceof STRING)) return new STRING();
BaseTypes.STRING.apply(this, arguments);
......@@ -18,6 +20,10 @@ STRING.prototype.toSql = function() {
};
BaseTypes.TEXT.prototype.toSql = function() {
if (this._length) {
this.warn('PostgreSQL does not support TEXT with options. Plain `TEXT` will be used instead.');
this._length = undefined;
}
return 'TEXT';
};
......@@ -59,10 +65,13 @@ var INTEGER = function() {
BaseTypes.INTEGER.apply(this, arguments);
// POSTGRES does not support any parameters for integer
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('PostgreSQL does not support INTEGER with options. Plain `INTEGER` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(INTEGER, BaseTypes.INTEGER);
......@@ -71,10 +80,13 @@ var BIGINT = function() {
BaseTypes.BIGINT.apply(this, arguments);
// POSTGRES does not support any parameters for bigint
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('PostgreSQL does not support BIGINT with options. Plain `BIGINT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(BIGINT, BaseTypes.BIGINT);
......@@ -83,10 +95,13 @@ var REAL = function() {
BaseTypes.REAL.apply(this, arguments);
// POSTGRES does not support any parameters for real
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('PostgreSQL does not support REAL with options. Plain `REAL` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(REAL, BaseTypes.REAL);
......@@ -95,10 +110,13 @@ var DOUBLE = function() {
BaseTypes.DOUBLE.apply(this, arguments);
// POSTGRES does not support any parameters for double
if (this._length || this.options.length || this._unsigned || this._zerofill) {
this.warn('PostgreSQL does not support DOUBLE with options. Plain `DOUBLE` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
};
util.inherits(DOUBLE, BaseTypes.DOUBLE);
......@@ -109,17 +127,29 @@ var FLOAT = function() {
// POSTGRES does only support lengths as parameter.
// Values between 1-24 result in REAL
// Values between 25-53 result in DOUBLE PRECISION
// If decimals are provided remove these and print a warning
if (this._decimals) {
this.warn('PostgreSQL does not support FLOAT with decimals. Plain `FLOAT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._decimals = undefined;
}
if (this._unsigned) {
this.warn('PostgreSQL does not support FLOAT unsigned. `UNSIGNED` was removed.');
this._unsigned = undefined;
}
if (this._zerofill) {
this.warn('PostgreSQL does not support FLOAT zerofill. `ZEROFILL` was removed.');
this._zerofill = undefined;
}
};
util.inherits(FLOAT, BaseTypes.FLOAT);
BaseTypes.BLOB.prototype.toSql = function() {
if (this._length) {
this.warn('PostgreSQL does not support BLOB (BYTEA) with options. Plain `BYTEA` will be used instead.');
this._length = undefined;
}
return 'BYTEA';
};
......
......@@ -890,12 +890,13 @@ var QueryGenerator = {
else if (DataTypes.ARRAY.is(field.type, DataTypes.RANGE)) { // escape array of ranges
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + range.stringify(v) + "'";}).join(',') + ']::' + field.type.toString();
}
} else if (field && (field.type instanceof DataTypes.JSON || field.type instanceof DataTypes.JSONB)) {
} else if (field && field.type instanceof DataTypes.JSON) {
value = JSON.stringify(value);
} else if (Array.isArray(value) && field && DataTypes.ARRAY.is(field.type, DataTypes.JSON)) {
var jsonType = field.type.type; // type may be JSON or JSONB
return 'ARRAY[' + value.map(function (v) {
return SqlString.escape(JSON.stringify(v), false, this.options.timezone, this.dialect, field);
}, this).join(',') + ']::JSON[]';
}, this).join(',') + ']::' + jsonType.key + '[]';
}
return SqlString.escape(value, false, this.options.timezone, this.dialect, field);
......
......@@ -4,6 +4,8 @@ var BaseTypes = require('../../data-types')
, util = require('util')
, _ = require('lodash');
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://www.sqlite.org/datatype3.html';
var STRING = function() {
if (!(this instanceof STRING)) return new STRING();
BaseTypes.STRING.apply(this, arguments);
......@@ -19,6 +21,10 @@ STRING.prototype.toSql = function() {
};
BaseTypes.TEXT.prototype.toSql = function() {
if (this._length) {
this.warn('SQLite does not support TEXT with options. Plain `TEXT` will be used instead.');
this._length = undefined;
}
return 'TEXT';
};
......
......@@ -65,7 +65,7 @@ var changeColumn = function(tableName, attributes, options) {
, self = this;
options = options || {};
return this.describeTable(tableName).then(function(fields) {
return this.describeTable(tableName, options).then(function(fields) {
fields[attributeName] = attributes[attributeName];
var sql = self.QueryGenerator.removeColumnQuery(tableName, fields)
......@@ -99,7 +99,7 @@ var renameColumn = function(tableName, attrNameBefore, attrNameAfter, options) {
var self = this;
options = options || {};
return this.describeTable(tableName).then(function(fields) {
return this.describeTable(tableName, options).then(function(fields) {
fields[attrNameAfter] = Utils._.clone(fields[attrNameBefore]);
delete fields[attrNameBefore];
......
......@@ -538,7 +538,7 @@ Instance.prototype.save = function(options) {
}
if (this.isNewRecord === false) {
if (primaryKeyName && !this.get(primaryKeyName, {raw: true})) {
if (primaryKeyName && this.get(primaryKeyName, {raw: true}) === undefined) {
throw new Error('You attempted to save an instance with no primary key, this is not allowed since it would result in a global update');
}
}
......@@ -617,7 +617,7 @@ Instance.prototype.save = function(options) {
transaction: options.transaction,
logging: options.logging
}).then(function () {
return self[include.association.accessors.set](instance, {save: false});
return self[include.association.accessors.set](instance, {save: false, logging: options.logging});
});
});
})
......
......@@ -881,7 +881,7 @@ Model.prototype.sync = function(options) {
});
return Promise.map(indexes, function (index) {
return self.QueryInterface.addIndex(self.getTableName(options), index, self.tableName);
return self.QueryInterface.addIndex(self.getTableName(options), _.assign({logging: options.logging}, index), self.tableName);
});
}).return(this);
};
......@@ -2255,8 +2255,8 @@ Model.prototype.update = function(values, options) {
*
* @return {Promise}
*/
Model.prototype.describe = function(schema) {
return this.QueryInterface.describeTable(this.tableName, schema || this.options.schema || undefined);
Model.prototype.describe = function(schema, options) {
return this.QueryInterface.describeTable(this.tableName, _.assign({schema: schema || this.options.schema || undefined}, options));
};
Model.prototype.$getDefaultTimestamp = function(attr) {
......
......@@ -65,31 +65,31 @@ CounterCache.prototype.injectHooks = function() {
previousTargetId;
CounterUtil = {
update: function (targetId) {
update: function (targetId, options) {
var query = CounterUtil._targetQuery(targetId);
return association.target.count({ where: query }).then(function (count) {
return association.target.count({ where: query, logging: options && options.logging }).then(function (count) {
var newValues = {};
query = CounterUtil._sourceQuery(targetId);
newValues[counterCacheInstance.columnName] = count;
return association.source.update(newValues, { where: query });
return association.source.update(newValues, { where: query, logging: options && options.logging });
});
},
increment: function (targetId) {
increment: function (targetId, options) {
var query = CounterUtil._sourceQuery(targetId);
return association.source.find({ where: query }).then(function (instance) {
return instance.increment(counterCacheInstance.columnName, { by: 1 });
return association.source.find({ where: query, logging: options && options.logging }).then(function (instance) {
return instance.increment(counterCacheInstance.columnName, { by: 1, logging: options && options.logging });
});
},
decrement: function (targetId) {
decrement: function (targetId, options) {
var query = CounterUtil._sourceQuery(targetId);
return association.source.find({ where: query }).then(function (instance) {
return instance.decrement(counterCacheInstance.columnName, { by: 1 });
return association.source.find({ where: query, logging: options && options.logging }).then(function (instance) {
return instance.decrement(counterCacheInstance.columnName, { by: 1, logging: options && options.logging });
});
},
// helpers
......@@ -109,51 +109,51 @@ CounterCache.prototype.injectHooks = function() {
}
};
fullUpdateHook = function (target) {
fullUpdateHook = function (target, options) {
var targetId = target.get(association.identifier)
, promises = [];
if (targetId) {
promises.push(CounterUtil.update(targetId));
promises.push(CounterUtil.update(targetId, options));
}
if (previousTargetId && previousTargetId !== targetId) {
promises.push(CounterUtil.update(previousTargetId));
promises.push(CounterUtil.update(previousTargetId, options));
}
return Promise.all(promises).return(undefined);
};
atomicHooks = {
create: function (target) {
create: function (target, options) {
var targetId = target.get(association.identifier);
if (targetId) {
return CounterUtil.increment(targetId);
return CounterUtil.increment(targetId, options);
}
},
update: function (target) {
update: function (target, options) {
var targetId = target.get(association.identifier)
, promises = [];
if (targetId && !previousTargetId) {
promises.push(CounterUtil.increment(targetId));
promises.push(CounterUtil.increment(targetId, options));
}
if (!targetId && previousTargetId) {
promises.push(CounterUtil.decrement(targetId));
promises.push(CounterUtil.decrement(targetId, options));
}
if (previousTargetId && targetId && previousTargetId !== targetId) {
promises.push(CounterUtil.increment(targetId));
promises.push(CounterUtil.decrement(previousTargetId));
promises.push(CounterUtil.increment(targetId, options));
promises.push(CounterUtil.decrement(previousTargetId, options));
}
return Promise.all(promises);
},
destroy: function (target) {
destroy: function (target, options) {
var targetId = target.get(association.identifier);
if (targetId) {
return CounterUtil.decrement(targetId);
return CounterUtil.decrement(targetId, options);
}
}
};
......
......@@ -32,11 +32,18 @@ shimCLS(Promise.prototype, 'error', [0]);
shimCLS(Promise.prototype, 'finally', [0]);
// Collections
shimCLS(Promise, 'map', [1]);
shimCLS(Promise, 'reduce', [1]);
shimCLS(Promise, 'filter', [1]);
shimCLS(Promise, 'each', [1]);
shimCLS(Promise.prototype, 'map', [0]);
shimCLS(Promise.prototype, 'reduce', [0]);
shimCLS(Promise.prototype, 'filter', [0]);
shimCLS(Promise.prototype, 'each', [0]);
// Promisification
shimCLS(Promise.prototype, 'nodeify', [0]);
// Utility
shimCLS(Promise.prototype, 'tap', [0]);
......
......@@ -243,7 +243,7 @@ QueryInterface.prototype.dropAllTables = function(options) {
};
var skip = options.skip || [];
return self.showAllTables().then(function(tableNames) {
return self.showAllTables({logging: options.logging}).then(function(tableNames) {
if (self.sequelize.options.dialect === 'sqlite') {
return self.sequelize.query('PRAGMA foreign_keys;', options).then(function(result) {
var foreignKeysAreEnabled = result.foreign_keys === 1;
......@@ -259,7 +259,7 @@ QueryInterface.prototype.dropAllTables = function(options) {
}
});
} else {
return self.getForeignKeysForTables(tableNames).then(function(foreignKeys) {
return self.getForeignKeysForTables(tableNames, {logging: options.logging}).then(function(foreignKeys) {
var promises = [];
tableNames.forEach(function(tableName) {
......@@ -422,20 +422,12 @@ QueryInterface.prototype.renameColumn = function(tableName, attrNameBefore, attr
}.bind(this));
};
QueryInterface.prototype.addIndex = function(tableName, _attributes, options, _rawTablename) {
var attributes, rawTablename;
QueryInterface.prototype.addIndex = function(tableName, attributes, options, rawTablename) {
// Support for passing tableName, attributes, options or tableName, options (with a fields param which is the attributes)
if (Array.isArray(_attributes)) {
attributes = _attributes;
rawTablename = _rawTablename;
} else {
// Support for passing an options object with a fields attribute instead of attributes, options
options = _attributes;
attributes = options.fields;
if (!Array.isArray(attributes)) {
rawTablename = options;
options = attributes;
attributes = options.fields;
}
if (!rawTablename) {
......
......@@ -67,7 +67,7 @@ var url = require('url')
* @param {Object} [options.query={}] Default options for sequelize.query
* @param {Object} [options.set={}] Default options for sequelize.set
* @param {Object} [options.sync={}] Default options for sequelize.sync
* @param {String} [options.timezone='+00:00'] The timezone used when converting a date from the database into a javascript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM.
* @param {String} [options.timezone='+00:00'] The timezone used when converting a date from the database into a JavaScript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM.
* @param {Function} [options.logging=console.log] A function that gets executed everytime Sequelize would log something.
* @param {Boolean} [options.omitNull=false] A flag that defines if null values should be passed to SQL queries or not.
* @param {Boolean} [options.native=false] A flag that defines if native library shall be used or not. Currently only has an effect for postgres
......@@ -458,7 +458,7 @@ Sequelize.prototype.getQueryInterface = function() {
* @param {String|DataType|Object} attributes.column The description of a database column
* @param {String|DataType} attributes.column.type A string or a data type
* @param {Boolean} [attributes.column.allowNull=true] If false, the column will have a NOT NULL constraint, and a not null validation will be run before an instance is saved.
* @param {Any} [attributes.column.defaultValue=null] A literal default value, a javascript function, or an SQL function (see `sequelize.fn`)
* @param {Any} [attributes.column.defaultValue=null] A literal default value, a JavaScript function, or an SQL function (see `sequelize.fn`)
* @param {String|Boolean} [attributes.column.unique=false] If true, the column will get a unique constraint. If a string is provided, the column will be part of a composite unique index. If multiple columns have the same string, they will be part of the same unique index
* @param {Boolean} [attributes.column.primaryKey=false]
* @param {String} [attributes.column.field=null] If set, sequelize will map the attribute name to a different name in the database
......@@ -603,10 +603,6 @@ Sequelize.prototype.import = function(path) {
return this.importCache[path];
};
Sequelize.prototype.migrate = function(options) {
return this.getMigrator().migrate(options);
};
/**
* Execute a query on the DB, with the posibility to bypass all the sequelize goodness.
*
......@@ -933,8 +929,8 @@ Sequelize.prototype.drop = function(options) {
* @alias validate
* @return {Promise}
*/
Sequelize.prototype.authenticate = function() {
return this.query('SELECT 1+1 AS result', { raw: true, plain: true }).return().catch(function(err) {
Sequelize.prototype.authenticate = function(options) {
return this.query('SELECT 1+1 AS result', Utils._.assign({ raw: true, plain: true }, options)).return().catch(function(err) {
throw new Error(err);
});
};
......
......@@ -95,6 +95,8 @@ var Utils = module.exports = {
},
cloneDeep: function(obj, fn) {
return lodash.cloneDeep(obj, function (elem) {
// Preserve special data-types like `fn` across clones. _.get() is used for checking up the prototype chain
if (!!Utils._.get(elem, 'clone') && typeof elem.clone === 'function') {return elem.clone(); }
// Unfortunately, lodash.cloneDeep doesn't preserve Buffer.isBuffer, which we have to rely on for binary data
if (Buffer.isBuffer(elem)) { return elem; }
......@@ -421,4 +423,8 @@ Utils.fn.prototype._isSequelizeMethod =
Utils.col.prototype._isSequelizeMethod =
Utils.json.prototype._isSequelizeMethod = true;
Utils.fn.prototype.clone = function() {
return new Utils.fn(this.fn, this.args);
};
Utils.Promise = require('./promise');
{
"name": "sequelize",
"description": "Multi dialect ORM for Node.JS/io.js",
"version": "3.1.1",
"version": "3.2.0",
"author": "Sascha Depold <sascha@depold.com>",
"contributors": [
{
......
......@@ -600,6 +600,54 @@ describe(Support.getTestDialectTeaser('BelongsTo'), function() {
expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING);
});
});
it('should support a non-primary key as the association column', function() {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
return this.sequelize.sync({ force: true }).then(function() {
return User.create({ username: 'bob' }).then(function(newUser) {
return Task.create({ title: 'some task' }).then(function(newTask) {
return newTask.setUser(newUser).then(function() {
return Task.findOne({title: 'some task'}).then(function (foundTask) {
return foundTask.getUser().then(function (foundUser) {
expect(foundUser.username).to.equal('bob');
});
});
});
});
});
});
});
it('should support a non-primary key as the association column with a field option', function() {
var User = this.sequelize.define('User', {
username: {
type: DataTypes.STRING,
field: 'the_user_name_field'
}
})
, Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
return this.sequelize.sync({ force: true }).then(function() {
return User.create({ username: 'bob' }).then(function(newUser) {
return Task.create({ title: 'some task' }).then(function(newTask) {
return newTask.setUser(newUser).then(function() {
return Task.findOne({title: 'some task'}).then(function (foundTask) {
return foundTask.getUser().then(function (foundUser) {
expect(foundUser.username).to.equal('bob');
});
});
});
});
});
});
});
});
describe('Association options', function() {
......
......@@ -240,6 +240,17 @@ if (current.dialect.supports.transactions) {
});
});
it('static map', function () {
var self = this;
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.sequelize.Promise.map(self.User.findAll(), function () {
expect(self.ns.get('transaction').id).to.be.ok;
expect(self.ns.get('transaction').id).to.equal(tid);
});
});
});
it('reduce', function () {
var self = this;
return this.sequelize.transaction(function () {
......@@ -251,6 +262,17 @@ if (current.dialect.supports.transactions) {
});
});
it('static reduce', function () {
var self = this;
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.sequelize.Promise.reduce(self.User.findAll(), function () {
expect(self.ns.get('transaction').id).to.be.ok;
expect(self.ns.get('transaction').id).to.equal(tid);
});
});
});
it('filter', function () {
var self = this;
return this.sequelize.transaction(function () {
......@@ -262,6 +284,17 @@ if (current.dialect.supports.transactions) {
});
});
it('static filter', function () {
var self = this;
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.sequelize.Promise.filter(self.User.findAll(), function () {
expect(self.ns.get('transaction').id).to.be.ok;
expect(self.ns.get('transaction').id).to.equal(tid);
});
});
});
it('each', function () {
var self = this;
return this.sequelize.transaction(function () {
......@@ -273,6 +306,28 @@ if (current.dialect.supports.transactions) {
});
});
it('static each', function () {
var self = this;
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.sequelize.Promise.each(self.User.findAll(), function () {
expect(self.ns.get('transaction').id).to.be.ok;
expect(self.ns.get('transaction').id).to.equal(tid);
});
});
});
it('nodeify', function () {
var self = this;
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.User.findAll().nodeify(function () {
expect(self.ns.get('transaction').id).to.be.ok;
expect(self.ns.get('transaction').id).to.equal(tid);
});
});
});
it('tap', function () {
var self = this;
return this.sequelize.transaction(function () {
......
......@@ -21,10 +21,8 @@ describe(Support.getTestDialectTeaser('Configuration'), function() {
if (dialect === 'sqlite') {
// SQLite doesn't have a breakdown of error codes, so we are unable to discern between the different types of errors.
return expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(seq.ConnectionError, 'SQLITE_CANTOPEN: unable to open database file');
} else if (dialect === 'mssql' || dialect === 'postgres') {
return expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith([seq.HostNotReachableError, seq.InvalidConnectionError]);
} else {
return expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(seq.InvalidConnectionError, 'connect EINVAL');
return expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith([seq.HostNotReachableError, seq.InvalidConnectionError]);
}
});
......
......@@ -170,6 +170,30 @@ describe(Support.getTestDialectTeaser('Include'), function() {
});
});
it('should support a belongsTo with the targetKey option', function() {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
return this.sequelize.sync({ force: true }).then(function() {
return User.create({ username: 'bob' }).then(function(newUser) {
return Task.create({ title: 'some task' }).then(function(newTask) {
return newTask.setUser(newUser).then(function() {
return Task.find({
title: 'some task',
include: [ { model: User } ]
})
.then(function (foundTask) {
expect(foundTask).to.be.ok;
expect(foundTask.User.username).to.equal('bob');
});
});
});
});
});
});
it('should support many levels of belongsTo (with a lower level having a where)', function() {
var A = this.sequelize.define('a', {})
, B = this.sequelize.define('b', {})
......
......@@ -898,6 +898,38 @@ describe(Support.getTestDialectTeaser('Instance'), function() {
});
});
it('handles an entry with primaryKey of zero', function() {
var username = 'user'
, newUsername = 'newUser'
, User2 = this.sequelize.define('User2',
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: false,
primaryKey: true
},
username: { type: DataTypes.STRING }
});
return User2.sync().then(function () {
return User2.create({id: 0, username: username}).then(function (user){
expect(user).to.be.ok;
expect(user.id).to.equal(0);
expect(user.username).to.equal(username);
return User2.findById(0).then(function (user) {
expect(user).to.be.ok;
expect(user.id).to.equal(0);
expect(user.username).to.equal(username);
return user.updateAttributes({username: newUsername}).then(function (user) {
expect(user).to.be.ok;
expect(user.id).to.equal(0);
expect(user.username).to.equal(newUsername);
});
});
});
});
});
it('updates the timestamps', function() {
var now = Date.now()
, user = null
......
'use strict';
'use strict';
/* jshint -W030 */
/* jshint -W110 */
......
......@@ -223,6 +223,31 @@ describe(Support.getTestDialectTeaser('Model'), function() {
expect(user.baz).to.equal('oof');
});
});
it('works with database functions', function() {
return this.User.upsert({ id: 42, username: 'john', foo: this.sequelize.fn('upper', 'mixedCase1')}).bind(this).then(function(created) {
if (dialect === 'sqlite') {
expect(created).to.be.undefined;
} else {
expect(created).to.be.ok;
}
return this.sequelize.Promise.delay(1000).bind(this).then(function() {
return this.User.upsert({ id: 42, username: 'doe', foo: this.sequelize.fn('upper', 'mixedCase2') });
});
}).then(function(created) {
if (dialect === 'sqlite') {
expect(created).to.be.undefined;
} else {
expect(created).not.to.be.ok;
}
return this.User.findById(42);
}).then(function(user) {
expect(user.createdAt).to.be.ok;
expect(user.username).to.equal('doe');
expect(user.foo).to.equal('MIXEDCASE2');
});
});
});
}
});
......@@ -39,7 +39,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), function() {
count = 0;
expect(tableNames).to.have.length(1);
return self.queryInterface.dropAllTables({logging: log}).then(function() {
expect(count).to.be.equal(1);
expect(count).to.be.at.least(1);
count = 0;
return self.queryInterface.showAllTables().then(function(tableNames) {
expect(tableNames).to.be.empty;
......@@ -72,7 +72,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), function() {
beforeEach(function() {
var self = this;
return this.queryInterface.dropTable('Group', {logging: log}).then(function() {
expect(count).to.be.equal(1);
expect(count).to.be.at.least(1);
count = 0;
return self.queryInterface.createTable('Group', {
username: DataTypes.STRING,
......@@ -156,7 +156,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), function() {
return Users.sync({ force: true }).then(function() {
return self.queryInterface.describeTable('_Users', {logging: log}).then(function(metadata) {
expect(count).to.be.equal(1);
expect(count).to.be.at.least(1);
count = 0;
var username = metadata.username;
......
......@@ -63,5 +63,27 @@ describe(Support.getTestDialectTeaser('belongsToMany'), function() {
expect(this.destroy).to.have.been.calledOnce;
});
});
describe('belongsToMany', function () {
it('works with singular and plural name for self-associations', function () {
// Models taken from https://github.com/sequelize/sequelize/issues/3796
var Service = current.define('service', {})
, Instance = Service.Instance;
Service.belongsToMany(Service, {through: 'Supplements', as: 'supplements'});
Service.belongsToMany(Service, {through: 'Supplements', as: {singular: 'supplemented', plural: 'supplemented'}});
expect(Instance.prototype).to.have.property('getSupplements').which.is.a.function;
expect(Instance.prototype).to.have.property('addSupplement').which.is.a.function;
expect(Instance.prototype).to.have.property('addSupplements').which.is.a.function;
expect(Instance.prototype).to.have.property('getSupplemented').which.is.a.function;
expect(Instance.prototype).not.to.have.property('getSupplementeds').which.is.a.function;
expect(Instance.prototype).to.have.property('addSupplemented').which.is.a.function;
expect(Instance.prototype).not.to.have.property('addSupplementeds').which.is.a.function;
});
});
});
});
......@@ -560,6 +560,13 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
});
});
// TODO: Fix Enums and add more tests
// suite('ENUM', function () {
// testsql('ENUM("value 1", "value 2")', DataTypes.ENUM('value 1', 'value 2'), {
// default: 'ENUM'
// });
// });
suite('BLOB', function () {
testsql('BLOB', DataTypes.BLOB, {
default: 'BLOB',
......
......@@ -40,6 +40,15 @@ if (current.dialect.supports.JSON) {
postgres: 'ARRAY[\'{"some":"nested","more":{"nested":true},"answer":42}\',\'43\',\'"joe"\']::JSON[]'
});
});
test('array of JSONB', function () {
expectsql(sql.escape([
{ some: 'nested', more: { nested: true }, answer: 42 },
43,
'joe'
], { type: DataTypes.ARRAY(DataTypes.JSONB)}), {
postgres: 'ARRAY[\'{"some":"nested","more":{"nested":true},"answer":42}\',\'43\',\'"joe"\']::JSONB[]'
});
});
});
});
});
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!