From 4caf1090abcc9868243d1ba9d002bc610e7450be Mon Sep 17 00:00:00 2001 From: zanamixx Date: Fri, 5 Apr 2013 12:18:16 +0200 Subject: [PATCH 01/86] Add string escape for postgresql in custom query --- .gitignore | 2 ++ lib/sequelize.js | 2 +- lib/sql-string.js | 33 +++++++++++++++++++-------------- lib/utils.js | 5 +++-- spec-jasmine/config/config.js | 3 ++- spec/config/config.js | 3 ++- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index bdf53afc5ef9..6d3e9c44eba2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ test*.js .DS_STORE node_modules npm-debug.log +spec/config/config.js +spec-jasmine/config/config.js *~ diff --git a/lib/sequelize.js b/lib/sequelize.js index 29810d78dc90..0954ccf40eff 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -171,7 +171,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) { 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..7f7357b823e5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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/spec-jasmine/config/config.js b/spec-jasmine/config/config.js index 6c5f5f90916c..07ad6b1543d7 100644 --- a/spec-jasmine/config/config.js +++ b/spec-jasmine/config/config.js @@ -18,7 +18,8 @@ module.exports = { postgres: { database: 'sequelize_test', - username: "postgres", + username: "root", + password: "toor", port: 5432, pool: { maxConnections: 5, maxIdleTime: 30} } diff --git a/spec/config/config.js b/spec/config/config.js index 5a557282f0b7..045f38dee3c5 100644 --- a/spec/config/config.js +++ b/spec/config/config.js @@ -24,7 +24,8 @@ module.exports = { postgres: { database: 'sequelize_test', - username: "postgres", + username: "root", + password: "toor", port: 5432, pool: { maxConnections: 5, maxIdleTime: 30} } From 773b2b6cd3b83f3faee1f052ec070961d1a7d519 Mon Sep 17 00:00:00 2001 From: zanamixx Date: Fri, 5 Apr 2013 14:58:30 +0200 Subject: [PATCH 02/86] Ajust Test config with default value --- .gitignore | 2 -- spec-jasmine/config/config.js | 3 +-- spec/config/config.js | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6d3e9c44eba2..bdf53afc5ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,4 @@ test*.js .DS_STORE node_modules npm-debug.log -spec/config/config.js -spec-jasmine/config/config.js *~ diff --git a/spec-jasmine/config/config.js b/spec-jasmine/config/config.js index 07ad6b1543d7..6c5f5f90916c 100644 --- a/spec-jasmine/config/config.js +++ b/spec-jasmine/config/config.js @@ -18,8 +18,7 @@ module.exports = { postgres: { database: 'sequelize_test', - username: "root", - password: "toor", + username: "postgres", port: 5432, pool: { maxConnections: 5, maxIdleTime: 30} } diff --git a/spec/config/config.js b/spec/config/config.js index 045f38dee3c5..5a557282f0b7 100644 --- a/spec/config/config.js +++ b/spec/config/config.js @@ -24,8 +24,7 @@ module.exports = { postgres: { database: 'sequelize_test', - username: "root", - password: "toor", + username: "postgres", port: 5432, pool: { maxConnections: 5, maxIdleTime: 30} } From 0d0865129d14581e417e9f05bf127fb871d8f4d0 Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Wed, 17 Apr 2013 19:12:23 +0100 Subject: [PATCH 03/86] - useful error message for bad data type (fixes #551) --- lib/sequelize.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/sequelize.js b/lib/sequelize.js index 559c2c5eaa28..bd59d2c43963 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -151,6 +151,12 @@ module.exports = (function() { options = options || {} var globalOptions = this.options + for (var name in attributes){ + if (attributes[name].type === undefined){ + throw new Error(daoName + '.' + name + ' has been set it an undefined data type.'); + } + } + if (globalOptions.define) { options = Utils._.extend({}, globalOptions.define, options) Utils._(['classMethods', 'instanceMethods']).each(function(key) { From db76b114dbc19f5a2f8a1becec564580c9df8604 Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Thu, 18 Apr 2013 12:46:05 +0100 Subject: [PATCH 04/86] - correcting error message --- lib/sequelize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index bd59d2c43963..ea3b8317ed26 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -153,7 +153,7 @@ module.exports = (function() { for (var name in attributes){ if (attributes[name].type === undefined){ - throw new Error(daoName + '.' + name + ' has been set it an undefined data type.'); + throw new Error(daoName + '.' + name + ' data type is undefined.'); } } From 94ce9588141e3be559ff9b612cb1c05f2ba0fe5b Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Thu, 18 Apr 2013 12:55:51 +0100 Subject: [PATCH 05/86] - changed error message to match expected test result - coding style - support both definition styles --- lib/sequelize.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index ea3b8317ed26..aaed737a501e 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -151,9 +151,15 @@ module.exports = (function() { options = options || {} var globalOptions = this.options - for (var name in attributes){ - if (attributes[name].type === undefined){ - throw new Error(daoName + '.' + name + ' data type is undefined.'); + for (var name in attributes) { + if (attributes.hasOwnProperty(name)) { + var dataType = attributes[name] + if (Utils.isHash(dataType)) { + dataType = dataType.type + } + if (dataType === undefined) { + throw new Error('Unrecognized data type for field '+ name) + } } } From 513aeb16da25a8b6cb9c5598316aee92573fc1f8 Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Thu, 25 Apr 2013 09:40:56 +0100 Subject: [PATCH 06/86] - adding second test for object style definition - removing incomplete dataType check - styling code more like the rest of the project --- lib/dao-factory.js | 5 ----- lib/sequelize.js | 18 ++++++++---------- spec/dao-factory.spec.js | 6 ++++++ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 80f3d227d19f..3c61b3481957 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -54,11 +54,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 } diff --git a/lib/sequelize.js b/lib/sequelize.js index aaed737a501e..95eb5832cae8 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -151,17 +151,15 @@ module.exports = (function() { options = options || {} var globalOptions = this.options - for (var name in attributes) { - if (attributes.hasOwnProperty(name)) { - var dataType = attributes[name] - if (Utils.isHash(dataType)) { - dataType = dataType.type - } - if (dataType === undefined) { - throw new Error('Unrecognized data type for field '+ name) - } + // 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) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 2296651e0ff0..a904084930fb 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -215,6 +215,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) { From 75d7eefe6824c05b597538504633a3474e829167 Mon Sep 17 00:00:00 2001 From: Rob Fletcher Date: Thu, 25 Apr 2013 23:12:10 -0400 Subject: [PATCH 07/86] Update the generated migration skeleton for async The auto-generated migrations don't mention done which is confusing if you're new to sequelize. --- bin/sequelize | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/sequelize b/bin/sequelize index 06b38b954a2d..fbd6f5544d7d 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -145,11 +145,11 @@ if(program.migrate) { 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", " },", - " down: function(migration) {", - " // add reverting commands here", + " down: function(migration, DataTypes, done) {", + " // add reverting commands here, calling 'done' when finished", " }", "}" ].join('\n') From f1ecabbe58cc86ce376f6d68e4ebfdcd7b39ecd7 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sun, 28 Apr 2013 13:22:22 +0300 Subject: [PATCH 08/86] Change from _.each to native forEach --- lib/sequelize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index 4d43226832df..799a265964ee 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -152,7 +152,7 @@ module.exports = (function() { var globalOptions = this.options // If you don't specify a valid data type lets help you debug it - Utils._.each(attributes, function(dataType, name){ + attributes.forEach(function(dataType, name){ if (Utils.isHash(dataType)) { dataType = dataType.type } From 8ccd34b4d742df2c69fed8d55cce1b545820091a Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sun, 28 Apr 2013 13:28:25 +0300 Subject: [PATCH 09/86] Revert back to _.each, idiot --- lib/sequelize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index 799a265964ee..4d43226832df 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -152,7 +152,7 @@ module.exports = (function() { var globalOptions = this.options // If you don't specify a valid data type lets help you debug it - attributes.forEach(function(dataType, name){ + Utils._.each(attributes, function(dataType, name){ if (Utils.isHash(dataType)) { dataType = dataType.type } From 3791a29653394ffe034193ea312a0af001d15356 Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Mon, 29 Apr 2013 16:01:14 +0300 Subject: [PATCH 10/86] When value is null, don't try to convert to Date, instead just leave it --- lib/dialects/sqlite/query.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From 9e82d76d7876c230d587228e47ff37327ca781c0 Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Mon, 29 Apr 2013 15:37:20 +0200 Subject: [PATCH 11/86] Add test for checking the new behaviour --- spec/dao.spec.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/spec/dao.spec.js b/spec/dao.spec.js index be61512c4591..1bc163bfc39c 100644 --- a/spec/dao.spec.js +++ b/spec/dao.spec.js @@ -19,7 +19,11 @@ 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 }, + dateAllowNullTrue: { + type: DataTypes.DATE, + allowNull: true + } }) self.HistoryLog = sequelize.define('HistoryLog', { @@ -320,6 +324,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() { From fe199e9e6cf45bab5b43e548a0adecd15caf7dce Mon Sep 17 00:00:00 2001 From: Filip Bonnevier Date: Mon, 29 Apr 2013 17:02:58 +0200 Subject: [PATCH 12/86] Change important MySQL query generator functions from using _.template to string concatenation. This has a large performance impact when doing many queries and having big result sets (attributesToSQL function) --- lib/dialects/mysql/query-generator.js | 108 +++++++++++++------------- 1 file changed, 52 insertions(+), 56 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 42e121a4572e..905f3dcb7880 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -116,8 +116,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) @@ -135,75 +135,74 @@ 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.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 += ";" + // console.log(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 replacements = { - table: QueryGenerator.addQuotes(tableName), - attributes: Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(","), - values: Utils._.values(attrValueHash).map(function(value){ + var table = QueryGenerator.addQuotes(tableName) + var attributes = Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(",") + var values = Utils._.values(attrValueHash).map(function(value){ return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value) }).join(",") - } - return Utils._.template(query)(replacements) + var query = "INSERT INTO " + table + " (" + attributes + ") VALUES (" + values + ");" + + 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] @@ -212,34 +211,31 @@ module.exports = (function() { values.push(QueryGenerator.addQuotes(key) + "=" + Utils.escape(_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) + var where = QueryGenerator.getWhereConditions(where) + var limit = Utils.escape(options.limit) + + var query = "DELETE FROM " + table + " WHERE " + where + " LIMIT " + limit - return Utils._.template(query)(replacements) + 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] @@ -247,14 +243,14 @@ module.exports = (function() { values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " +Utils.escape(_value)) } + + var table = QueryGenerator.addQuotes(tableName) + var values = values.join(",") + var where = QueryGenerator.getWhereConditions(where) - var replacements = { - 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) { @@ -377,17 +373,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)) { @@ -399,8 +396,7 @@ module.exports = (function() { } if ((dataType.defaultValue != undefined) && (dataType.defaultValue != DataTypes.NOW)) { - template += " DEFAULT <%= defaultValue %>" - replacements.defaultValue = Utils.escape(dataType.defaultValue) + template += " DEFAULT " + Utils.escape(dataType.defaultValue) } if (dataType.unique) { @@ -411,7 +407,7 @@ module.exports = (function() { template += " PRIMARY KEY" } - result[name] = Utils._.template(template)(replacements) + result[name] = template } else { result[name] = dataType } From 1009cb9a1c694638cc936b00e7348eff3ce2faa0 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 29 Apr 2013 21:30:37 +0200 Subject: [PATCH 13/86] #572 --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index bedb30f11c15..1a51790f32bd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,6 @@ # v1.7.0 # - [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango +- [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From b2184b24f52eb36e02430814d2c3f434d9d1d8b5 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 29 Apr 2013 21:31:57 +0200 Subject: [PATCH 14/86] v1.7.0-alpha1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b30589be96bd..07fe5729ad50 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": [ { From 4827513fe6b9071ef49052d6203e331ba1971755 Mon Sep 17 00:00:00 2001 From: Filip Bonnevier Date: Mon, 29 Apr 2013 17:02:58 +0200 Subject: [PATCH 15/86] Change important MySQL query generator functions from using _.template to string concatenation. This has a large performance impact when doing many queries and having big result sets (attributesToSQL function) --- lib/dialects/mysql/query-generator.js | 108 +++++++++++++------------- 1 file changed, 52 insertions(+), 56 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 1c7be85a4e1a..04e6e1b4a8fc 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -149,8 +149,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,75 +168,74 @@ 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 += ";" + // console.log(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 replacements = { - table: QueryGenerator.addQuotes(tableName), - attributes: Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(","), - values: Utils._.values(attrValueHash).map(function(value){ + var table = QueryGenerator.addQuotes(tableName) + var attributes = Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(",") + var values = Utils._.values(attrValueHash).map(function(value){ return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value) }).join(",") - } - return Utils._.template(query)(replacements) + var query = "INSERT INTO " + table + " (" + attributes + ") VALUES (" + values + ");" + + 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] @@ -245,34 +244,31 @@ module.exports = (function() { values.push(QueryGenerator.addQuotes(key) + "=" + Utils.escape(_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) + var where = QueryGenerator.getWhereConditions(where) + var limit = Utils.escape(options.limit) + + var query = "DELETE FROM " + table + " WHERE " + where + " LIMIT " + limit - return Utils._.template(query)(replacements) + 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] @@ -280,14 +276,14 @@ module.exports = (function() { values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " +Utils.escape(_value)) } + + var table = QueryGenerator.addQuotes(tableName) + var values = values.join(",") + var where = QueryGenerator.getWhereConditions(where) - var replacements = { - 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) { @@ -410,17 +406,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)) { @@ -432,8 +429,7 @@ module.exports = (function() { } if ((dataType.defaultValue != undefined) && (dataType.defaultValue != DataTypes.NOW)) { - template += " DEFAULT <%= defaultValue %>" - replacements.defaultValue = Utils.escape(dataType.defaultValue) + template += " DEFAULT " + Utils.escape(dataType.defaultValue) } if (dataType.unique) { @@ -444,7 +440,7 @@ module.exports = (function() { template += " PRIMARY KEY" } - result[name] = Utils._.template(template)(replacements) + result[name] = template } else { result[name] = dataType } From efa85a94c525a4e95516e7154793a21406de7334 Mon Sep 17 00:00:00 2001 From: Filip Bonnevier Date: Thu, 2 May 2013 13:30:53 +0200 Subject: [PATCH 16/86] Remove unnecessary semi-colon from query --- lib/dialects/mysql/query-generator.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 04e6e1b4a8fc..b5aa4b786dd2 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -213,7 +213,6 @@ module.exports = (function() { } query += ";" - // console.log(query) return query }, @@ -246,8 +245,7 @@ module.exports = (function() { var query = "UPDATE " + QueryGenerator.addQuotes(tableName) + " SET " + values.join(",") + - " WHERE " + QueryGenerator.getWhereConditions(where) + - ";" + " WHERE " + QueryGenerator.getWhereConditions(where) return query }, From 92bb5fe22c2a864370d56194a10b79b9fd4fc818 Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 2 May 2013 14:51:22 -0400 Subject: [PATCH 17/86] Added decimal support for min/max, closes #579 --- lib/dao-factory.js | 4 ++-- lib/query-interface.js | 4 ++++ spec/dao-factory.spec.js | 38 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 87ef96eed604..7421955b9307 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -271,14 +271,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') } diff --git a/lib/query-interface.js b/lib/query-interface.js index a5361b58efd0..49f3fd4b6e1a 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -296,6 +296,10 @@ module.exports = (function() { result = parseInt(result) } + if (options && options.parseFloat) { + result = parseFloat(result) + } + self.emit('rawSelect', null) emitter.emit('success', result) }) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 58f6016113f4..288cd8b6710d 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -1031,7 +1031,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) { @@ -1052,6 +1058,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() { @@ -1060,7 +1077,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) { @@ -1074,6 +1097,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() From 34ed0d70f5f7d7788a1964030f12489ca059d77f Mon Sep 17 00:00:00 2001 From: Alexandre Joly Date: Thu, 2 May 2013 23:55:54 +0200 Subject: [PATCH 18/86] 'ORDER BY' should come after 'GROUP BY' --- lib/dialects/postgres/query-generator.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 2c386366ac5e..e70188cdf276 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -262,13 +262,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 +274,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 %>" From c78b739640933e1f5874406e9f6154294f811afa Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 2 May 2013 19:17:33 -0400 Subject: [PATCH 19/86] Updated changelog --- changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 1a51790f32bd..ec267f930289 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,6 @@ # v1.7.0 # +- [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 - [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango - [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel From 644f6effab8bdbc2404be9f3662794a856eb920a Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 30 Apr 2013 22:10:15 +0100 Subject: [PATCH 20/86] Support for foreign keys in creating tables --- lib/dialects/mysql/query-generator.js | 33 +++++++++++++++++++ lib/dialects/postgres/query-generator.js | 25 +++++++++++++- lib/dialects/sqlite/query-generator.js | 23 +++++++++++++ spec-jasmine/mysql/query-generator.spec.js | 11 +++++++ spec-jasmine/postgres/query-generator.spec.js | 11 +++++++ spec-jasmine/sqlite/query-generator.spec.js | 30 +++++++++++++++++ 6 files changed, 132 insertions(+), 1 deletion(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index b5aa4b786dd2..c1a604bfadcc 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -45,6 +45,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 +55,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 +78,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() + ";" }, @@ -438,6 +450,27 @@ module.exports = (function() { template += " PRIMARY KEY" } + if(dataType.references) { + template += " REFERENCES " + Utils.escape(dataType.references) + + + if(dataType.referencesKeys) { + // TODO: This isn't really right - for composite primary keys we need a different approach + template += "(" + dataType.referencesKeys.map(Utils.escape).join(', ') + ")" + } else { + template += "(" + Utils.escape('id') + ")" + } + + if(dataType.onDelete) { + template += " ON DELETE " + dataType.onDeleteAction.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE " + dataType.onUpdateAction.toUpperCase() + } + + } + result[name] = template } else { result[name] = dataType diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index e70188cdf276..5654ca3a00e8 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){ @@ -556,6 +556,29 @@ module.exports = (function() { template += " PRIMARY KEY" } + if(dataType.references) { + template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" + replacements.referencesTable = dataType.references + + if(dataType.referencesKeys) { + // TODO: This isn't really right - for composite primary keys we need a different approach + replacements.referencesKeys = dataType.referencesKeys.map(Utils.escape).join(', ') + } else { + replacements.referencesKeys = Utils.escape('id') + } + + if(dataType.onDelete) { + template += " ON DELETE <%= onDeleteAction %>" + replacements.onDeleteAction = dataType.onDeleteAction.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE <%= onUpdateAction %>" + replacements.onUpdateAction = dataType.onUpdateAction.toUpperCase() + } + + } + result[name] = Utils._.template(template)(replacements) } else { result[name] = dataType diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 000fe1de10d0..27c3be51b86b 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -217,6 +217,29 @@ module.exports = (function() { } } + if(dataType.references) { + template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" + replacements.referencesTable = dataType.references + + if(dataType.referencesKeys) { + // TODO: This isn't really right - for composite primary keys we need a different approach + replacements.referencesKeys = dataType.referencesKeys.map(Utils.escape).join(', ') + } else { + replacements.referencesKeys = Utils.escape('id') + } + + if(dataType.onDelete) { + template += " ON DELETE <%= onDeleteAction %>" + replacements.onDeleteAction = dataType.onDeleteAction.toUpperCase() + } + + if(dataType.onUpdate) { + template += " ON UPDATE <%= onUpdateAction %>" + replacements.onUpdateAction = dataType.onUpdateAction.toUpperCase() + } + + } + result[name] = Utils._.template(template)(replacements) } else { result[name] = dataType diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index a2a4e78580e9..f3bf7ad3e6d8 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -10,6 +10,9 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + // TODO: Test attributesToSQL + createTableQuery: [ { arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], @@ -26,6 +29,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;" } ], diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index e8e0598113e4..4e3b2747ee4e 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -14,6 +14,9 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + // TODO: Test attributesToSQL + createTableQuery: [ { arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], @@ -26,6 +29,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);" } ], diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 40fce4870cde..97f5f3498ba6 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -9,6 +9,36 @@ describe('QueryGenerator', function() { afterEach(function() { Helpers.drop() }) var suites = { + + // TODO: Test attributesToSQL + + 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: 'VARCHAR(255)', name: 'VARCHAR(255)'}], + expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));" + }, + { + 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' }], From 8f8615338014b6fc560546318f599ef60445a3d4 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 30 Apr 2013 22:31:45 +0100 Subject: [PATCH 21/86] Tests + fixes for attributesToSQL --- lib/dialects/mysql/query-generator.js | 6 +- lib/dialects/postgres/query-generator.js | 10 ++-- lib/dialects/sqlite/query-generator.js | 10 ++-- spec-jasmine/mysql/query-generator.spec.js | 55 ++++++++++++++++++- spec-jasmine/postgres/query-generator.spec.js | 55 ++++++++++++++++++- spec-jasmine/sqlite/query-generator.spec.js | 55 ++++++++++++++++++- 6 files changed, 175 insertions(+), 16 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index c1a604bfadcc..b0bdfb66b0d0 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -451,14 +451,14 @@ module.exports = (function() { } if(dataType.references) { - template += " REFERENCES " + Utils.escape(dataType.references) + template += " REFERENCES " + Utils.addTicks(dataType.references) if(dataType.referencesKeys) { // TODO: This isn't really right - for composite primary keys we need a different approach - template += "(" + dataType.referencesKeys.map(Utils.escape).join(', ') + ")" + template += "(" + dataType.referencesKeys.map(Utils.addTicks).join(', ') + ")" } else { - template += "(" + Utils.escape('id') + ")" + template += "(" + Utils.addTicks('id') + ")" } if(dataType.onDelete) { diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 5654ca3a00e8..9897447ce7c5 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -558,23 +558,23 @@ module.exports = (function() { if(dataType.references) { template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" - replacements.referencesTable = dataType.references + replacements.referencesTable = QueryGenerator.addQuotes(dataType.references) if(dataType.referencesKeys) { // TODO: This isn't really right - for composite primary keys we need a different approach - replacements.referencesKeys = dataType.referencesKeys.map(Utils.escape).join(', ') + replacements.referencesKeys = dataType.referencesKeys.map(QueryGenerator.addQuotes).join(', ') } else { - replacements.referencesKeys = Utils.escape('id') + replacements.referencesKeys = QueryGenerator.addQuotes('id') } if(dataType.onDelete) { template += " ON DELETE <%= onDeleteAction %>" - replacements.onDeleteAction = dataType.onDeleteAction.toUpperCase() + replacements.onDeleteAction = dataType.onDelete.toUpperCase() } if(dataType.onUpdate) { template += " ON UPDATE <%= onUpdateAction %>" - replacements.onUpdateAction = dataType.onUpdateAction.toUpperCase() + replacements.onUpdateAction = dataType.onUpdate.toUpperCase() } } diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 27c3be51b86b..4c88cea2bc23 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -219,23 +219,23 @@ module.exports = (function() { if(dataType.references) { template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" - replacements.referencesTable = dataType.references + replacements.referencesTable = Utils.addTicks(dataType.references) if(dataType.referencesKeys) { // TODO: This isn't really right - for composite primary keys we need a different approach - replacements.referencesKeys = dataType.referencesKeys.map(Utils.escape).join(', ') + replacements.referencesKeys = dataType.referencesKeys.map(Utils.addTicks).join(', ') } else { - replacements.referencesKeys = Utils.escape('id') + replacements.referencesKeys = Utils.addTicks('id') } if(dataType.onDelete) { template += " ON DELETE <%= onDeleteAction %>" - replacements.onDeleteAction = dataType.onDeleteAction.toUpperCase() + replacements.onDeleteAction = dataType.onDelete.toUpperCase() } if(dataType.onUpdate) { template += " ON UPDATE <%= onUpdateAction %>" - replacements.onUpdateAction = dataType.onUpdateAction.toUpperCase() + replacements.onUpdateAction = dataType.onUpdate.toUpperCase() } } diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index f3bf7ad3e6d8..53df41eec03e 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -11,7 +11,60 @@ describe('QueryGenerator', function() { var suites = { - // TODO: Test attributesToSQL + 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', referencesKeys: ['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: [ { diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index 4e3b2747ee4e..dceb934f0196 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -15,7 +15,60 @@ describe('QueryGenerator', function() { var suites = { - // TODO: Test attributesToSQL + 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', referencesKeys: ['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: [ { diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 97f5f3498ba6..565e163e07f2 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -10,7 +10,60 @@ describe('QueryGenerator', function() { var suites = { - // TODO: Test attributesToSQL + 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', referencesKeys: ['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: [ { From 559e1cef9640f279c2a8a392beb1fc11c0be21d2 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 30 Apr 2013 23:31:05 +0100 Subject: [PATCH 22/86] Basic implementation of capturing foreign key status --- lib/associations/belongs-to.js | 6 ++++++ lib/associations/has-many.js | 6 ++++++ lib/associations/has-one.js | 6 ++++++ spec/associations/has-many.spec.js | 23 +++++++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index 020f14abe2ec..dfd613054695 100644 --- a/lib/associations/belongs-to.js +++ b/lib/associations/belongs-to.js @@ -24,6 +24,12 @@ 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 } + if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + newAttributes[this.identifier].references = this.target.tableName, + newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].onDelete = this.options.onDelete, + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } 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..bd70f243183c 100644 --- a/lib/associations/has-many.js +++ b/lib/associations/has-many.js @@ -65,6 +65,12 @@ module.exports = (function() { } else { var newAttributes = {} newAttributes[this.identifier] = { type: DataTypes.INTEGER } + if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + newAttributes[this.identifier].references = this.source.tableName + newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].onDelete = this.options.onDelete + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } Utils._.defaults(this.target.rawAttributes, newAttributes) } diff --git a/lib/associations/has-one.js b/lib/associations/has-one.js index 69a93898b4f1..79f12845e186 100644 --- a/lib/associations/has-one.js +++ b/lib/associations/has-one.js @@ -29,6 +29,12 @@ 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 } + if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + newAttributes[this.identifier].references = this.source.tableName, + newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].onDelete = this.options.onDelete, + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } Utils._.defaults(this.target.rawAttributes, newAttributes) // Sync attributes to DAO proto each time a new assoc is added diff --git a/spec/associations/has-many.spec.js b/spec/associations/has-many.spec.js index f298f0c9ce1f..6a658e594af9 100644 --- a/spec/associations/has-many.spec.js +++ b/spec/associations/has-many.spec.js @@ -322,5 +322,28 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { }) }) }) + + describe("Foreign key constraints", function() { + + it("can cascade deletes", function(done) { + var User = this.sequelize.define('User', { username: Sequelize.STRING }) + , Task = this.sequelize.define('Task', { title: 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() { + debugger + done() + }) + }) + }) + }) + }) + + }) + }) }) From 10ec7ff6b985e84aac12a3c3e1fa70dd34802aa4 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 4 May 2013 23:25:41 +0100 Subject: [PATCH 23/86] Tidy up setting of new attributes --- lib/associations/belongs-to.js | 6 ++++-- lib/associations/has-many.js | 6 ++++-- lib/associations/has-one.js | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index dfd613054695..ce90d771b48c 100644 --- a/lib/associations/belongs-to.js +++ b/lib/associations/belongs-to.js @@ -24,12 +24,14 @@ 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 } - if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + + if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { newAttributes[this.identifier].references = this.target.tableName, - newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.target.rawAttributes), function(key) { return this.target.rawAttributes[key].primaryKey }, this) newAttributes[this.identifier].onDelete = this.options.onDelete, newAttributes[this.identifier].onUpdate = this.options.onUpdate } + 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 bd70f243183c..c0bff1516d4e 100644 --- a/lib/associations/has-many.js +++ b/lib/associations/has-many.js @@ -65,12 +65,14 @@ module.exports = (function() { } else { var newAttributes = {} newAttributes[this.identifier] = { type: DataTypes.INTEGER } - if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + + if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { newAttributes[this.identifier].references = this.source.tableName - newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) newAttributes[this.identifier].onDelete = this.options.onDelete newAttributes[this.identifier].onUpdate = this.options.onUpdate } + Utils._.defaults(this.target.rawAttributes, newAttributes) } diff --git a/lib/associations/has-one.js b/lib/associations/has-one.js index 79f12845e186..07f819edc452 100644 --- a/lib/associations/has-one.js +++ b/lib/associations/has-one.js @@ -29,12 +29,14 @@ 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 } - if(typeof this.options.foreignKey === "undefined" || this.options.foreignKey === true) { + + if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { newAttributes[this.identifier].references = this.source.tableName, - newAttributes[this.identifier].referencesKeys = Utils._.reduce(this.source.rawAttributes, function(memo, value, key) { if(value.primaryKey) { memo.push(key) } return memo }, []) + newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) newAttributes[this.identifier].onDelete = this.options.onDelete, newAttributes[this.identifier].onUpdate = this.options.onUpdate } + Utils._.defaults(this.target.rawAttributes, newAttributes) // Sync attributes to DAO proto each time a new assoc is added From 556a6639d30f1a35200ae61e05757340ddec9722 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 4 May 2013 23:26:04 +0100 Subject: [PATCH 24/86] Topologically sorted iterator for DAOs taking dependencies into account --- lib/dao-factory-manager.js | 31 +++++++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 33 insertions(+), 1 deletion(-) 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/package.json b/package.json index 07fe5729ad50..4c7e0c37595a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "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", From f293a11122c2c10a64a6cf281d20a18908a5e8b3 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 4 May 2013 23:26:25 +0100 Subject: [PATCH 25/86] Create tables in dependency order --- lib/sequelize.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index 8f317fd49717..f4efec7bedf4 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -261,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() { From be4ffc9bac11dcc8eadcce86aad04f317d2cd372 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 4 May 2013 23:30:14 +0100 Subject: [PATCH 26/86] Support for dropping tables with constraints --- lib/dialects/mysql/query-generator.js | 10 ++++++ lib/dialects/postgres/query-generator.js | 12 +++++-- lib/dialects/query-generator.js | 15 ++++++++ lib/dialects/sqlite/query-generator.js | 10 ++++++ lib/query-interface.js | 34 +++++++++++++++++-- spec-jasmine/postgres/query-generator.spec.js | 4 +-- 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index b0bdfb66b0d0..26374a56ecd3 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -496,6 +496,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 9897447ce7c5..e488d93e21b4 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -78,7 +78,7 @@ 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) }) @@ -602,8 +602,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), diff --git a/lib/dialects/query-generator.js b/lib/dialects/query-generator.js index 669b99348049..3377997b8e21 100644 --- a/lib/dialects/query-generator.js +++ b/lib/dialects/query-generator.js @@ -229,7 +229,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/query-generator.js b/lib/dialects/sqlite/query-generator.js index 4c88cea2bc23..dbca580646f4 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -265,6 +265,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/query-interface.js b/lib/query-interface.js index 49f3fd4b6e1a..4bd5c95b93d4 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -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]) }) + + chainer.add(self, 'enableForeignKeyConstraints', []) + chainer - .run() + .runSerially() .success(function() { self.emit('dropAllTables', null) emitter.emit('success', null) @@ -313,6 +319,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/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index dceb934f0196..e0d14fbfa9c0 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -96,11 +96,11 @@ describe('QueryGenerator', function() { dropTableQuery: [ { arguments: ['myTable'], - expectation: "DROP TABLE IF EXISTS \"myTable\";" + expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;" }, { arguments: ['mySchema.myTable'], - expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";" + expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\" CASCADE;" } ], From 417a832a965a4d96de345a8b93726f4a5ae180eb Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 4 May 2013 23:30:23 +0100 Subject: [PATCH 27/86] Skeletal test --- spec/associations/has-many.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/associations/has-many.spec.js b/spec/associations/has-many.spec.js index 6a658e594af9..ec5dfe0a71a8 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() @@ -326,8 +326,8 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { describe("Foreign key constraints", function() { it("can cascade deletes", function(done) { - var User = this.sequelize.define('User', { username: Sequelize.STRING }) - , Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) + , User = this.sequelize.define('User', { username: Sequelize.STRING }) User.hasMany(Task, {onDelete: 'cascade'}) From 03ca02917f5e16f9a219f0c6a705f0a9a8fd9e74 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 5 May 2013 14:36:00 +0100 Subject: [PATCH 28/86] Fixes after rebase --- lib/dialects/mysql/query-generator.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 26374a56ecd3..50b0d9a8c4b0 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -456,17 +456,17 @@ module.exports = (function() { if(dataType.referencesKeys) { // TODO: This isn't really right - for composite primary keys we need a different approach - template += "(" + dataType.referencesKeys.map(Utils.addTicks).join(', ') + ")" + template += " (" + dataType.referencesKeys.map(Utils.addTicks).join(', ') + ")" } else { - template += "(" + Utils.addTicks('id') + ")" + template += " (" + Utils.addTicks('id') + ")" } if(dataType.onDelete) { - template += " ON DELETE " + dataType.onDeleteAction.toUpperCase() + template += " ON DELETE " + dataType.onDelete.toUpperCase() } if(dataType.onUpdate) { - template += " ON UPDATE " + dataType.onUpdateAction.toUpperCase() + template += " ON UPDATE " + dataType.onUpdate.toUpperCase() } } From 1df9027787817e0d84968d742854859295d05b26 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 5 May 2013 14:47:04 +0100 Subject: [PATCH 29/86] Don't pretend like composite primary keys work: they need to be handled at table level, not attribute level --- lib/associations/belongs-to.js | 12 ++++++++---- lib/associations/has-many.js | 12 ++++++++---- lib/associations/has-one.js | 12 ++++++++---- lib/dialects/mysql/query-generator.js | 5 ++--- lib/dialects/postgres/query-generator.js | 9 ++++----- lib/dialects/sqlite/query-generator.js | 9 ++++----- spec-jasmine/mysql/query-generator.spec.js | 2 +- spec-jasmine/postgres/query-generator.spec.js | 2 +- spec-jasmine/sqlite/query-generator.spec.js | 2 +- 9 files changed, 37 insertions(+), 28 deletions(-) diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index ce90d771b48c..1e5a197721af 100644 --- a/lib/associations/belongs-to.js +++ b/lib/associations/belongs-to.js @@ -26,10 +26,14 @@ module.exports = (function() { newAttributes[this.identifier] = { type: DataTypes.INTEGER } if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - newAttributes[this.identifier].references = this.target.tableName, - newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.target.rawAttributes), function(key) { return this.target.rawAttributes[key].primaryKey }, this) - newAttributes[this.identifier].onDelete = this.options.onDelete, - newAttributes[this.identifier].onUpdate = this.options.onUpdate + var primaryKeys = Utils._.filter(Utils._.keys(this.target.rawAttributes), function(key) { return this.target.rawAttributes[key].primaryKey }, this) + + if(primaryKeys.length == 1) { // composite keys not supported with this approach + newAttributes[this.identifier].references = this.target.tableName, + newAttributes[this.identifier].referencesKey = primaryKeys[0] + newAttributes[this.identifier].onDelete = this.options.onDelete, + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } } Utils._.defaults(this.source.rawAttributes, newAttributes) diff --git a/lib/associations/has-many.js b/lib/associations/has-many.js index c0bff1516d4e..24a0b5322c2f 100644 --- a/lib/associations/has-many.js +++ b/lib/associations/has-many.js @@ -67,10 +67,14 @@ module.exports = (function() { newAttributes[this.identifier] = { type: DataTypes.INTEGER } if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - newAttributes[this.identifier].references = this.source.tableName - newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) - newAttributes[this.identifier].onDelete = this.options.onDelete - newAttributes[this.identifier].onUpdate = this.options.onUpdate + var primaryKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) + + if(primaryKeys.length == 1) { // composite keys not supported with this approach + newAttributes[this.identifier].references = this.source.tableName + newAttributes[this.identifier].referencesKey = primaryKeys[0] + newAttributes[this.identifier].onDelete = this.options.onDelete + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } } Utils._.defaults(this.target.rawAttributes, newAttributes) diff --git a/lib/associations/has-one.js b/lib/associations/has-one.js index 07f819edc452..bf11d88fde5b 100644 --- a/lib/associations/has-one.js +++ b/lib/associations/has-one.js @@ -31,10 +31,14 @@ module.exports = (function() { newAttributes[this.identifier] = { type: DataTypes.INTEGER } if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - newAttributes[this.identifier].references = this.source.tableName, - newAttributes[this.identifier].referencesKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) - newAttributes[this.identifier].onDelete = this.options.onDelete, - newAttributes[this.identifier].onUpdate = this.options.onUpdate + var primaryKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) + + if(primaryKeys.length == 1) { // composite keys not supported with this approach + newAttributes[this.identifier].references = this.source.tableName, + newAttributes[this.identifier].referencesKey = primaryKeys[0] + newAttributes[this.identifier].onDelete = this.options.onDelete, + newAttributes[this.identifier].onUpdate = this.options.onUpdate + } } Utils._.defaults(this.target.rawAttributes, newAttributes) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 50b0d9a8c4b0..6d55f3d1c627 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -454,9 +454,8 @@ module.exports = (function() { template += " REFERENCES " + Utils.addTicks(dataType.references) - if(dataType.referencesKeys) { - // TODO: This isn't really right - for composite primary keys we need a different approach - template += " (" + dataType.referencesKeys.map(Utils.addTicks).join(', ') + ")" + if(dataType.referencesKey) { + template += " (" + Utils.addTicks(dataType.referencesKey) + ")" } else { template += " (" + Utils.addTicks('id') + ")" } diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index e488d93e21b4..e5c6880ded8c 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -557,14 +557,13 @@ module.exports = (function() { } if(dataType.references) { - template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" + template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)" replacements.referencesTable = QueryGenerator.addQuotes(dataType.references) - if(dataType.referencesKeys) { - // TODO: This isn't really right - for composite primary keys we need a different approach - replacements.referencesKeys = dataType.referencesKeys.map(QueryGenerator.addQuotes).join(', ') + if(dataType.referencesKey) { + replacements.referencesKey = QueryGenerator.addQuotes(dataType.referencesKey) } else { - replacements.referencesKeys = QueryGenerator.addQuotes('id') + replacements.referencesKey = QueryGenerator.addQuotes('id') } if(dataType.onDelete) { diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index dbca580646f4..66c2987f98d1 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -218,14 +218,13 @@ module.exports = (function() { } if(dataType.references) { - template += " REFERENCES <%= referencesTable %> (<%= referencesKeys %>)" + template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)" replacements.referencesTable = Utils.addTicks(dataType.references) - if(dataType.referencesKeys) { - // TODO: This isn't really right - for composite primary keys we need a different approach - replacements.referencesKeys = dataType.referencesKeys.map(Utils.addTicks).join(', ') + if(dataType.referencesKey) { + replacements.referencesKey = Utils.addTicks(dataType.referencesKey) } else { - replacements.referencesKeys = Utils.addTicks('id') + replacements.referencesKey = Utils.addTicks('id') } if(dataType.onDelete) { diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index 53df41eec03e..70e64e2b3e37 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -49,7 +49,7 @@ describe('QueryGenerator', function() { expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'} }, { - arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKeys: ['pk']}}], + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'} }, { diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index e0d14fbfa9c0..f58cf6c66585 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -53,7 +53,7 @@ describe('QueryGenerator', function() { expectation: {id: 'INTEGER REFERENCES "Bar" ("id")'} }, { - arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKeys: ['pk']}}], + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], expectation: {id: 'INTEGER REFERENCES "Bar" ("pk")'} }, { diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 565e163e07f2..6fa87b7389bb 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -48,7 +48,7 @@ describe('QueryGenerator', function() { expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'} }, { - arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKeys: ['pk']}}], + arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}], expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'} }, { From 151a23f64cdba8f20ffbdb630696f92d79d64b9a Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 5 May 2013 14:57:45 +0100 Subject: [PATCH 30/86] Factor FK constraint logic out into a helper function --- lib/associations/belongs-to.js | 14 ++------------ lib/associations/has-many.js | 14 ++------------ lib/associations/has-one.js | 14 ++------------ lib/associations/helpers.js | 25 +++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 36 deletions(-) create mode 100644 lib/associations/helpers.js diff --git a/lib/associations/belongs-to.js b/lib/associations/belongs-to.js index 1e5a197721af..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,18 +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 } - - if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - var primaryKeys = Utils._.filter(Utils._.keys(this.target.rawAttributes), function(key) { return this.target.rawAttributes[key].primaryKey }, this) - - if(primaryKeys.length == 1) { // composite keys not supported with this approach - newAttributes[this.identifier].references = this.target.tableName, - newAttributes[this.identifier].referencesKey = primaryKeys[0] - newAttributes[this.identifier].onDelete = this.options.onDelete, - newAttributes[this.identifier].onUpdate = this.options.onUpdate - } - } - + 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 24a0b5322c2f..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,18 +66,7 @@ module.exports = (function() { } else { var newAttributes = {} newAttributes[this.identifier] = { type: DataTypes.INTEGER } - - if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - var primaryKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) - - if(primaryKeys.length == 1) { // composite keys not supported with this approach - newAttributes[this.identifier].references = this.source.tableName - newAttributes[this.identifier].referencesKey = primaryKeys[0] - newAttributes[this.identifier].onDelete = this.options.onDelete - newAttributes[this.identifier].onUpdate = this.options.onUpdate - } - } - + 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 bf11d88fde5b..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,18 +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 } - - if(this.options.foreignKeyConstraint || this.options.onDelete || this.options.onUpdate) { - var primaryKeys = Utils._.filter(Utils._.keys(this.source.rawAttributes), function(key) { return this.source.rawAttributes[key].primaryKey }, this) - - if(primaryKeys.length == 1) { // composite keys not supported with this approach - newAttributes[this.identifier].references = this.source.tableName, - newAttributes[this.identifier].referencesKey = primaryKeys[0] - newAttributes[this.identifier].onDelete = this.options.onDelete, - newAttributes[this.identifier].onUpdate = this.options.onUpdate - } - } - + 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 + } + } + } + +} From 6643167ad226842e40c03b724e53836bf783eb17 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 13:55:12 +0100 Subject: [PATCH 31/86] Make cascade optional --- lib/dialects/postgres/query-generator.js | 5 +++-- lib/query-interface.js | 6 +++--- spec-jasmine/postgres/query-generator.spec.js | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index e5c6880ded8c..47c3c48fc426 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -78,9 +78,10 @@ module.exports = (function() { dropTableQuery: function(tableName, options) { options = options || {} - var query = "DROP TABLE IF EXISTS <%= table %> CASCADE;" + var query = "DROP TABLE IF EXISTS <%= table %><%= cascade %>;" return Utils._.template(query)({ - table: QueryGenerator.addQuotes(tableName) + table: QueryGenerator.addQuotes(tableName), + cascade: options.cascade? " CASCADE" : "" }) }, diff --git a/lib/query-interface.js b/lib/query-interface.js index 4bd5c95b93d4..7aa0b8098380 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') } @@ -95,7 +95,7 @@ module.exports = (function() { chainer.add(self, 'disableForeignKeyConstraints', []) tableNames.forEach(function(tableName) { - chainer.add(self, 'dropTable', [tableName]) + chainer.add(self, 'dropTable', [tableName, {cascade: true}]) }) chainer.add(self, 'enableForeignKeyConstraints', []) diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index f58cf6c66585..2b3dec5f588e 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -95,11 +95,11 @@ describe('QueryGenerator', function() { dropTableQuery: [ { - arguments: ['myTable'], + arguments: ['myTable', {cascade: true}], expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;" }, { - arguments: ['mySchema.myTable'], + arguments: ['mySchema.myTable', {cascade: true}], expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\" CASCADE;" } ], From 47f7e4c8ce1646fb00b8541d8b59ba910940729e Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 13:55:21 +0100 Subject: [PATCH 32/86] Turn on foreign key checking for SQLite --- lib/dialects/sqlite/connector-manager.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/dialects/sqlite/connector-manager.js b/lib/dialects/sqlite/connector-manager.js index 85436f8372bf..1324497cb45d 100644 --- a/lib/dialects/sqlite/connector-manager.js +++ b/lib/dialects/sqlite/connector-manager.js @@ -6,10 +6,21 @@ module.exports = (function() { var ConnectorManager = function(sequelize) { this.sequelize = sequelize this.database = new sqlite3.Database(sequelize.options.storage || ':memory:') + this.opened = false } Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype) ConnectorManager.prototype.query = function(sql, callee, options) { + var self = this + // Turn on foreign key checking (if the database has any) unless explicitly + // disallowed globally. + if(!this.opened && this.sequelize.options.foreignKeys !== false) { + this.database.serialize(function() { + self.database.run("PRAGMA FOREIGN_KEYS = ON") + self.opened = true + }) + } + return new Query(this.database, this.sequelize, callee, options).run(sql) } From db1a504c5a0474a6cfcb283d08d2ce02f708e0e9 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 15:03:48 +0100 Subject: [PATCH 33/86] Improve pragma handling for sqlite3 --- lib/dialects/sqlite/connector-manager.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/dialects/sqlite/connector-manager.js b/lib/dialects/sqlite/connector-manager.js index 1324497cb45d..cf12f46d851f 100644 --- a/lib/dialects/sqlite/connector-manager.js +++ b/lib/dialects/sqlite/connector-manager.js @@ -5,22 +5,17 @@ var Utils = require("../../utils") module.exports = (function() { var ConnectorManager = function(sequelize) { this.sequelize = sequelize - this.database = new sqlite3.Database(sequelize.options.storage || ':memory:') - this.opened = false + 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 unelss + // explicitly disallowed. It's still opt-in per relation + db.run('PRAGMA FOREIGN_KEYS=ON') + } + }) } Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype) ConnectorManager.prototype.query = function(sql, callee, options) { - var self = this - // Turn on foreign key checking (if the database has any) unless explicitly - // disallowed globally. - if(!this.opened && this.sequelize.options.foreignKeys !== false) { - this.database.serialize(function() { - self.database.run("PRAGMA FOREIGN_KEYS = ON") - self.opened = true - }) - } - return new Query(this.database, this.sequelize, callee, options).run(sql) } From 918084475ed802de3aa3a7768c91d398f05b7210 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 15:03:59 +0100 Subject: [PATCH 34/86] Improve test coverage for cascade deletion in postgres --- spec-jasmine/postgres/query-generator.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index 2b3dec5f588e..0086582341b8 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -94,6 +94,14 @@ describe('QueryGenerator', function() { ], dropTableQuery: [ + { + arguments: ['myTable'], + expectation: "DROP TABLE IF EXISTS \"myTable\";" + }, + { + arguments: ['mySchema.myTable'], + expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";" + }, { arguments: ['myTable', {cascade: true}], expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;" From 7491c223fb4f1b8e2659095a2e0b3d57bdc724a0 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 15:04:04 +0100 Subject: [PATCH 35/86] Tests for delete cascade --- spec/associations/belongs-to.spec.js | 72 ++++++++++++++++++++++++++++ spec/associations/has-many.spec.js | 70 ++++++++++++++++++++++----- spec/associations/has-one.spec.js | 72 ++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 11 deletions(-) diff --git a/spec/associations/belongs-to.spec.js b/spec/associations/belongs-to.spec.js index 932396b63fce..4b7128503337 100644 --- a/spec/associations/belongs-to.spec.js +++ b/spec/associations/belongs-to.spec.js @@ -47,4 +47,76 @@ 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() + }) + }) + }) + }) + }) + }) + }) + + }) + }) diff --git a/spec/associations/has-many.spec.js b/spec/associations/has-many.spec.js index ec5dfe0a71a8..e0487f0fe26f 100644 --- a/spec/associations/has-many.spec.js +++ b/spec/associations/has-many.spec.js @@ -322,27 +322,75 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { }) }) }) + }) - describe("Foreign key constraints", function() { + describe("Foreign key constraints", function() { - it("can cascade deletes", function(done) { - var Task = this.sequelize.define('Task', { title: Sequelize.STRING }) - , User = this.sequelize.define('User', { username: Sequelize.STRING }) + 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, {onDelete: 'cascade'}) + 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() { - debugger - done() + 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() + }) + }) + }) + }) + }) + }) }) }) diff --git a/spec/associations/has-one.spec.js b/spec/associations/has-one.spec.js index 0791c637881b..5ee3684c8907 100644 --- a/spec/associations/has-one.spec.js +++ b/spec/associations/has-one.spec.js @@ -47,4 +47,76 @@ 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() + }) + }) + }) + }) + }) + }) + }) + + }) + }) From 27e9f9a1340ebba174074cedf761d62f6b0e829e Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Mon, 6 May 2013 15:54:27 -0400 Subject: [PATCH 36/86] Replaced underscore with lodash. --- lib/utils.js | 2 +- package.json | 2 +- spec/dao-factory.spec.js | 2 +- spec/dao.spec.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 7f7357b823e5..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()) diff --git a/package.json b/package.json index 07fe5729ad50..3087ff03b7b7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "url": "https://github.com/sdepold/sequelize/issues" }, "dependencies": { - "underscore": "~1.4.0", + "lodash": "~1.2.1", "underscore.string": "~2.3.0", "lingo": "~0.0.5", "validator": "0.4.x", diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 288cd8b6710d..074bb38c4c89 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() } diff --git a/spec/dao.spec.js b/spec/dao.spec.js index 1bc163bfc39c..256e1d3f9af0 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() From 35aeb2c812b607c4d705107761479194ee18b63d Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 23:10:42 +0100 Subject: [PATCH 37/86] Test for update cascade and restrict --- spec/associations/belongs-to.spec.js | 61 ++++++++++++++++++++++++++++ spec/associations/has-many.spec.js | 61 ++++++++++++++++++++++++++++ spec/associations/has-one.spec.js | 61 ++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/spec/associations/belongs-to.spec.js b/spec/associations/belongs-to.spec.js index 4b7128503337..74e9d17f1ec1 100644 --- a/spec/associations/belongs-to.spec.js +++ b/spec/associations/belongs-to.spec.js @@ -117,6 +117,67 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() { }) }) + 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() { + // Should fail due to FK restriction + 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 e0487f0fe26f..9e3752949984 100644 --- a/spec/associations/has-many.spec.js +++ b/spec/associations/has-many.spec.js @@ -393,5 +393,66 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { }) }) + 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() { + // Should fail due to FK restriction + 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 5ee3684c8907..3a50d5392ad5 100644 --- a/spec/associations/has-one.spec.js +++ b/spec/associations/has-one.spec.js @@ -117,6 +117,67 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() { }) }) + 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() { + // Should fail due to FK restriction + 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() + }) + }) + }) + }) + }) + }) + }) + }) }) From 809453f7f3fd0c9461145b69c1cc57df1f0bd18e Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 7 May 2013 23:55:49 +0100 Subject: [PATCH 38/86] Minor updates following comments form @janmeier --- lib/dialects/sqlite/connector-manager.js | 2 +- spec-jasmine/sqlite/query-generator.spec.js | 8 -------- spec/associations/belongs-to.spec.js | 1 - spec/associations/has-many.spec.js | 1 - spec/associations/has-one.spec.js | 1 - 5 files changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/dialects/sqlite/connector-manager.js b/lib/dialects/sqlite/connector-manager.js index cf12f46d851f..f3c6b9b238b2 100644 --- a/lib/dialects/sqlite/connector-manager.js +++ b/lib/dialects/sqlite/connector-manager.js @@ -7,7 +7,7 @@ module.exports = (function() { this.sequelize = sequelize 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 unelss + // 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') } diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 6fa87b7389bb..429a4f424340 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -66,14 +66,6 @@ describe('QueryGenerator', function() { ], 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: 'VARCHAR(255)', name: 'VARCHAR(255)'}], - expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));" - }, { arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));" diff --git a/spec/associations/belongs-to.spec.js b/spec/associations/belongs-to.spec.js index 74e9d17f1ec1..40c8c11f017b 100644 --- a/spec/associations/belongs-to.spec.js +++ b/spec/associations/belongs-to.spec.js @@ -135,7 +135,6 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() { var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) user.QueryInterface.update(user, tableName, {id: 999}, user.id) .success(function() { - // Should fail due to FK restriction Task.findAll().success(function(tasks) { expect(tasks.length).toEqual(1) expect(tasks[0].UserId).toEqual(999) diff --git a/spec/associations/has-many.spec.js b/spec/associations/has-many.spec.js index 9e3752949984..d8fc58575711 100644 --- a/spec/associations/has-many.spec.js +++ b/spec/associations/has-many.spec.js @@ -411,7 +411,6 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) user.QueryInterface.update(user, tableName, {id: 999}, user.id) .success(function() { - // Should fail due to FK restriction Task.findAll().success(function(tasks) { expect(tasks.length).toEqual(1) expect(tasks[0].UserId).toEqual(999) diff --git a/spec/associations/has-one.spec.js b/spec/associations/has-one.spec.js index 3a50d5392ad5..e00a7432d153 100644 --- a/spec/associations/has-one.spec.js +++ b/spec/associations/has-one.spec.js @@ -135,7 +135,6 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() { var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory) user.QueryInterface.update(user, tableName, {id: 999}, user.id) .success(function() { - // Should fail due to FK restriction Task.findAll().success(function(tasks) { expect(tasks.length).toEqual(1) expect(tasks[0].UserId).toEqual(999) From 3b03f5dcabc37489d932f5ef3e23df3c96e52336 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Wed, 8 May 2013 08:19:57 +0300 Subject: [PATCH 39/86] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 65364b40e0c4..f10a50861c88 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,8 @@ 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 From d25e697d9adb53230bddb83001ba83c22973e62c Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Wed, 8 May 2013 08:21:13 +0300 Subject: [PATCH 40/86] lodash --- changelog.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 2d29ca05c5cc..94d5d26c24ef 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,10 @@ # v1.7.0 # -- [BUG] Fix string escape with postgresql on raw SQL queries/ [#586](https://github.com/sequelize/sequelize/pull/586). thanks to zanamixx +- [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 -- [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango - [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel +- [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From d0cfa94b888b2b27a9c1a99eb4c6f1dd35868742 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Wed, 8 May 2013 08:25:03 +0300 Subject: [PATCH 41/86] fk constraints --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 94d5d26c24ef..619fcb290ba1 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ - [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 - [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 # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From cfa224b7e3607c7217a25eca0746f33031aba235 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Wed, 8 May 2013 10:24:12 +0300 Subject: [PATCH 42/86] Updated roadmap with foreign key support --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f10a50861c88..acef1ade547b 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl - 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 +- 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 From 08c69a818d8515d58579a8f8aaf77c0680426ba6 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Wed, 8 May 2013 19:45:19 +0200 Subject: [PATCH 43/86] Added deletedAt buster tests borrowed from @gustawpursche --- spec-jasmine/dao-factory.spec.js | 19 ----------- spec/dao.spec.js | 57 +++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 20 deletions(-) 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/dao.spec.js b/spec/dao.spec.js index 256e1d3f9af0..8657f6585dac 100644 --- a/spec/dao.spec.js +++ b/spec/dao.spec.js @@ -31,10 +31,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) + }) }) } }) @@ -494,6 +504,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' }} From 7affa31c5dabe0cebbf560ab434875b8528bf1cb Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Thu, 9 May 2013 20:44:26 +0200 Subject: [PATCH 44/86] fixed url to irc channel --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index acef1ade547b..37f84b06592f 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) From 3c05453120ce4b12af6389a16e69fc7923823eef Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 9 May 2013 16:06:13 -0400 Subject: [PATCH 45/86] Validations will now be called upon .save() and allowNull: true skips validations (if the value is null). --- lib/dao.js | 13 +++++++++++-- spec/dao.spec.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 071af287cc0c..5bc8594c7b89 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -150,7 +150,14 @@ 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) { return this.QueryInterface.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values) } else { var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id; @@ -211,7 +218,9 @@ module.exports = (function() { Utils._.each(self.values, function(value, field) { // if field has validators - if (self.validators.hasOwnProperty(field)) { + var allowsNulls = (self.rawAttributes[field].allowNull && self.rawAttributes[field].allowNull === true && (value === null || value === undefined)); + + if (self.validators.hasOwnProperty(field) && !allowsNulls) { // for each validator Utils._.each(self.validators[field], function(details, validatorType) { diff --git a/spec/dao.spec.js b/spec/dao.spec.js index 8657f6585dac..81f35ea127f9 100644 --- a/spec/dao.spec.js +++ b/spec/dao.spec.js @@ -20,6 +20,18 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, aNumber: { 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 @@ -378,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) From 421fc4f1eca20d5d55967bead9c7f5c02248f5b3 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 21 Apr 2013 10:08:37 +0100 Subject: [PATCH 46/86] DAO factory API sketch --- lib/dao-factory.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 7421955b9307..d7db7f216a88 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -328,6 +328,43 @@ module.exports = (function() { }).run() } + /** + * Create and insert multiple instances + * + * @param {Array} values List of objects (key/value pairs) to create instances from + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + * + * The `success` handler is passed a list of newly inserted models. + */ + DaoFactory.prototype.bulkCreate = function(values, options) { + var factory = this + return this.bulkInsert(values.map(function(attrs) { + return factory.build(attrs, options) + }) + } + + /** + * Insert multiple instances + * + * @param {Array} daos List of built DAOs + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + * + * The `success` handler is passed a list of newly inserted models. + */ + DaoFactory.prototype.bulkInsert = function(daos, fields) { + // XXX: TODO + } + + /** + * Delete multiple instances + * + * @param {Object} options Options to describe the scope of the search. + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + */ + DaoFactory.prototype.bulkDelete = function(options) { + // XXX: TODO + } + // private var query = function() { From 2029345bcb22c2fca40dac60b7bf79df79d2ec58 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 21 Apr 2013 10:18:00 +0100 Subject: [PATCH 47/86] Query interface and generator API sketch --- lib/dialects/mysql/query-generator.js | 4 ++++ lib/dialects/postgres/query-generator.js | 4 ++++ lib/dialects/query-generator.js | 8 ++++++++ lib/dialects/sqlite/query-generator.js | 4 ++++ lib/query-interface.js | 12 ++++++++++++ 5 files changed, 32 insertions(+) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 6d55f3d1c627..ae1f02c7b8b2 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -243,6 +243,10 @@ module.exports = (function() { return query }, + bulkInsertQuery: function(tableName, attrValueHashes) { + throwMethodUndefined('bulkInsertQuery') + }, + updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 47c3c48fc426..b10f7d26a5fd 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -327,6 +327,10 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkInsertQuery: function(tableName, attrValueHashes) { + throwMethodUndefined('bulkInsertQuery') + }, + updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/dialects/query-generator.js b/lib/dialects/query-generator.js index 3377997b8e21..033804cac22e 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: diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 66c2987f98d1..c6100115f157 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -125,6 +125,10 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkInsertQuery: function(tableName, attrValueHashes) { + throwMethodUndefined('bulkInsertQuery') + }, + updateQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/query-interface.js b/lib/query-interface.js index 7aa0b8098380..edf42e1d797e 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -260,6 +260,13 @@ module.exports = (function() { }) } + QueryInterface.prototype.bulkInsert = function(tableName, records) { + var sql = this.QueryGenerator.bulkInsertQuery(tableName, records) + return queryAndEmit.call(this, sql, 'bulkInsert', { + success: function(objs) { objs.forEach(function(v) { v.isNewRecord = false }) } + }) + } + QueryInterface.prototype.update = function(dao, tableName, values, identifier) { var sql = this.QueryGenerator.updateQuery(tableName, values, identifier) return queryAndEmit.call(this, [sql, dao], 'update') @@ -270,6 +277,11 @@ module.exports = (function() { return queryAndEmit.call(this, [sql, dao], 'delete') } + QueryInterface.prototype.bulkDelete = function(tableName, identifier) { + var sql = this.QueryGenerator.deleteQuery(tableName, identifier) + return queryAndEmit.call(this, sql, 'bulkDelete') + } + QueryInterface.prototype.select = function(factory, tableName, options, queryOptions) { options = options || {} From cfb657adca2bce6fcf07e8dd1545c5b9f382b5bf Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 23 Apr 2013 09:40:40 +0100 Subject: [PATCH 48/86] Implementation sketch and query generators --- lib/dao-factory.js | 71 +++++++++++++++++-- lib/dialects/mysql/query-generator.js | 19 ++++- lib/dialects/postgres/query-generator.js | 21 +++++- lib/dialects/sqlite/query-generator.js | 19 ++++- lib/query-interface.js | 5 ++ spec-jasmine/mysql/query-generator.spec.js | 31 ++++++++ spec-jasmine/postgres/query-generator.spec.js | 40 +++++++++++ spec-jasmine/sqlite/query-generator.spec.js | 37 ++++++++++ 8 files changed, 234 insertions(+), 9 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index d7db7f216a88..45d174ccdcf6 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -336,23 +336,75 @@ module.exports = (function() { * * The `success` handler is passed a list of newly inserted models. */ - DaoFactory.prototype.bulkCreate = function(values, options) { + DAOFactory.prototype.bulkCreate = function(values, options) { var factory = this return this.bulkInsert(values.map(function(attrs) { return factory.build(attrs, options) - }) + })) } /** * Insert multiple instances * * @param {Array} daos List of built DAOs + * @param {Array} fields Fields to insert (defaults to all fields) * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. * * The `success` handler is passed a list of newly inserted models. */ - DaoFactory.prototype.bulkInsert = function(daos, fields) { - // XXX: TODO + DAOFactory.prototype.bulkInsert = function(daos, fields) { + var self = this + , records = [] + , updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt' + , createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt' + + if (fields) { + if (self.options.timestamps) { + if (fields.indexOf(updatedAtAttr) === -1) { + fields.push(updatedAtAttr) + } + + if (fields.indexOf(createdAtAttr) === -1) { + fields.push(createdAtAttr) + } + } + + daos.forEach(function(dao) { + var values = {}; + fields.forEach(function(field) { + if (dao.values[field] !== undefined) { + values[field] = dao.values[field] + } + }) + records.push(values); + }) + } else { + daos.forEach(function(dao) { + records.push(dao.values) + }) + } + + 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(', ')) + } + } + } + + if (self.options.timestamps && dao.hasOwnProperty(updatedAtAttr)) { + dao[updatedAtAttr] = values[updatedAtAttr] = Utils.now() + } + + }) + + return self.QueryInterface.bulkInsert(self.tableName, records) } /** @@ -361,8 +413,15 @@ module.exports = (function() { * @param {Object} options Options to describe the scope of the search. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DaoFactory.prototype.bulkDelete = function(options) { - // XXX: TODO + DAOFactory.prototype.bulkDelete = function(options) { + if (this.options.timestamps && this.options.paranoid) { + var attr = this.options.underscored ? 'deleted_at' : 'deletedAt' + var attrValueHash = {} + attrValueHash[attr] = new Date() + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, options) + } else { + return this.QueryInterface.bulkDelete(this.tableName, options) + } } // private diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index ae1f02c7b8b2..eda65cb72937 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -244,7 +244,24 @@ module.exports = (function() { }, bulkInsertQuery: function(tableName, attrValueHashes) { - throwMethodUndefined('bulkInsertQuery') + var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;" + , tuples = [] + + Utils._.forEach(attrValueHashes, function(attrValueHash) { + tuples.push("(" + + Utils._.values(attrValueHash).map(function(value){ + return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : 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) { diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index b10f7d26a5fd..57c12abf8da8 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -328,7 +328,26 @@ module.exports = (function() { }, bulkInsertQuery: function(tableName, attrValueHashes) { - throwMethodUndefined('bulkInsertQuery') + var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %> RETURNING *;" + , tuples = [] + + Utils._.forEach(attrValueHashes, function(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) { diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index c6100115f157..6d35dcb78358 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -126,7 +126,24 @@ module.exports = (function() { }, bulkInsertQuery: function(tableName, attrValueHashes) { - throwMethodUndefined('bulkInsertQuery') + 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) { diff --git a/lib/query-interface.js b/lib/query-interface.js index edf42e1d797e..e3b83241151c 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -272,6 +272,11 @@ module.exports = (function() { 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') diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index 70e64e2b3e37..22c94e2da624 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -200,6 +200,37 @@ describe('QueryGenerator', function() { } ], + 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 + } + ], + updateQuery: [ { arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {id: 2}], diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index 0086582341b8..eabdb314c1b9 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -208,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}], diff --git a/spec-jasmine/sqlite/query-generator.spec.js b/spec-jasmine/sqlite/query-generator.spec.js index 429a4f424340..037af166ea87 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -121,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 }], From 290e89542073f9ec44243baf2a6d772ecd924b34 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Thu, 25 Apr 2013 00:30:17 +0100 Subject: [PATCH 49/86] Refactored and simplified create method plus outline tests --- lib/dao-factory.js | 47 ++++++++--------- lib/dialects/postgres/query.js | 32 ++++++------ lib/query-interface.js | 4 +- spec/dao-factory.spec.js | 93 ++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 46 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 45d174ccdcf6..da96e025b43d 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -331,34 +331,28 @@ module.exports = (function() { /** * Create and insert multiple instances * - * @param {Array} values List of objects (key/value pairs) to create instances from - * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. - * - * The `success` handler is passed a list of newly inserted models. - */ - DAOFactory.prototype.bulkCreate = function(values, options) { - var factory = this - return this.bulkInsert(values.map(function(attrs) { - return factory.build(attrs, options) - })) - } - - /** - * Insert multiple instances - * - * @param {Array} daos List of built DAOs + * @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`. * - * The `success` handler is passed a list of newly inserted models. + * 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.bulkInsert = function(daos, fields) { + DAOFactory.prototype.bulkCreate = function(records, fields) { var self = this - , records = [] + , 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) @@ -369,21 +363,25 @@ module.exports = (function() { } } + // Build records for the fields we know about daos.forEach(function(dao) { var values = {}; fields.forEach(function(field) { - if (dao.values[field] !== undefined) { - values[field] = dao.values[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)) { @@ -397,11 +395,6 @@ module.exports = (function() { } } } - - if (self.options.timestamps && dao.hasOwnProperty(updatedAtAttr)) { - dao[updatedAtAttr] = values[updatedAtAttr] = Utils.now() - } - }) return self.QueryInterface.bulkInsert(self.tableName, records) diff --git a/lib/dialects/postgres/query.js b/lib/dialects/postgres/query.js index 4dbaba9003d6..a83d2057eb8e 100644 --- a/lib/dialects/postgres/query.js +++ b/lib/dialects/postgres/query.js @@ -108,27 +108,27 @@ 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)) { - 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) - } - - this.callee[key] = 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.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.emit('success', this.callee) } else if (this.send('isUpdateQuery')) { - for (var key in rows[0]) { - if (rows[0].hasOwnProperty(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) - } - - this.callee[key] = 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.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 } } diff --git a/lib/query-interface.js b/lib/query-interface.js index e3b83241151c..b49984e87c76 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -262,9 +262,7 @@ module.exports = (function() { QueryInterface.prototype.bulkInsert = function(tableName, records) { var sql = this.QueryGenerator.bulkInsertQuery(tableName, records) - return queryAndEmit.call(this, sql, 'bulkInsert', { - success: function(objs) { objs.forEach(function(v) { v.isNewRecord = false }) } - }) + return queryAndEmit.call(this, sql, 'bulkInsert') } QueryInterface.prototype.update = function(dao, tableName, values, identifier) { diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 074bb38c4c89..c256d8f8d905 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -387,6 +387,99 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) }) + describe('bulkCreate', function() { + + it('inserts multiple values', function(done) { + var self = this + , data = [{ username: 'Peter', secretValue: '42' }, + { username: 'Paul', secretValue: '23'}] + + this.User.bulkCreate(data, ['username']).success(function(users) { + // self.User.find(user.id).success(function(_user) { + // expect(_user.username).toEqual(data.username) + // expect(_user.secretValue).not.toEqual(data.secretValue) + // expect(_user.secretValue).toEqual(null) + done() + }) + }) + + }) // - bulkCreate + + // it('should only store the values passed in the witelist', function(done) { + // var self = this + // , data = { username: 'Peter', secretValue: '42' } + + // this.User.create(data, ['username']).success(function(user) { + // self.User.find(user.id).success(function(_user) { + // expect(_user.username).toEqual(data.username) + // expect(_user.secretValue).not.toEqual(data.secretValue) + // expect(_user.secretValue).toEqual(null) + // done() + // }) + // }) + // }) + + // it('should store all values if no whitelist is specified', function(done) { + // var self = this + // , data = { username: 'Peter', secretValue: '42' } + + // this.User.create(data).success(function(user) { + // self.User.find(user.id).success(function(_user) { + // expect(_user.username).toEqual(data.username) + // expect(_user.secretValue).toEqual(data.secretValue) + // done() + // }) + // }) + // }) + + // it('saves data with single quote', function(done) { + // var quote = "single'quote" + // , self = this + + // this.User.create({ data: quote }).success(function(user) { + // expect(user.data).toEqual(quote, 'memory single quote') + + // self.User.find({where: { id: user.id }}).success(function(user) { + // expect(user.data).toEqual(quote, 'SQL single quote') + // done() + // }) + // }) + // }) + + // it('saves data with double quote', function(done) { + // var quote = 'double"quote' + // , self = this + + // this.User.create({ data: quote }).success(function(user) { + // expect(user.data).toEqual(quote, 'memory double quote') + + // self.User.find({where: { id: user.id }}).success(function(user) { + // expect(user.data).toEqual(quote, 'SQL double quote') + // done() + // }) + // }) + // }) + + // it('saves stringified JSON data', function(done) { + // var json = JSON.stringify({ key: 'value' }) + // , self = this + + // this.User.create({ data: json }).success(function(user) { + // expect(user.data).toEqual(json, 'memory data') + // self.User.find({where: { id: user.id }}).success(function(user) { + // expect(user.data).toEqual(json, 'SQL data') + // done() + // }) + // }) + // }) + + // it('stores the current date in createdAt', function(done) { + // this.User.create({ username: 'foo' }).success(function(user) { + // expect(parseInt(+user.createdAt/5000)).toEqual(parseInt(+new Date()/5000)) + // done() + // }) + // }) + describe('find', function find() { before(function(done) { this.User.create({ From eff68d6bcdd965a1609b4d14b1154af848291a14 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 21:01:34 +0100 Subject: [PATCH 50/86] Refactor given understanding of how it's possible to return autogenerated ids; tests for bulkCreate --- lib/dao-factory.js | 19 ++- lib/dialects/postgres/query-generator.js | 12 ++ spec/dao-factory.spec.js | 198 +++++++++++++---------- 3 files changed, 145 insertions(+), 84 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index da96e025b43d..045f3d106e18 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -404,19 +404,34 @@ module.exports = (function() { * Delete multiple instances * * @param {Object} options Options to describe the scope of the search. - * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ DAOFactory.prototype.bulkDelete = function(options) { if (this.options.timestamps && this.options.paranoid) { var attr = this.options.underscored ? 'deleted_at' : 'deletedAt' var attrValueHash = {} - attrValueHash[attr] = new Date() + attrValueHash[attr] = Utils.now() return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, options) } else { return this.QueryInterface.bulkDelete(this.tableName, options) } } + /** + * Update multiple instances + * + * @param {Object} attrValueHash A hash of fields to change and their new values + * @param {Object} options Options to describe the scope of the search. + * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. + */ + DAOFactory.prototype.bulkUpdate = function(attrValueHash, options) { + if(this.options.timestamps) { + var attr = this.options.underscored ? 'updated_at' : 'updatedAt' + attrValueHash[attr] = Utils.now() + } + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, options) + } + // private var query = function() { diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 57c12abf8da8..3fac092aa24c 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -332,6 +332,18 @@ module.exports = (function() { , tuples = [] Utils._.forEach(attrValueHashes, function(attrValueHash) { + + // don't insert serials (Postgres doesn't like it when they're NULL) + Utils._.forEach(attrValueHash, function(value, key, hash) { + if (tables[tableName] && tables[tableName][key]) { + switch (tables[tableName][key]) { + case 'serial': + delete hash[key] + break + } + } + }); + tuples.push("(" + Utils._.values(attrValueHash).map(function(value){ return QueryGenerator.pgEscape(value) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index c256d8f8d905..7ecd9300cc5d 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -389,96 +389,130 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { describe('bulkCreate', function() { - it('inserts multiple values', function(done) { + 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(users) { - // self.User.find(user.id).success(function(_user) { - // expect(_user.username).toEqual(data.username) - // expect(_user.secretValue).not.toEqual(data.secretValue) - // expect(_user.secretValue).toEqual(null) - done() + this.User.bulkCreate(data, ['username']).success(function() { + self.User.findAll().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() + }) }) }) - }) // - bulkCreate + 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().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().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().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().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().success(function(users) { + expect(users.length).toEqual(2) - // it('should only store the values passed in the witelist', function(done) { - // var self = this - // , data = { username: 'Peter', secretValue: '42' } - - // this.User.create(data, ['username']).success(function(user) { - // self.User.find(user.id).success(function(_user) { - // expect(_user.username).toEqual(data.username) - // expect(_user.secretValue).not.toEqual(data.secretValue) - // expect(_user.secretValue).toEqual(null) - // done() - // }) - // }) - // }) - - // it('should store all values if no whitelist is specified', function(done) { - // var self = this - // , data = { username: 'Peter', secretValue: '42' } - - // this.User.create(data).success(function(user) { - // self.User.find(user.id).success(function(_user) { - // expect(_user.username).toEqual(data.username) - // expect(_user.secretValue).toEqual(data.secretValue) - // done() - // }) - // }) - // }) - - // it('saves data with single quote', function(done) { - // var quote = "single'quote" - // , self = this - - // this.User.create({ data: quote }).success(function(user) { - // expect(user.data).toEqual(quote, 'memory single quote') - - // self.User.find({where: { id: user.id }}).success(function(user) { - // expect(user.data).toEqual(quote, 'SQL single quote') - // done() - // }) - // }) - // }) - - // it('saves data with double quote', function(done) { - // var quote = 'double"quote' - // , self = this - - // this.User.create({ data: quote }).success(function(user) { - // expect(user.data).toEqual(quote, 'memory double quote') - - // self.User.find({where: { id: user.id }}).success(function(user) { - // expect(user.data).toEqual(quote, 'SQL double quote') - // done() - // }) - // }) - // }) - - // it('saves stringified JSON data', function(done) { - // var json = JSON.stringify({ key: 'value' }) - // , self = this - - // this.User.create({ data: json }).success(function(user) { - // expect(user.data).toEqual(json, 'memory data') - // self.User.find({where: { id: user.id }}).success(function(user) { - // expect(user.data).toEqual(json, 'SQL data') - // done() - // }) - // }) - // }) - - // it('stores the current date in createdAt', function(done) { - // this.User.create({ username: 'foo' }).success(function(user) { - // expect(parseInt(+user.createdAt/5000)).toEqual(parseInt(+new Date()/5000)) - // done() - // }) - // }) + 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() + }) + }) + }) + + }) // - bulkCreate describe('find', function find() { before(function(done) { From ea0926f4a3ea45c4793645aa94ae22fa563f07ba Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 21:10:10 +0100 Subject: [PATCH 51/86] Enum test --- spec/dao-factory.spec.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 7ecd9300cc5d..a33a886479a6 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -512,6 +512,28 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) }) + 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('find', function find() { From daf936fe1c352014e2725cc0f7de9dacfa410b98 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 21:23:08 +0100 Subject: [PATCH 52/86] Test for bulk update --- spec/dao-factory.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index a33a886479a6..b22784dbd8a4 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -536,6 +536,33 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) // - bulkCreate + describe('bulkUpdate', 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, data).success(function() { + + self.User.bulkUpdate({username: 'Bill'}, {where: {secretValue: '42'}}) + .success(function() { + self.User.findAll().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") + + done() + }) + }); + }) + }) + + )} + describe('find', function find() { before(function(done) { this.User.create({ From a8a1254217b67a551eac056eb8606ee7bbbce26e Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 21:36:05 +0100 Subject: [PATCH 53/86] Tests for bulkUpdate --- spec/dao-factory.spec.js | 49 +++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index b22784dbd8a4..2056f51570c6 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -395,7 +395,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', secretValue: '23'}] this.User.bulkCreate(data, ['username']).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -415,7 +415,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', secretValue: '23'}] this.User.bulkCreate(data).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -436,7 +436,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', data: quote}] this.User.bulkCreate(data).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -457,7 +457,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', data: quote}] this.User.bulkCreate(data).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -478,7 +478,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', data: json}] this.User.bulkCreate(data).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -498,7 +498,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul'}] this.User.bulkCreate(data).success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(2) expect(users[0].username).toEqual("Peter") @@ -544,11 +544,11 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { { username: 'Paul', secretValue: '42' }, { username: 'Bob', secretValue: '43' }] - this.User.bulkCreate(data, data).success(function() { + this.User.bulkCreate(data).success(function() { - self.User.bulkUpdate({username: 'Bill'}, {where: {secretValue: '42'}}) + self.User.bulkUpdate({username: 'Bill'}, {secretValue: '42'}) .success(function() { - self.User.findAll().success(function(users) { + self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(3) expect(users[0].username).toEqual("Bill") @@ -557,11 +557,38 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { 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.bulkUpdate({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].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) + expect(parseInt(+users[1].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) + expect(parseInt(+users[2].createdAt)).not.toEqual(parseInt(+users[0].createdAt/5000)) + + done() + }) + }) }) }) - )} + }) // - bulkUpdate describe('find', function find() { before(function(done) { From ade36f1e02ef5d90534e8891c2d33fdc97ff00f5 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 22:25:56 +0100 Subject: [PATCH 54/86] Test + fix for bulk delete --- lib/dao-factory.js | 14 ++-- lib/dialects/mysql/query-generator.js | 12 ++++ lib/dialects/postgres/query-generator.js | 26 ++++++++ lib/dialects/query-generator.js | 13 ++++ lib/dialects/sqlite/query-generator.js | 12 ++++ lib/query-interface.js | 2 +- spec-jasmine/mysql/query-generator.spec.js | 13 ++++ spec-jasmine/postgres/query-generator.spec.js | 19 ++++++ spec/dao-factory.spec.js | 65 +++++++++++++++++++ 9 files changed, 168 insertions(+), 8 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 045f3d106e18..11d3c94993b5 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -403,17 +403,17 @@ module.exports = (function() { /** * Delete multiple instances * - * @param {Object} options Options to describe the scope of the search. + * @param {Object} where Options to describe the scope of the search. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAOFactory.prototype.bulkDelete = function(options) { + DAOFactory.prototype.bulkDelete = 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, options) + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where) } else { - return this.QueryInterface.bulkDelete(this.tableName, options) + return this.QueryInterface.bulkDelete(this.tableName, where) } } @@ -421,15 +421,15 @@ module.exports = (function() { * Update multiple instances * * @param {Object} attrValueHash A hash of fields to change and their new values - * @param {Object} options Options to describe the scope of the search. + * @param {Object} where Options to describe the scope of the search. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAOFactory.prototype.bulkUpdate = function(attrValueHash, options) { + DAOFactory.prototype.bulkUpdate = 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, options) + return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where) } // private diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index eda65cb72937..ed4d2e4df075 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -296,6 +296,18 @@ module.exports = (function() { return query }, + bulkDeleteQuery: function(tableName, where, options) { + options = options || {} + + var query = "DELETE FROM <%= table %> WHERE <%= where %>" + var replacements = { + table: QueryGenerator.addQuotes(tableName), + where: QueryGenerator.getWhereConditions(where) + } + + return Utils._.template(query)(replacements) + }, + incrementQuery: function (tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index 3fac092aa24c..f07d7ccfd2f2 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -410,6 +410,32 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkDeleteQuery: function(tableName, where, options) { + options = options || {} + + primaryKeys[tableName] = primaryKeys[tableName] || []; + + var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %>)" + + var pks; + if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) { + pks = primaryKeys[tableName].map(function(pk) { + return QueryGenerator.addQuotes(pk) + }).join(',') + } else { + pks = QueryGenerator.addQuotes('id') + } + + var replacements = { + table: QueryGenerator.addQuotes(tableName), + where: QueryGenerator.getWhereConditions(where), + primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, + primaryKeysSelection: pks + } + + return Utils._.template(query)(replacements) + }, + incrementQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/dialects/query-generator.js b/lib/dialects/query-generator.js index 033804cac22e..b9ea58e47407 100644 --- a/lib/dialects/query-generator.js +++ b/lib/dialects/query-generator.js @@ -155,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. + */ + deleteQuery: function(tableName, where, options) { + throwMethodUndefined('bulkDeleteQuery') + }, + /* Returns an update query. Parameters: diff --git a/lib/dialects/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 6d35dcb78358..9fcf8e2d215a 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -179,6 +179,18 @@ module.exports = (function() { return Utils._.template(query)(replacements) }, + bulkDeleteQuery: function(tableName, where, options) { + options = options || {} + + var query = "DELETE FROM <%= table %> WHERE <%= where %>" + var replacements = { + table: Utils.addTicks(tableName), + where: this.getWhereConditions(where) + } + + return Utils._.template(query)(replacements) + }, + incrementQuery: function(tableName, attrValueHash, where) { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) diff --git a/lib/query-interface.js b/lib/query-interface.js index b49984e87c76..7836dda10eed 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -281,7 +281,7 @@ module.exports = (function() { } QueryInterface.prototype.bulkDelete = function(tableName, identifier) { - var sql = this.QueryGenerator.deleteQuery(tableName, identifier) + var sql = this.QueryGenerator.bulkDeleteQuery(tableName, identifier) return queryAndEmit.call(this, sql, 'bulkDelete') } diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index 22c94e2da624..bdbb5abe7378 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -274,6 +274,19 @@ describe('QueryGenerator', function() { } ], + bulkDeleteQuery: [ + { + arguments: ['myTable', {name: 'foo'}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo'" + }, { + arguments: ['myTable', 1], + expectation: "DELETE FROM `myTable` WHERE `id`=1" + }, { + arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}], + expectation: "DELETE FROM `myTable` WHERE `name`='foo\\';DROP TABLE myTable;'" + } + ], + addIndexQuery: [ { arguments: ['User', ['username', 'isAdmin']], diff --git a/spec-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index eabdb314c1b9..3409e88df7dc 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -307,6 +307,25 @@ describe('QueryGenerator', function() { } ], + bulkDeleteQuery: [ + { + arguments: ['myTable', {name: 'foo'}], + expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo')" + }, { + arguments: ['myTable', 1], + expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1)" + }, { + arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}], + expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo'';DROP TABLE myTable;')" + }, { + arguments: ['mySchema.myTable', {name: 'foo'}], + expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo')" + }, { + arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}], + expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;')" + } + ], + addIndexQuery: [ { arguments: ['User', ['username', 'isAdmin']], diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index 2056f51570c6..c7f83f25f99c 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -590,6 +590,71 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) // - bulkUpdate + describe('bulkDelete', 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.bulkDelete({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.bulkDelete({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)) + expect(parseInt(+users[2].deletedAt)).not.toEqual(parseInt(+new Date()/5000)) + + done() + }) + }) + }) + + }) + + }) + + }) // - bulkDelete + describe('find', function find() { before(function(done) { this.User.create({ From 65db2ad095e35e3269b8f60c2e5b4e081f08b2af Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 22:27:40 +0100 Subject: [PATCH 55/86] Changelog entry --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 619fcb290ba1..751fce4d4baa 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel - [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 (``.bulkUpdate()`) and delete (``.bulkDelete()`) # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From 31ec3a194d3324073ecd01fe8e93ab1bb9568a18 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sat, 27 Apr 2013 22:38:08 +0100 Subject: [PATCH 56/86] Fix typo in interface method name --- lib/dialects/query-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dialects/query-generator.js b/lib/dialects/query-generator.js index b9ea58e47407..ea5b4a4cf8f4 100644 --- a/lib/dialects/query-generator.js +++ b/lib/dialects/query-generator.js @@ -164,7 +164,7 @@ module.exports = (function() { OR a string with conditions (e.g. 'name="foo"'). If you use a string, you have to escape it on your own. */ - deleteQuery: function(tableName, where, options) { + bulkDeleteQuery: function(tableName, where, options) { throwMethodUndefined('bulkDeleteQuery') }, From 3b951e13eaa441dc47b74f797dd11854d3c60733 Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Sun, 28 Apr 2013 14:03:55 +0100 Subject: [PATCH 57/86] Rename and rationalise methods as per discussion on GitHub --- changelog.md | 2 +- lib/dao-factory.js | 4 +- lib/dialects/mysql/query-generator.js | 13 +++- lib/dialects/postgres/query-generator.js | 74 ++++++------------- lib/dialects/sqlite/query-generator.js | 15 +--- lib/query-interface.js | 2 +- spec-jasmine/mysql/query-generator.spec.js | 14 +--- spec-jasmine/postgres/query-generator.spec.js | 20 +---- spec-jasmine/sqlite/query-generator.spec.js | 19 +++++ spec/dao-factory.spec.js | 16 ++-- 10 files changed, 69 insertions(+), 110 deletions(-) diff --git a/changelog.md b/changelog.md index 751fce4d4baa..fc4d42199245 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,7 @@ - [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel - [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 (``.bulkUpdate()`) and delete (``.bulkDelete()`) +- [FEATURE] Support for bulk insert (`.bulkCreate()`, update (``.update()`) and delete (``.destroy()`) # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 11d3c94993b5..3f2f5c314598 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -406,7 +406,7 @@ module.exports = (function() { * @param {Object} where Options to describe the scope of the search. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAOFactory.prototype.bulkDelete = function(where) { + DAOFactory.prototype.destroy = function(where) { if (this.options.timestamps && this.options.paranoid) { var attr = this.options.underscored ? 'deleted_at' : 'deletedAt' var attrValueHash = {} @@ -424,7 +424,7 @@ module.exports = (function() { * @param {Object} where Options to describe the scope of the search. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. */ - DAOFactory.prototype.bulkUpdate = function(attrValueHash, where) { + DAOFactory.prototype.update = function(attrValueHash, where) { if(this.options.timestamps) { var attr = this.options.underscored ? 'updated_at' : 'updatedAt' attrValueHash[attr] = Utils.now() diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index ed4d2e4df075..fdd373c14aae 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -285,13 +285,20 @@ module.exports = (function() { deleteQuery: function(tableName, where, options) { options = options || {} - options.limit = options.limit || 1 var table = QueryGenerator.addQuotes(tableName) var where = QueryGenerator.getWhereConditions(where) - var limit = Utils.escape(options.limit) + var limit = "" - var query = "DELETE FROM " + table + " WHERE " + where + " LIMIT " + limit + if(Utils._.isUndefined(options.limit)) { + options.limit = 1; + } + + if(!!options.limit) { + limit = " LIMIT " + Utils.escape(options.limit) + } + + var query = "DELETE FROM " + table + " WHERE " + where + limit return query }, diff --git a/lib/dialects/postgres/query-generator.js b/lib/dialects/postgres/query-generator.js index f07d7ccfd2f2..5a81693728df 100644 --- a/lib/dialects/postgres/query-generator.js +++ b/lib/dialects/postgres/query-generator.js @@ -301,18 +301,7 @@ module.exports = (function() { attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) 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) @@ -332,18 +321,7 @@ module.exports = (function() { , tuples = [] Utils._.forEach(attrValueHashes, function(attrValueHash) { - - // don't insert serials (Postgres doesn't like it when they're NULL) - Utils._.forEach(attrValueHash, function(value, key, hash) { - if (tables[tableName] && tables[tableName][key]) { - switch (tables[tableName][key]) { - case 'serial': - delete hash[key] - break - } - } - }); - + removeSerialsFromHash(tableName, attrValueHash) tuples.push("(" + Utils._.values(attrValueHash).map(function(value){ return QueryGenerator.pgEscape(value) @@ -384,38 +362,14 @@ module.exports = (function() { deleteQuery: function(tableName, where, options) { options = options || {} - 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 pks; - if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) { - pks = primaryKeys[tableName].map(function(pk) { - return QueryGenerator.addQuotes(pk) - }).join(',') - } else { - pks = QueryGenerator.addQuotes('id') - } - var replacements = { - table: QueryGenerator.addQuotes(tableName), - where: QueryGenerator.getWhereConditions(where), - limit: QueryGenerator.pgEscape(options.limit), - primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, - primaryKeysSelection: pks + if(Utils._.isUndefined(options.limit)) { + options.limit = 1; } - return Utils._.template(query)(replacements) - }, - - bulkDeleteQuery: function(tableName, where, options) { - options = options || {} - primaryKeys[tableName] = primaryKeys[tableName] || []; - var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %>)" + var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %><%= limit %>)" var pks; if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) { @@ -429,6 +383,7 @@ module.exports = (function() { var replacements = { table: QueryGenerator.addQuotes(tableName), where: QueryGenerator.getWhereConditions(where), + limit: !!options.limit? " LIMIT " + QueryGenerator.pgEscape(options.limit) : "", primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, primaryKeysSelection: pks } @@ -782,5 +737,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/sqlite/query-generator.js b/lib/dialects/sqlite/query-generator.js index 9fcf8e2d215a..fe850d29f444 100644 --- a/lib/dialects/sqlite/query-generator.js +++ b/lib/dialects/sqlite/query-generator.js @@ -172,20 +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) - } - - return Utils._.template(query)(replacements) - }, - - bulkDeleteQuery: function(tableName, where, options) { - options = options || {} - - var query = "DELETE FROM <%= table %> WHERE <%= where %>" - var replacements = { - table: Utils.addTicks(tableName), - where: this.getWhereConditions(where) + where: MySqlQueryGenerator.getWhereConditions(where) } return Utils._.template(query)(replacements) diff --git a/lib/query-interface.js b/lib/query-interface.js index 7836dda10eed..8a8287c5efeb 100644 --- a/lib/query-interface.js +++ b/lib/query-interface.js @@ -281,7 +281,7 @@ module.exports = (function() { } QueryInterface.prototype.bulkDelete = function(tableName, identifier) { - var sql = this.QueryGenerator.bulkDeleteQuery(tableName, identifier) + var sql = this.QueryGenerator.deleteQuery(tableName, identifier, {limit: null}) return queryAndEmit.call(this, sql, 'bulkDelete') } diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index bdbb5abe7378..d34d9f8c34c6 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -271,19 +271,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" - } - ], - - bulkDeleteQuery: [ - { - arguments: ['myTable', {name: 'foo'}], - expectation: "DELETE FROM `myTable` WHERE `name`='foo'" - }, { - arguments: ['myTable', 1], - expectation: "DELETE FROM `myTable` WHERE `id`=1" }, { - arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}], - 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-jasmine/postgres/query-generator.spec.js b/spec-jasmine/postgres/query-generator.spec.js index 3409e88df7dc..606ea19dca80 100644 --- a/spec-jasmine/postgres/query-generator.spec.js +++ b/spec-jasmine/postgres/query-generator.spec.js @@ -304,25 +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)" - } - ], - - bulkDeleteQuery: [ - { - arguments: ['myTable', {name: 'foo'}], - expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo')" - }, { - arguments: ['myTable', 1], - expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1)" - }, { - arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}], - expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo'';DROP TABLE myTable;')" }, { - arguments: ['mySchema.myTable', {name: 'foo'}], - expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo')" - }, { - arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}], - expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;')" + 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 037af166ea87..8dd60a4081d9 100644 --- a/spec-jasmine/sqlite/query-generator.spec.js +++ b/spec-jasmine/sqlite/query-generator.spec.js @@ -189,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/dao-factory.spec.js b/spec/dao-factory.spec.js index c7f83f25f99c..f4c5ba6fa71c 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -536,7 +536,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) // - bulkCreate - describe('bulkUpdate', function() { + describe('update', function() { it('updates only values that match filter', function(done) { var self = this @@ -546,7 +546,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { this.User.bulkCreate(data).success(function() { - self.User.bulkUpdate({username: 'Bill'}, {secretValue: '42'}) + self.User.update({username: 'Bill'}, {secretValue: '42'}) .success(function() { self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(3) @@ -569,7 +569,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { this.User.bulkCreate(data).success(function() { - self.User.bulkUpdate({username: 'Bill'}, {secretValue: '42'}) + self.User.update({username: 'Bill'}, {secretValue: '42'}) .success(function() { self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(3) @@ -588,9 +588,9 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) }) - }) // - bulkUpdate + }) // - update - describe('bulkDelete', function() { + describe('destroy', function() { it('deletes values that match filter', function(done) { var self = this @@ -600,7 +600,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { this.User.bulkCreate(data).success(function() { - self.User.bulkDelete({secretValue: '42'}) + self.User.destroy({secretValue: '42'}) .success(function() { self.User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(1) @@ -631,7 +631,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { User.bulkCreate(data).success(function() { - User.bulkDelete({secretValue: '42'}) + User.destroy({secretValue: '42'}) .success(function() { User.findAll({order: 'id'}).success(function(users) { expect(users.length).toEqual(3) @@ -653,7 +653,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { }) - }) // - bulkDelete + }) // - destroy describe('find', function find() { before(function(done) { From d2e9adcb9700d874a5695bda9a46facd59e00def Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Mon, 6 May 2013 23:56:19 +0100 Subject: [PATCH 58/86] Rebase onto sequelize master and reflect MySQL query generator refactoring there --- lib/dialects/mysql/query-generator.js | 27 ++++++++++++--------------- lib/dialects/postgres/query.js | 2 ++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index fdd373c14aae..659408602b44 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -244,8 +244,7 @@ module.exports = (function() { }, bulkInsertQuery: function(tableName, attrValueHashes) { - var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;" - , tuples = [] + var tuples = [] Utils._.forEach(attrValueHashes, function(attrValueHash) { tuples.push("(" + @@ -255,13 +254,12 @@ module.exports = (function() { ")") }) - var replacements = { - table: QueryGenerator.addQuotes(tableName), - attributes: Object.keys(attrValueHashes[0]).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(","), - tuples: tuples.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 Utils._.template(query)(replacements) + return query }, updateQuery: function(tableName, attrValueHash, where) { @@ -306,13 +304,12 @@ module.exports = (function() { bulkDeleteQuery: function(tableName, where, options) { options = options || {} - var query = "DELETE FROM <%= table %> WHERE <%= where %>" - var replacements = { - table: QueryGenerator.addQuotes(tableName), - where: QueryGenerator.getWhereConditions(where) - } + var table = QueryGenerator.addQuotes(tableName) + var where = QueryGenerator.getWhereConditions(where) + + var query = "DELETE FROM " + table + " WHERE " + where - return Utils._.template(query)(replacements) + return query }, incrementQuery: function (tableName, attrValueHash, where) { @@ -326,7 +323,7 @@ module.exports = (function() { values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " +Utils.escape(_value)) } - + var table = QueryGenerator.addQuotes(tableName) var values = values.join(",") var where = QueryGenerator.getWhereConditions(where) diff --git a/lib/dialects/postgres/query.js b/lib/dialects/postgres/query.js index a83d2057eb8e..61f045468de8 100644 --- a/lib/dialects/postgres/query.js +++ b/lib/dialects/postgres/query.js @@ -116,6 +116,7 @@ module.exports = (function() { record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) } this.callee[key] = record + } } } @@ -129,6 +130,7 @@ module.exports = (function() { record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record) } this.callee[key] = record + } } } From f5d935964ee0ed6f657049e145db9dd69720bb6c Mon Sep 17 00:00:00 2001 From: Martin Aspeli Date: Tue, 7 May 2013 23:48:38 +0100 Subject: [PATCH 59/86] Update tests thanks to @janmeier's review --- spec/dao-factory.spec.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index f4c5ba6fa71c..a9ef5e27c9bb 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -551,9 +551,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), 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") + users.forEach(function (user) { + if (user.secretValue == '42') { + expect(user.username).toEqual("Bill") + } else { + expect(user.username).toEqual("Bob") + } + }) done() }) @@ -578,9 +582,8 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { expect(users[1].username).toEqual("Bill") expect(users[2].username).toEqual("Bob") - expect(parseInt(+users[0].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) - expect(parseInt(+users[1].createdAt/5000)).toEqual(parseInt(+new Date()/5000)) - expect(parseInt(+users[2].createdAt)).not.toEqual(parseInt(+users[0].createdAt/5000)) + expect(parseInt(+users[0].updatedAt/5000)).toEqual(parseInt(+new Date()/5000)) + expect(parseInt(+users[1].updatedAt/5000)).toEqual(parseInt(+new Date()/5000)) done() }) @@ -642,7 +645,6 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { expect(parseInt(+users[0].deletedAt/5000)).toEqual(parseInt(+new Date()/5000)) expect(parseInt(+users[1].deletedAt/5000)).toEqual(parseInt(+new Date()/5000)) - expect(parseInt(+users[2].deletedAt)).not.toEqual(parseInt(+new Date()/5000)) done() }) From 00f5f771df8c62a9eec7680f2a8467be92843c63 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Thu, 9 May 2013 23:21:11 +0200 Subject: [PATCH 60/86] Change roadmap for bulk update, insert, delete --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37f84b06592f..3e9b239cdb32 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl - 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) From ecd16720dc8010d588a0c088d91aff415d067f76 Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 9 May 2013 17:05:09 -0400 Subject: [PATCH 61/86] Added final changes as requested by sdepold --- README.md | 3 ++- changelog.md | 1 + lib/dao.js | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index acef1ade547b..0770865c4e5f 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl - Support for update and delete calls for whole tables without previous loading of instances - 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) +- ~~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 diff --git a/changelog.md b/changelog.md index 619fcb290ba1..178ba6bc2213 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,5 @@ # v1.7.0 # +- [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 diff --git a/lib/dao.js b/lib/dao.js index 5bc8594c7b89..0207307bad12 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -218,9 +218,9 @@ module.exports = (function() { Utils._.each(self.values, function(value, field) { // if field has validators - var allowsNulls = (self.rawAttributes[field].allowNull && self.rawAttributes[field].allowNull === true && (value === null || value === undefined)); + var hasAllowedNull = (self.rawAttributes[field].allowNull && self.rawAttributes[field].allowNull === true && (value === null || value === undefined)); - if (self.validators.hasOwnProperty(field) && !allowsNulls) { + if (self.validators.hasOwnProperty(field) && !hasAllowedNull) { // for each validator Utils._.each(self.validators[field], function(details, validatorType) { From 054a6031d71eacb2f22e1369b0378d1ffa2d39e6 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Fri, 10 May 2013 00:23:34 +0300 Subject: [PATCH 62/86] Added link to bulk insert, update, delete --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index fc4d42199245..d66890b785fb 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,7 @@ - [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel - [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()`) +- [FEATURE] Support for bulk insert (`.bulkCreate()`, update (`.update()`) and delete (`.destroy()`) [#569](https://github.com/sequelize/sequelize/pull/569). thanks to optilude # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From eea3828211a8a136ca29cfb10bfa60f4579313b9 Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 9 May 2013 17:42:05 -0400 Subject: [PATCH 63/86] Some PR broke this concept, so I'm not safely type checking all the way down for HSTORE. --- lib/dialects/postgres/query.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dialects/postgres/query.js b/lib/dialects/postgres/query.js index 61f045468de8..6dff30fed944 100644 --- a/lib/dialects/postgres/query.js +++ b/lib/dialects/postgres/query.js @@ -112,7 +112,7 @@ module.exports = (function() { for (var key in rows[0]) { if (rows[0].hasOwnProperty(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) { + 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 @@ -126,7 +126,7 @@ module.exports = (function() { for (var key in rows[0]) { if (rows[0].hasOwnProperty(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) { + 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 From d066f936d29f7209e437b1ca8a96f6c999d2606b Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Thu, 9 May 2013 19:19:43 -0400 Subject: [PATCH 64/86] Upgraded validation for IPv6 support. Closes #371 --- changelog.md | 3 ++- package.json | 2 +- spec/dao.validations.spec.js | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 5a105248bfb0..d8b316c76895 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,6 @@ # v1.7.0 # -- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601), thanks to durango +- [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 diff --git a/package.json b/package.json index 7f0f8e3ac60a..99469ee43d21 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "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", 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" From b168e5577bf4c854a855bc747fcb96e07215cd2e Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Fri, 10 May 2013 19:59:32 +0200 Subject: [PATCH 65/86] error messages ftw --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 006490e65392..eab181e13326 100644 --- a/README.md +++ b/README.md @@ -71,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 ## From 5e8eac2adb02761e73a722c6eeaaa56a5b5e0422 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Fri, 10 May 2013 14:19:45 -0400 Subject: [PATCH 66/86] Add failing case for MySQL query gen with bool. (Issue #607) --- spec-jasmine/mysql/query-generator.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index d34d9f8c34c6..aed6c470b089 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -197,6 +197,12 @@ 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);" } ], @@ -228,6 +234,9 @@ describe('QueryGenerator', function() { 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);" } ], @@ -255,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'" } ], From 42a44f101b061b7b510ac084896835b269d0cbc1 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Fri, 10 May 2013 15:04:33 -0400 Subject: [PATCH 67/86] MySQL: Outgoing booleans are turned into ints In the MySQL dialect, values used to generate insert, bulk insert, and update queries are now checked for boolean-ness and will be turned into an int. There was already duplicated logic applied to outgoing values to check if the value was a Date or not. I factored out value processing to a single function and added a check for typeof boolean. The new function also escapes the return value since that was also being done everywhere. Fixes #607 --- lib/dialects/mysql/query-generator.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 659408602b44..d53b97d8dacb 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -2,6 +2,16 @@ 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) { @@ -234,9 +244,7 @@ module.exports = (function() { var table = QueryGenerator.addQuotes(tableName) var attributes = Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(",") - var values = Utils._.values(attrValueHash).map(function(value){ - return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value) - }).join(",") + var values = Utils._.values(attrValueHash).map(processAndEscapeValue).join(",") var query = "INSERT INTO " + table + " (" + attributes + ") VALUES (" + values + ");" @@ -248,9 +256,7 @@ module.exports = (function() { Utils._.forEach(attrValueHashes, function(attrValueHash) { tuples.push("(" + - Utils._.values(attrValueHash).map(function(value){ - return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value) - }).join(",") + + Utils._.values(attrValueHash).map(processAndEscapeValue).join(",") + ")") }) @@ -269,9 +275,9 @@ module.exports = (function() { 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 query = "UPDATE " + QueryGenerator.addQuotes(tableName) + @@ -319,9 +325,9 @@ module.exports = (function() { 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 table = QueryGenerator.addQuotes(tableName) From 72d2737b7dc57b2162611900e030282b894f6648 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Fri, 10 May 2013 15:10:35 -0400 Subject: [PATCH 68/86] Minor low-hanging changes to improve jshint performance --- lib/dialects/mysql/query-generator.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index d53b97d8dacb..bc15e09b9f20 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -15,7 +15,7 @@ var processAndEscapeValue = function(value) { 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) @@ -141,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 %>')({ @@ -291,7 +291,7 @@ module.exports = (function() { options = options || {} var table = QueryGenerator.addQuotes(tableName) - var where = QueryGenerator.getWhereConditions(where) + where = QueryGenerator.getWhereConditions(where) var limit = "" if(Utils._.isUndefined(options.limit)) { @@ -311,7 +311,7 @@ module.exports = (function() { options = options || {} var table = QueryGenerator.addQuotes(tableName) - var where = QueryGenerator.getWhereConditions(where) + where = QueryGenerator.getWhereConditions(where) var query = "DELETE FROM " + table + " WHERE " + where @@ -331,8 +331,8 @@ module.exports = (function() { } var table = QueryGenerator.addQuotes(tableName) - var values = values.join(",") - var where = QueryGenerator.getWhereConditions(where) + values = values.join(",") + where = QueryGenerator.getWhereConditions(where) var query = "UPDATE " + table + " SET " + values + " WHERE " + where @@ -431,7 +431,7 @@ module.exports = (function() { if (Array.isArray(value)) { // is value an array? - if (value.length == 0) { value = [null] } + if (value.length === 0) { value = [null] } _value = "(" + value.map(function(subValue) { return Utils.escape(subValue); }).join(',') + ")" @@ -481,7 +481,7 @@ module.exports = (function() { template += " auto_increment" } - if ((dataType.defaultValue != undefined) && (dataType.defaultValue != DataTypes.NOW)) { + if ((dataType.defaultValue !== undefined) && (dataType.defaultValue != DataTypes.NOW)) { template += " DEFAULT " + Utils.escape(dataType.defaultValue) } From 026efd4b030dcc19ac1ae613b3692af78781cf56 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Fri, 10 May 2013 15:43:02 -0400 Subject: [PATCH 69/86] Failing tests for MySQL where clause value processing --- spec-jasmine/mysql/query-generator.spec.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec-jasmine/mysql/query-generator.spec.js b/spec-jasmine/mysql/query-generator.spec.js index aed6c470b089..58387185414b 100644 --- a/spec-jasmine/mysql/query-generator.spec.js +++ b/spec-jasmine/mysql/query-generator.spec.js @@ -340,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')" } ] } From 3f20fb5a92c59c87ad4fa8df2350d2d34102eab2 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Fri, 10 May 2013 15:43:26 -0400 Subject: [PATCH 70/86] Process values going into WHERE clauses in MySQL --- lib/dialects/mysql/query-generator.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index bc15e09b9f20..f77e5e026e85 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -432,19 +432,17 @@ 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(',') + ")" + _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("=")) } } From 46f5caf93c702fea1c3670b1005dab122b435ccf Mon Sep 17 00:00:00 2001 From: terraflubb Date: Sat, 11 May 2013 00:20:04 -0400 Subject: [PATCH 71/86] Migration environment set from command line args Still defaults to 'development' if NODE_ENV isn't set so backwards compatibility is ensured. --- bin/sequelize | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/sequelize b/bin/sequelize index fbd6f5544d7d..aee6d5fd9ddd 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -8,6 +8,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()) @@ -52,10 +53,9 @@ var createMigrationsFolder = function(force) { var readConfig = function() { try { var config = JSON.parse(fs.readFileSync(configFile)) - , env = process.env.NODE_ENV || 'development' - if (config[env]) { - config = config[env] + if (config[environment]) { + config = config[environment] } return config @@ -68,11 +68,17 @@ program .version(packageJson.version) .option('-i, --init', 'Initializes the project. Creates a config/config.json') .option('-m, --migrate', 'Runs undone migrations') + .option('-e, --env ', 'Specify the environment.') .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.') .parse(process.argv) +if(typeof program.env === 'string') { + environment = program.env +} +console.log("Using environment '" + environment + "'.") + if(program.migrate) { if(configFileExists) { var config = readConfig() From 37244f1873f4af9f08c9e035b14c74f5ad482dba Mon Sep 17 00:00:00 2001 From: terraflubb Date: Sat, 11 May 2013 00:27:51 -0400 Subject: [PATCH 72/86] (Subjectively) improve copy in binary Also replaced one thrown error with a console.log. The rest of the errors were not thrown, and since this file is run from the shell, it will be an edge case that it will ever be caught. It looks messy when it happens. So it now returns an exit code of 1 to indicate things went sideways. --- bin/sequelize | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bin/sequelize b/bin/sequelize index aee6d5fd9ddd..91143de3f650 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -66,12 +66,12 @@ var readConfig = function() { 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') { @@ -110,7 +110,8 @@ if(program.migrate) { sequelize.migrate() } } else { - throw new Error('Please add a configuration file under config/config.json. You might run "sequelize --init".') + console.log('Cannot find "config/config.json". Have you run "sequelize --init"?') + process.exit(1) } } else if(program.init) { if(!configFileExists || !!program.force) { @@ -135,9 +136,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) @@ -162,5 +164,5 @@ if(program.migrate) { fs.writeFileSync(migrationsPath + '/' + migrationName, migrationContent) } else { - console.log('Please define any params!') + console.log('Try "sequelize --help" for usage information.') } From 6f65e6dec64c2f4a247a2c755bd0b75d319e3beb Mon Sep 17 00:00:00 2001 From: terraflubb Date: Sat, 11 May 2013 00:38:55 -0400 Subject: [PATCH 73/86] Include a done() in the migration skeleton Because I always forget to add one to 'down' and then my migrations don't undo. Also upgraded a ==. --- bin/sequelize | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/sequelize b/bin/sequelize index 91143de3f650..df8c20c65e84 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -148,16 +148,18 @@ 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, done) {", " // add altering commands here, calling 'done' when finished", + " done()", " },", " down: function(migration, DataTypes, done) {", " // add reverting commands here, calling 'done' when finished", + " done()", " }", "}" ].join('\n') From 4c23c8a103f6e7a7752529ed9044891b9c39cd4c Mon Sep 17 00:00:00 2001 From: terraflubb Date: Sat, 11 May 2013 01:38:27 -0400 Subject: [PATCH 74/86] Throw nicer errors when we can't read config.json And quit with a code after outputting it. --- bin/sequelize | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/bin/sequelize b/bin/sequelize index df8c20c65e84..191b4d9545f7 100755 --- a/bin/sequelize +++ b/bin/sequelize @@ -51,17 +51,23 @@ var createMigrationsFolder = function(force) { } var readConfig = function() { + var config try { - var config = JSON.parse(fs.readFileSync(configFile)) + config = fs.readFileSync(configFile) + } catch(e) { + throw new Error('Error reading "config/config.json".') + } - if (config[environment]) { - config = config[environment] - } + try { + config = JSON.parse(config) + } catch (e) { + throw new Error('Error parsing "config/config.json" as JSON.') + } - return config - } catch(e) { - throw new Error('The config.json is not available or contains invalid JSON.') + if (config[environment]) { + config = config[environment] } + return config } program @@ -81,9 +87,16 @@ console.log("Using environment '" + environment + "'.") if(program.migrate) { if(configFileExists) { - var config = readConfig() + var config , 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 From 61482c0b1c3fa7fc7652a1e8b174a084d151e943 Mon Sep 17 00:00:00 2001 From: terraflubb Date: Sat, 11 May 2013 01:41:54 -0400 Subject: [PATCH 75/86] Make migrations more verbose and time migrations --- lib/migrator.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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) { From eda888a60a6a5625758596d436e7b878825ba7b1 Mon Sep 17 00:00:00 2001 From: William Riancho Date: Sat, 11 May 2013 14:09:06 +0200 Subject: [PATCH 76/86] DataTypes improved --- lib/data-types.js | 148 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 4 deletions(-) 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() { From 3935784dd2964a6a705980720900358321c5c028 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sat, 11 May 2013 20:55:59 +0200 Subject: [PATCH 77/86] package.json now links to the right repo --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 99469ee43d21..05b5142f74a7 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ ], "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": { "lodash": "~1.2.1", From ae1d4f6bc6a85e64600b3f20d6dea5b5a371dbfe Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sat, 11 May 2013 22:04:57 +0200 Subject: [PATCH 78/86] Allow find and findall to take query options --- lib/dao-factory.js | 17 +++++---- spec/dao-factory.spec.js | 79 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/lib/dao-factory.js b/lib/dao-factory.js index 3f2f5c314598..ea31b8c875b8 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -156,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) @@ -177,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 @@ -199,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 data * @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? @@ -253,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) { diff --git a/spec/dao-factory.spec.js b/spec/dao-factory.spec.js index a9ef5e27c9bb..fd7b4da00f4b 100644 --- a/spec/dao-factory.spec.js +++ b/spec/dao-factory.spec.js @@ -1060,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() { @@ -1292,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 From 3d1043c54dc9062a27f914206fb7097ec03a4215 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sat, 11 May 2013 22:19:05 +0200 Subject: [PATCH 79/86] update changelog --- changelog.md | 1 + lib/dao-factory.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d8b316c76895..2e883f1fc3ea 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ - [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 # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work diff --git a/lib/dao-factory.js b/lib/dao-factory.js index ea31b8c875b8..b8bc36e8b245 100644 --- a/lib/dao-factory.js +++ b/lib/dao-factory.js @@ -199,7 +199,7 @@ 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 data + * @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, queryOptions) { From 023e751650b8ec416c961d458e3c7fc3215ec696 Mon Sep 17 00:00:00 2001 From: Jan Aagaard Meier Date: Sat, 11 May 2013 23:00:48 +0200 Subject: [PATCH 80/86] Yay TRAVIS --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 01b66ce02cb3f9a517be6826208222bd2878469f Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 20:44:51 +0200 Subject: [PATCH 81/86] added tests for new data types --- spec/data-types.spec.js | 52 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) 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]) + }) + }) +}) From 6d5b8a196c1e3e4ef23b21e6518a7d92437860f2 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 20:55:31 +0200 Subject: [PATCH 82/86] convenient data types --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 2e883f1fc3ea..92222fc38e23 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - [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 # v1.6.0 # - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work From b4361e6a58e1e3d05253f42723e950ae5dc527ff Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 21:19:52 +0200 Subject: [PATCH 83/86] fixed logging --- spec/migrator.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 2514c2de58c30eb53c2671b887cccac636611a8f Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 21:21:18 +0200 Subject: [PATCH 84/86] more verbose binary --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 92222fc38e23..62ab845d6e95 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - [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 From 1f2ef88532eaf98876a2ae506091b7065730a2c1 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 21:50:12 +0200 Subject: [PATCH 85/86] a test that makes sure that null values are correctly restored --- spec/dao.spec.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/dao.spec.js b/spec/dao.spec.js index 81f35ea127f9..b86d46c903ac 100644 --- a/spec/dao.spec.js +++ b/spec/dao.spec.js @@ -644,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() + }) + }) + }) + }) + }) + }) }) From 2cf8d708672885e6f92cef1d0c4f68af36f6a807 Mon Sep 17 00:00:00 2001 From: Sascha Depold Date: Mon, 13 May 2013 21:56:33 +0200 Subject: [PATCH 86/86] boolean handling --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 62ab845d6e95..157526dff2f8 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - [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