diff --git a/.travis.yml b/.travis.yml index b4e3afeac71c..cd439b421dd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,5 +19,4 @@ env: language: node_js node_js: - - 0.8 - \ No newline at end of file + - 0.8 \ No newline at end of file diff --git a/README.md b/README.md index 65364b40e0c4..eab181e13326 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Also make sure to take a look at the examples in the repository. The website wil - [Documentation](http://www.sequelizejs.com) - [Twitter](http://twitter.com/sdepold) -- [IRC](irc://irc.freenode.net/sequelizejs) +- [IRC](http://webchat.freenode.net?channels=sequelizejs) - [Google Groups](https://groups.google.com/forum/#!forum/sequelize) - [XING](https://www.xing.com/net/priec1b5cx/sequelize) (pretty much inactive, but you might want to name it on your profile) @@ -46,21 +46,18 @@ Also make sure to take a look at the examples in the repository. The website wil A very basic roadmap. Chances aren't too bad, that not mentioned things are implemented as well. Don't panic :) -### 1.6.0 (ToDo) -- ~~Fix last issues with eager loading of associated data~~ -- ~~Find out why Person.belongsTo(House) would add person_id to house. It should add house_id to person~~ - ### 1.7.0 -- Check if lodash is a proper alternative to current underscore usage. +- ~~Check if lodash is a proper alternative to current underscore usage.~~ - Transactions - Support for update of tables without primary key - MariaDB support -- Support for update and delete calls for whole tables without previous loading of instances +- ~~Support for update and delete calls for whole tables without previous loading of instances~~ Implemented in [#569](https://github.com/sequelize/sequelize/pull/569) thanks to @optiltude - Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099) - Model#delete -- Validate a model before it gets saved. (Move validation of enum attribute value to validate method) -- BLOB [#99](https://github.com/sdepold/sequelize/issues/99) -- Support for foreign keys +- ~~Validate a model before it gets saved.~~ Implemented in [#601](https://github.com/sequelize/sequelize/pull/601), thanks to @durango +- Move validation of enum attribute value to validate method +- BLOB [#99](https://github.com/sequelize/sequelize/issues/99) +- ~~Support for foreign keys~~ Implemented in [#595](https://github.com/sequelize/sequelize/pull/595), thanks to @optilude ### 1.7.x - Complete support for non-id primary keys @@ -74,6 +71,7 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl ### 2.0.0 - ~~save datetimes in UTC~~ - encapsulate attributes if a dao inside the attributes property + add getters and setters +- add proper error message everywhere ## Collaboration 2.0 ## diff --git a/bin/sequelize b/bin/sequelize index b4a60b5a1169..7af4d3e03363 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -9,6 +9,7 @@ const path = require("path") , _ = Sequelize.Utils._ var configPath = process.cwd() + '/config' + , environment = process.env.NODE_ENV || 'development' , migrationsPath = process.cwd() + '/migrations' , packageJsonPath = __dirname + '/../package.json' , packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()) @@ -78,22 +79,36 @@ var readConfig = function() { throw new Error('The config.json is not available or contains invalid JSON.') } } + return config } program .version(packageJson.version) - .option('-i, --init', 'Initializes the project. Creates a config/config.json') - .option('-m, --migrate', 'Runs undone migrations') + .option('-i, --init', 'Initializes the project.') + .option('-e, --env ', 'Specify the environment.') + .option('-m, --migrate', 'Run pending migrations.') .option('-u, --undo', 'Undo the last migration.') .option('-f, --force', 'Forces the action to be done.') - .option('-c, --create-migration [migration-name]', 'Create a new migration skeleton file.') + .option('-c, --create-migration [migration-name]', 'Creates a new migration.') .parse(process.argv) +if(typeof program.env === 'string') { + environment = program.env +} +console.log("Using environment '" + environment + "'.") + if(program.migrate) { if(process.env.DATABASE_URL || configFileExists) { var config = readConfig() , options = {} + try { + config = readConfig() + } catch(e) { + console.log(e.message) + process.exit(1) + } + _.each(config, function(value, key) { if(['database', 'username', 'password'].indexOf(key) == -1) { options[key] = value @@ -146,9 +161,10 @@ if(program.migrate) { } }) - console.log('Successfully created config.json') + console.log('Created "config/config.json"') } else { - console.log('A config.json already exists. Run "sequelize --init --force" to overwrite it.') + console.log('"config/config.json" already exists. Run "sequelize --init --force" to overwrite.') + process.exit(1) } createMigrationsFolder(program.force) @@ -157,21 +173,23 @@ if(program.migrate) { var migrationName = [ moment().format('YYYYMMDDHHmmss'), - (typeof program.createMigration == 'string') ? program.createMigration : 'unnamed-migration' + (typeof program.createMigration === 'string') ? program.createMigration : 'unnamed-migration' ].join('-') + '.js' var migrationContent = [ "module.exports = {", - " up: function(migration, DataTypes) {", - " // add altering commands here", + " up: function(migration, DataTypes, done) {", + " // add altering commands here, calling 'done' when finished", + " done()", " },", - " down: function(migration) {", - " // add reverting commands here", + " down: function(migration, DataTypes, done) {", + " // add reverting commands here, calling 'done' when finished", + " done()", " }", "}" ].join('\n') fs.writeFileSync(migrationsPath + '/' + migrationName, migrationContent) } else { - console.log('Please define any params!') + console.log('Try "sequelize --help" for usage information.') } diff --git a/changelog.md b/changelog.md index bedb30f11c15..157526dff2f8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # v1.7.0 # +- [DEPENDENCIES] Upgraded validator for IPv6 support. [#603](https://github.com/sequelize/sequelize/pull/603). thanks to durango +- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango +- [DEPENDENCIES] replaced underscore by lodash. [#954](https://github.com/sequelize/sequelize/pull/594). thanks to durango +- [BUG] Fix string escape with postgresql on raw SQL queries. [#586](https://github.com/sequelize/sequelize/pull/586). thanks to zanamixx +- [BUG] "order by" is now after "group by". [#585](https://github.com/sequelize/sequelize/pull/585). thanks to mekanics +- [BUG] Added decimal support for min/max. [#583](https://github.com/sequelize/sequelize/pull/583). thanks to durango +- [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel +- [BUG] Correctly handle booleans in MySQL. [#608](https://github.com/sequelize/sequelize/pull/608). Thanks to terraflubb - [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango +- [FEATURE] Foreign key constraints. [#595](https://github.com/sequelize/sequelize/pull/595). thanks to optilude +- [FEATURE] Support for bulk insert (`.bulkCreate()`, update (`.update()`) and delete (`.destroy()`) [#569](https://github.com/sequelize/sequelize/pull/569). thanks to optilude +- [FEATURE] Add an extra `queryOptions` parameter to `DAOFactory.find` and `DAOFactory.findAll`. This allows a user to specify `{ raw: true }`, meaning that the raw result should be returned, instead of built DAOs. Usefull for queries returning large datasets, see [#611](https://github.com/sequelize/sequelize/pull/611) janmeier +- [FEATURE] Added convenient data types. [#616](https://github.com/sequelize/sequelize/pull/616). Thanks to Costent +- [FEATURE] Binary is more verbose now. [#612](https://github.com/sequelize/sequelize/pull/612). Thanks to terraflubb # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index 020f14abe2ec..ce82c607a852 100644 --- a/lib/associations/belongs-to.js +++ b/lib/associations/belongs-to.js @@ -1,5 +1,6 @@ var Utils = require("./../utils") , DataTypes = require('./../data-types') + , Helpers = require('./helpers') module.exports = (function() { var BelongsTo = function(srcDAO, targetDAO, options) { @@ -24,6 +25,7 @@ module.exports = (function() { this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.target.tableName) + "Id", this.source.options.underscored) newAttributes[this.identifier] = { type: DataTypes.INTEGER } + Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.target, this.source, this.options) Utils._.defaults(this.source.rawAttributes, newAttributes) // Sync attributes to DAO proto each time a new assoc is added diff --git a/lib/associations/has-many.js b/lib/associations/has-many.js index b9f6738521db..e0b4e36b0734 100644 --- a/lib/associations/has-many.js +++ b/lib/associations/has-many.js @@ -1,5 +1,6 @@ var Utils = require("./../utils") , DataTypes = require('./../data-types') + , Helpers = require('./helpers') var HasManySingleLinked = require("./has-many-single-linked") , HasManyMultiLinked = require("./has-many-double-linked") @@ -65,6 +66,7 @@ module.exports = (function() { } else { var newAttributes = {} newAttributes[this.identifier] = { type: DataTypes.INTEGER } + Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options) Utils._.defaults(this.target.rawAttributes, newAttributes) } diff --git a/lib/associations/has-one.js b/lib/associations/has-one.js index 69a93898b4f1..6fe8121a9c2d 100644 --- a/lib/associations/has-one.js +++ b/lib/associations/has-one.js @@ -1,5 +1,6 @@ var Utils = require("./../utils") , DataTypes = require('./../data-types') + , Helpers = require("./helpers") module.exports = (function() { var HasOne = function(srcDAO, targetDAO, options) { @@ -29,6 +30,7 @@ module.exports = (function() { this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName) + "Id", this.options.underscored) newAttributes[this.identifier] = { type: DataTypes.INTEGER } + Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options) Utils._.defaults(this.target.rawAttributes, newAttributes) // Sync attributes to DAO proto each time a new assoc is added diff --git a/lib/associations/helpers.js b/lib/associations/helpers.js new file mode 100644 index 000000000000..7ea29f4d3527 --- /dev/null +++ b/lib/associations/helpers.js @@ -0,0 +1,25 @@ +var Utils = require("./../utils") + +module.exports = { + + addForeignKeyConstraints: function(newAttribute, source, target, options) { + // FK constraints are opt-in: users must either rset `foreignKeyConstraints` + // on the association, or request an `onDelete` or `onUpdate` behaviour + + if(options.foreignKeyConstraint || options.onDelete || options.onUpdate) { + + // Find primary keys: composite keys not supported with this approach + var primaryKeys = Utils._.filter(Utils._.keys(source.rawAttributes), function(key) { + return source.rawAttributes[key].primaryKey + }) + + if(primaryKeys.length == 1) { + newAttribute.references = source.tableName, + newAttribute.referencesKey = primaryKeys[0] + newAttribute.onDelete = options.onDelete, + newAttribute.onUpdate = options.onUpdate + } + } + } + +} diff --git a/lib/dao-factory-manager.js b/lib/dao-factory-manager.js index 092edfa83483..a42ed8d3447b 100644 --- a/lib/dao-factory-manager.js +++ b/lib/dao-factory-manager.js @@ -1,3 +1,5 @@ +var Toposort = require('toposort-class') + module.exports = (function() { var DAOFactoryManager = function(sequelize) { this.daos = [] @@ -31,5 +33,34 @@ module.exports = (function() { return this.daos }) + /** + * Iterate over DAOs in an order suitable for e.g. creating tables. Will + * take foreign key constraints into account so that dependencies are visited + * before dependents. + */ + DAOFactoryManager.prototype.forEachDAO = function(iterator) { + var daos = {} + , sorter = new Toposort() + + this.daos.forEach(function(dao) { + daos[dao.tableName] = dao + var deps = [] + + for(var attrName in dao.rawAttributes) { + if(dao.rawAttributes.hasOwnProperty(attrName)) { + if(dao.rawAttributes[attrName].references) { + deps.push(dao.rawAttributes[attrName].references) + } + } + } + + sorter.add(dao.tableName, deps) + }) + + sorter.sort().reverse().forEach(function(name) { + iterator(daos[name]) + }) + } + return DAOFactoryManager })() diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 5538480c88df..b8bc36e8b245 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -56,11 +56,6 @@ module.exports = (function() { this.primaryKeys = {}; Utils._.each(this.attributes, function(dataTypeString, attributeName) { - // If you don't specify a valid data type lets help you debug it - if (dataTypeString === undefined) { - throw new Error("Unrecognized data type for field " + attributeName ); - } - if ((attributeName !== 'id') && (dataTypeString.indexOf('PRIMARY KEY') !== -1)) { self.primaryKeys[attributeName] = dataTypeString } @@ -161,11 +156,11 @@ module.exports = (function() { } // alias for findAll - DAOFactory.prototype.all = function(options) { - return this.findAll(options) + DAOFactory.prototype.all = function(options, queryOptions) { + return this.findAll(options, queryOptions) } - DAOFactory.prototype.findAll = function(options) { + DAOFactory.prototype.findAll = function(options, queryOptions) { var hasJoin = false var options = Utils._.clone(options) @@ -182,10 +177,10 @@ module.exports = (function() { this.options.whereCollection = options.where || null } - return this.QueryInterface.select(this, this.tableName, options, { + return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({ type: 'SELECT', hasJoin: hasJoin - }) + }, queryOptions)) } //right now, the caller (has-many-double-linked) is in charge of the where clause @@ -204,9 +199,10 @@ module.exports = (function() { * * @param {Object} options Options to describe the scope of the search. * @param {Array} include A list of associations which shall get eagerly loaded. Supported is either { include: [ DaoFactory1, DaoFactory2, ...] } or { include: [ { daoFactory: DaoFactory1, as: 'Alias' } ] }. + * @param {Object} set the query options, e.g. raw, specifying that you want raw data instead of built DAOs * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAOFactory.prototype.find = function(options) { + DAOFactory.prototype.find = function(options, queryOptions) { var hasJoin = false // no options defined? @@ -258,11 +254,11 @@ module.exports = (function() { options.limit = 1 - return this.QueryInterface.select(this, this.getTableName(), options, { + return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({ plain: true, type: 'SELECT', hasJoin: hasJoin - }) + }, queryOptions)) } DAOFactory.prototype.count = function(options) { @@ -276,14 +272,14 @@ module.exports = (function() { DAOFactory.prototype.max = function(field, options) { options = Utils._.extend({ attributes: [] }, options || {}) options.attributes.push(['max(' + field + ')', 'max']) - options.parseInt = true + options.parseFloat = true return this.QueryInterface.rawSelect(this.getTableName(), options, 'max') } DAOFactory.prototype.min = function(field, options) { options = Utils._.extend({ attributes: [] }, options || {}) options.attributes.push(['min(' + field + ')', 'min']) - options.parseInt = true + options.parseFloat = true return this.QueryInterface.rawSelect(this.getTableName(), options, 'min') } @@ -333,6 +329,110 @@ module.exports = (function() { }).run() } + /** + * Create and insert multiple instances + * + * @param {Array} records List of objects (key/value pairs) to create instances from + * @param {Array} fields Fields to insert (defaults to all fields) + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + * + * Note: the `success` handler is not passed any arguments. To obtain DAOs for + * the newly created values, you will need to query for them again. This is + * because MySQL and SQLite do not make it easy to obtain back automatically + * generated IDs and other default values in a way that can be mapped to + * multiple records + */ + DAOFactory.prototype.bulkCreate = function(records, fields) { + var self = this + , daos = records.map(function(v) { return self.build(v) }) + , updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt' + , createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt' + + // we will re-create from DAOs, which may have set up default attributes + records = [] + + if (fields) { + + // Always insert updated and created time stamps + if (self.options.timestamps) { + if (fields.indexOf(updatedAtAttr) === -1) { + fields.push(updatedAtAttr) + } + + if (fields.indexOf(createdAtAttr) === -1) { + fields.push(createdAtAttr) + } + } + + // Build records for the fields we know about + daos.forEach(function(dao) { + var values = {}; + fields.forEach(function(field) { + values[field] = dao.values[field] + }) + if (self.options.timestamps) { + values[updatedAtAttr] = Utils.now() + } + records.push(values); + }) + + } else { + daos.forEach(function(dao) { + records.push(dao.values) + }) + } + + // Validate enums + records.forEach(function(values) { + for (var attrName in self.rawAttributes) { + if (self.rawAttributes.hasOwnProperty(attrName)) { + var definition = self.rawAttributes[attrName] + , isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString())) + , hasValue = (typeof values[attrName] !== 'undefined') + , valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1) + + if (isEnum && hasValue && valueOutOfScope) { + throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', ')) + } + } + } + }) + + return self.QueryInterface.bulkInsert(self.tableName, records) + } + + /** + * Delete multiple instances + * + * @param {Object} where Options to describe the scope of the search. + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + */ + DAOFactory.prototype.destroy = function(where) { + if (this.options.timestamps && this.options.paranoid) { + var attr = this.options.underscored ? 'deleted_at' : 'deletedAt' + var attrValueHash = {} + attrValueHash[attr] = Utils.now() + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where) + } else { + return this.QueryInterface.bulkDelete(this.tableName, where) + } + } + + /** + * Update multiple instances + * + * @param {Object} attrValueHash A hash of fields to change and their new values + * @param {Object} where Options to describe the scope of the search. + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + */ + DAOFactory.prototype.update = function(attrValueHash, where) { + if(this.options.timestamps) { + var attr = this.options.underscored ? 'updated_at' : 'updatedAt' + attrValueHash[attr] = Utils.now() + } + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where) + } + // private var query = function() { diff --git a/lib/dao.js b/lib/dao.js index 4bb8caaf248a..a9640a0c66dd 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -150,11 +150,19 @@ module.exports = (function() { this[updatedAtAttr] = values[updatedAtAttr] = Utils.now() } - if (this.isNewRecord) { + var errors = this.validate() + + if (!!errors) { + return new Utils.CustomEventEmitter(function(emitter) { + emitter.emit('error', errors) + }).run() + } + else if (this.isNewRecord) { // added by Scott Rutherford to set created post class creation if(this.__options.timestamps && this.hasOwnProperty(createdAtAttr)) { this[createdAtAttr] = values[createdAtAttr] = new Date() } + return this.QueryInterface.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values) } else { var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id; @@ -215,7 +223,9 @@ module.exports = (function() { Utils._.each(self.values, function(value, field) { // if field has validators - if (self.validators.hasOwnProperty(field)) { + var hasAllowedNull = (self.rawAttributes[field].allowNull && self.rawAttributes[field].allowNull === true && (value === null || value === undefined)); + + if (self.validators.hasOwnProperty(field) && !hasAllowedNull) { // for each validator Utils._.each(self.validators[field], function(details, validatorType) { diff --git a/lib/data-types.js b/lib/data-types.js index 9e07841b4661..6b5bd6b35b2a 100644 --- a/lib/data-types.js +++ b/lib/data-types.js @@ -1,11 +1,151 @@ +var STRING = function(length, binary) { + if (this instanceof STRING) { + this._binary = !!binary; + if (typeof length === 'number') { + this._length = length; + } else { + this._length = 255; + } + } else { + return new STRING(length, binary); + } +}; + +STRING.prototype = { + get BINARY() { + this._binary = true; + return this; + }, + get type() { + return this.toString(); + }, + toString: function() { + return 'VARCHAR(' + this._length + ')' + ((this._binary) ? ' BINARY' : ''); + } +}; + +Object.defineProperty(STRING, 'BINARY', { + get: function() { + return new STRING(undefined, true); + } +}); + +var INTEGER = function() { + return INTEGER.prototype.construct.apply(this, [INTEGER].concat(Array.prototype.slice.apply(arguments))); +}; + +var BIGINT = function() { + return BIGINT.prototype.construct.apply(this, [BIGINT].concat(Array.prototype.slice.apply(arguments))); +}; + +var FLOAT = function() { + return FLOAT.prototype.construct.apply(this, [FLOAT].concat(Array.prototype.slice.apply(arguments))); +}; + +FLOAT._type = FLOAT; +FLOAT._typeName = 'FLOAT'; +INTEGER._type = INTEGER; +INTEGER._typeName = 'INTEGER'; +BIGINT._type = BIGINT; +BIGINT._typeName = 'BIGINT'; +STRING._type = STRING; +STRING._typeName = 'VARCHAR'; + +STRING.toString = INTEGER.toString = FLOAT.toString = BIGINT.toString = function() { + return new this._type().toString(); +}; + +FLOAT.prototype = BIGINT.prototype = INTEGER.prototype = { + + construct: function(RealType, length, decimals, unsigned, zerofill) { + if (this instanceof RealType) { + this._typeName = RealType._typeName; + this._unsigned = !!unsigned; + this._zerofill = !!zerofill; + if (typeof length === 'number') { + this._length = length; + } + if (typeof decimals === 'number') { + this._decimals = decimals; + } + } else { + return new RealType(length, decimals, unsigned, zerofill); + } + }, + + get type() { + return this.toString(); + }, + + get UNSIGNED() { + this._unsigned = true; + return this; + }, + + get ZEROFILL() { + this._zerofill = true; + return this; + }, + + toString: function() { + var result = this._typeName; + if (this._length) { + result += '(' + this._length; + if (typeof this._decimals === 'number') { + result += ',' + this._decimals; + } + result += ')'; + } + if (this._unsigned) { + result += ' UNSIGNED'; + } + if (this._zerofill) { + result += ' ZEROFILL'; + } + return result; + } +}; + +var unsignedDesc = { + get: function() { + return new this._type(undefined, undefined, true); + } +}; + +var zerofillDesc = { + get: function() { + return new this._type(undefined, undefined, undefined, true); + } +}; + +var typeDesc = { + get: function() { + return new this._type().toString(); + } +}; + +Object.defineProperty(STRING, 'type', typeDesc); +Object.defineProperty(INTEGER, 'type', typeDesc); +Object.defineProperty(BIGINT, 'type', typeDesc); +Object.defineProperty(FLOAT, 'type', typeDesc); + +Object.defineProperty(INTEGER, 'UNSIGNED', unsignedDesc); +Object.defineProperty(BIGINT, 'UNSIGNED', unsignedDesc); +Object.defineProperty(FLOAT, 'UNSIGNED', unsignedDesc); + +Object.defineProperty(INTEGER, 'ZEROFILL', zerofillDesc); +Object.defineProperty(BIGINT, 'ZEROFILL', zerofillDesc); +Object.defineProperty(FLOAT, 'ZEROFILL', zerofillDesc); + module.exports = { - STRING: 'VARCHAR(255)', + STRING: STRING, + TEXT: 'TEXT', - INTEGER: 'INTEGER', - BIGINT: 'BIGINT', + INTEGER: INTEGER, + BIGINT: BIGINT, DATE: 'DATETIME', BOOLEAN: 'TINYINT(1)', - FLOAT: 'FLOAT', + FLOAT: FLOAT, NOW: 'NOW', get ENUM() { diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 1c7be85a4e1a..f77e5e026e85 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -2,10 +2,20 @@ var Utils = require("../../utils") , DataTypes = require("../../data-types") , util = require("util") +var processAndEscapeValue = function(value) { + var processedValue = value + if (value instanceof Date) { + processedValue = Utils.toSqlDate(value) + } else if (typeof value === 'boolean') { + processedValue = value ? 1 : 0 + } + return Utils.escape(processedValue) +} + module.exports = (function() { var QueryGenerator = { addSchema: function(opts) { - var tableName = undefined + var tableName var schema = (!!opts && !!opts.options && !!opts.options.schema ? opts.options.schema : undefined) var schemaDelimiter = (!!opts && !!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined) @@ -45,6 +55,7 @@ module.exports = (function() { var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %> <%= charset %>" , primaryKeys = [] + , foreignKeys = {} , attrStr = [] for (var attr in attributes) { @@ -54,6 +65,11 @@ module.exports = (function() { if (Utils._.includes(dataType, 'PRIMARY KEY')) { primaryKeys.push(attr) attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType.replace(/PRIMARY KEY/, '')) + } else if (Utils._.includes(dataType, 'REFERENCES')) { + // MySQL doesn't support inline REFERENCES declarations: move to the end + var m = dataType.match(/^(.+) (REFERENCES.*)$/) + attrStr.push(QueryGenerator.addQuotes(attr) + " " + m[1]) + foreignKeys[attr] = m[2] } else { attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType) } @@ -72,6 +88,12 @@ module.exports = (function() { values.attributes += ", PRIMARY KEY (" + pkString + ")" } + for (var fkey in foreignKeys) { + if(foreignKeys.hasOwnProperty(fkey)) { + values.attributes += ", FOREIGN KEY (" + QueryGenerator.addQuotes(fkey) + ") " + foreignKeys[fkey] + } + } + return Utils._.template(query)(values).trim() + ";" }, @@ -119,7 +141,7 @@ module.exports = (function() { var query = "ALTER TABLE `<%= tableName %>` CHANGE <%= attributes %>;" var attrString = [] - for (attrName in attributes) { + for (var attrName in attributes) { var definition = attributes[attrName] attrString.push(Utils._.template('`<%= attrName %>` `<%= attrName %>` <%= definition %>')({ @@ -149,8 +171,8 @@ module.exports = (function() { }, selectQuery: function(tableName, options) { - var query = "SELECT <%= attributes %> FROM <%= table %>" - , table = null + var table = null, + joinQuery = "" options = options || {} options.table = table = Array.isArray(tableName) ? tableName.map(function(tbl){ return QueryGenerator.addQuotes(tbl) }).join(", ") : QueryGenerator.addQuotes(tableName) @@ -168,126 +190,153 @@ module.exports = (function() { options.include.forEach(function(include) { var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) { - var template = Utils._.template("`<%= as %>`.`<%= attr %>` AS `<%= as %>.<%= attr %>`") - return template({ as: include.as, attr: attr }) + return "`" + include.as + "`.`" + attr + "` AS `" + include.as + "." + attr + "`" }) optAttributes = optAttributes.concat(attributes) - var joinQuery = " LEFT OUTER JOIN `<%= table %>` AS `<%= as %>` ON `<%= tableLeft %>`.`<%= attrLeft %>` = `<%= tableRight %>`.`<%= attrRight %>`" - query += Utils._.template(joinQuery)({ - table: include.daoFactory.tableName, - as: include.as, - tableLeft: ((include.association.associationType === 'BelongsTo') ? include.as : tableName), - attrLeft: 'id', - tableRight: ((include.association.associationType === 'BelongsTo') ? tableName : include.as), - attrRight: include.association.identifier - }) + var table = include.daoFactory.tableName + var as = include.as + var tableLeft = ((include.association.associationType === 'BelongsTo') ? include.as : tableName) + var attrLeft = 'id' + var tableRight = ((include.association.associationType === 'BelongsTo') ? tableName : include.as) + var attrRight = include.association.identifier + joinQuery += " LEFT OUTER JOIN `" + table + "` AS `" + as + "` ON `" + tableLeft + "`.`" + attrLeft + "` = `" + tableRight + "`.`" + attrRight + "`" + }) options.attributes = optAttributes.join(', ') } + var query = "SELECT " + options.attributes + " FROM " + options.table + query += joinQuery + if (options.hasOwnProperty('where')) { options.where = this.getWhereConditions(options.where, tableName) - query += " WHERE <%= where %>" + query += " WHERE " + options.where } if (options.group) { options.group = Array.isArray(options.group) ? options.group.map(function(grp){return QueryGenerator.addQuotes(grp)}).join(', ') : QueryGenerator.addQuotes(options.group) - query += " GROUP BY <%= group %>" + query += " GROUP BY " + options.group } if (options.order) { - query += " ORDER BY <%= order %>" + query += " ORDER BY " + options.order } if (options.limit && !(options.include && (options.limit === 1))) { if (options.offset) { - query += " LIMIT <%= offset %>, <%= limit %>" + query += " LIMIT " + options.offset + ", " + options.limit } else { - query += " LIMIT <%= limit %>" + query += " LIMIT " + options.limit } } query += ";" - return Utils._.template(query)(options) + return query }, insertQuery: function(tableName, attrValueHash) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) - var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>);" + var table = QueryGenerator.addQuotes(tableName) + var attributes = Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(",") + var values = Utils._.values(attrValueHash).map(processAndEscapeValue).join(",") - var replacements = { - table: QueryGenerator.addQuotes(tableName), - attributes: Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(","), - values: Utils._.values(attrValueHash).map(function(value){ - return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value) - }).join(",") - } + var query = "INSERT INTO " + table + " (" + attributes + ") VALUES (" + values + ");" - return Utils._.template(query)(replacements) + return query + }, + + bulkInsertQuery: function(tableName, attrValueHashes) { + var tuples = [] + + Utils._.forEach(attrValueHashes, function(attrValueHash) { + tuples.push("(" + + Utils._.values(attrValueHash).map(processAndEscapeValue).join(",") + + ")") + }) + + var table = QueryGenerator.addQuotes(tableName) + var attributes = Object.keys(attrValueHashes[0]).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(",") + + var query = "INSERT INTO " + table + " (" + attributes + ") VALUES " + tuples.join(",") + ";" + + return query }, updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) - var query = "UPDATE <%= table %> SET <%= values %> WHERE <%= where %>" - , values = [] + var values = [] for (var key in attrValueHash) { var value = attrValueHash[key] - , _value = (value instanceof Date) ? Utils.toSqlDate(value) : value + , _value = processAndEscapeValue(value) - values.push(QueryGenerator.addQuotes(key) + "=" + Utils.escape(_value)) + values.push(QueryGenerator.addQuotes(key) + "=" + _value) } - var replacements = { - table: QueryGenerator.addQuotes(tableName), - values: values.join(","), - where: QueryGenerator.getWhereConditions(where) - } + var query = "UPDATE " + QueryGenerator.addQuotes(tableName) + + " SET " + values.join(",") + + " WHERE " + QueryGenerator.getWhereConditions(where) - return Utils._.template(query)(replacements) + return query }, deleteQuery: function(tableName, where, options) { options = options || {} - options.limit = options.limit || 1 - var query = "DELETE FROM <%= table %> WHERE <%= where %> LIMIT <%= limit %>" - var replacements = { - table: QueryGenerator.addQuotes(tableName), - where: QueryGenerator.getWhereConditions(where), - limit: Utils.escape(options.limit) + var table = QueryGenerator.addQuotes(tableName) + where = QueryGenerator.getWhereConditions(where) + var limit = "" + + if(Utils._.isUndefined(options.limit)) { + options.limit = 1; } - return Utils._.template(query)(replacements) + if(!!options.limit) { + limit = " LIMIT " + Utils.escape(options.limit) + } + + var query = "DELETE FROM " + table + " WHERE " + where + limit + + return query + }, + + bulkDeleteQuery: function(tableName, where, options) { + options = options || {} + + var table = QueryGenerator.addQuotes(tableName) + where = QueryGenerator.getWhereConditions(where) + + var query = "DELETE FROM " + table + " WHERE " + where + + return query }, incrementQuery: function (tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) - var query = "UPDATE <%= table %> SET <%= values %> WHERE <%= where %> " - , values = [] + var values = [] for (var key in attrValueHash) { var value = attrValueHash[key] - , _value = (value instanceof Date) ? Utils.toSqlDate(value) : value + , _value = processAndEscapeValue(value) - values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " +Utils.escape(_value)) + values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " + _value) } - var replacements = { - table: QueryGenerator.addQuotes(tableName), - values: values.join(","), - where: QueryGenerator.getWhereConditions(where) - } + var table = QueryGenerator.addQuotes(tableName) + values = values.join(",") + where = QueryGenerator.getWhereConditions(where) + + var query = "UPDATE " + table + " SET " + values + " WHERE " + where - return Utils._.template(query)(replacements) + return query }, addIndexQuery: function(tableName, attributes, options) { @@ -382,20 +431,18 @@ module.exports = (function() { if (Array.isArray(value)) { // is value an array? - if (value.length == 0) { value = [null] } - _value = "(" + value.map(function(subValue) { - return Utils.escape(subValue); - }).join(',') + ")" + if (value.length === 0) { value = [null] } + _value = "(" + value.map(processAndEscapeValue).join(',') + ")" result.push([_key, _value].join(" IN ")) - } else if ((value) && (typeof value == 'object')) { + } else if ((value) && (typeof value == 'object') && !(value instanceof Date)) { // is value an object? //using as sentinel for join column => value _value = value.join.split('.').map(function(col){ return QueryGenerator.addQuotes(col) }).join(".") result.push([_key, _value].join("=")) } else { - _value = Utils.escape(value) + _value = processAndEscapeValue(value) result.push((_value == 'NULL') ? _key + " IS NULL" : [_key, _value].join("=")) } } @@ -410,17 +457,18 @@ module.exports = (function() { var dataType = attributes[name] if (Utils.isHash(dataType)) { - var template = "<%= type %>" - , replacements = { type: dataType.type } + var template if (dataType.type.toString() === DataTypes.ENUM.toString()) { if (Array.isArray(dataType.values) && (dataType.values.length > 0)) { - replacements.type = "ENUM(" + Utils._.map(dataType.values, function(value) { + template = "ENUM(" + Utils._.map(dataType.values, function(value) { return Utils.escape(value) }).join(", ") + ")" } else { throw new Error('Values for ENUM haven\'t been defined.') } + } else { + template = dataType.type.toString(); } if (dataType.hasOwnProperty('allowNull') && (!dataType.allowNull)) { @@ -431,9 +479,8 @@ module.exports = (function() { template += " auto_increment" } - if ((dataType.defaultValue != undefined) && (dataType.defaultValue != DataTypes.NOW)) { - template += " DEFAULT <%= defaultValue %>" - replacements.defaultValue = Utils.escape(dataType.defaultValue) + if ((dataType.defaultValue !== undefined) && (dataType.defaultValue != DataTypes.NOW)) { + template += " DEFAULT " + Utils.escape(dataType.defaultValue) } if (dataType.unique) { @@ -444,7 +491,27 @@ module.exports = (function() { template += " PRIMARY KEY" } - result[name] = Utils._.template(template)(replacements) + if(dataType.references) { + template += " REFERENCES " + Utils.addTicks(dataType.references) + + + if(dataType.referencesKey) { + template += " (" + Utils.addTicks(dataType.referencesKey) + ")" + } else { + template += " (" + Utils.addTicks('id') + ")" + } + + if(dataType.onDelete) { + template += " ON DELETE " + dataType.onDelete.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE " + dataType.onUpdate.toUpperCase() + } + + } + + result[name] = template } else { result[name] = dataType } @@ -469,6 +536,16 @@ module.exports = (function() { return fields }, + enableForeignKeyConstraintsQuery: function() { + var sql = "SET FOREIGN_KEY_CHECKS = 1;" + return Utils._.template(sql, {}) + }, + + disableForeignKeyConstraintsQuery: function() { + var sql = "SET FOREIGN_KEY_CHECKS = 0;" + return Utils._.template(sql, {}) + }, + addQuotes: function(s, quoteChar) { return Utils.addTicks(s, quoteChar) }, diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 1ca8204b785d..b6773685c23c 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -62,7 +62,7 @@ module.exports = (function() { var values = { table: QueryGenerator.addQuotes(tableName), - attributes: attrStr.join(", "), + attributes: attrStr.join(", ") } var pks = primaryKeys[tableName].map(function(pk){ @@ -78,9 +78,10 @@ module.exports = (function() { dropTableQuery: function(tableName, options) { options = options || {} - var query = "DROP TABLE IF EXISTS <%= table %>;" + var query = "DROP TABLE IF EXISTS <%= table %><%= cascade %>;" return Utils._.template(query)({ - table: QueryGenerator.addQuotes(tableName) + table: QueryGenerator.addQuotes(tableName), + cascade: options.cascade? " CASCADE" : "" }) }, @@ -262,13 +263,6 @@ module.exports = (function() { query += " WHERE <%= where %>" } - if(options.order) { - options.order = options.order.replace(/([^ ]+)(.*)/, function(m, g1, g2) { - return QueryGenerator.addQuotes(g1) + g2 - }) - query += " ORDER BY <%= order %>" - } - if(options.group) { if (Array.isArray(options.group)) { options.group = options.group.map(function(grp){ @@ -281,6 +275,13 @@ module.exports = (function() { query += " GROUP BY <%= group %>" } + if(options.order) { + options.order = options.order.replace(/([^ ]+)(.*)/, function(m, g1, g2) { + return QueryGenerator.addQuotes(g1) + g2 + }) + query += " ORDER BY <%= order %>" + } + if (!(options.include && (options.limit === 1))) { if (options.limit) { query += " LIMIT <%= limit %>" @@ -303,18 +304,7 @@ module.exports = (function() { delete attrValueHash['id']; var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>) RETURNING *;" - , returning = [] - - Utils._.forEach(attrValueHash, function(value, key, hash) { - if (tables[tableName] && tables[tableName][key]) { - switch (tables[tableName][key]) { - case 'serial': - delete hash[key] - returning.push(key) - break - } - } - }); + , returning = removeSerialsFromHash(tableName, attrValueHash) var replacements = { table: QueryGenerator.addQuotes(tableName) @@ -329,6 +319,30 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkInsertQuery: function(tableName, attrValueHashes) { + var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %> RETURNING *;" + , tuples = [] + + Utils._.forEach(attrValueHashes, function(attrValueHash) { + removeSerialsFromHash(tableName, attrValueHash) + tuples.push("(" + + Utils._.values(attrValueHash).map(function(value){ + return QueryGenerator.pgEscape(value) + }).join(",") + + ")") + }) + + var replacements = { + table: QueryGenerator.addQuotes(tableName) + , attributes: Object.keys(attrValueHashes[0]).map(function(attr){ + return QueryGenerator.addQuotes(attr) + }).join(",") + , tuples: tuples.join(",") + } + + return Utils._.template(query)(replacements) + }, + updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) @@ -351,11 +365,14 @@ module.exports = (function() { deleteQuery: function(tableName, where, options) { options = options || {} - options.limit = options.limit || 1 + + if(Utils._.isUndefined(options.limit)) { + options.limit = 1; + } primaryKeys[tableName] = primaryKeys[tableName] || []; - var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %> LIMIT <%= limit %>)" + var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %><%= limit %>)" var pks; if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) { @@ -369,7 +386,7 @@ module.exports = (function() { var replacements = { table: QueryGenerator.addQuotes(tableName), where: QueryGenerator.getWhereConditions(where), - limit: QueryGenerator.pgEscape(options.limit), + limit: !!options.limit? " LIMIT " + QueryGenerator.pgEscape(options.limit) : "", primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, primaryKeysSelection: pks } @@ -559,6 +576,28 @@ module.exports = (function() { template += " PRIMARY KEY" } + if(dataType.references) { + template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)" + replacements.referencesTable = QueryGenerator.addQuotes(dataType.references) + + if(dataType.referencesKey) { + replacements.referencesKey = QueryGenerator.addQuotes(dataType.referencesKey) + } else { + replacements.referencesKey = QueryGenerator.addQuotes('id') + } + + if(dataType.onDelete) { + template += " ON DELETE <%= onDeleteAction %>" + replacements.onDeleteAction = dataType.onDelete.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE <%= onUpdateAction %>" + replacements.onUpdateAction = dataType.onUpdate.toUpperCase() + } + + } + result[name] = Utils._.template(template)(replacements) } else { result[name] = dataType @@ -582,8 +621,16 @@ module.exports = (function() { return fields }, + enableForeignKeyConstraintsQuery: function() { + return false // not supported by dialect + }, + + disableForeignKeyConstraintsQuery: function() { + return false // not supported by dialect + }, + databaseConnectionUri: function(config) { - var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>'; + var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>' return Utils._.template(template)({ user: encodeURIComponent(config.username), @@ -693,5 +740,22 @@ module.exports = (function() { } } + // Private + + var removeSerialsFromHash = function(tableName, attrValueHash) { + var returning = []; + Utils._.forEach(attrValueHash, function(value, key, hash) { + if (tables[tableName] && tables[tableName][key]) { + switch (tables[tableName][key]) { + case 'serial': + delete hash[key] + returning.push(key) + break + } + } + }); + return returning; + } + return Utils._.extend(Utils._.clone(require("../query-generator")), QueryGenerator) })() diff --git a/lib/dialects/postgres/query.js b/lib/dialects/postgres/query.js index 49c405cf6529..6dff30fed944 100644 --- a/lib/dialects/postgres/query.js +++ b/lib/dialects/postgres/query.js @@ -108,27 +108,29 @@ module.exports = (function() { } else if (this.send('isShowOrDescribeQuery')) { this.emit('success', results) } else if (this.send('isInsertQuery')) { - for (var key in rows[0]) { - if (rows[0].hasOwnProperty(key) && this.callee.daoFactory.rawAttributes[key]) { - var record = rows[0][key] - if (!!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) { - record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) + if(this.callee !== null) { // may happen for bulk inserts + for (var key in rows[0]) { + if (rows[0].hasOwnProperty(key)) { + var record = rows[0][key] + if (!!this.callee.daoFactory && !!this.callee.daoFactory.rawAttributes && !!this.callee.daoFactory.rawAttributes[key] && !!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) { + record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) + } + this.callee[key] = record } - - this.callee[key] = record } } this.emit('success', this.callee) } else if (this.send('isUpdateQuery')) { - for (var key in rows[0]) { - if (rows[0].hasOwnProperty(key) && this.callee.daoFactory.rawAttributes[key]) { - var record = rows[0][key] - if (!!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) { - record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) + if(this.callee !== null) { // may happen for bulk updates + for (var key in rows[0]) { + if (rows[0].hasOwnProperty(key)) { + var record = rows[0][key] + if (!!this.callee.daoFactory && !!this.callee.daoFactory.rawAttributes && !!this.callee.daoFactory.rawAttributes[key] && !!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) { + record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) + } + this.callee[key] = record } - - this.callee[key] = record } } diff --git a/lib/dialects/query-generator.js b/lib/dialects/query-generator.js index 669b99348049..ea5b4a4cf8f4 100644 --- a/lib/dialects/query-generator.js +++ b/lib/dialects/query-generator.js @@ -118,6 +118,14 @@ module.exports = (function() { throwMethodUndefined('insertQuery') }, + /* + Returns an insert into command for multiple values. + Parameters: table name + list of hashes of attribute-value-pairs. + */ + bulkInsertQuery: function(tableName, attrValueHashes) { + throwMethodUndefined('bulkInsertQuery') + }, + /* Returns an update query. Parameters: @@ -147,6 +155,19 @@ module.exports = (function() { throwMethodUndefined('deleteQuery') }, + /* + Returns a bulk deletion query. + Parameters: + - tableName -> Name of the table + - where -> A hash with conditions (e.g. {name: 'foo'}) + OR an ID as integer + OR a string with conditions (e.g. 'name="foo"'). + If you use a string, you have to escape it on your own. + */ + bulkDeleteQuery: function(tableName, where, options) { + throwMethodUndefined('bulkDeleteQuery') + }, + /* Returns an update query. Parameters: @@ -229,7 +250,22 @@ module.exports = (function() { */ findAutoIncrementField: function(factory) { throwMethodUndefined('findAutoIncrementField') + }, + + /* + Globally enable foreign key constraints + */ + enableForeignKeyConstraintsQuery: function() { + throwMethodUndefined('enableForeignKeyConstraintsQuery') + }, + + /* + Globally disable foreign key constraints + */ + disableForeignKeyConstraintsQuery: function() { + throwMethodUndefined('disableForeignKeyConstraintsQuery') } + } var throwMethodUndefined = function(methodName) { diff --git a/lib/dialects/sqlite/connector-manager.js b/lib/dialects/sqlite/connector-manager.js index 85436f8372bf..f3c6b9b238b2 100644 --- a/lib/dialects/sqlite/connector-manager.js +++ b/lib/dialects/sqlite/connector-manager.js @@ -5,7 +5,13 @@ var Utils = require("../../utils") module.exports = (function() { var ConnectorManager = function(sequelize) { this.sequelize = sequelize - this.database = new sqlite3.Database(sequelize.options.storage || ':memory:') + this.database = db = new sqlite3.Database(sequelize.options.storage || ':memory:', function(err) { + if(!err && sequelize.options.foreignKeys !== false) { + // Make it possible to define and use foreign key constraints unless + // explicitly disallowed. It's still opt-in per relation + db.run('PRAGMA FOREIGN_KEYS=ON') + } + }) } Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype) diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 000fe1de10d0..fe850d29f444 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -125,6 +125,27 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkInsertQuery: function(tableName, attrValueHashes) { + var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;" + , tuples = [] + + Utils._.forEach(attrValueHashes, function(attrValueHash) { + tuples.push("(" + + Utils._.values(attrValueHash).map(function(value){ + return escape((value instanceof Date) ? Utils.toSqlDate(value) : value) + }).join(",") + + ")") + }) + + var replacements = { + table: Utils.addTicks(tableName), + attributes: Object.keys(attrValueHashes[0]).map(function(attr){return Utils.addTicks(attr)}).join(","), + tuples: tuples + } + + return Utils._.template(query)(replacements) + }, + updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) @@ -151,8 +172,7 @@ module.exports = (function() { var query = "DELETE FROM <%= table %> WHERE <%= where %>" var replacements = { table: Utils.addTicks(tableName), - where: this.getWhereConditions(where), - limit: Utils.escape(options.limit) + where: MySqlQueryGenerator.getWhereConditions(where) } return Utils._.template(query)(replacements) @@ -217,6 +237,28 @@ module.exports = (function() { } } + if(dataType.references) { + template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)" + replacements.referencesTable = Utils.addTicks(dataType.references) + + if(dataType.referencesKey) { + replacements.referencesKey = Utils.addTicks(dataType.referencesKey) + } else { + replacements.referencesKey = Utils.addTicks('id') + } + + if(dataType.onDelete) { + template += " ON DELETE <%= onDeleteAction %>" + replacements.onDeleteAction = dataType.onDelete.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE <%= onUpdateAction %>" + replacements.onUpdateAction = dataType.onUpdate.toUpperCase() + } + + } + result[name] = Utils._.template(template)(replacements) } else { result[name] = dataType @@ -242,6 +284,16 @@ module.exports = (function() { return fields }, + enableForeignKeyConstraintsQuery: function() { + var sql = "PRAGMA foreign_keys = ON;" + return Utils._.template(sql, {}) + }, + + disableForeignKeyConstraintsQuery: function() { + var sql = "PRAGMA foreign_keys = OFF;" + return Utils._.template(sql, {}) + }, + hashToWhereConditions: function(hash) { for (var key in hash) { if (hash.hasOwnProperty(key)) { diff --git a/lib/dialects/sqlite/query.js b/lib/dialects/sqlite/query.js index 532182c666bb..f545011e7047 100644 --- a/lib/dialects/sqlite/query.js +++ b/lib/dialects/sqlite/query.js @@ -91,7 +91,10 @@ module.exports = (function() { results = results.map(function(result) { for (var name in result) { if (result.hasOwnProperty(name) && (metaData.columnTypes[name] === 'DATETIME')) { - result[name] = new Date(result[name]+'Z'); // Z means UTC + var val = result[name]; + if(val !== null) { + result[name] = new Date(val+'Z'); // Z means UTC + } } } return result diff --git a/lib/migrator.js b/lib/migrator.js index f9768b85fb5d..5d0c7a69fc4e 100644 --- a/lib/migrator.js +++ b/lib/migrator.js @@ -53,16 +53,26 @@ module.exports = (function() { migrations.reverse() } + if (migrations.length === 0) { + self.options.logging("There are no pending migrations.") + } else { + self.options.logging("Running migrations...") + } migrations.forEach(function(migration) { + var migrationTime chainer.add(migration, 'execute', [options], { before: function(migration) { if (self.options.logging !== false) { - self.options.logging('Executing migration: ' + migration.filename) + self.options.logging(migration.filename) } + migrationTime = process.hrtime() }, after: function(migration) { + migrationTime = process.hrtime(migrationTime) + migrationTime = Math.round( (migrationTime[0] * 1000) + (migrationTime[1] / 1000000)); + if (self.options.logging !== false) { - self.options.logging('Executed migration: ' + migration.filename) + self.options.logging('Completed in ' + migrationTime + 'ms') } }, success: function(migration, callback) { diff --git a/lib/query-interface.js b/lib/query-interface.js index a5361b58efd0..8a8287c5efeb 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -79,8 +79,8 @@ module.exports = (function() { return queryAndEmit.call(this, sql, 'createTable') } - QueryInterface.prototype.dropTable = function(tableName) { - var sql = this.QueryGenerator.dropTableQuery(tableName) + QueryInterface.prototype.dropTable = function(tableName, options) { + var sql = this.QueryGenerator.dropTableQuery(tableName, options) return queryAndEmit.call(this, sql, 'dropTable') } @@ -91,11 +91,17 @@ module.exports = (function() { var chainer = new Utils.QueryChainer() self.showAllTables().success(function(tableNames) { + + chainer.add(self, 'disableForeignKeyConstraints', []) + tableNames.forEach(function(tableName) { - chainer.add(self.dropTable(tableName)) + chainer.add(self, 'dropTable', [tableName, {cascade: true}]) }) + + chainer.add(self, 'enableForeignKeyConstraints', []) + chainer - .run() + .runSerially() .success(function() { self.emit('dropAllTables', null) emitter.emit('success', null) @@ -254,16 +260,31 @@ module.exports = (function() { }) } + QueryInterface.prototype.bulkInsert = function(tableName, records) { + var sql = this.QueryGenerator.bulkInsertQuery(tableName, records) + return queryAndEmit.call(this, sql, 'bulkInsert') + } + QueryInterface.prototype.update = function(dao, tableName, values, identifier) { var sql = this.QueryGenerator.updateQuery(tableName, values, identifier) return queryAndEmit.call(this, [sql, dao], 'update') } + QueryInterface.prototype.bulkUpdate = function(tableName, values, identifier) { + var sql = this.QueryGenerator.updateQuery(tableName, values, identifier) + return queryAndEmit.call(this, sql, 'bulkUpdate') + } + QueryInterface.prototype.delete = function(dao, tableName, identifier) { var sql = this.QueryGenerator.deleteQuery(tableName, identifier) return queryAndEmit.call(this, [sql, dao], 'delete') } + QueryInterface.prototype.bulkDelete = function(tableName, identifier) { + var sql = this.QueryGenerator.deleteQuery(tableName, identifier, {limit: null}) + return queryAndEmit.call(this, sql, 'bulkDelete') + } + QueryInterface.prototype.select = function(factory, tableName, options, queryOptions) { options = options || {} @@ -296,6 +317,10 @@ module.exports = (function() { result = parseInt(result) } + if (options && options.parseFloat) { + result = parseFloat(result) + } + self.emit('rawSelect', null) emitter.emit('success', result) }) @@ -309,6 +334,30 @@ module.exports = (function() { }).run() } + QueryInterface.prototype.enableForeignKeyConstraints = function() { + var sql = this.QueryGenerator.enableForeignKeyConstraintsQuery() + if(sql) { + return queryAndEmit.call(this, sql, 'enableForeignKeyConstraints') + } else { + return new Utils.CustomEventEmitter(function(emitter) { + this.emit('enableForeignKeyConstraints', null) + emitter.emit('success') + }).run() + } + } + + QueryInterface.prototype.disableForeignKeyConstraints = function() { + var sql = this.QueryGenerator.disableForeignKeyConstraintsQuery() + if(sql){ + return queryAndEmit.call(this, sql, 'disableForeignKeyConstraints') + } else { + return new Utils.CustomEventEmitter(function(emitter) { + this.emit('disableForeignKeyConstraints', null) + emitter.emit('success') + }).run() + } + } + // private var queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) { diff --git a/lib/sequelize.js b/lib/sequelize.js index 6918a6830c42..f4efec7bedf4 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -151,6 +151,16 @@ module.exports = (function() { options = options || {} var globalOptions = this.options + // If you don't specify a valid data type lets help you debug it + Utils._.each(attributes, function(dataType, name){ + if (Utils.isHash(dataType)) { + dataType = dataType.type + } + if (dataType === undefined) { + throw new Error('Unrecognized data type for field '+ name) + } + }) + if (globalOptions.define) { options = Utils._.extend({}, globalOptions.define, options) Utils._(['classMethods', 'instanceMethods']).each(function(key) { @@ -192,7 +202,7 @@ module.exports = (function() { Sequelize.prototype.query = function(sql, callee, options, replacements) { if (arguments.length === 4) { - sql = Utils.format([sql].concat(replacements)) + sql = Utils.format([sql].concat(replacements), this.options.dialect) } else if (arguments.length === 3) { options = options } else if (arguments.length === 2) { @@ -251,11 +261,14 @@ module.exports = (function() { var chainer = new Utils.QueryChainer() - this.daoFactoryManager.daos.forEach(function(dao) { - chainer.add(dao.sync(options)) + // Topologically sort by foreign key constraints to give us an appropriate + // creation order + + this.daoFactoryManager.forEachDAO(function(dao) { + chainer.add(dao, 'sync', [options]) }) - return chainer.run() + return chainer.runSerially() } Sequelize.prototype.drop = function() { diff --git a/lib/sql-string.js b/lib/sql-string.js index 5dfa3bebb1a5..96be2c7e6ae4 100644 --- a/lib/sql-string.js +++ b/lib/sql-string.js @@ -7,7 +7,7 @@ SqlString.escapeId = function (val, forbidQualified) { return '`' + val.replace(/`/g, '``').replace(/\./g, '`.`') + '`'; }; -SqlString.escape = function(val, stringifyObjects, timeZone) { +SqlString.escape = function(val, stringifyObjects, timeZone, dialect) { if (val === undefined || val === null) { return 'NULL'; } @@ -37,17 +37,22 @@ SqlString.escape = function(val, stringifyObjects, timeZone) { } } - val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { - switch(s) { - case "\0": return "\\0"; - case "\n": return "\\n"; - case "\r": return "\\r"; - case "\b": return "\\b"; - case "\t": return "\\t"; - case "\x1a": return "\\Z"; - default: return "\\"+s; - } - }); + if (dialect == "postgres") { + // http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS + val = val.replace(/'/g, "''"); + } else { + val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { + switch(s) { + case "\0": return "\\0"; + case "\n": return "\\n"; + case "\r": return "\\r"; + case "\b": return "\\b"; + case "\t": return "\\t"; + case "\x1a": return "\\Z"; + default: return "\\"+s; + } + }); + } return "'"+val+"'"; }; @@ -58,7 +63,7 @@ SqlString.arrayToList = function(array, timeZone) { }).join(', '); }; -SqlString.format = function(sql, values, timeZone) { +SqlString.format = function(sql, values, timeZone, dialect) { values = [].concat(values); return sql.replace(/\?/g, function(match) { @@ -66,7 +71,7 @@ SqlString.format = function(sql, values, timeZone) { return match; } - return SqlString.escape(values.shift(), false, timeZone); + return SqlString.escape(values.shift(), false, timeZone, dialect); }); }; diff --git a/lib/utils.js b/lib/utils.js index 72acf9ee23b8..73adbd260233 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,7 @@ var util = require("util") var Utils = module.exports = { _: (function() { - var _ = require("underscore") + var _ = require("lodash") , _s = require('underscore.string') _.mixin(_s.exports()) @@ -47,8 +47,9 @@ var Utils = module.exports = { escape: function(s) { return SqlString.escape(s, true, "local").replace(/\\"/g, '"') }, - format: function(arr) { - return SqlString.format(arr.shift(), arr) + format: function(arr, dialect) { + var timeZone = null; + return SqlString.format(arr.shift(), arr, timeZone, dialect) }, isHash: function(obj) { return Utils._.isObject(obj) && !Array.isArray(obj); diff --git a/package.json b/package.json index b30589be96bd..05b5142f74a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sequelize", "description": "Multi dialect ORM for Node.JS", - "version": "1.6.0", + "version": "1.7.0-alpha1", "author": "Sascha Depold ", "contributors": [ { @@ -23,20 +23,21 @@ ], "repository": { "type": "git", - "url": "https://github.com/sdepold/sequelize.git" + "url": "https://github.com/sequelize/sequelize.git" }, "bugs": { - "url": "https://github.com/sdepold/sequelize/issues" + "url": "https://github.com/sequelize/sequelize/issues" }, "dependencies": { - "underscore": "~1.4.0", + "lodash": "~1.2.1", "underscore.string": "~2.3.0", "lingo": "~0.0.5", - "validator": "0.4.x", + "validator": "1.1.1", "moment": "~1.7.0", "commander": "~0.6.0", "generic-pool": "1.0.9", - "dottie": "0.0.6-1" + "dottie": "0.0.6-1", + "toposort-class": "0.1.4" }, "devDependencies": { "jasmine-node": "1.5.0", diff --git a/spec-jasmine/dao-factory.spec.js b/spec-jasmine/dao-factory.spec.js index 71ec088cb4c2..27342e9e1ee5 100644 --- a/spec-jasmine/dao-factory.spec.js +++ b/spec-jasmine/dao-factory.spec.js @@ -90,25 +90,6 @@ describe('DAOFactory', function() { }) }) - it('marks the database entry as deleted if dao is paranoid', function() { - Helpers.async(function(done) { - User = sequelize.define('User', { - name: Sequelize.STRING, bio: Sequelize.TEXT - }, { paranoid:true }) - User.sync({ force: true }).success(done) - }) - - Helpers.async(function(done) { - User.create({ name: 'asd', bio: 'asd' }).success(function(u) { - expect(u.deletedAt).toBeNull() - u.destroy().success(function(u) { - expect(u.deletedAt).toBeTruthy() - done() - }) - }) - }) - }) - it('allows sql logging of update statements', function() { Helpers.async(function(done) { User = sequelize.define('User', { diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index a2a4e78580e9..58387185414b 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -10,6 +10,62 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + attributesToSQL: [ + { + arguments: [{id: 'INTEGER'}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}], + expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'} + }, + { + arguments: [{id: {type: 'INTEGER'}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false}}], + expectation: {id: 'INTEGER NOT NULL'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: true}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}], + expectation: {id: 'INTEGER auto_increment PRIMARY KEY'} + }, + { + arguments: [{id: {type: 'INTEGER', defaultValue: 0}}], + expectation: {id: 'INTEGER DEFAULT 0'} + }, + { + arguments: [{id: {type: 'INTEGER', unique: true}}], + expectation: {id: 'INTEGER UNIQUE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER NOT NULL auto_increment DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'} + }, + ], + createTableQuery: [ { arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], @@ -26,6 +82,14 @@ describe('QueryGenerator', function() { { arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}, {charset: 'latin1'}], expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255)) ENGINE=InnoDB DEFAULT CHARSET=latin1;" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER , PRIMARY KEY (`id`)) ENGINE=InnoDB;" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER, FOREIGN KEY (`otherId`) REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE=InnoDB;" } ], @@ -133,6 +197,46 @@ describe('QueryGenerator', function() { arguments: ['myTable', {name: 'foo', foo: 1, nullValue: undefined}], expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);", context: {options: {omitNull: true}} + }, { + arguments: ['myTable', {foo: false}], + expectation: "INSERT INTO `myTable` (`foo`) VALUES (0);" + }, { + arguments: ['myTable', {foo: true}], + expectation: "INSERT INTO `myTable` (`foo`) VALUES (1);" + } + ], + + bulkInsertQuery: [ + { + arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]], + expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo'),('bar');" + }, { + arguments: ['myTable', [{name: "foo';DROP TABLE myTable;"}, {name: 'bar'}]], + expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo\\';DROP TABLE myTable;'),('bar');" + }, { + arguments: ['myTable', [{name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {name: 'bar', birthday: new Date(Date.UTC(2012, 2, 27, 10, 1, 55))}]], + expectation: "INSERT INTO `myTable` (`name`,`birthday`) VALUES ('foo','2011-03-27 10:01:55'),('bar','2012-03-27 10:01:55');" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1}, {name: 'bar', foo: 2}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1),('bar',2);" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',NULL);" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: false}} + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: undefined}, {name: 'bar', foo: 2, undefinedValue: undefined}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: true}} // Note: As above + }, { + arguments: ['myTable', [{name: "foo", value: true}, {name: 'bar', value: false}]], + expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',1),('bar',0);" } ], @@ -160,6 +264,12 @@ describe('QueryGenerator', function() { arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}], expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'", context: {options: {omitNull: true}} + }, { + arguments: ['myTable', {bar: false}, {name: 'foo'}], + expectation: "UPDATE `myTable` SET `bar`=0 WHERE `name`='foo'" + }, { + arguments: ['myTable', {bar: true}, {name: 'foo'}], + expectation: "UPDATE `myTable` SET `bar`=1 WHERE `name`='foo'" } ], @@ -176,6 +286,9 @@ describe('QueryGenerator', function() { }, { arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}], expectation: "DELETE FROM `myTable` WHERE `name`='foo\\';DROP TABLE myTable;' LIMIT 10" + }, { + arguments: ['myTable', {name: 'foo'}, {limit: null}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo'" } ], @@ -227,6 +340,27 @@ describe('QueryGenerator', function() { { arguments: [{ id: [] }], expectation: "`id` IN (NULL)" + }, + { + arguments: [{ maple: false, bacon: true }], + expectation: "`maple`=0 AND `bacon`=1" + }, + { + arguments: [{ beaver: [false, true] }], + expectation: "`beaver` IN (0,1)" + }, + { + arguments: [{birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55))}], + expectation: "`birthday`='2011-07-01 10:01:55'" + }, + { + arguments: [{ birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55)), + otherday: new Date(Date.UTC(2013, 6, 2, 10, 1, 22)) }], + expectation: "`birthday`='2011-07-01 10:01:55' AND `otherday`='2013-07-02 10:01:22'" + }, + { + arguments: [{ birthday: [new Date(Date.UTC(2011, 6, 1, 10, 1, 55)), new Date(Date.UTC(2013, 6, 2, 10, 1, 22))] }], + expectation: "`birthday` IN ('2011-07-01 10:01:55','2013-07-02 10:01:22')" } ] } diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index e8e0598113e4..606ea19dca80 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -14,6 +14,62 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + attributesToSQL: [ + { + arguments: [{id: 'INTEGER'}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}], + expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'} + }, + { + arguments: [{id: {type: 'INTEGER'}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false}}], + expectation: {id: 'INTEGER NOT NULL'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: true}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}], + expectation: {id: 'INTEGER SERIAL PRIMARY KEY'} + }, + { + arguments: [{id: {type: 'INTEGER', defaultValue: 0}}], + expectation: {id: 'INTEGER DEFAULT 0'} + }, + { + arguments: [{id: {type: 'INTEGER', unique: true}}], + expectation: {id: 'INTEGER UNIQUE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar'}}], + expectation: {id: 'INTEGER REFERENCES "Bar" ("id")'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], + expectation: {id: 'INTEGER REFERENCES "Bar" ("pk")'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}], + expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT'} + }, + ], + createTableQuery: [ { arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], @@ -26,6 +82,14 @@ describe('QueryGenerator', function() { { arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}], expectation: "DROP TYPE IF EXISTS \"enum_myTable_title\"; CREATE TYPE \"enum_myTable_title\" AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}], + expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"id\" INTEGER , PRIMARY KEY (\"id\"));" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES "otherTable" ("id") ON DELETE CASCADE ON UPDATE NO ACTION'}], + expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"otherId\" INTEGER REFERENCES \"otherTable\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION);" } ], @@ -37,6 +101,14 @@ describe('QueryGenerator', function() { { arguments: ['mySchema.myTable'], expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";" + }, + { + arguments: ['myTable', {cascade: true}], + expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;" + }, + { + arguments: ['mySchema.myTable', {cascade: true}], + expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\" CASCADE;" } ], @@ -136,6 +208,46 @@ describe('QueryGenerator', function() { } ], + bulkInsertQuery: [ + { + arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]], + expectation: "INSERT INTO \"myTable\" (\"name\") VALUES ('foo'),('bar') RETURNING *;" + }, { + arguments: ['myTable', [{name: "foo';DROP TABLE myTable;"}, {name: 'bar'}]], + expectation: "INSERT INTO \"myTable\" (\"name\") VALUES ('foo'';DROP TABLE myTable;'),('bar') RETURNING *;" + }, { + arguments: ['myTable', [{name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {name: 'bar', birthday: new Date(Date.UTC(2012, 2, 27, 10, 1, 55))}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"birthday\") VALUES ('foo','2011-03-27 10:01:55.0Z'),('bar','2012-03-27 10:01:55.0Z') RETURNING *;" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1}, {name: 'bar', foo: 2}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"foo\") VALUES ('foo',1),('bar',2) RETURNING *;" + }, { + arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;" + }, { + arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;", + context: {options: {omitNull: false}} + }, { + arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;", + context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not + }, { + arguments: ['myTable', [{name: 'foo', nullValue: undefined}, {name: 'bar', nullValue: undefined}]], + expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;", + context: {options: {omitNull: true}} // Note: As above + }, { + arguments: ['mySchema.myTable', [{name: 'foo'}, {name: 'bar'}]], + expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'),('bar') RETURNING *;" + }, { + arguments: ['mySchema.myTable', [{name: JSON.stringify({info: 'Look ma a " quote'})}, {name: JSON.stringify({info: 'Look ma another " quote'})}]], + expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('{\"info\":\"Look ma a \\\" quote\"}'),('{\"info\":\"Look ma another \\\" quote\"}') RETURNING *;" + }, { + arguments: ['mySchema.myTable', [{name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'bar'}]], + expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'';DROP TABLE mySchema.myTable;'),('bar') RETURNING *;" + } + ], + updateQuery: [ { arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {id: 2}], @@ -192,6 +304,9 @@ describe('QueryGenerator', function() { }, { arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {limit: 10}], expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;' LIMIT 10)" + }, { + arguments: ['myTable', {name: 'foo'}, {limit: null}], + expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo')" } ], diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 40fce4870cde..8dd60a4081d9 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -9,6 +9,81 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + attributesToSQL: [ + { + arguments: [{id: 'INTEGER'}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}], + expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'} + }, + { + arguments: [{id: {type: 'INTEGER'}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false}}], + expectation: {id: 'INTEGER NOT NULL'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: true}}], + expectation: {id: 'INTEGER'} + }, + { + arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}], + expectation: {id: 'INTEGER PRIMARY KEY AUTOINCREMENT'} + }, + { + arguments: [{id: {type: 'INTEGER', defaultValue: 0}}], + expectation: {id: 'INTEGER DEFAULT 0'} + }, + { + arguments: [{id: {type: 'INTEGER', unique: true}}], + expectation: {id: 'INTEGER UNIQUE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'} + }, + { + arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'} + }, + { + arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}], + expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'} + }, + ], + + createTableQuery: [ + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));" + }, + { + arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255));" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER PRIMARY KEY);" + }, + { + arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION);" + } + ], + insertQuery: [ { arguments: ['myTable', { name: 'foo' }], @@ -46,6 +121,43 @@ describe('QueryGenerator', function() { } ], + bulkInsertQuery: [ + { + arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]], + expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo'),('bar');" + }, { + arguments: ['myTable', [{name: "'bar'"}, {name: 'foo'}]], + expectation: "INSERT INTO `myTable` (`name`) VALUES ('''bar'''),('foo');" + }, { + arguments: ['myTable', [{name: "bar", value: null}, {name: 'foo', value: 1}]], + expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL),('foo',1);" + }, { + arguments: ['myTable', [{name: "bar", value: undefined}, {name: 'bar', value: 2}]], + expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL),('bar',2);" + }, { + arguments: ['myTable', [{name: "foo", value: true}, {name: 'bar', value: false}]], + expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',1),('bar',0);" + }, { + arguments: ['myTable', [{name: "foo", value: false}, {name: 'bar', value: false}]], + expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',0),('bar',0);" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);" + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: false}} + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not + }, { + arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]], + expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);", + context: {options: {omitNull: true}} // Note: As above + } + ], + updateQuery: [ { arguments: ['myTable', { name: 'foo' }, { id: 2 }], @@ -77,6 +189,25 @@ describe('QueryGenerator', function() { expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'", context: {options: {omitNull: true}} } + ], + + deleteQuery: [ + { + arguments: ['myTable', {name: 'foo'}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo'" + }, { + arguments: ['myTable', 1], + expectation: "DELETE FROM `myTable` WHERE `id`=1" + }, { + arguments: ['myTable', 1, {limit: 10}], + expectation: "DELETE FROM `myTable` WHERE `id`=1" + }, { + arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo\\';DROP TABLE myTable;'" + }, { + arguments: ['myTable', {name: 'foo'}, {limit: null}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo'" + } ] }; diff --git a/spec/associations/belongs-to.spec.js b/spec/associations/belongs-to.spec.js index 932396b63fce..40c8c11f017b 100644 --- a/spec/associations/belongs-to.spec.js +++ b/spec/associations/belongs-to.spec.js @@ -47,4 +47,136 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() { }) }) }) + + describe("Foreign key constraints", function() { + + it("are not enabled by default", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + Task.belongsTo(User) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + task.setUser(user).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + Task.belongsTo(User, {onDelete: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + task.setUser(user).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(0) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + Task.belongsTo(User, {onDelete: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + task.setUser(user).success(function() { + user.destroy().error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + Task.belongsTo(User, {onUpdate: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + task.setUser(user).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + expect(tasks[0].UserId).toEqual(999) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + Task.belongsTo(User, {onUpdate: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + task.setUser(user).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + }) + }) diff --git a/spec/associations/has-many.spec.js b/spec/associations/has-many.spec.js index f298f0c9ce1f..d8fc58575711 100644 --- a/spec/associations/has-many.spec.js +++ b/spec/associations/has-many.spec.js @@ -288,7 +288,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { var add = this.spy() - this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, run: function(){} }) + this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, runSerially: function(){} }) this.sequelize.sync({ force: true }) expect(add).toHaveBeenCalledThrice() @@ -323,4 +323,135 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { }) }) }) + + describe("Foreign key constraints", function() { + + it("are not enabled by default", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasMany(Task) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTasks([task]).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasMany(Task, {onDelete: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTasks([task]).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(0) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasMany(Task, {onDelete: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTasks([task]).success(function() { + user.destroy().error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasMany(Task, {onUpdate: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTasks([task]).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + expect(tasks[0].UserId).toEqual(999) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasMany(Task, {onUpdate: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTasks([task]).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + }) }) diff --git a/spec/associations/has-one.spec.js b/spec/associations/has-one.spec.js index 0791c637881b..e00a7432d153 100644 --- a/spec/associations/has-one.spec.js +++ b/spec/associations/has-one.spec.js @@ -47,4 +47,136 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() { }) }) }) + + describe("Foreign key constraints", function() { + + it("are not enabled by default", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasOne(Task) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTask(task).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasOne(Task, {onDelete: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTask(task).success(function() { + user.destroy().success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(0) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict deletes", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasOne(Task, {onDelete: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTask(task).success(function() { + user.destroy().error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can cascade updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasOne(Task, {onUpdate: 'cascade'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTask(task).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .success(function() { + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + expect(tasks[0].UserId).toEqual(999) + done() + }) + }) + }) + }) + }) + }) + }) + + it("can restrict updates", function(done) { + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) + + User.hasOne(Task, {onUpdate: 'restrict'}) + + this.sequelize.sync({ force: true }).success(function() { + User.create({ username: 'foo' }).success(function(user) { + Task.create({ title: 'task' }).success(function(task) { + user.setTask(task).success(function() { + + // Changing the id of a DAO requires a little dance since + // the `UPDATE` query generated by `save()` uses `id` in the + // `WHERE` clause + + var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) + user.QueryInterface.update(user, tableName, {id: 999}, user.id) + .error(function() { + // Should fail due to FK restriction + Task.findAll().success(function(tasks) { + expect(tasks.length).toEqual(1) + done() + }) + }) + }) + }) + }) + }) + }) + + }) + }) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index ff6e7c635d0d..fd7b4da00f4b 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -2,7 +2,7 @@ if(typeof require === 'function') { const buster = require("buster") , Sequelize = require("../index") , Helpers = require('./buster-helpers') - , _ = require('underscore') + , _ = require('lodash') , dialect = Helpers.getTestDialect() } @@ -216,6 +216,12 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { activity_date: Sequelize.DATe }) }.bind(this), 'Unrecognized data type for field activity_date') + + Helpers.assertException(function() { + this.sequelize.define('UserBadDataType', { + activity_date: {type: Sequelize.DATe} + }) + }.bind(this), 'Unrecognized data type for field activity_date') }) it('sets a 64 bit int in bigint', function(done) { @@ -381,6 +387,276 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) }) + describe('bulkCreate', function() { + + it('inserts multiple values respecting the white list', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '23'}] + + this.User.bulkCreate(data, ['username']).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(users[0].secretValue).toBeNull(); + + expect(users[1].username).toEqual("Paul") + expect(users[1].secretValue).toBeNull(); + + done() + }) + }) + }) + + it('should store all values if no whitelist is specified', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '23'}] + + this.User.bulkCreate(data).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(users[0].secretValue).toEqual('42') + + expect(users[1].username).toEqual("Paul") + expect(users[1].secretValue).toEqual('23') + + done() + }) + }) + }) + + it('saves data with single quote', function(done) { + var self = this + , quote = "Single'Quote" + , data = [{ username: 'Peter', data: quote}, + { username: 'Paul', data: quote}] + + this.User.bulkCreate(data).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(users[0].data).toEqual(quote) + + expect(users[1].username).toEqual("Paul") + expect(users[1].data).toEqual(quote) + + done() + }) + }) + }) + + it('saves data with double quote', function(done) { + var self = this + , quote = 'Double"Quote' + , data = [{ username: 'Peter', data: quote}, + { username: 'Paul', data: quote}] + + this.User.bulkCreate(data).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(users[0].data).toEqual(quote) + + expect(users[1].username).toEqual("Paul") + expect(users[1].data).toEqual(quote) + + done() + }) + }) + }) + + it('saves stringified JSON data', function(done) { + var self = this + , json = JSON.stringify({ key: 'value' }) + , data = [{ username: 'Peter', data: json}, + { username: 'Paul', data: json}] + + this.User.bulkCreate(data).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(users[0].data).toEqual(json) + + expect(users[1].username).toEqual("Paul") + expect(users[1].data).toEqual(json) + + done() + }) + }) + }) + + it('stores the current date in createdAt', function(done) { + var self = this + , data = [{ username: 'Peter'}, + { username: 'Paul'}] + + this.User.bulkCreate(data).success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(2) + + expect(users[0].username).toEqual("Peter") + expect(parseInt(+users[0].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) + + expect(users[1].username).toEqual("Paul") + expect(parseInt(+users[1].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) + + done() + }) + }) + }) + + describe('enums', function() { + before(function(done) { + this.Item = this.sequelize.define('Item', { + state: { type: Helpers.Sequelize.ENUM, values: ['available', 'in_cart', 'shipped'] }, + name: Sequelize.STRING + }) + + this.sequelize.sync({ force: true }).success(function() { + this.Item.bulkCreate([{state: 'in_cart', name: 'A'}, { state: 'available', name: 'B'}]).success(function() { + done() + }.bind(this)) + }.bind(this)) + }) + + it('correctly restores enum values', function(done) { + this.Item.find({ where: { state: 'available' }}).success(function(item) { + expect(item.name).toEqual('B') + done() + }.bind(this)) + }) + }) + + }) // - bulkCreate + + describe('update', function() { + + it('updates only values that match filter', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '42' }, + { username: 'Bob', secretValue: '43' }] + + this.User.bulkCreate(data).success(function() { + + self.User.update({username: 'Bill'}, {secretValue: '42'}) + .success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(3) + + users.forEach(function (user) { + if (user.secretValue == '42') { + expect(user.username).toEqual("Bill") + } else { + expect(user.username).toEqual("Bob") + } + }) + + done() + }) + }) + }) + }) + + it('sets updatedAt to the current timestamp', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '42' }, + { username: 'Bob', secretValue: '43' }] + + this.User.bulkCreate(data).success(function() { + + self.User.update({username: 'Bill'}, {secretValue: '42'}) + .success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(3) + + expect(users[0].username).toEqual("Bill") + expect(users[1].username).toEqual("Bill") + expect(users[2].username).toEqual("Bob") + + expect(parseInt(+users[0].updatedAt/5000)).toEqual(parseInt(+new Date()/5000)) + expect(parseInt(+users[1].updatedAt/5000)).toEqual(parseInt(+new Date()/5000)) + + done() + }) + }) + }) + }) + + }) // - update + + describe('destroy', function() { + + it('deletes values that match filter', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '42' }, + { username: 'Bob', secretValue: '43' }] + + this.User.bulkCreate(data).success(function() { + + self.User.destroy({secretValue: '42'}) + .success(function() { + self.User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(1) + + expect(users[0].username).toEqual("Bob") + + done() + }) + }) + }) + }) + + it('sets deletedAt to the current timestamp if paranoid is true', function(done) { + + var self = this + , User = this.sequelize.define('ParanoidUser', { + username: Sequelize.STRING, + secretValue: Sequelize.STRING, + data: Sequelize.STRING + }, { + paranoid: true + }) + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '42' }, + { username: 'Bob', secretValue: '43' }] + + User.sync({ force: true }).success(function() { + + User.bulkCreate(data).success(function() { + + User.destroy({secretValue: '42'}) + .success(function() { + User.findAll({order: 'id'}).success(function(users) { + expect(users.length).toEqual(3) + + expect(users[0].username).toEqual("Peter") + expect(users[1].username).toEqual("Paul") + expect(users[2].username).toEqual("Bob") + + expect(parseInt(+users[0].deletedAt/5000)).toEqual(parseInt(+new Date()/5000)) + expect(parseInt(+users[1].deletedAt/5000)).toEqual(parseInt(+new Date()/5000)) + + done() + }) + }) + }) + + }) + + }) + + }) // - destroy + describe('find', function find() { before(function(done) { this.User.create({ @@ -784,6 +1060,42 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) }) }) + + describe('queryOptions', function() { + before(function(done) { + this.User.create({ + username: 'barfooz' + }).success(function(user) { + this.user = user + done() + }.bind(this)) + }) + + it("should return a DAO when queryOptions are not set", function (done) { + this.User.find({ where: { username: 'barfooz'}}).done(function (err, user) { + expect(user).toHavePrototype(this.User.DAO.prototype) + + done(); + }.bind(this)) + }) + + it("should return a DAO when raw is false", function (done) { + this.User.find({ where: { username: 'barfooz'}}, { raw: false }).done(function (err, user) { + expect(user).toHavePrototype(this.User.DAO.prototype) + + done(); + }.bind(this)) + }) + + it("should return raw data when raw is true", function (done) { + this.User.find({ where: { username: 'barfooz'}}, { raw: true }).done(function (err, user) { + expect(user).not.toHavePrototype(this.User.DAO.prototype) + expect(user).toBeObject() + + done(); + }.bind(this)) + }) + }) // - describe: queryOptions }) //- describe: find describe('findAll', function findAll() { @@ -1016,6 +1328,49 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }.bind(this)) }) }) + + describe('queryOptions', function() { + before(function(done) { + this.User.create({ + username: 'barfooz' + }).success(function(user) { + this.user = user + done() + }.bind(this)) + }) + + it("should return a DAO when queryOptions are not set", function (done) { + this.User.findAll({ where: { username: 'barfooz'}}).done(function (err, users) { + users.forEach(function (user) { + expect(user).toHavePrototype(this.User.DAO.prototype) + }, this) + + + done(); + }.bind(this)) + }) + + it("should return a DAO when raw is false", function (done) { + this.User.findAll({ where: { username: 'barfooz'}}, { raw: false }).done(function (err, users) { + users.forEach(function (user) { + expect(user).toHavePrototype(this.User.DAO.prototype) + }, this) + + done(); + }.bind(this)) + }) + + it("should return raw data when raw is true", function (done) { + this.User.findAll({ where: { username: 'barfooz'}}, { raw: true }).done(function (err, users) { + users.forEach(function (user) { + expect(user).not.toHavePrototype(this.User.DAO.prototype) + expect(users[0]).toBeObject() + }, this) + + done(); + }.bind(this)) + }) + }) // - describe: queryOptions }) }) //- describe: findAll @@ -1025,7 +1380,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { age: Sequelize.INTEGER }) - this.UserWithAge.sync({ force: true }).success(done) + this.UserWithDec = this.sequelize.define('UserWithDec', { + value: Sequelize.DECIMAL(10, 3) + }) + + this.UserWithAge.sync({ force: true }).success(function(){ + this.UserWithDec.sync({ force: true }).success(done) + }.bind(this)) }) it("should return the min value", function(done) { @@ -1046,6 +1407,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { done() }) }) + + it("should allow decimals in min", function(done){ + this.UserWithDec.create({value: 3.5}).success(function(){ + this.UserWithDec.create({ value: 5.5 }).success(function(){ + this.UserWithDec.min('value').success(function(min){ + expect(min).toEqual(3.5) + done() + }) + }.bind(this)) + }.bind(this)) + }) }) //- describe: min describe('max', function() { @@ -1054,7 +1426,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { age: Sequelize.INTEGER }) - this.UserWithAge.sync({ force: true }).success(done) + this.UserWithDec = this.sequelize.define('UserWithDec', { + value: Sequelize.DECIMAL(10, 3) + }) + + this.UserWithAge.sync({ force: true }).success(function(){ + this.UserWithDec.sync({ force: true }).success(done) + }.bind(this)) }) it("should return the max value", function(done) { @@ -1068,6 +1446,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }.bind(this)) }) + it("should allow decimals in max", function(done){ + this.UserWithDec.create({value: 3.5}).success(function(){ + this.UserWithDec.create({ value: 5.5 }).success(function(){ + this.UserWithDec.max('value').success(function(max){ + expect(max).toEqual(5.5) + done() + }) + }.bind(this)) + }.bind(this)) + }) + it('allows sql logging', function(done) { this.UserWithAge.max('age').on('sql', function(sql) { expect(sql).toBeDefined() diff --git a/spec/dao.spec.js b/spec/dao.spec.js index be61512c4591..b86d46c903ac 100644 --- a/spec/dao.spec.js +++ b/spec/dao.spec.js @@ -2,7 +2,7 @@ if (typeof require === 'function') { const buster = require("buster") , Helpers = require('./buster-helpers') , dialect = Helpers.getTestDialect() - , _ = require('underscore') + , _ = require('lodash') } buster.spec.expose() @@ -19,7 +19,23 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { username: { type: DataTypes.STRING }, touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, aNumber: { type: DataTypes.INTEGER }, - bNumber: { type: DataTypes.INTEGER } + bNumber: { type: DataTypes.INTEGER }, + + validateTest: { + type: DataTypes.INTEGER, + allowNull: true, + validate: {isInt: true} + }, + validateCustom: { + type: DataTypes.STRING, + allowNull: true, + validate: {len: {msg: 'Length failed.', args: [1,20]}} + }, + + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + } }) self.HistoryLog = sequelize.define('HistoryLog', { @@ -27,10 +43,20 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { aNumber: { type: DataTypes.INTEGER }, aRandomId: { type: DataTypes.INTEGER } }) + + self.ParanoidUser = sequelize.define('ParanoidUser', { + username: { type: DataTypes.STRING } + }, { + paranoid: true + }) + + self.ParanoidUser.hasOne( self.ParanoidUser ) }, onComplete: function() { self.User.sync({ force: true }).success(function(){ - self.HistoryLog.sync({ force: true }).success(done) + self.HistoryLog.sync({ force: true }).success(function(){ + self.ParanoidUser.sync({force: true }).success(done) + }) }) } }) @@ -320,6 +346,29 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { expect(+user.touchedAt).toBe(5000) }) }) + + describe('allowNull date', function() { + it('should be just "null" and not Date with Invalid Date', function(done) { + var self = this; + this.User.build({ username: 'a user'}).save().success(function() { + self.User.find({where: {username: 'a user'}}).success(function(user) { + expect(user.dateAllowNullTrue).toBe(null) + done() + }) + }) + }) + + it('should be the same valid date when saving the date', function(done) { + var self = this; + var date = new Date(); + this.User.build({ username: 'a user', dateAllowNullTrue: date}).save().success(function() { + self.User.find({where: {username: 'a user'}}).success(function(user) { + expect(user.dateAllowNullTrue.toString()).toEqual(date.toString()) + done() + }) + }) + }) + }) }) describe('complete', function() { @@ -341,6 +390,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { }) describe('save', function() { + it('should fail a validation upon creating', function(done){ + this.User.create({aNumber: 0, validateTest: 'hello'}).error(function(err){ + expect(err).toBeDefined() + expect(err).toBeObject() + expect(err.validateTest).toBeArray() + expect(err.validateTest[0]).toBeDefined() + expect(err.validateTest[0].indexOf('Invalid integer')).toBeGreaterThan(-1); + done(); + }); + }) + + it('should fail a validation upon building', function(done){ + this.User.build({aNumber: 0, validateCustom: 'aaaaaaaaaaaaaaaaaaaaaaaaaa'}).save() + .error(function(err){ + expect(err).toBeDefined() + expect(err).toBeObject() + expect(err.validateCustom).toBeDefined() + expect(err.validateCustom).toBeArray() + expect(err.validateCustom[0]).toBeDefined() + expect(err.validateCustom[0]).toEqual('Length failed.') + done() + }) + }) + + it('should fail a validation when updating', function(done){ + this.User.create({aNumber: 0}).success(function(user){ + user.updateAttributes({validateTest: 'hello'}).error(function(err){ + expect(err).toBeDefined() + expect(err).toBeObject() + expect(err.validateTest).toBeDefined() + expect(err.validateTest).toBeArray() + expect(err.validateTest[0]).toBeDefined() + expect(err.validateTest[0].indexOf('Invalid integer:')).toBeGreaterThan(-1) + done() + }) + }) + }) + it('takes zero into account', function(done) { this.User.build({ aNumber: 0 }).save([ 'aNumber' ]).success(function(user) { expect(user.aNumber).toEqual(0) @@ -467,6 +554,51 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { }.bind(this)) }) + it("creates the deletedAt property, when defining paranoid as true", function(done) { + this.ParanoidUser.create({ username: 'fnord' }).success(function() { + this.ParanoidUser.findAll().success(function(users) { + expect(users[0].deletedAt).toBeDefined() + expect(users[0].deletedAt).toBe(null) + done() + }.bind(this)) + }.bind(this)) + }) + + it("sets deletedAt property to a specific date when deleting an instance", function(done) { + this.ParanoidUser.create({ username: 'fnord' }).success(function() { + this.ParanoidUser.findAll().success(function(users) { + users[0].destroy().success(function(user) { + expect(user.deletedAt.getMonth).toBeDefined() + done() + }.bind(this)) + }.bind(this)) + }.bind(this)) + }) + + it("keeps the deletedAt-attribute with value null, when running updateAttributes", function(done) { + this.ParanoidUser.create({ username: 'fnord' }).success(function() { + this.ParanoidUser.findAll().success(function(users) { + users[0].updateAttributes({username: 'newFnord'}).success(function(user) { + expect(user.deletedAt).toBe(null) + done() + }.bind(this)) + }.bind(this)) + }.bind(this)) + }) + + it("keeps the deletedAt-attribute with value null, when updating associations", function(done) { + this.ParanoidUser.create({ username: 'fnord' }).success(function() { + this.ParanoidUser.findAll().success(function(users) { + this.ParanoidUser.create({ username: 'linkedFnord' }).success(function( linkedUser ) { + users[0].setParanoidUser( linkedUser ).success(function(user) { + expect(user.deletedAt).toBe(null) + done() + }.bind(this)) + }.bind(this)) + }.bind(this)) + }.bind(this)) + }) + it("can reuse query option objects", function(done) { this.User.create({ username: 'fnord' }).success(function() { var query = { where: { username: 'fnord' }} @@ -512,4 +644,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { }.bind(this)) }) }) + + describe('updateAttributes', function() { + it('stores and restores null values', function(done) { + var Download = this.sequelize.define('download', { + startedAt: Helpers.Sequelize.DATE, + canceledAt: Helpers.Sequelize.DATE, + finishedAt: Helpers.Sequelize.DATE + }) + + Download.sync({ force: true }).success(function() { + Download.create({ + startedAt: new Date() + }).success(function(download) { + expect(download.startedAt instanceof Date).toBeTrue() + expect(download.canceledAt).toBeFalsy() + expect(download.finishedAt).toBeFalsy() + + download.updateAttributes({ + canceledAt: new Date() + }).success(function(download) { + expect(download.startedAt instanceof Date).toBeTrue() + expect(download.canceledAt instanceof Date).toBeTrue() + expect(download.finishedAt).toBeFalsy() + + Download.all({ + where: (dialect === 'postgres' ? '"finishedAt" IS NULL' : "`finishedAt` IS NULL") + }).success(function(downloads) { + downloads.forEach(function(download) { + expect(download.startedAt instanceof Date).toBeTrue() + expect(download.canceledAt instanceof Date).toBeTrue() + expect(download.finishedAt).toBeFalsy() + }) + + done() + }) + }) + }) + }) + }) + }) }) diff --git a/spec/dao.validations.spec.js b/spec/dao.validations.spec.js index 4d17c2d34bf7..db23e6850315 100644 --- a/spec/dao.validations.spec.js +++ b/spec/dao.validations.spec.js @@ -42,6 +42,10 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { fail: "abc", pass: "129.89.23.1" } + , isIPv6 : { + fail: '1111:2222:3333::5555:', + pass: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156' + } , isAlpha : { fail: "012", pass: "abc" diff --git a/spec/data-types.spec.js b/spec/data-types.spec.js index 373f9e686c59..898bf100dc48 100644 --- a/spec/data-types.spec.js +++ b/spec/data-types.spec.js @@ -7,7 +7,7 @@ if(typeof require === 'function') { buster.spec.expose() -describe(Helpers.getTestDialectTeaser('Data types'), function() { +describe(Helpers.getTestDialectTeaser('DataTypes'), function() { it('should return DECIMAL for the default decimal type', function() { expect(Sequelize.DECIMAL).toEqual('DECIMAL'); }); @@ -15,4 +15,52 @@ describe(Helpers.getTestDialectTeaser('Data types'), function() { it('should return DECIMAL(10,2) for the default decimal type with arguments', function() { expect(Sequelize.DECIMAL(10, 2)).toEqual('DECIMAL(10,2)'); }); -}); + + var tests = [ + [Sequelize.STRING, 'STRING', 'VARCHAR(255)'], + [Sequelize.STRING(1234), 'STRING(1234)', 'VARCHAR(1234)'], + [Sequelize.STRING(1234).BINARY, 'STRING(1234).BINARY', 'VARCHAR(1234) BINARY'], + [Sequelize.STRING.BINARY, 'STRING.BINARY', 'VARCHAR(255) BINARY'], + + [Sequelize.TEXT, 'TEXT', 'TEXT'], + [Sequelize.DATE, 'DATE', 'DATETIME'], + [Sequelize.NOW, 'NOW', 'NOW'], + [Sequelize.BOOLEAN, 'BOOLEAN', 'TINYINT(1)'], + + [Sequelize.INTEGER, 'INTEGER', 'INTEGER'], + [Sequelize.INTEGER.UNSIGNED, 'INTEGER.UNSIGNED', 'INTEGER UNSIGNED'], + [Sequelize.INTEGER(11), 'INTEGER(11)','INTEGER(11)'], + [Sequelize.INTEGER(11).UNSIGNED, 'INTEGER(11).UNSIGNED', 'INTEGER(11) UNSIGNED'], + [Sequelize.INTEGER(11).UNSIGNED.ZEROFILL,'INTEGER(11).UNSIGNED.ZEROFILL','INTEGER(11) UNSIGNED ZEROFILL'], + [Sequelize.INTEGER(11).ZEROFILL,'INTEGER(11).ZEROFILL', 'INTEGER(11) ZEROFILL'], + [Sequelize.INTEGER(11).ZEROFILL.UNSIGNED,'INTEGER(11).ZEROFILL.UNSIGNED', 'INTEGER(11) UNSIGNED ZEROFILL'], + + [Sequelize.BIGINT, 'BIGINT', 'BIGINT'], + [Sequelize.BIGINT.UNSIGNED, 'BIGINT.UNSIGNED', 'BIGINT UNSIGNED'], + [Sequelize.BIGINT(11), 'BIGINT(11)','BIGINT(11)'], + [Sequelize.BIGINT(11).UNSIGNED, 'BIGINT(11).UNSIGNED', 'BIGINT(11) UNSIGNED'], + [Sequelize.BIGINT(11).UNSIGNED.ZEROFILL, 'BIGINT(11).UNSIGNED.ZEROFILL','BIGINT(11) UNSIGNED ZEROFILL'], + [Sequelize.BIGINT(11).ZEROFILL, 'BIGINT(11).ZEROFILL', 'BIGINT(11) ZEROFILL'], + [Sequelize.BIGINT(11).ZEROFILL.UNSIGNED, 'BIGINT(11).ZEROFILL.UNSIGNED', 'BIGINT(11) UNSIGNED ZEROFILL'], + + [Sequelize.FLOAT, 'FLOAT', 'FLOAT'], + [Sequelize.FLOAT.UNSIGNED, 'FLOAT.UNSIGNED', 'FLOAT UNSIGNED'], + [Sequelize.FLOAT(11), 'FLOAT(11)','FLOAT(11)'], + [Sequelize.FLOAT(11).UNSIGNED, 'FLOAT(11).UNSIGNED', 'FLOAT(11) UNSIGNED'], + [Sequelize.FLOAT(11).UNSIGNED.ZEROFILL,'FLOAT(11).UNSIGNED.ZEROFILL','FLOAT(11) UNSIGNED ZEROFILL'], + [Sequelize.FLOAT(11).ZEROFILL,'FLOAT(11).ZEROFILL', 'FLOAT(11) ZEROFILL'], + [Sequelize.FLOAT(11).ZEROFILL.UNSIGNED,'FLOAT(11).ZEROFILL.UNSIGNED', 'FLOAT(11) UNSIGNED ZEROFILL'], + + [Sequelize.FLOAT(11, 12), 'FLOAT(11,12)','FLOAT(11,12)'], + [Sequelize.FLOAT(11, 12).UNSIGNED, 'FLOAT(11,12).UNSIGNED', 'FLOAT(11,12) UNSIGNED'], + [Sequelize.FLOAT(11, 12).UNSIGNED.ZEROFILL,'FLOAT(11,12).UNSIGNED.ZEROFILL','FLOAT(11,12) UNSIGNED ZEROFILL'], + [Sequelize.FLOAT(11, 12).ZEROFILL,'FLOAT(11,12).ZEROFILL', 'FLOAT(11,12) ZEROFILL'], + [Sequelize.FLOAT(11, 12).ZEROFILL.UNSIGNED,'FLOAT(11,12).ZEROFILL.UNSIGNED', 'FLOAT(11,12) UNSIGNED ZEROFILL'] + ] + + tests.forEach(function(test) { + it('transforms "' + test[1] + '" to "' + test[2] + '"', function() { + expect(test[0]).toEqual(test[2]) + }) + }) +}) diff --git a/spec/migrator.spec.js b/spec/migrator.spec.js index d65665e8baca..dd39606ca33d 100644 --- a/spec/migrator.spec.js +++ b/spec/migrator.spec.js @@ -14,8 +14,8 @@ describe(Helpers.getTestDialectTeaser("Migrator"), function() { before(function(done) { this.init = function(options, callback) { options = Helpers.Sequelize.Utils._.extend({ - path: __dirname + '/assets/migrations', - logging: false + path: __dirname + '/assets/migrations', + logging: function(){} }, options || {}) var migrator = new Migrator(this.sequelize, options)