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

Commit 72391bf6 by Mick Hansen

merge

2 parents 148647f4 8399d6f5
......@@ -4,4 +4,5 @@ test
README.md
.watchr.js
changelog.md
Makefile
\ No newline at end of file
Makefile
coverage*
......@@ -13,9 +13,9 @@ teaser:
test:
@if [ "$$GREP" ]; then \
make teaser && ./node_modules/mocha/bin/mocha --check-leaks --colors -t 10000 --reporter $(REPORTER) -g "$$GREP" $(TESTS); \
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 --check-leaks --colors -t 10000 --reporter $(REPORTER) $(TESTS); \
make teaser && ./node_modules/mocha/bin/mocha --globals setImmediate,clearImmediate --check-leaks --colors -t 10000 --reporter $(REPORTER) $(TESTS); \
fi
cover:
......
......@@ -20,6 +20,7 @@ To install 2.x.x branch - which has a unstable API and will break backwards comp
- [Changelog](https://github.com/sequelize/sequelize/blob/master/changelog.md)
- [Collaboration and pull requests](https://github.com/sequelize/sequelize/wiki/Collaboration)
- [Roadmap](https://github.com/sequelize/sequelize/wiki/Roadmap)
- [Meetups](https://github.com/sequelize/sequelize/wiki/Meetups)
## Important Notes ##
......
Notice: All 1.7.x changes are present in 2.0.x aswell
# v1.7.0-rc3
- dropAllTables now takes an option parameter with `skip` as an option [#1280](https://github.com/sequelize/sequelize/pull/1280)
- implements .spread for eventemitters [#1277](https://github.com/sequelize/sequelize/pull/1277)
- fixes some of the mysql connection error bugs [#1282](https://github.com/sequelize/sequelize/pull/1282)
- [Feature] Support for OR queries.
- [Feature] Support for HAVING queries. [#1286](https://github.com/sequelize/sequelize/pull/1286)
- bulkUpdate and bulkDestroy now returns affected rows. [#1293](https://github.com/sequelize/sequelize/pull/1293)
- fixes transaction memory leak issue
- fixes security issue where it was possible to overwrite the id attribute when defined by sequelize (screwup - and fix - by mickhansen)
# v1.7.0-rc2
- fixes unixSocket connections for mariadb [#1248](https://github.com/sequelize/sequelize/pull/1248)
- fixes a hangup issue for mysql [#1244](https://github.com/sequelize/sequelize/pull/1244)
......
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
var Person = sequelize.define('Person',
{ name: Sequelize.STRING,
age : Sequelize.INTEGER,
gender: Sequelize.ENUM('male', 'female')
})
, chainer = new Sequelize.Utils.QueryChainer
sequelize.sync({force: true}).on('success', function() {
var count = 10,
queries = []
for(var i = 0; i < count; i++)
chainer.add(Person.create({name: 'someone' + (i % 3), age : i+5, gender: (i % 2 == 0) ? 'male' : 'female'}))
console.log("Begin to save " + count + " items!")
chainer.run().on('success', function() {
console.log("finished")
Person.sum('age').on('success', function(sum) {
console.log("Sum of all peoples' ages: " + sum)
});
Person.sum('age', { where: { 'gender': 'male' } }).on('success', function(sum) {
console.log("Sum of all males' ages: " + sum)
});
})
})
......@@ -128,8 +128,9 @@ module.exports = (function() {
// the id is in the target table
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
var doubleLinked = this.doubleLinked
, self = this
var doubleLinked = this.doubleLinked
, self = this
, primaryKeyDeleted = false
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)
......@@ -152,21 +153,29 @@ module.exports = (function() {
}
}
// remove any PKs previously defined by sequelize
Utils._.each(this.through.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1 && self.through.rawAttributes[attributeName]._autoGenerated === true) {
delete self.through.rawAttributes[attributeName]
primaryKeyDeleted = true
}
})
// define a new model, which connects the models
var combinedTableAttributes = {}
var sourceKeys = Object.keys(this.source.primaryKeys);
var sourceKeyType = ((!this.source.hasPrimaryKeys || sourceKeys.length !== 1) ? DataTypes.INTEGER : this.source.rawAttributes[sourceKeys[0]].type)
var targetKeys = Object.keys(this.target.primaryKeys);
var targetKeyType = ((!this.target.hasPrimaryKeys || targetKeys.length !== 1) ? DataTypes.INTEGER : this.target.rawAttributes[targetKeys[0]].type)
combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
// remove any previously defined PKs
Utils._.each(this.through.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1) {
delete self.through.rawAttributes[attributeName]
}
})
if (primaryKeyDeleted) {
combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
} else {
var uniqueKey = [this.through.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_')
combinedTableAttributes[this.identifier] = {type: sourceKeyType, unique: uniqueKey}
combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, unique: uniqueKey}
}
this.through.rawAttributes = Utils._.merge(this.through.rawAttributes, combinedTableAttributes)
this.through.init(this.through.daoFactoryManager)
......
......@@ -5,6 +5,7 @@ var Utils = require("./utils")
, sql = require('sql')
, SqlString = require('./sql-string')
, Transaction = require('./transaction')
, QueryTypes = require('./query-types')
module.exports = (function() {
var DAOFactory = function(name, attributes, options) {
......@@ -101,7 +102,7 @@ module.exports = (function() {
result.exec = function(options) {
options = Utils._.extend({
transaction: null,
type: 'SELECT'
type: QueryTypes.SELECT
}, options || {})
return self.QueryInterface.queryAndEmit([result.toSql(), self, options], 'snafu')
......@@ -114,10 +115,24 @@ module.exports = (function() {
})()
DAOFactory.prototype.init = function(daoFactoryManager) {
var self = this;
var self = this
this.daoFactoryManager = daoFactoryManager
this.primaryKeys = {}
self.options.uniqueKeys = {}
this.daoFactoryManager = daoFactoryManager
this.primaryKeys = {};
Utils._.each(this.rawAttributes, function(columnValues, columnName) {
if (columnValues.hasOwnProperty('unique') && columnValues.unique !== true) {
var idxName = columnValues.unique
if (typeof columnValues.unique === "object") {
idxName = columnValues.unique.name
}
self.options.uniqueKeys[idxName] = self.options.uniqueKeys[idxName] || {fields: [], msg: null}
self.options.uniqueKeys[idxName].fields.push(columnName)
self.options.uniqueKeys[idxName].msg = self.options.uniqueKeys[idxName].msg || columnValues.unique.msg || null
}
})
Utils._.each(this.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.indexOf('PRIMARY KEY') !== -1) {
......@@ -149,7 +164,7 @@ module.exports = (function() {
this.DAO.prototype._hasPrimaryKeys = this.options.hasPrimaryKeys
this.DAO.prototype._isPrimaryKey = Utils._.memoize(function (key) {
return self.primaryKeyAttributes.indexOf(key) !== -1
return self.primaryKeyAttributes.indexOf(key) !== -1 && key !== 'id'
})
if (this.options.timestamps) {
......@@ -439,7 +454,7 @@ module.exports = (function() {
options = paranoidClause.call(this, options)
return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({
type: 'SELECT',
type: QueryTypes.SELECT,
hasJoin: hasJoin
}, queryOptions, { transaction: (options || {}).transaction }))
}
......@@ -453,7 +468,7 @@ module.exports = (function() {
this.options.whereCollection = optcpy.where || null;
return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, Utils._.defaults({
type: 'SELECT'
type: QueryTypes.SELECT
}, queryOptions, { transaction: (options || {}).transaction }))
}
......@@ -534,7 +549,7 @@ module.exports = (function() {
return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
plain: true,
type: 'SELECT',
type: QueryTypes.SELECT,
hasJoin: hasJoin
}, queryOptions, { transaction: (options || {}).transaction }))
}
......@@ -619,6 +634,10 @@ module.exports = (function() {
return this.aggregate(field, 'min', options)
}
DAOFactory.prototype.sum = function(field, options) {
return this.aggregate(field, 'sum', options)
}
DAOFactory.prototype.build = function(values, options) {
options = options || { isNewRecord: true, isDirty: true }
......@@ -927,6 +946,7 @@ module.exports = (function() {
DAOFactory.prototype.destroy = function(where, options) {
options = options || {}
options.force = options.force === undefined ? false : Boolean(options.force)
options.type = QueryTypes.BULKDELETE
var self = this
, query = null
......@@ -1050,6 +1070,7 @@ module.exports = (function() {
options = options || {}
options.validate = options.validate === undefined ? true : Boolean(options.validate)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks)
options.type = QueryTypes.BULKUPDATE
if (self.options.timestamps) {
var attr = Utils._.underscoredIf(self.options.updatedAt, self.options.underscored)
......@@ -1248,7 +1269,8 @@ module.exports = (function() {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
_autoGenerated: true
}
}
......@@ -1429,10 +1451,7 @@ module.exports = (function() {
var optClone = function (options) {
return Utils._.cloneDeep(options, function (elem) {
// The DAOFactories used for include are pass by ref, so don't clone them. Otherwise return undefined, meaning, 'handle this lodash'
if (elem instanceof DAOFactory) {
return elem
}
if (elem instanceof Utils.col || elem instanceof Utils.literal || elem instanceof Utils.cast || elem instanceof Utils.fn) {
if (elem instanceof DAOFactory || elem instanceof Utils.col || elem instanceof Utils.literal || elem instanceof Utils.cast || elem instanceof Utils.fn || elem instanceof Utils.and || elem instanceof Utils.or) {
return elem
}
......
......@@ -170,6 +170,12 @@ module.exports = (function() {
return
}
// If attempting to set generated id and id is already defined, return
// This is hack since generated id is not in primaryKeys, although it should be
if (originalValue && key === "id") {
return
}
// If attempting to set read only attributes, return
if (!options.raw && this._hasReadOnlyAttributes && this._isReadOnlyAttribute(key)) {
return
......@@ -396,7 +402,23 @@ module.exports = (function() {
args[2] = values
self.QueryInterface[query].apply(self.QueryInterface, args)
.proxy(emitter, {events: ['sql', 'error']})
.proxy(emitter, {events: ['sql']})
.error(function(err) {
if (!!self.__options.uniqueKeys && err.code && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.code) {
var fields = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.toString())
if (fields !== false) {
fields = fields.filter(function(f) { return f !== self.daoFactory.tableName; })
Utils._.each(self.__options.uniqueKeys, function(value, key) {
if (Utils._.isEqual(value.fields, fields) && !!value.msg) {
err = value.msg
}
})
}
}
emitter.emit('error', err)
})
.success(function(result) {
// Transfer database generated values (defaults, autoincrement, etc)
values = _.extend(values, result.dataValues)
......@@ -466,14 +488,13 @@ module.exports = (function() {
return validator.hookValidate()
}
DAO.prototype.updateAttributes = function(updates, fieldsOrOptions) {
if (fieldsOrOptions instanceof Array) {
fieldsOrOptions = { fields: fieldsOrOptions }
DAO.prototype.updateAttributes = function(updates, options) {
if (options instanceof Array) {
options = { fields: options }
}
this.setAttributes(updates)
return this.save(fieldsOrOptions)
this.set(updates)
return this.save(options)
}
DAO.prototype.setAttributes = function(updates) {
......
......@@ -464,7 +464,7 @@ module.exports = (function() {
selectQuery: function(tableName, options, factory) {
// Enter and change at your own peril -- Mick Hansen
options = options || {}
options = options || {}
var table = null
, self = this
......@@ -703,6 +703,16 @@ module.exports = (function() {
mainQueryItems.push(" GROUP BY " + options.group)
}
}
// Add HAVING to sub or main query
if (options.hasOwnProperty('having')) {
options.having = this.getWhereConditions(options.having, tableName, factory, options, false)
if (subQuery) {
subQueryItems.push(" HAVING " + options.having)
} else {
mainQueryItems.push(" HAVING " + options.having)
}
}
// Add ORDER to sub or main query
if (options.order) {
......@@ -715,7 +725,6 @@ module.exports = (function() {
}
}
var limitOrder = this.addLimitAndOffset(options, query)
// Add LIMIT, OFFSET to sub or main query
......@@ -804,15 +813,30 @@ module.exports = (function() {
/*
Takes something and transforms it into values of a where condition.
*/
getWhereConditions: function(smth, tableName, factory, options) {
getWhereConditions: function(smth, tableName, factory, options, prepend) {
var result = null
, where = {}
, where = {}
, self = this
if (typeof prepend === 'undefined')
prepend = true
if ((smth instanceof Utils.and) || (smth instanceof Utils.or)) {
var connector = (smth instanceof Utils.and) ? ' AND ' : ' OR '
if (Utils.isHash(smth)) {
smth = Utils.prependTableNameToHash(tableName, smth)
result = smth.args.map(function(arg) {
return self.getWhereConditions(arg, tableName, factory, options, prepend)
}).join(connector)
result = "(" + result + ")"
} else if (Utils.isHash(smth)) {
if (prepend) {
smth = Utils.prependTableNameToHash(tableName, smth)
}
result = this.hashToWhereConditions(smth, factory, options)
} else if (typeof smth === 'number') {
var primaryKeys = !!factory ? Object.keys(factory.primaryKeys) : []
if (primaryKeys.length > 0) {
// Since we're just a number, assume only the first key
primaryKeys = primaryKeys[0]
......@@ -826,7 +850,20 @@ module.exports = (function() {
} else if (typeof smth === "string") {
result = smth
} else if (Array.isArray(smth)) {
result = Utils.format(smth, this.dialect)
var treatAsAnd = smth.reduce(function(treatAsAnd, arg) {
if (treatAsAnd) {
return treatAsAnd
} else {
return !(arg instanceof Date) && ((arg instanceof Utils.and) || (arg instanceof Utils.or) || Utils.isHash(arg) || Array.isArray(arg))
}
}, false)
if (treatAsAnd) {
var _smth = self.sequelize.and.apply(null, smth)
result = self.getWhereConditions(_smth, tableName, factory, options, prepend)
} else {
result = Utils.format(smth, this.dialect)
}
}
return result ? result : '1=1'
......
......@@ -2,6 +2,7 @@ var Utils = require('../../utils')
, CustomEventEmitter = require("../../emitters/custom-event-emitter")
, Dot = require('dottie')
, _ = require('lodash')
, QueryTypes = require('../../query-types')
module.exports = (function() {
var AbstractQuery = function(database, sequelize, callee, options) {}
......@@ -95,6 +96,8 @@ module.exports = (function() {
}
} else if (isCallQuery.call(this)) {
result = data[0]
} else if (isBulkUpateQuery.call(this) || isBulkDeleteQuery.call(this)) {
result = data.affectedRows
}
return result
......@@ -191,7 +194,15 @@ module.exports = (function() {
}
var isSelectQuery = function() {
return this.options.type === 'SELECT'
return this.options.type === QueryTypes.SELECT
}
var isBulkUpateQuery = function() {
return this.options.type === QueryTypes.BULKUPDATE
}
var isBulkDeleteQuery = function() {
return this.options.type === QueryTypes.BULKDELETE
}
var isUpdateQuery = function() {
......
......@@ -24,5 +24,11 @@ module.exports = (function(){
this.connect()
}
ConnectorManager.prototype.cleanup = function() {
if (this.onProcessExit) {
process.removeListener('exit', this.onProcessExit)
}
}
return ConnectorManager
})()
......@@ -90,8 +90,11 @@ module.exports = (function() {
var config = self.config.replication.read[reads++]
connect.call(self, function (err, connection) {
connection.queryType = 'read'
done(null, connection)
if (connection) {
connection.queryType = 'read'
}
done(err, connection)
}, config)
},
destroy: function(client) {
......@@ -106,8 +109,11 @@ module.exports = (function() {
name: 'sequelize-write',
create: function (done) {
connect.call(self, function (err, connection) {
connection.queryType = 'write'
done(null, connection)
if (connection) {
connection.queryType = 'write'
}
done(err, connection)
}, self.config.replication.write)
},
destroy: function(client) {
......@@ -136,7 +142,7 @@ module.exports = (function() {
})
}
process.on('exit', function () {
this.onProcessExit = function () {
//be nice & close our connections on exit
if (self.pool) {
self.pool.drain()
......@@ -145,7 +151,9 @@ module.exports = (function() {
}
return
})
}.bind(this);
process.on('exit', this.onProcessExit)
}
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype)
......
......@@ -2,7 +2,19 @@ var Utils = require("../../utils")
module.exports = (function() {
var QueryGenerator = {
dialect: 'mariadb'
dialect: 'mariadb',
uniqueConstraintMapping: {
code: 1062,
map: function(str) {
// we're manually remvoving uniq_ here for a future capability of defining column names explicitly
var match = str.replace('uniq_', '').match(/Duplicate entry .* for key '(.*?)'$/)
if (match === null || match.length < 2) {
return false
}
return match[1].split('_')
}
},
}
// "MariaDB is a drop-in replacement for MySQL." - so thats exactly what we do, drop in the mysql query generator
......
......@@ -38,8 +38,8 @@ module.exports = (function() {
var self = this
if (this.useReplicaton) {
var reads = 0,
writes = 0;
var reads = 0
, writes = 0;
// Init configs with options from config if not present
for (var i in config.replication.read) {
......@@ -88,8 +88,11 @@ module.exports = (function() {
var config = self.config.replication.read[reads++];
connect.call(self, function (err, connection) {
connection.queryType = 'read'
done(null, connection)
if (connection) {
connection.queryType = 'read'
}
done(err, connection)
}, config);
},
destroy: function(client) {
......@@ -104,8 +107,11 @@ module.exports = (function() {
name: 'sequelize-write',
create: function (done) {
connect.call(self, function (err, connection) {
connection.queryType = 'write'
done(null, connection)
if (connection) {
connection.queryType = 'read'
}
done(err, connection)
}, self.config.replication.write);
},
destroy: function(client) {
......@@ -122,7 +128,10 @@ module.exports = (function() {
this.pool = Pooling.Pool({
name: 'sequelize-mysql',
create: function (done) {
connect.call(self, done)
connect.call(self, function (err, connection) {
// This has to be nested for some reason, else the error won't propagate correctly
done(err, connection);
})
},
destroy: function(client) {
disconnect.call(self, client)
......@@ -134,7 +143,7 @@ module.exports = (function() {
})
}
process.on('exit', function () {
this.onProcessExit = function () {
//be nice & close our connections on exit
if (self.pool) {
self.pool.drain()
......@@ -143,30 +152,27 @@ module.exports = (function() {
}
return
})
}.bind(this);
process.on('exit', this.onProcessExit)
}
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype);
var isConnecting = false;
ConnectorManager.prototype.query = function(sql, callee, options) {
if (!this.isConnected && !this.pool) {
this.connect()
}
if (this.useQueue) {
// If queueing we'll let the execQueueItem method handle connecting
var queueItem = {
query: new Query(this.client, this.sequelize, callee, options || {}),
query: new Query(null, this.sequelize, callee, options || {}),
sql: sql
};
queueItem.query.options.uuid = this.config.uuid
enqueue.call(this, queueItem, options)
return queueItem.query
enqueue.call(this, queueItem, options);
return queueItem.query;
}
var self = this, query = new Query(this.client, this.sequelize, callee, options || {});
var self = this, query = new Query(null, this.sequelize, callee, options || {});
this.pendingQueries++;
query.options.uuid = this.config.uuid
......@@ -177,40 +183,69 @@ module.exports = (function() {
} else {
if (self.pendingQueries === 0) {
setTimeout(function() {
self.pendingQueries === 0 && self.disconnect.call(self)
if (self.pendingQueries === 0){
self.disconnect.call(self);
}
}, 100);
}
}
});
if (!this.pool) {
query.run(sql);
} else {
this.pool.acquire(function(err, client) {
if (err) {
return query.emit('error', err)
}
query.client = client
query.run(sql)
return;
}, undefined, options.type)
}
this.getConnection(options, function (err, client) {
if (err) {
return query.emit('error', err)
}
query.client = client
query.run(sql)
});
return query;
};
ConnectorManager.prototype.connect = function() {
ConnectorManager.prototype.getConnection = function(options, callback) {
var self = this;
// in case database is slow to connect, prevent orphaning the client
if (this.isConnecting || this.pool) {
return;
if (typeof options === "function") {
callback = options;
options = {};
}
connect.call(self, function(err, client) {
self.client = client;
return;
});
return;
return new Utils.CustomEventEmitter(function (emitter) {
if (!self.pool) {
// Regular client caching
if (self.client) {
return emitter.emit('success', self.client);
} else {
// Cache for concurrent queries
if (self._getConnection) {
self._getConnection.proxy(emitter);
return;
}
// Set cache and acquire connection
self._getConnection = emitter;
connect.call(self, function(err, client) {
if (err) {
return emitter.emit('error', err);
}
// Unset caching, should now be caught by the self.client check
self._getConnection = null;
self.client = client;
emitter.emit('success', client);
});
}
}
if (self.pool) {
// Acquire from pool
self.pool.acquire(function(err, client) {
if (err) {
return emitter.emit('error', err);
}
emitter.emit('success', client);
}, options.priority, options.type);
}
}).run().done(callback);
};
ConnectorManager.prototype.disconnect = function() {
......@@ -269,7 +304,6 @@ module.exports = (function() {
}
var connection = mysql.createConnection(connectionConfig);
connection.connect(function(err) {
if (err) {
switch(err.code) {
......@@ -282,17 +316,30 @@ module.exports = (function() {
case 'EINVAL':
emitter.emit('error', 'Failed to find MySQL server. Please double check your settings.')
break
default:
emitter.emit('error', err);
break;
}
return;
}
emitter.emit('success', connection);
})
connection.query("SET time_zone = '+0:00'");
// client.setMaxListeners(self.maxConcurrentQueries)
this.isConnecting = false
if (config.pool != null && config.pool.handleDisconnects) {
if (config.pool !== null && config.pool.handleDisconnects) {
handleDisconnect(this.pool, connection)
}
done(null, connection)
emitter.on('error', function (err) {
done(err);
});
emitter.on('success', function (connection) {
done(null, connection);
});
}
var handleDisconnect = function(pool, client) {
......@@ -312,23 +359,7 @@ module.exports = (function() {
options = options || {}
if (this.activeQueue.length < this.maxConcurrentQueries) {
this.activeQueue.push(queueItem)
if (this.pool) {
var self = this
this.pool.acquire(function(err, client) {
if (err) {
queueItem.query.emit('error', err)
return
}
//we set the client here, asynchronously, when getting a pooled connection
//allowing the ConnectorManager.query method to remain synchronous
queueItem.query.client = client
queueItem.client = client
execQueueItem.call(self, queueItem)
return
}, undefined, options.type)
} else {
execQueueItem.call(this, queueItem)
}
execQueueItem.call(this, queueItem)
} else {
this.queue.push(queueItem)
}
......@@ -361,11 +392,23 @@ module.exports = (function() {
var execQueueItem = function(queueItem) {
var self = this
queueItem.query
.success(function(){ afterQuery.call(self, queueItem) })
.error(function(){ afterQuery.call(self, queueItem) })
self.getConnection({
priority: queueItem.query.options.priority,
type: queueItem.query.options.type
}, function (err, connection) {
if (err) {
queueItem.query.emit('error', err)
return
}
queueItem.query.run(queueItem.sql, queueItem.client)
queueItem.query.client = connection
queueItem.client = connection
queueItem.query
.success(function(){ afterQuery.call(self, queueItem) })
.error(function(){ afterQuery.call(self, queueItem) })
queueItem.query.run(queueItem.sql, queueItem.client)
})
}
ConnectorManager.prototype.__defineGetter__('hasQueuedItems', function() {
......
......@@ -76,6 +76,12 @@ module.exports = (function() {
}
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk) }.bind(this)).join(", ")
if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns) {
values.attributes += ", UNIQUE uniq_" + tableName + '_' + columns.fields.join('_') + " (" + columns.fields.join(', ') + ")"
})
}
if (pkString.length > 0) {
values.attributes += ", PRIMARY KEY (" + pkString + ")"
}
......@@ -103,6 +109,19 @@ module.exports = (function() {
return 'SHOW TABLES;'
},
uniqueConstraintMapping: {
code: 'ER_DUP_ENTRY',
map: function(str) {
// we're manually remvoving uniq_ here for a future capability of defining column names explicitly
var match = str.replace('uniq_', '').match(/Duplicate entry .* for key '(.*?)'$/)
if (match === null || match.length < 2) {
return false
}
return match[1].split('_')
}
},
addColumnQuery: function(tableName, attributes) {
var query = "ALTER TABLE `<%= tableName %>` ADD <%= attributes %>;"
, attrString = []
......@@ -317,7 +336,7 @@ module.exports = (function() {
template += " DEFAULT " + this.escape(dataType.defaultValue)
}
if (dataType.unique) {
if (dataType.unique === true) {
template += " UNIQUE"
}
......
......@@ -13,7 +13,6 @@ module.exports = (function() {
this.pooling = (!!this.config.pool && (this.config.pool.maxConnections > 0))
this.pg = this.config.native ? require(pgModule).native : require(pgModule)
this.poolIdentifier = null
// Better support for BigInts
// https://github.com/brianc/node-postgres/issues/166#issuecomment-9514935
this.pg.types.setTypeParser(20, String);
......@@ -30,9 +29,11 @@ module.exports = (function() {
this.clientDrained = true
this.maxConcurrentQueries = (this.config.maxConcurrentQueries || 50)
process.on('exit', function() {
this.onProcessExit = function () {
this.disconnect()
}.bind(this))
}.bind(this);
process.on('exit', this.onProcessExit)
}
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype)
......
......@@ -44,6 +44,8 @@ module.exports = (function() {
},
createTableQuery: function(tableName, attributes, options) {
var self = this
options = Utils._.extend({
}, options || {})
......@@ -76,6 +78,12 @@ module.exports = (function() {
comments: Utils._.template(comments, { table: this.quoteIdentifiers(tableName)})
}
if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns) {
values.attributes += ", UNIQUE (" + columns.fields.map(function(f) { return self.quoteIdentifiers(f) }).join(', ') + ")"
})
}
var pks = primaryKeys[tableName].map(function(pk){
return this.quoteIdentifier(pk)
}.bind(this)).join(",")
......@@ -114,6 +122,18 @@ module.exports = (function() {
})
},
uniqueConstraintMapping: {
code: '23505',
map: function(str) {
var match = str.match(/duplicate key value violates unique constraint "(.*?)_key"/)
if (match === null || match.length < 2) {
return false
}
return match[1].split('_').splice(1)
}
},
addColumnQuery: function(tableName, attributes) {
var query = "ALTER TABLE <%= tableName %> ADD COLUMN <%= attributes %>;"
, attrString = []
......@@ -448,7 +468,7 @@ module.exports = (function() {
replacements.defaultValue = this.escape(dataType.defaultValue)
}
if (dataType.unique) {
if (dataType.unique === true) {
template += " UNIQUE"
}
......
......@@ -2,6 +2,7 @@ var Utils = require("../../utils")
, AbstractQuery = require('../abstract/query')
, DataTypes = require('../../data-types')
, hstore = require('./hstore')
, QueryTypes = require('../../query-types')
module.exports = (function() {
var Query = function(client, sequelize, callee, options) {
......@@ -41,14 +42,14 @@ module.exports = (function() {
self.emit('error', err, self.callee)
})
query.on('end', function() {
query.on('end', function(result) {
self.emit('sql', self.sql)
if (receivedError) {
return
}
onSuccess.call(self, rows, sql)
onSuccess.call(self, rows, sql, result)
})
return this
......@@ -58,7 +59,7 @@ module.exports = (function() {
return 'id'
}
var onSuccess = function(rows, sql) {
var onSuccess = function(rows, sql, result) {
var results = rows
, self = this
, isTableNameQuery = (sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
......@@ -144,6 +145,8 @@ module.exports = (function() {
}
}
this.emit('success', this.callee)
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(this.options.type) !== -1) {
this.emit('success', result.rowCount)
} else if (this.send('isUpdateQuery')) {
if(this.callee !== null) { // may happen for bulk updates
for (var key in rows[0]) {
......
......@@ -5,6 +5,7 @@ var sqlite3
module.exports = (function() {
var ConnectorManager = function(sequelize, config) {
this.sequelize = sequelize
this.config = config
if (config.dialectModulePath) {
sqlite3 = require(config.dialectModulePath).verbose()
......
......@@ -109,6 +109,12 @@ module.exports = (function() {
}
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk) }.bind(this)).join(", ")
if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns) {
values.attributes += ", UNIQUE (" + columns.fields.join(', ') + ")"
})
}
if (pkString.length > 0) {
values.attributes += ", PRIMARY KEY (" + pkString + ")"
}
......@@ -131,6 +137,18 @@ module.exports = (function() {
})
},
uniqueConstraintMapping: {
code: 'SQLITE_CONSTRAINT',
map: function(str) {
var match = str.match(/columns (.*?) are/)
if (match === null || match.length < 2) {
return false
}
return match[1].split(', ')
}
},
addLimitAndOffset: function(options, query){
query = query || ""
if (options.offset && !options.limit) {
......@@ -184,6 +202,26 @@ module.exports = (function() {
return Utils._.template(query)(replacements)
},
updateQuery: function(tableName, attrValueHash, where, options) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull, options)
var query = "UPDATE <%= table %> SET <%= values %> WHERE <%= where %>"
, values = []
for (var key in attrValueHash) {
var value = attrValueHash[key]
values.push(this.quoteIdentifier(key) + "=" + this.escape(value))
}
var replacements = {
table: this.quoteIdentifier(tableName),
values: values.join(","),
where: this.getWhereConditions(where)
}
return Utils._.template(query)(replacements)
},
deleteQuery: function(tableName, where, options) {
options = options || {}
......@@ -226,7 +264,7 @@ module.exports = (function() {
replacements.defaultValue = this.escape(dataType.defaultValue)
}
if (dataType.unique) {
if (dataType.unique === true) {
template += " UNIQUE"
}
......
......@@ -120,9 +120,15 @@ var QueryInterface = module.exports = {
}.bind(this))
},
dropAllTables: function() {
dropAllTables: function(options) {
var self = this
if (!options) {
options = {}
}
var skip = options.skip || [];
return new Utils.CustomEventEmitter(function(dropAllTablesEmitter) {
var events = []
, chainer = new Utils.QueryChainer()
......@@ -149,7 +155,10 @@ var QueryInterface = module.exports = {
queries.push('PRAGMA foreign_keys = OFF')
tableNames.forEach(function(tableName) {
queries.push(self.QueryGenerator.dropTableQuery(tableName).replace(';', ''))
// if tableName is not in the Array of tables names then dont drop it
if (skip.indexOf(tableName) === -1) {
queries.push(self.QueryGenerator.dropTableQuery(tableName).replace(';', ''))
}
})
queries.push('PRAGMA foreign_keys = ON')
......
var Utils = require("../../utils")
, AbstractQuery = require('../abstract/query')
, QueryTypes = require('../../query-types')
module.exports = (function() {
var Query = function(database, sequelize, callee, options) {
......@@ -81,7 +82,7 @@ module.exports = (function() {
//private
var getDatabaseMethod = function() {
if (this.send('isInsertQuery') || this.send('isUpdateQuery') || (this.sql.toLowerCase().indexOf('CREATE TEMPORARY TABLE'.toLowerCase()) !== -1)) {
if (this.send('isInsertQuery') || this.send('isUpdateQuery') || (this.sql.toLowerCase().indexOf('CREATE TEMPORARY TABLE'.toLowerCase()) !== -1) || this.options.type === QueryTypes.BULKDELETE) {
return 'run'
} else {
return 'all'
......@@ -110,7 +111,9 @@ module.exports = (function() {
result[name] = new Date(val+'Z') // Z means UTC
}
} else if (metaData.columnTypes[name].lastIndexOf('BLOB') !== -1) {
result[name] = new Buffer(result[name])
if (result[name]) {
result[name] = new Buffer(result[name])
}
}
}
}
......@@ -157,6 +160,8 @@ module.exports = (function() {
result = results[0]
} else if (this.sql.indexOf('PRAGMA foreign_keys') !== -1) {
result = results
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(this.options.type) !== -1) {
result = metaData.changes
}
this.emit('success', result)
......
......@@ -143,9 +143,23 @@ module.exports = (function() {
return new Promise(function (resolve, reject) {
self.on('error', reject)
.on('success', resolve);
.on('success', resolve)
}).then(onFulfilled, onRejected)
}
CustomEventEmitter.prototype.spread = function(onFulfilled, onRejected) {
var self = this
onFulfilled = bindToProcess(onFulfilled)
onRejected = bindToProcess(onRejected)
return new Promise(function (resolve, reject) {
self.on('error', reject)
.on('success', function () {
resolve(Array.prototype.slice.apply(arguments)) // Transform args to an array
})
}).spread(onFulfilled, onRejected)
}
return CustomEventEmitter
})()
......@@ -2,6 +2,7 @@ var Utils = require(__dirname + '/utils')
, DataTypes = require(__dirname + '/data-types')
, SQLiteQueryInterface = require(__dirname + '/dialects/sqlite/query-interface')
, Transaction = require(__dirname + '/transaction')
, QueryTypes = require('./query-types')
module.exports = (function() {
var QueryInterface = function(sequelize) {
......@@ -102,7 +103,7 @@ module.exports = (function() {
for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].toString().match(/^ENUM\(/)) {
sql = self.QueryGenerator.pgListEnums(getTableName, keys[i], options)
chainer.add(self.sequelize.query(sql, null, { plain: true, raw: true, type: 'SELECT', logging: options.logging }))
chainer.add(self.sequelize.query(sql, null, { plain: true, raw: true, type: QueryTypes.SELECT, logging: options.logging }))
}
}
......@@ -234,12 +235,18 @@ module.exports = (function() {
}).run()
}
QueryInterface.prototype.dropAllTables = function() {
QueryInterface.prototype.dropAllTables = function(options) {
var self = this
if (!options) {
options = {}
}
var skip = options.skip || [];
if (this.sequelize.options.dialect === 'sqlite') {
// sqlite needs some special treatment as it cannot drop a column
return SQLiteQueryInterface.dropAllTables.call(this)
return SQLiteQueryInterface.dropAllTables.call(this, options)
} else {
return new Utils.CustomEventEmitter(function(dropAllTablesEmitter) {
var events = []
......@@ -262,7 +269,10 @@ module.exports = (function() {
// add the table removal query to the chainer
tableNames.forEach(function(tableName) {
chainer.add(self, 'dropTable', [ tableName, { cascade: true } ])
// if tableName is not in the Array of tables names then dont drop it
if (skip.indexOf(tableName) === -1) {
chainer.add(self, 'dropTable', [ tableName, { cascade: true } ])
}
})
chainer
......@@ -526,7 +536,7 @@ module.exports = (function() {
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
emitter.emit('success', results[0])
})
.error(function(err) {
emitter.query = { sql: sql }
......@@ -641,7 +651,7 @@ module.exports = (function() {
.success(function(results){
emitter.query = { sql: sql }
emitter.emit('sql', sql)
emitter.emit('success', results[1])
emitter.emit('success', results[0])
})
.error(function(err) {
emitter.query = { sql: sql }
......@@ -690,7 +700,7 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) {
var sql = self.QueryGenerator.selectQuery(tableName, options)
, queryOptions = Utils._.extend({ transaction: options.transaction }, { plain: true, raw: true, type: 'SELECT' })
, queryOptions = Utils._.extend({ transaction: options.transaction }, { plain: true, raw: true, type: QueryTypes.SELECT })
, query = self.sequelize.query(sql, null, queryOptions)
query
......
module.exports = {
SELECT: 'SELECT',
BULKUPDATE: 'BULKUPDATE',
BULKDELETE: 'BULKDELETE'
}
\ No newline at end of file
......@@ -7,6 +7,7 @@ var url = require("url")
, QueryInterface = require("./query-interface")
, Transaction = require("./transaction")
, TransactionManager = require('./transaction-manager')
, QueryTypes = require('./query-types')
module.exports = (function() {
/**
......@@ -19,7 +20,7 @@ module.exports = (function() {
@param {String} [options.dialect='mysql'] The dialect of the relational database.
@param {String} [options.dialectModulePath=null] If specified, load the dialect library from this path.
@param {String} [options.host='localhost'] The host of the relational database.
@param {Integer} [options.port=3306] The port of the relational database.
@param {Integer} [options.port=] The port of the relational database.
@param {String} [options.protocol='tcp'] The protocol of the relational database.
@param {Object} [options.define={}] Options, which shall be default for every model definition.
@param {Object} [options.query={}] I have absolutely no idea.
......@@ -74,7 +75,6 @@ module.exports = (function() {
dialect: 'mysql',
dialectModulePath: null,
host: 'localhost',
port: 3306,
protocol: 'tcp',
define: {},
query: {},
......@@ -129,6 +129,8 @@ module.exports = (function() {
*/
Sequelize.Utils = Utils
Sequelize.QueryTypes = QueryTypes
for (var dataType in DataTypes) {
Sequelize[dataType] = DataTypes[dataType]
}
......@@ -313,7 +315,7 @@ module.exports = (function() {
options = Utils._.extend(Utils._.clone(this.options.query), options)
options = Utils._.defaults(options, {
logging: this.options.hasOwnProperty('logging') ? this.options.logging : console.log,
type: (sql.toLowerCase().indexOf('select') === 0) ? 'SELECT' : false
type: (sql.toLowerCase().indexOf('select') === 0) ? QueryTypes.SELECT : false
})
return this.transactionManager.query(sql, callee, options)
......@@ -423,6 +425,14 @@ module.exports = (function() {
return new Utils.asIs(val)
}
Sequelize.and = Sequelize.prototype.and = function() {
return new Utils.and(Array.prototype.slice.call(arguments))
}
Sequelize.or = Sequelize.prototype.or = function() {
return new Utils.or(Array.prototype.slice.call(arguments))
}
Sequelize.prototype.transaction = function(_options, _callback) {
var options = (typeof _options === 'function') ? {} : _options
, callback = (typeof _options === 'function') ? _options : _callback
......
......@@ -18,7 +18,14 @@ TransactionManager.prototype.getConnectorManager = function(uuid) {
var config = Utils._.extend({ uuid: uuid }, this.sequelize.config)
if (uuid !== 'default') {
config.pool = { maxConnections: 0, useReplicaton: false }
config.pool = Utils._.extend(
{},
Utils._.clone(config.pool || {}),
{
maxConnections: 0,
useReplicaton: false
}
)
config.keepDefaultTimezone = true
}
......@@ -28,6 +35,11 @@ TransactionManager.prototype.getConnectorManager = function(uuid) {
return this.connectorManagers[uuid]
}
TransactionManager.prototype.releaseConnectionManager = function(uuid) {
this.connectorManagers[uuid].cleanup();
delete this.connectorManagers[uuid]
}
TransactionManager.prototype.query = function(sql, callee, options) {
options = options || {}
options.uuid = 'default'
......
......@@ -25,6 +25,7 @@ Transaction.prototype.commit = function() {
.getQueryInterface()
.commitTransaction(this, {})
.proxy(this)
.done(this.cleanup.bind(this))
}
......@@ -34,6 +35,7 @@ Transaction.prototype.rollback = function() {
.getQueryInterface()
.rollbackTransaction(this, {})
.proxy(this)
.done(this.cleanup.bind(this))
}
Transaction.prototype.prepareEnvironment = function(callback) {
......@@ -76,8 +78,12 @@ Transaction.prototype.setIsolationLevel = function(callback) {
.error(onError.bind(this))
}
Transaction.prototype.cleanup = function() {
this.sequelize.transactionManager.releaseConnectionManager(this.id)
}
// private
var onError = function(err) {
this.emit('error', err)
}
}
\ No newline at end of file
......@@ -507,7 +507,7 @@ var Utils = module.exports = {
},
tick: function(func) {
var tick = (typeof setImmediate !== "undefined" ? setImmediate : process.nextTick)
var tick = (global.hasOwnProperty('setImmediate') ? global.setImmediate : process.nextTick)
tick(func)
},
......@@ -523,6 +523,7 @@ var Utils = module.exports = {
tickChar = tickChar || Utils.TICK_CHAR
return s.replace(new RegExp(tickChar, 'g'), "")
},
/*
* Utility functions for representing SQL functions, and columns that should be escaped.
* Please do not use these functions directly, use Sequelize.fn and Sequelize.col instead.
......@@ -531,13 +532,16 @@ var Utils = module.exports = {
this.fn = fn
this.args = args
},
col: function (col) {
this.col = col
},
cast: function (val, type) {
this.val = val
this.type = (type || '').trim()
},
literal: function (val) {
this.val = val
},
......@@ -546,6 +550,14 @@ var Utils = module.exports = {
this.val = val
},
and: function(args) {
this.args = args
},
or: function(args) {
this.args = args
},
generateUUID: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8)
......
......@@ -56,7 +56,7 @@
},
"devDependencies": {
"sqlite3": "~2.1.12",
"mysql": "2.0.0-alpha9",
"mysql": "2.0.1",
"pg": "~2.8.1",
"watchr": "~2.4.3",
"yuidocjs": "~0.3.36",
......@@ -64,7 +64,7 @@
"mocha": "~1.13.0",
"chai-datetime": "~1.1.1",
"sinon": "~1.7.3",
"mariasql": "git://github.com/mscdex/node-mariasql.git",
"mariasql": "0.1.20",
"chai-spies": "~0.5.1",
"lcov-result-merger": "0.0.2",
"istanbul": "~0.1.45",
......
......@@ -889,7 +889,53 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
}, 50)
})
})
describe('primary key handling for join table', function () {
it('removes the primary key if it was added by sequelize', function () {
var self = this
this.UserTasks = this.sequelize.define('usertasks', {});
this.User.hasMany(this.Task, { through: this.UserTasks })
this.Task.hasMany(this.User, { through: this.UserTasks })
expect(Object.keys(self.UserTasks.primaryKeys)).to.deep.equal(['taskId', 'userId'])
})
it('keeps the primary key if it was added by the user', function () {
var self = this
, fk
this.UserTasks = this.sequelize.define('usertasks', {
id: {
type: Sequelize.INTEGER,
autoincrement: true,
primaryKey: true
}
});
this.UserTasks2 = this.sequelize.define('usertasks2', {
userTasksId: {
type: Sequelize.INTEGER,
autoincrement: true,
primaryKey: true
}
});
this.User.hasMany(this.Task, { through: this.UserTasks })
this.Task.hasMany(this.User, { through: this.UserTasks })
this.User.hasMany(this.Task, { through: this.UserTasks2 })
this.Task.hasMany(this.User, { through: this.UserTasks2 })
expect(Object.keys(self.UserTasks.primaryKeys)).to.deep.equal(['id'])
expect(Object.keys(self.UserTasks2.primaryKeys)).to.deep.equal(['userTasksId'])
_.each([self.UserTasks, self.UserTasks2], function (model) {
fk = Object.keys(model.options.uniqueKeys)[0]
expect(model.options.uniqueKeys[fk].fields).to.deep.equal([ 'taskId', 'userId' ])
})
})
})
describe('join table model', function () {
beforeEach(function (done) {
this.User = this.sequelize.define('User', {})
......
......@@ -21,7 +21,7 @@ module.exports = {
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 30
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
},
......@@ -47,7 +47,7 @@ module.exports = {
host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 1,
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
}
......
......@@ -70,13 +70,23 @@ describe(Support.getTestDialectTeaser("Configuration"), function() {
done()
})
it('should use the default port when no other is specified', function(done) {
var sequelize = new Sequelize('mysql://example.com/dbname')
var config = sequelize.config
it('should use the default port when no other is specified', function() {
var sequelize = new Sequelize('dbname', 'root', 'pass', {
dialect: dialect
})
, config = sequelize.connectorManager.config
, port
if (Support.dialectIsMySQL()) {
port = 3306
} else if (dialect === "postgres" || dialect === "postgres-native") {
port = 5432
} else {
// sqlite has no concept of ports when connecting
return
}
// The default port should be set
expect(config.port).to.equal(3306)
done()
expect(config.port).to.equal(port)
})
})
......
......@@ -246,6 +246,39 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
it('allows multiple column unique keys to be defined', function(done) {
var User = this.sequelize.define('UserWithUniqueUsername', {
username: { type: Sequelize.STRING, unique: 'user_and_email' },
email: { type: Sequelize.STRING, unique: 'user_and_email' },
aCol: { type: Sequelize.STRING, unique: 'a_and_b' },
bCol: { type: Sequelize.STRING, unique: 'a_and_b' }
})
User.sync({ force: true }).on('sql', function(sql) {
expect(sql).to.match(/UNIQUE\s*(uniq_UserWithUniqueUsernames_username_email)?\s*\([`"]?username[`"]?, [`"]?email[`"]?\)/)
expect(sql).to.match(/UNIQUE\s*(uniq_UserWithUniqueUsernames_aCol_bCol)?\s*\([`"]?aCol[`"]?, [`"]?bCol[`"]?\)/)
done()
})
})
it('allows us to customize the error message for unique constraint', function(done) {
var User = this.sequelize.define('UserWithUniqueUsername', {
username: { type: Sequelize.STRING, unique: { name: 'user_and_email', msg: 'User and email must be unique' }},
email: { type: Sequelize.STRING, unique: 'user_and_email' },
aCol: { type: Sequelize.STRING, unique: 'a_and_b' },
bCol: { type: Sequelize.STRING, unique: 'a_and_b' }
})
User.sync({ force: true }).success(function() {
User.create({username: 'tobi', email: 'tobi@tobi.me'}).success(function() {
User.create({username: 'tobi', email: 'tobi@tobi.me'}).error(function(err) {
expect(err).to.equal('User and email must be unique')
done()
})
})
})
})
})
describe('build', function() {
......@@ -690,7 +723,6 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
this.User.bulkCreate(data).success(function() {
self.User.update({username: 'Bill'}, {secretValue: '42'}).done(function(err) {
console.log(err)
expect(err).not.to.be.ok
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).to.equal(3)
......@@ -707,6 +739,30 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
it('returns the number of affected rows', function(_done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
, done = _.after(2, _done)
this.User.bulkCreate(data).success(function() {
self.User.update({username: 'Bill'}, {secretValue: '42'}).done(function(err, affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(2)
done()
})
self.User.update({username: 'Bill'}, {secretValue: '44'}).done(function(err, affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(0)
done()
})
})
})
})
describe('destroy', function() {
......@@ -867,6 +923,31 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
it('returns the number of affected rows', function(_done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
, done = _.after(2, _done)
this.User.bulkCreate(data).success(function() {
self.User.destroy({secretValue: '42'}).done(function(err, affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(2)
done()
})
self.User.destroy({secretValue: '44'}).done(function(err, affectedRows) {
expect(err).not.to.be.ok
expect(affectedRows).to.equal(0)
done()
})
})
})
})
describe('equals', function() {
......@@ -1177,6 +1258,77 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
describe('sum', function() {
beforeEach(function(done) {
var self = this
this.UserWithAge = this.sequelize.define('UserWithAge', {
age: Sequelize.INTEGER,
order: Sequelize.INTEGER,
gender: Sequelize.ENUM('male', 'female')
})
this.UserWithDec = this.sequelize.define('UserWithDec', {
value: Sequelize.DECIMAL(10, 3)
})
this.UserWithAge.sync({ force: true }).success(function() {
self.UserWithDec.sync({ force: true }).success(function() {
done()
})
})
})
it("should return the sum of the values for a field named the same as an SQL reserved keyword", function(done) {
var self = this
this.UserWithAge.bulkCreate([{age: 2, order: 3}, {age: 3, order: 5}]).success(function(){
self.UserWithAge.sum('order').success(function(sum) {
expect(sum).to.equal(8)
done()
})
})
})
it("should return the sum of a field in various records", function(done) {
var self = this
self.UserWithAge.bulkCreate([{age: 2}, {age: 3}]).success(function() {
self.UserWithAge.sum('age').success(function(sum) {
expect(sum).to.equal(5)
done()
})
})
})
it("should allow decimals in sum", function(done) {
var self = this
this.UserWithDec.bulkCreate([{value: 3.5}, {value: 5.25}]).success(function(){
self.UserWithDec.sum('value').success(function(sum){
expect(sum).to.equal(8.75)
done()
})
})
})
it('should accept a where clause', function (done) {
var options = { where: { 'gender': 'male' }}
var self = this
self.UserWithAge.bulkCreate([{age: 2, gender: 'male'}, {age: 3, gender: 'female'}]).success(function() {
self.UserWithAge.sum('age', options).success(function(sum) {
expect(sum).to.equal(2)
done()
})
})
})
it('allows sql logging', function(done) {
this.UserWithAge.sum('age').on('sql', function(sql) {
expect(sql).to.exist
expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1)
done()
})
})
})
describe('schematic support', function() {
beforeEach(function(done){
var self = this;
......@@ -1653,6 +1805,18 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
it("should work when the database returns null", function (done) {
var self = this
this.BlobUser.create({
// create a null column
}).success(function (user) {
self.BlobUser.find(user.id).success(function (user) {
expect(user.data).to.be.null
done()
})
})
})
})
describe("strings", function () {
......
/* jshint camelcase: false */
/* jshint expr: true */
var chai = require('chai')
, Sequelize = require('../../index')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + "/../../lib/data-types")
, dialect = Support.getTestDialect()
, config = require(__dirname + "/../config/config")
, sinon = require('sinon')
, datetime = require('chai-datetime')
, _ = require('lodash')
, moment = require('moment')
, async = require('async')
chai.use(datetime)
chai.Assertion.includeStack = true
describe(Support.getTestDialectTeaser("DAOFactory"), function () {
beforeEach(function(done) {
this.User = this.sequelize.define('User', {
username: DataTypes.STRING,
secretValue: DataTypes.STRING,
data: DataTypes.STRING,
intVal: DataTypes.INTEGER,
theDate: DataTypes.DATE,
aBool: DataTypes.BOOLEAN
})
this.User.sync({ force: true }).success(function() {
done()
})
})
;(['or', 'and']).forEach(function(method) {
var word = method.toUpperCase()
describe('Sequelize.' + method, function() {
it('can handle plain strings', function(done) {
this.User.find({
where: Sequelize[method]( "1=1", "2=2" )
}).on('sql', function(sql) {
expect(sql).to.contain("WHERE (1=1 " + word + " 2=2) LIMIT 1")
done()
})
})
it('can handle arrays', function(done) {
this.User.find({
where: Sequelize[method]( ["1=?", 1], ["2=?", 2] )
}).on('sql', function(sql) {
expect(sql).to.contain("WHERE (1=1 " + word + " 2=2) LIMIT 1")
done()
})
})
it('can handle objects', function(done) {
this.User.find({
where: Sequelize[method]( { username: "foo", intVal: 2 }, { secretValue: 'bar' } )
}).on('sql', function(sql) {
var expectation = ({
mysql: "WHERE (`Users`.`username`='foo' AND `Users`.`intVal`=2 " + word + " `Users`.`secretValue`='bar')",
sqlite: "WHERE (`Users`.`username`='foo' AND `Users`.`intVal`=2 " + word + " `Users`.`secretValue`='bar')",
postgres: 'WHERE ("Users"."username"=\'foo\' AND "Users"."intVal"=2 ' + word + ' "Users"."secretValue"=\'bar\')',
mariadb: "WHERE (`Users`.`username`='foo' AND `Users`.`intVal`=2 " + word + " `Users`.`secretValue`='bar')"
})[Support.getTestDialect()]
if (!expectation) {
console.log(sql)
throw new Error('Undefined expectation for ' + Support.getTestDialect())
}
expect(sql).to.contain(expectation)
done()
})
})
it('can handle numbers', function(done) {
this.User.find({
where: Sequelize[method]( 1, 2 )
}).on('sql', function(sql) {
var expectation = ({
mysql: "WHERE (`Users`.`id`=1 " + word + " `Users`.`id`=2)",
sqlite: "WHERE (`Users`.`id`=1 " + word + " `Users`.`id`=2)",
postgres: 'WHERE ("Users"."id"=1 ' + word + ' "Users"."id"=2)',
mariadb: "WHERE (`Users`.`id`=1 " + word + " `Users`.`id`=2)"
})[Support.getTestDialect()]
if (!expectation) {
console.log(sql)
throw new Error('Undefined expectation for ' + Support.getTestDialect())
}
expect(sql).to.contain(expectation)
done()
})
})
})
})
describe('Combinations of Sequelize.and and Sequelize.or', function() {
it('allows nesting of Sequelize.or', function(done) {
this.User.find({
where: Sequelize.and( Sequelize.or("1=1", "2=2"), Sequelize.or("3=3", "4=4") )
}).on('sql', function(sql) {
expect(sql).to.contain("WHERE ((1=1 OR 2=2) AND (3=3 OR 4=4)) LIMIT 1")
done()
})
})
it('allows nesting of Sequelize.and', function(done) {
this.User.find({
where: Sequelize.or( Sequelize.and("1=1", "2=2"), Sequelize.and("3=3", "4=4") )
}).on('sql', function(sql) {
expect(sql).to.contain("WHERE ((1=1 AND 2=2) OR (3=3 AND 4=4)) LIMIT 1")
done()
})
})
;(['find', 'findAll']).forEach(function(finderMethod) {
it('correctly handles complex combinations', function(done) {
this.User[finderMethod]({
where: [
42, "2=2", ["1=?", 1], { username: "foo" },
Sequelize.or(
42, "2=2", ["1=?", 1], { username: "foo" },
Sequelize.and( 42, "2=2", ["1=?", 1], { username: "foo" } ),
Sequelize.or( 42, "2=2", ["1=?", 1], { username: "foo" } )
),
Sequelize.and(
42, "2=2", ["1=?", 1], { username: "foo" },
Sequelize.or( 42, "2=2", ["1=?", 1], { username: "foo" } ),
Sequelize.and( 42, "2=2", ["1=?", 1], { username: "foo" } )
)
]
}).on('sql', function(sql) {
if (Support.getTestDialect() === 'postgres') {
expect(sql).to.contain(
'WHERE (' + [
'"Users"."id"=42 AND 2=2 AND 1=1 AND "Users"."username"=\'foo\' AND ',
'(',
'"Users"."id"=42 OR 2=2 OR 1=1 OR "Users"."username"=\'foo\' OR ',
'("Users"."id"=42 AND 2=2 AND 1=1 AND "Users"."username"=\'foo\') OR ',
'("Users"."id"=42 OR 2=2 OR 1=1 OR "Users"."username"=\'foo\')',
') AND ',
'(',
'"Users"."id"=42 AND 2=2 AND 1=1 AND "Users"."username"=\'foo\' AND ',
'("Users"."id"=42 OR 2=2 OR 1=1 OR "Users"."username"=\'foo\') AND ',
'("Users"."id"=42 AND 2=2 AND 1=1 AND "Users"."username"=\'foo\')',
')'
].join("") +
')'
)
} else {
expect(sql).to.contain(
"WHERE (" + [
"`Users`.`id`=42 AND 2=2 AND 1=1 AND `Users`.`username`='foo' AND ",
"(",
"`Users`.`id`=42 OR 2=2 OR 1=1 OR `Users`.`username`='foo' OR ",
"(`Users`.`id`=42 AND 2=2 AND 1=1 AND `Users`.`username`='foo') OR ",
"(`Users`.`id`=42 OR 2=2 OR 1=1 OR `Users`.`username`='foo')",
") AND ",
"(",
"`Users`.`id`=42 AND 2=2 AND 1=1 AND `Users`.`username`='foo' AND ",
"(`Users`.`id`=42 OR 2=2 OR 1=1 OR `Users`.`username`='foo') AND ",
"(`Users`.`id`=42 AND 2=2 AND 1=1 AND `Users`.`username`='foo')",
")"
].join("") +
")"
)
}
done()
})
})
})
})
})
......@@ -85,6 +85,15 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
it('treats questionmarks in an array', function(done) {
this.UserPrimary.find({
where: ['specialkey = ?', 'awesome']
}).on('sql', function(sql) {
expect(sql).to.contain("WHERE specialkey = 'awesome'")
done()
})
})
it('doesn\'t throw an error when entering in a non integer value for a specified primary field', function(done) {
this.UserPrimary.find('a string').success(function(user) {
expect(user.specialkey).to.equal('a string')
......@@ -1015,4 +1024,4 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
})
\ No newline at end of file
})
......@@ -17,7 +17,24 @@ chai.Assertion.includeStack = true
describe(Support.getTestDialectTeaser("DAO"), function () {
describe('Values', function () {
describe('set', function () {
it('doesn\'t overwrite primary keys', function () {
it('doesn\'t overwrite generated primary keys', function () {
var User = this.sequelize.define('User', {
name: {type: DataTypes.STRING}
})
var user = User.build({id: 1, name: 'Mick'})
expect(user.get('id')).to.equal(1)
expect(user.get('name')).to.equal('Mick')
user.set({
id: 2,
name: 'Jan'
})
expect(user.get('id')).to.equal(1)
expect(user.get('name')).to.equal('Jan')
})
it('doesn\'t overwrite defined primary keys', function () {
var User = this.sequelize.define('User', {
identifier: {type: DataTypes.STRING, primaryKey: true}
})
......
......@@ -236,6 +236,30 @@ if (Support.dialectIsMySQL()) {
expectation: "SELECT * FROM `myTable` GROUP BY name ORDER BY id DESC;",
context: QueryGenerator
}, {
title: 'HAVING clause works with string replacements',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: ['creationYear > ?', 2002]
}
}],
expectation: "SELECT *, YEAR(`createdAt`) as `creationYear` FROM `myTable` GROUP BY `creationYear`, `title` HAVING creationYear > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
title: 'HAVING clause works with where-like hash',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: { creationYear: { gt: 2002 } }
}
}],
expectation: "SELECT *, YEAR(`createdAt`) as `creationYear` FROM `myTable` GROUP BY `creationYear`, `title` HAVING `creationYear` > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
arguments: ['myTable', {limit: 10}],
expectation: "SELECT * FROM `myTable` LIMIT 10;",
context: QueryGenerator
......
......@@ -318,6 +318,30 @@ if (dialect.match(/^postgres/)) {
arguments: ['myTable', {group: ["name","title"]}],
expectation: "SELECT * FROM \"myTable\" GROUP BY \"name\", \"title\";"
}, {
title: 'HAVING clause works with string replacements',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: ['creationYear > ?', 2002]
}
}],
expectation: "SELECT *, YEAR(\"createdAt\") as \"creationYear\" FROM \"myTable\" GROUP BY \"creationYear\", \"title\" HAVING creationYear > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
title: 'HAVING clause works with where-like hash',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: { creationYear: { gt: 2002 } }
}
}],
expectation: "SELECT *, YEAR(\"createdAt\") as \"creationYear\" FROM \"myTable\" GROUP BY \"creationYear\", \"title\" HAVING \"creationYear\" > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
arguments: ['myTable', {limit: 10}],
expectation: "SELECT * FROM \"myTable\" LIMIT 10;"
}, {
......
......@@ -309,4 +309,52 @@ describe(Support.getTestDialectTeaser("Promise"), function () {
})
})
})
describe('findOrCreate', function () {
beforeEach(function(done) {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).done(done)
})
it('with then', function (done) {
this.User
.findOrCreate({ id: 1})
.then(function(user) {
expect(user.id).to.equal(1)
expect(arguments.length).to.equal(1)
done()
})
})
describe('with spread', function () {
it('user not created', function (done) {
this.User
.findOrCreate({ id: 1})
.spread(function(user, created) {
expect(user.id).to.equal(1)
expect(created).to.equal(false)
expect(arguments.length).to.equal(2)
done()
})
})
it('user created', function (done) {
this.User
.findOrCreate({ id: 2})
.spread(function(user, created) {
expect(user.id).to.equal(2)
expect(created).to.equal(true)
expect(arguments.length).to.equal(2)
done()
})
})
it('works for functions with only one return value', function (done) {
this.User
.find({ id: 1})
.spread(function(user) {
expect(user.id).to.equal(1)
expect(arguments.length).to.equal(1)
done()
})
})
})
})
})
......@@ -45,6 +45,24 @@ describe(Support.getTestDialectTeaser("QueryInterface"), function () {
})
})
})
it('should be able to skip given tables', function(done){
var self = this
self.queryInterface.createTable('skipme', {
name: DataTypes.STRING,
}).success(function() {
self.queryInterface.dropAllTables({skip: ['skipme']}).complete(function(err){
expect(err).to.be.null
self.queryInterface.showAllTables().complete(function(err, tableNames) {
expect(err).to.be.null
expect(tableNames).to.contain('skipme');
done();
})
})
})
})
})
describe('indexes', function() {
......
......@@ -64,6 +64,22 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
done()
})
})
it('triggers the error event when using replication', function (done) {
new Sequelize('sequelize', null, null, {
replication: {
read: {
host: 'localhost',
username: 'omg',
password: 'lol'
}
}
}).authenticate()
.complete(function(err, result) {
expect(err).to.not.be.null
done()
})
})
})
})
}
......@@ -430,6 +446,37 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
done()
})
})
it('fails with incorrect database credentials (3)', function (done) {
var sequelize = new Sequelize('db', 'user', 'pass', {
dialect: this.sequelize.options.dialect,
port: 99999
});
var Project = sequelize.define('Project', {title: Sequelize.STRING})
var Task = sequelize.define('Task', {title: Sequelize.STRING})
sequelize.sync({force: true}).done(function (err) {
expect(err).to.be.ok
done()
})
})
it('fails with incorrect database credentials (4)', function (done) {
var sequelize = new Sequelize('db', 'user', 'pass', {
dialect: this.sequelize.options.dialect,
port: 99999,
pool: {}
});
var Project = sequelize.define('Project', {title: Sequelize.STRING})
var Task = sequelize.define('Task', {title: Sequelize.STRING})
sequelize.sync({force: true}).done(function (err) {
expect(err).to.be.ok
done()
})
})
}
describe("doesn't emit logging when explicitly saying not to", function() {
......
......@@ -223,6 +223,30 @@ if (dialect === 'sqlite') {
expectation: "SELECT * FROM `myTable` GROUP BY name ORDER BY id DESC;",
context: QueryGenerator
}, {
title: 'HAVING clause works with string replacements',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: ['creationYear > ?', 2002]
}
}],
expectation: "SELECT *, YEAR(`createdAt`) as `creationYear` FROM `myTable` GROUP BY `creationYear`, `title` HAVING creationYear > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
title: 'HAVING clause works with where-like hash',
arguments: ['myTable', function (sequelize) {
return {
attributes: ['*', [sequelize.fn('YEAR', sequelize.col('createdAt')), 'creationYear']],
group: ['creationYear', 'title'],
having: { creationYear: { gt: 2002 } }
}
}],
expectation: "SELECT *, YEAR(`createdAt`) as `creationYear` FROM `myTable` GROUP BY `creationYear`, `title` HAVING `creationYear` > 2002;",
context: QueryGenerator,
needsSequelize: true
}, {
arguments: ['myTable', {limit: 10}],
expectation: "SELECT * FROM `myTable` LIMIT 10;",
context: QueryGenerator
......
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, TransactionManager = require(__dirname + '/../lib/transaction-manager')
describe(Support.getTestDialectTeaser("TransactionManager"), function () {
beforeEach(function() {
this.transactionManager = new TransactionManager(this.sequelize)
})
describe('getConnectorManager', function() {
describe('if no uuid is passed', function() {
it('uses the default connector', function() {
var connectorManager = this.transactionManager.getConnectorManager()
expect(connectorManager.config.uuid).to.equal('default')
})
it('uses the pooling configuration of sequelize', function() {
var connectorManager = this.transactionManager.getConnectorManager()
, self = this
if (Support.getTestDialect() !== 'sqlite') {
expect(this.sequelize.config.pool.maxConnections).to.equal(5)
}
Object.keys(this.sequelize.config.pool || {}).forEach(function(key) {
expect(connectorManager.config.pool[key]).to.equal(self.sequelize.config.pool[key])
})
})
})
describe('if the passed uuid is not equal to "default"', function() {
it('uses the non-default connector', function() {
var connectorManager = this.transactionManager.getConnectorManager('a-uuid')
expect(connectorManager.config.uuid).to.equal('a-uuid')
})
it('creates a new connector manager', function() {
this.transactionManager.getConnectorManager()
expect(Object.keys(this.transactionManager.connectorManagers).length).to.equal(1)
this.transactionManager.getConnectorManager('a-uuid')
expect(Object.keys(this.transactionManager.connectorManagers).length).to.equal(2)
this.transactionManager.getConnectorManager('a-uuid')
expect(Object.keys(this.transactionManager.connectorManagers).length).to.equal(2)
})
it('treats the connector managers as singleton', function() {
var connectorManager1 = this.transactionManager.getConnectorManager('a-uuid')
, connectorManager2 = this.transactionManager.getConnectorManager('a-uuid')
expect(connectorManager1).to.equal(connectorManager2)
})
it('uses the pooling configuration of sequelize but with disabled replication and forced to one connection', function() {
var connectorManager = this.transactionManager.getConnectorManager('a-uuid')
, self = this
if (Support.getTestDialect() !== 'sqlite') {
expect(this.sequelize.config.pool.maxConnections).to.equal(5)
}
Object.keys(this.sequelize.config.pool || {}).forEach(function(key) {
if (key !== 'maxConnections') {
expect(connectorManager.config.pool[key]).to.equal(self.sequelize.config.pool[key])
}
})
expect(connectorManager.config.pool.maxConnections).to.equal(0)
})
})
})
})
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!