From 85f931df1eee44380fd406ff4ccd9eb5bc36a698 Mon Sep 17 00:00:00 2001 From: Daniel Durante Date: Wed, 2 Oct 2013 17:54:36 -0400 Subject: [PATCH] Adds sequelize.literal() and sequelize.cast() sequelize.literal() will tell Sequelize to not escape whatever you type into the argument, for example: sequelize.literal('1-2') will result in "1-2" unescaped. sequelize.cast(input, type) will cast input into type, for example: sequelize.cast('1-2', 'integer') will result in: ```CAST('1-2' AS INTEGER)``` --- lib/dialects/abstract/query-generator.js | 10 +-- lib/sequelize.js | 8 ++ lib/utils.js | 27 ++++++- test/dao-factory.test.js | 93 ++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/lib/dialects/abstract/query-generator.js b/lib/dialects/abstract/query-generator.js index fced09f8d8cb..913f22bc34ee 100644 --- a/lib/dialects/abstract/query-generator.js +++ b/lib/dialects/abstract/query-generator.js @@ -281,7 +281,7 @@ module.exports = (function() { Objects: * If raw is set, that value should be returned verbatim, without quoting * If fn is set, the string should start with the value of fn, starting paren, followed by - the values of cols (which is assumed to be an array), quoted and joined with ', ', + the values of cols (which is assumed to be an array), quoted and joined with ', ', unless they are themselves objects * If direction is set, should be prepended @@ -293,7 +293,7 @@ module.exports = (function() { return this.quoteIdentifiers(obj, force) } else if (Array.isArray(obj)) { return this.quote(obj[0], force) + ' ' + obj[1] - } else if (obj instanceof Utils.fn || obj instanceof Utils.col) { + } else if (obj instanceof Utils.fn || obj instanceof Utils.col || obj instanceof Utils.literal || obj instanceof Utils.cast) { return obj.toString(this) } else if (Utils._.isObject(obj) && 'raw' in obj) { return obj.raw @@ -363,11 +363,11 @@ module.exports = (function() { Escape a value (e.g. a string, number or date) */ escape: function(value, field) { - if (value instanceof Utils.fn || value instanceof Utils.col) { + if (value instanceof Utils.fn || value instanceof Utils.col || value instanceof Utils.literal || value instanceof Utils.cast) { return value.toString(this) } else { - return SqlString.escape(value, false, null, this.dialect, field) - } + return SqlString.escape(value, false, null, this.dialect, field) + } } } diff --git a/lib/sequelize.js b/lib/sequelize.js index 792e88d34f74..5561048668f3 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -363,5 +363,13 @@ module.exports = (function() { return new Utils.col(col) } + Sequelize.prototype.cast = function (val, type) { + return new Utils.cast(val, type) + } + + Sequelize.prototype.literal = function (val) { + return new Utils.literal(val) + } + return Sequelize })() diff --git a/lib/utils.js b/lib/utils.js index addf39b24b07..e5362c045530 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -492,7 +492,31 @@ var Utils = module.exports = { }, col: function (col) { this.col = col + }, + cast: function (val, type) { + this.val = val + this.type = (type || '').trim() + }, + literal: function (val) { + this.val = val + } +} + +// I know this may seem silly, but this gives us the ability to recognize whether +// or not we should be escaping or if we should trust the user. Basically, it +// keeps things in perspective and organized. +Utils.literal.prototype.toString = function() { + return this.val +} + +Utils.cast.prototype.toString = function(queryGenerator) { + if (!this.val instanceof Utils.fn && !this.val instanceof Utils.col && !this.val instanceof Utils.literal) { + this.val = queryGenerator.escape(this.val) + } else { + this.val = this.val.toString(queryGenerator) } + + return 'CAST(' + this.val + ' AS ' + this.type.toUpperCase() + ')' } Utils.fn.prototype.toString = function(queryGenerator) { @@ -501,9 +525,10 @@ Utils.fn.prototype.toString = function(queryGenerator) { return arg.toString(queryGenerator) } else { return queryGenerator.escape(arg) - } + } }).join(', ') + ')' } + Utils.col.prototype.toString = function (queryGenerator) { return queryGenerator.quote(this.col) } diff --git a/test/dao-factory.test.js b/test/dao-factory.test.js index 87adb1433f4d..8991fb106892 100644 --- a/test/dao-factory.test.js +++ b/test/dao-factory.test.js @@ -383,6 +383,82 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) describe('create', function() { + it('is possible to use casting when creating an instance', function (done) { + var self = this + , type = dialect === "mysql" ? 'signed' : 'integer' + , _done = _.after(2, function() { + done() + }) + + this.User.create({ + intVal: this.sequelize.cast('1', type) + }).on('sql', function (sql) { + expect(sql).to.match(new RegExp('CAST\\(1 AS ' + type.toUpperCase() + '\\)')) + _done() + }) + .success(function (user) { + self.User.find(user.id).success(function (user) { + expect(user.intVal).to.equal(1) + _done() + }) + }) + }) + + it('is possible to use casting multiple times mixed in with other utilities', function (done) { + var self = this + , type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'integer'), 'integer') + , _done = _.after(2, function() { + done() + }) + + if (dialect === "mysql") { + type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'unsigned'), 'signed') + } + + this.User.create({ + intVal: type + }).on('sql', function (sql) { + if (dialect === "mysql") { + expect(sql).to.contain('CAST(CAST(1-2 AS UNSIGNED) AS SIGNED)') + } else { + expect(sql).to.contain('CAST(CAST(1-2 AS INTEGER) AS INTEGER)') + } + + _done() + }).success(function (user) { + self.User.find(user.id).success(function (user) { + expect(user.intVal).to.equal(-1) + _done() + }) + }) + }) + + it('is possible to just use .literal() to bypass escaping', function (done) { + var self = this + + this.User.create({ + intVal: this.sequelize.literal('CAST(1-2 AS ' + (dialect === "mysql" ? 'SIGNED' : 'INTEGER') + ')') + }).success(function (user) { + self.User.find(user.id).success(function (user) { + expect(user.intVal).to.equal(-1) + done() + }) + }) + }) + + it('is possible for .literal() to contain other utility functions', function (done) { + var self = this + + this.User.create({ + intVal: this.sequelize.literal(this.sequelize.cast('1-2', (dialect === "mysql" ? 'SIGNED' : 'INTEGER'))) + }).success(function (user) { + self.User.find(user.id).success(function (user) { + expect(user.intVal).to.equal(-1) + done() + }) + }) + }) + it('is possible to use funtions when creating an instance', function (done) { var self = this this.User.create({ @@ -394,6 +470,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) }) + it('is possible to use functions as default values', function (done) { var self = this , userWithDefaults @@ -446,6 +523,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { done() } }) + it("casts empty arrays correctly for postgresql insert", function(done) { if (dialect !== "postgres" && dialect !== "postgresql-native") { expect('').to.equal('') @@ -1068,6 +1146,21 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { }) }) + it('updates with casting', function (done) { + var self = this + + this.User.create({ + username: 'John' + }).success(function(user) { + self.User.update({username: self.sequelize.cast('1', 'char')}, {username: 'John'}).success(function() { + self.User.all().success(function(users) { + expect(users[0].username).to.equal('1') + done() + }) + }) + }) + }) + it('updates with function and column value', function (done) { var self = this