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

Commit 1b8d2ade by Mick Hansen

Merge pull request #4688 from User4martin/master

implement bind-parameter for custom queries on postgres
2 parents 44eee347 6e7cee2c
......@@ -47,3 +47,31 @@ sequelize.query('SELECT * FROM projects WHERE status = :status ',
console.log(projects)
})
```
# Bind Parameter
Bind parameters are like replacemnets. Except replacements are escaped and inserted into the query by sequelize before the query is sent to the database, while bind parameters are sent to the database outside the sql query text. A query can have either bind parameters or replacments.
Only Sqlite and Postgresql support bind parameters. Other dialects will insert them into the sql query in the same way it is done for replacements. Bind parameters are referred to by either $1, $2, ... (numeric) or $key (alpha-numeric). This is independent of the dialect.
* If an array is passed, `$1` will be bound to the 1st element in the array (`bind[0]`)
* If an object is passed, `$key` will be bound to `object['key']`. Each key must have a none numeric char. `$1` is not a valid key, even if `object['1']` exists.
* In either case `$$` can be used to escape a literal `$` sign.
All bound values must be present in the array/object or an exception will be thrown. This applies even to cases in which the database may ignore the bound parameter.
The database may add further restrictions to this. Bind parameters can not be sql keywords, nor table or column names. They are also ignored in quoted text/data. In Postgresql it may also be needed to typecast them, if the type can not be inferred from the context `$1::varchar`.
```js
sequelize.query('SELECT *, "text with literal $$1 and literal $$status" as t FROM projects WHERE status = $1',
{ bind: ['active'], type: sequelize.QueryTypes.SELECT }
).then(function(projects) {
console.log(projects)
})
sequelize.query('SELECT *, "text with literal $$1 and literal $$status" as t FROM projects WHERE status = $status',
{ bind: { status: 'active' }, type: sequelize.QueryTypes.SELECT }
).then(function(projects) {
console.log(projects)
})
```
'use strict';
var Utils = require('../../utils')
, SqlString = require('../../sql-string')
, Dot = require('dottie')
, QueryTypes = require('../../query-types');
......@@ -381,6 +382,85 @@ var groupJoinData = function(rows, includeOptions, options) {
};
/**
* rewrite query with parameters
*
* Examples:
*
* query.formatBindParameters('select $1 as foo', ['fooval']);
*
* query.formatBindParameters('select $foo as foo', { foo: 'fooval' });
*
* Options
* skipUnescape: bool, skip unescaping $$
* skipValueReplace: bool, do not replace (but do unescape $$). Check correct syntax and if all values are available
*/
AbstractQuery.formatBindParameters = function(sql, values, dialect, replacementFunc, options) {
if (!values) {
return [sql, []];
}
options = options || {};
if (typeof replacementFunc !== 'function') {
options = replacementFunc || {};
replacementFunc = undefined;
}
if (!replacementFunc) {
if (options.skipValueReplace) {
replacementFunc = function(match, key, values, timeZone, dialect, options) {
if (values[key] !== undefined) {
return match;
}
return undefined;
};
} else {
replacementFunc = function(match, key, values, timeZone, dialect, options) {
if (values[key] !== undefined) {
return SqlString.escape(values[key], false, timeZone, dialect);
}
return undefined;
};
}
} else {
if (options.skipValueReplace) {
var origReplacementFunc = replacementFunc;
replacementFunc = function(match, key, values, timeZone, dialect, options) {
if (origReplacementFunc(match, key, values, timeZone, dialect, options) !== undefined) {
return match;
}
return undefined;
};
}
}
var timeZone = null;
var list = Array.isArray(values);
sql = sql.replace(/\$(\$|\w+)/g, function(match, key) {
if ('$' === key) {
return options.skipUnescape ? match : key;
}
var replVal;
if (list) {
if (key.match(/^[1-9]\d*$/)) {
key = key - 1;
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
} else {
if (!key.match(/^\d*$/)) {
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
}
if (replVal === undefined) {
throw new Error('Named bind parameter "' + match + '" has no value in the given object.');
}
return replVal;
});
return [sql, []];
};
/**
* Execute the passed sql query.
*
* Examples:
......
......@@ -24,7 +24,8 @@ Query.prototype.getInsertIdField = function() {
return 'id';
};
Query.prototype.run = function(sql) {
Query.formatBindParameters = AbstractQuery.formatBindParameters;
Query.prototype.run = function(sql, parameters) {
var self = this;
this.sql = sql;
......
......@@ -21,7 +21,8 @@ var Query = function(connection, sequelize, options) {
};
Utils.inherit(Query, AbstractQuery);
Query.prototype.run = function(sql) {
Query.formatBindParameters = AbstractQuery.formatBindParameters;
Query.prototype.run = function(sql, parameters) {
var self = this;
this.sql = sql;
......
......@@ -87,7 +87,35 @@ Utils.inherit(Query, AbstractQuery);
Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;
Query.prototype.run = function(sql) {
/**
* rewrite query with parameters
*/
Query.formatBindParameters = function(sql, values, dialect) {
var bindParam = [];
if (Array.isArray(values)) {
bindParam = values;
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
} else {
var i = 0;
var seen = {};
var replacementFunc = function(match, key, values, timeZone, dialect, options) {
if (seen[key] !== undefined) {
return seen[key];
}
if (values[key] !== undefined) {
i = i + 1;
bindParam.push(values[key]);
seen[key] = '$'+i;
return '$'+i;
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
}
return [sql, bindParam];
};
Query.prototype.run = function(sql, parameters) {
this.sql = sql;
if(!Utils._.isEmpty(this.options.searchPath)){
......@@ -96,7 +124,7 @@ Query.prototype.run = function(sql) {
var self = this
, receivedError = false
, query = this.client.query(this.sql)
, query = ((parameters && parameters.length) ? this.client.query(this.sql, parameters) : this.client.query(this.sql))
, rows = [];
this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);
......
......@@ -24,11 +24,40 @@ Query.prototype.getInsertIdField = function() {
return 'lastID';
};
Query.prototype.run = function(sql) {
/**
* rewrite query with parameters
*/
Query.formatBindParameters = function(sql, values, dialect) {
var bindParam = [];
if (Array.isArray(values)) {
bindParam = {};
values.forEach(function(v, i) {
bindParam['$'+(i+1)] = v;
});
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
} else {
bindParam = {};
if (typeof values === 'object') {
Object.keys(values).forEach(function(k) {
bindParam['$'+k] = values[k];
});
}
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
}
return [sql, bindParam];
};
Query.prototype.run = function(sql, parameters) {
var self = this
, promise;
this.sql = sql;
var method = self.getDatabaseMethod();
if (method === 'exec') {
// exec does not support bind parameter
sql = AbstractQuery.formatBindParameters(sql, self.options.bind, self.options.dialect, { skipUnescape: true })[0];
this.sql = sql;
}
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
......@@ -41,7 +70,7 @@ Query.prototype.run = function(sql) {
return resolve();
} else {
resolve(new Utils.Promise(function(resolve, reject) {
self.database[self.getDatabaseMethod()](self.sql, function(err, results) {
var afterExecute = function(err, results) {
if (err) {
err.sql = self.sql;
reject(self.formatError(err));
......@@ -139,7 +168,15 @@ Query.prototype.run = function(sql) {
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);
}
}));
}
};
......
......@@ -666,6 +666,7 @@ Sequelize.prototype.import = function(path) {
* @param {Boolean} [options.nest=false] If true, transforms objects with `.` separated property names into nested objects using [dottie.js](https://github.com/mickhansen/dottie.js). For example { 'user.username': 'john' } becomes { user: { username: 'john' }}. When `nest` is true, the query type is assumed to be `'SELECT'`, unless otherwise specified
* @param {Boolean} [options.plain=false] Sets the query type to `SELECT` and return a single row
* @param {Object|Array} [options.replacements] Either an object of named parameter replacements in the format `:param` or an array of unnamed replacements to replace `?` in your SQL.
* @param {Object|Array} [options.bind] Either an object of named bind parameter in the format `$param` or an array of unnamed bind parameter to replace `$1, $2, ...` in your SQL.
* @param {Boolean} [options.useMaster=false] Force the query to use the write pool, regardless of the query type.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Instance} [options.instance] A sequelize instance used to build the return instance
......@@ -707,6 +708,14 @@ Sequelize.prototype.query = function(sql, options) {
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) {
sql = sql.query;
}
......@@ -718,6 +727,9 @@ Sequelize.prototype.query = function(sql, options) {
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 (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
......@@ -726,6 +738,12 @@ Sequelize.prototype.query = function(sql, options) {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
}
var bindParameters;
if (options.bind) {
var bindSql = self.dialect.Query.formatBindParameters(sql, options.bind, this.options.dialect);
sql = bindSql[0];
bindParameters = bindSql[1];
}
options = Utils._.extend(Utils._.clone(this.options.query), options);
options = Utils._.defaults(options, {
......@@ -768,7 +786,7 @@ Sequelize.prototype.query = function(sql, options) {
options.transaction ? options.transaction.connection : self.connectionManager.getConnection(options)
).then(function (connection) {
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;
return self.connectionManager.releaseConnection(connection);
});
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!