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

Commit 1e709899 by User4martin

implement bind-parameter for custom queries on postgres / sqlite

1 parent 7d5ecfcb
...@@ -381,6 +381,13 @@ var groupJoinData = function(rows, includeOptions, options) { ...@@ -381,6 +381,13 @@ var groupJoinData = function(rows, includeOptions, options) {
}; };
/** /**
* rewrite query with parameters
*/
AbstractQuery.prototype.formatBindParameters = function(sql, parameters, dialect) {
sql = Utils.formatBindParameters(sql, parameters, dialect);
return [sql, []];
};
/**
* Execute the passed sql query. * Execute the passed sql query.
* *
* Examples: * Examples:
......
...@@ -24,7 +24,7 @@ Query.prototype.getInsertIdField = function() { ...@@ -24,7 +24,7 @@ Query.prototype.getInsertIdField = function() {
return 'id'; return 'id';
}; };
Query.prototype.run = function(sql) { Query.prototype.run = function(sql, parameters) {
var self = this; var self = this;
this.sql = sql; this.sql = sql;
......
...@@ -21,7 +21,7 @@ var Query = function(connection, sequelize, options) { ...@@ -21,7 +21,7 @@ var Query = function(connection, sequelize, options) {
}; };
Utils.inherit(Query, AbstractQuery); Utils.inherit(Query, AbstractQuery);
Query.prototype.run = function(sql) { Query.prototype.run = function(sql, parameters) {
var self = this; var self = this;
this.sql = sql; this.sql = sql;
......
...@@ -87,7 +87,50 @@ Utils.inherit(Query, AbstractQuery); ...@@ -87,7 +87,50 @@ Utils.inherit(Query, AbstractQuery);
Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields; Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;
Query.prototype.run = function(sql) { /**
* rewrite query with parameters
*/
Query.prototype.formatBindParameters = function(sql, values, timeZone, dialect) {
var bindParam = [];
if (Array.isArray(values)) {
bindParam = values;
sql = sql.replace(/\$(\$|(?:\d+))/g, function(value, key) {
if ('$' === key) {
return key;
}
key = key - 1;
if (values[key] !== undefined) {
return value;
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
} else {
var i = 0;
var seen = {};
sql = sql.replace(/\$(\$|(?!\d)(?:\w+))/g, function(value, key) {
if ('$' === key) {
return key;
}
if (seen[key] !== undefined) {
return seen[key];
}
if (values[key] !== undefined) {
i = i + 1;
bindParam.push(values[key]);
seen[key] = '$'+i;
return '$'+i;
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
}
return [sql, bindParam];
};
Query.prototype.run = function(sql, parameters) {
this.sql = sql; this.sql = sql;
if(!Utils._.isEmpty(this.options.searchPath)){ if(!Utils._.isEmpty(this.options.searchPath)){
...@@ -96,7 +139,7 @@ Query.prototype.run = function(sql) { ...@@ -96,7 +139,7 @@ Query.prototype.run = function(sql) {
var self = this var self = this
, receivedError = false , receivedError = false
, query = this.client.query(this.sql) , query = ((parameters && parameters.length) ? this.client.query(this.sql, parameters) : this.client.query(this.sql))
, rows = []; , rows = [];
this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options); this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);
......
...@@ -24,11 +24,61 @@ Query.prototype.getInsertIdField = function() { ...@@ -24,11 +24,61 @@ Query.prototype.getInsertIdField = function() {
return 'lastID'; return 'lastID';
}; };
Query.prototype.run = function(sql) { /**
* rewrite query with parameters
*/
Query.prototype.formatBindParameters = function(sql, values, timeZone, dialect) {
var bindParam = [];
if (Array.isArray(values)) {
bindParam = {};
values.forEach(function(v, i) {
bindParam['$'+(i+1)] = v;
});
sql = sql.replace(/\$(\$|(?:\d+))/g, function(value, key) {
if ('$' === key) {
return key;
}
key = key - 1;
if (values[key] !== undefined) {
return value;
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
} else {
bindParam = {};
if (typeof values === 'object') {
Object.keys(values).forEach(function(k) {
bindParam['$'+k] = values[k];
});
}
sql = sql.replace(/\$(\$|(?!\d)(?:\w+))/g, function(value, key) {
if ('$' === key) {
return key;
}
if (values[key] !== undefined) {
return value;
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
}
return [sql, bindParam];
};
Query.prototype.run = function(sql, parameters) {
var self = this var self = this
, promise; , promise;
this.sql = sql; this.sql = sql;
var method = self.getDatabaseMethod();
if (method === 'exec') {
// exec does not support bind parameter
sql = Utils.formatBindParameters(sql, self.options.bind, self.options.dialect, { noUnescape: true });
this.sql = sql;
}
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options); this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
...@@ -41,7 +91,7 @@ Query.prototype.run = function(sql) { ...@@ -41,7 +91,7 @@ Query.prototype.run = function(sql) {
return resolve(); return resolve();
} else { } else {
resolve(new Utils.Promise(function(resolve, reject) { resolve(new Utils.Promise(function(resolve, reject) {
self.database[self.getDatabaseMethod()](self.sql, function(err, results) { var afterExecute = function(err, results) {
if (err) { if (err) {
err.sql = self.sql; err.sql = self.sql;
reject(self.formatError(err)); reject(self.formatError(err));
...@@ -139,7 +189,15 @@ Query.prototype.run = function(sql) { ...@@ -139,7 +189,15 @@ Query.prototype.run = function(sql) {
resolve(result); resolve(result);
} }
}); };
if (method === 'exec') {
// exec does not support bind parameter
self.database[method](self.sql, afterExecute);
} else {
if (!parameters) parameters = [];
self.database[method](self.sql, parameters, afterExecute);
}
})); }));
} }
}; };
......
...@@ -707,6 +707,14 @@ Sequelize.prototype.query = function(sql, options) { ...@@ -707,6 +707,14 @@ Sequelize.prototype.query = function(sql, options) {
options.replacements = sql.values; options.replacements = sql.values;
} }
if (sql.bind !== undefined) {
if (options.bind !== undefined) {
throw new Error('Both `sql.bind` and `options.bind` cannot be set at the same time');
}
options.bind= sql.bind;
}
if (sql.query !== undefined) { if (sql.query !== undefined) {
sql = sql.query; sql = sql.query;
} }
...@@ -718,6 +726,9 @@ Sequelize.prototype.query = function(sql, options) { ...@@ -718,6 +726,9 @@ Sequelize.prototype.query = function(sql, options) {
options.raw = true; options.raw = true;
} }
if (options.replacements && options.bind) {
throw new Error('Both `replacements` and `bind` cannot be set at the same time');
}
if (options.replacements) { if (options.replacements) {
if (Array.isArray(options.replacements)) { if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect); sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
...@@ -726,6 +737,12 @@ Sequelize.prototype.query = function(sql, options) { ...@@ -726,6 +737,12 @@ Sequelize.prototype.query = function(sql, options) {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect); sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
} }
} }
var bindParameters;
if (options.bind) {
var bindSql = self.dialect.Query.prototype.formatBindParameters(sql, options.bind, this.options.dialect);
sql = bindSql[0];
bindParameters = bindSql[1];
}
options = Utils._.extend(Utils._.clone(this.options.query), options); options = Utils._.extend(Utils._.clone(this.options.query), options);
options = Utils._.defaults(options, { options = Utils._.defaults(options, {
...@@ -768,7 +785,7 @@ Sequelize.prototype.query = function(sql, options) { ...@@ -768,7 +785,7 @@ Sequelize.prototype.query = function(sql, options) {
options.transaction ? options.transaction.connection : self.connectionManager.getConnection(options) options.transaction ? options.transaction.connection : self.connectionManager.getConnection(options)
).then(function (connection) { ).then(function (connection) {
var query = new self.dialect.Query(connection, self, options); var query = new self.dialect.Query(connection, self, options);
return query.run(sql).finally(function() { return query.run(sql, bindParameters).finally(function() {
if (options.transaction) return; if (options.transaction) return;
return self.connectionManager.releaseConnection(connection); return self.connectionManager.releaseConnection(connection);
}); });
......
...@@ -161,6 +161,36 @@ SqlString.formatNamedParameters = function(sql, values, timeZone, dialect) { ...@@ -161,6 +161,36 @@ SqlString.formatNamedParameters = function(sql, values, timeZone, dialect) {
}); });
}; };
SqlString.formatBindParameters = function(sql, values, timeZone, dialect, options) {
options = options || {};
if (Array.isArray(values)) {
return sql.replace(/\$(\$|(?:\d+))/g, function(value, key) {
if ('$' === key) {
return options.noUnescape ? value : key;
}
key = key - 1;
if (values[key] !== undefined) {
return SqlString.escape(values[key], false, timeZone, dialect);
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
} else {
return sql.replace(/\$(\$|(?!\d)(?:\w+))/g, function(value, key) {
if ('$' === key) {
return options.noUnescape ? value : key;
}
if (values[key] !== undefined) {
return SqlString.escape(values[key], false, timeZone, dialect);
} else {
throw new Error('Named bind parameter "' + value + '" has no value in the given object.');
}
});
}
};
SqlString.dateToString = function(date, timeZone, dialect) { SqlString.dateToString = function(date, timeZone, dialect) {
if (moment.tz.zone(timeZone)) { if (moment.tz.zone(timeZone)) {
date = moment(date).tz(timeZone); date = moment(date).tz(timeZone);
......
...@@ -63,6 +63,13 @@ var Utils = module.exports = { ...@@ -63,6 +63,13 @@ var Utils = module.exports = {
var timeZone = null; var timeZone = null;
return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect); return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect);
}, },
formatBindParameters: function(sql, parameters, dialect, options) {
if (parameters) {
var timeZone = null;
return SqlString.formatBindParameters(sql, parameters, timeZone, dialect, options);
}
return sql;
},
cloneDeep: function(obj, fn) { cloneDeep: function(obj, fn) {
return _.cloneDeep(obj, function (elem) { return _.cloneDeep(obj, function (elem) {
// Do not try to customize cloning of plain objects and strings // Do not try to customize cloning of plain objects and strings
......
...@@ -349,9 +349,58 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -349,9 +349,58 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
}).to.throw(Error, 'Both `sql.values` and `options.replacements` cannot be set at the same time'); }).to.throw(Error, 'Both `sql.values` and `options.replacements` cannot be set at the same time');
}); });
it('throw an exception if `sql.bind` and `options.bind` are both passed', function() {
var self = this;
expect(function() {
return self.sequelize.query({ query: 'select $1 + ? as foo, $2 + ? as bar', bind: [1, 2] }, { raw: true, bind: [1, 2] });
}).to.throw(Error, 'Both `sql.bind` and `options.bind` cannot be set at the same time');
});
it('throw an exception if `options.replacements` and `options.bind` are both passed', function() {
var self = this;
expect(function() {
return self.sequelize.query('select $1 + ? as foo, $2 + ? as bar', { raw: true, bind: [1, 2], replacements: [1, 2] });
}).to.throw(Error, 'Both `replacements` and `bind` cannot be set at the same time');
});
it('throw an exception if `sql.bind` and `sql.values` are both passed', function() {
var self = this;
expect(function() {
return self.sequelize.query({ query: 'select $1 + ? as foo, $2 + ? as bar', bind: [1, 2], values: [1, 2] }, { raw: true });
}).to.throw(Error, 'Both `replacements` and `bind` cannot be set at the same time');
});
it('throw an exception if `sql.bind` and `options.replacements`` are both passed', function() {
var self = this;
expect(function() {
return self.sequelize.query({ query: 'select $1 + ? as foo, $2 + ? as bar', bind: [1, 2] }, { raw: true, replacements: [1, 2] });
}).to.throw(Error, 'Both `replacements` and `bind` cannot be set at the same time');
});
it('throw an exception if `options.bind` and `sql.replacements` are both passed', function() {
var self = this;
expect(function() {
return self.sequelize.query({ query: 'select $1 + ? as foo, $1 _ ? as bar', values: [1, 2] }, { raw: true, bind: [1, 2] });
}).to.throw(Error, 'Both `replacements` and `bind` cannot be set at the same time');
});
it('uses properties `query` and `values` if query is tagged', function() { it('uses properties `query` and `values` if query is tagged', function() {
return this.sequelize.query({ query: 'select ? as foo, ? as bar', values: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT }).then(function(result) { var logSql;
return this.sequelize.query({ query: 'select ? as foo, ? as bar', values: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result).to.deep.equal([{ foo: 1, bar: 2 }]);
expect(logSql.indexOf('?')).to.equal(-1);
});
});
it('uses properties `query` and `bind` if query is tagged', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query({ query: 'select $1'+typeCast+' as foo, $2'+typeCast+' as bar', bind: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result).to.deep.equal([{ foo: 1, bar: 2 }]); expect(result).to.deep.equal([{ foo: 1, bar: 2 }]);
if ((dialect === 'postgres') || (dialect === 'sqlite')) {
expect(logSql.indexOf('$1')).to.be.above(-1);
expect(logSql.indexOf('$2')).to.be.above(-1);
}
}); });
}); });
...@@ -432,6 +481,129 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -432,6 +481,129 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
}).to.throw(Error, /Named parameter ":\w+" has no value in the given object\./g); }).to.throw(Error, /Named parameter ":\w+" has no value in the given object\./g);
}); });
it('binds token with the passed array', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $1'+typeCast+' as foo, $2'+typeCast+' as bar', { type: this.sequelize.QueryTypes.SELECT, bind: [1, 2], logging: function(s) { logSql = s; } }).then(function(result) {
expect(result).to.deep.equal([{ foo: 1, bar: 2 }]);
if ((dialect === 'postgres') || (dialect === 'sqlite')) {
expect(logSql.indexOf('$1')).to.be.above(-1);
}
});
});
it('binds named parameters with the passed object', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $one'+typeCast+' as foo, $two'+typeCast+' as bar', { raw: true, bind: { one: 1, two: 2 }, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: 2 }]);
if ((dialect === 'postgres')) {
expect(logSql.indexOf('$1')).to.be.above(-1);
}
if ((dialect === 'sqlite')) {
expect(logSql.indexOf('$one')).to.be.above(-1);
}
});
});
it('binds named parameters with the passed object and ignore numeric $1', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $one'+typeCast+' as foo, $two'+typeCast+' as bar, \'$1\' as baz', { raw: true, bind: { one: 1, two: 2 }, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: 2, baz: '$1' }]);
});
});
it('binds named parameters with the passed array and ignore $alpha', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $1'+typeCast+' as foo, $2'+typeCast+' as bar, \'$foo\' as baz', { raw: true, bind: [1, 2], logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: 2, baz: '$foo' }]);
});
});
it('binds named parameters with the passed object using the same key twice', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $one'+typeCast+' as foo, $two'+typeCast+' as bar, $one'+typeCast+' as baz', { raw: true, bind: { one: 1, two: 2 }, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: 2, baz: 1 }]);
if ((dialect === 'postgres')) {
expect(logSql.indexOf('$1')).to.be.above(-1);
expect(logSql.indexOf('$2')).to.be.above(-1);
expect(logSql.indexOf('$3')).to.equal(-1);
}
});
});
it('binds named parameters with the passed object having a null property', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $one'+typeCast+' as foo, $two'+typeCast+' as bar', { raw: true, bind: { one: 1, two: null }, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: null }]);
});
});
it('binds named parameters array handles escaped $$', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $1'+typeCast+' as foo, \'$$\' as bar', { raw: true, bind: [1 ], logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: '$' }]);
if ((dialect === 'postgres') || (dialect === 'sqlite')) {
expect(logSql.indexOf('$1')).to.be.above(-1);
}
});
});
it('binds named parameters object handles escaped $$', function() {
var typeCast = (dialect === 'postgres') ? '::int' : '';
var logSql;
return this.sequelize.query('select $one'+typeCast+' as foo, \'$$\' as bar', { raw: true, bind: { one: 1 }, logging: function(s) { logSql = s; } }).then(function(result) {
expect(result[0]).to.deep.equal([{ foo: 1, bar: '$' }]);
});
});
it('throw an exception when bind key is missing in the passed array', function() {
var self = this;
expect(function() {
self.sequelize.query('select $1 as foo, $2 as bar, $3 as baz', { raw: true, bind: [1, 2] });
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('throw an exception when bind key is missing in the passed object', function() {
var self = this;
expect(function() {
self.sequelize.query('select $one as foo, $two as bar, $three as baz', { raw: true, bind: { one: 1, two: 2 }});
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('throw an exception with the passed number for bind', function() {
var self = this;
expect(function() {
self.sequelize.query('select $one as foo, $two as bar', { raw: true, bind: 2 });
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('throw an exception with the passed empty object for bind', function() {
var self = this;
expect(function() {
self.sequelize.query('select $one as foo, $two as bar', { raw: true, bind: {}});
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('throw an exception with the passed string for bind', function() {
var self = this;
expect(function() {
self.sequelize.query('select $one as foo, $two as bar', { raw: true, bind: 'foobar'});
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('throw an exception with the passed date for bind', function() {
var self = this;
expect(function() {
self.sequelize.query('select $one as foo, $two as bar', { raw: true, bind: new Date()});
}).to.throw(Error, /Named bind parameter "\$\w+" has no value in the given object\./g);
});
it('handles AS in conjunction with functions just fine', function() { it('handles AS in conjunction with functions just fine', function() {
var datetime = (dialect === 'sqlite' ? 'date(\'now\')' : 'NOW()'); var datetime = (dialect === 'sqlite' ? 'date(\'now\')' : 'NOW()');
if (dialect === 'mssql') { if (dialect === 'mssql') {
...@@ -454,6 +626,27 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -454,6 +626,27 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
.to.eventually.deep.equal([{ 'sum': '5050' }]); .to.eventually.deep.equal([{ 'sum': '5050' }]);
}); });
} }
if (Support.getTestDialect() === 'sqlite') {
it('binds array parameters for upsert are replaced. $$ unescapes only once', function() {
var logSql;
return this.sequelize.query('select $1 as foo, $2 as bar, \'$$$$\' as baz', { type: this.sequelize.QueryTypes.UPSERT, bind: [1, 2], logging: function(s) { logSql = s; } }).then(function(result) {
// sqlite.exec does not return a result
expect(logSql.indexOf('$one')).to.equal(-1);
expect(logSql.indexOf('\'$$\'')).to.be.above(-1);
});
});
it('binds named parameters for upsert are replaced. $$ unescapes only once', function() {
var logSql;
return this.sequelize.query('select $one as foo, $two as bar, \'$$$$\' as baz', { type: this.sequelize.QueryTypes.UPSERT, bind: { one: 1, two: 2 }, logging: function(s) { logSql = s; } }).then(function(result) {
// sqlite.exec does not return a result
expect(logSql.indexOf('$one')).to.equal(-1);
expect(logSql.indexOf('\'$$\'')).to.be.above(-1);
});
});
}
}); });
describe('set', function() { describe('set', function() {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!