From 98951896d223ea31ad62a3909cf2f0690873dbe6 Mon Sep 17 00:00:00 2001 From: Serkan Serttop Date: Sat, 12 Jul 2014 08:56:40 +0300 Subject: [PATCH] Implemented Named Parameters #496 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Named parameters facilitate building advanced queries. This implementation simply converts the sql statement into the version with “?” and builds an array of parameters to be passed in. Two tests are written and they are passing. There are few caveats as to how to use the function, but accounting for named parameter like text within quotes is not easy without writing a parser. The workaround provided for this rare case is to pass in custom regexes. --- Readme.md | 66 ++++++++++++++++++++++++++++++++++++++ lib/Drivers/DML/_shared.js | 30 ++++++++++++++++- test/integration/db.js | 29 +++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 0e6b1c19..dcac5e33 100755 --- a/Readme.md +++ b/Readme.md @@ -497,6 +497,72 @@ db.driver.execQuery( ) ``` +#### Raw queries with Named Parameters + +You can also use named parameters via db.driver.execNamedQuery. + +You can begin with "::" for identifiers and other meta values, but beware that they are not escaped, only replaced as is. + +Generally this should not be an issue, but make sure to understand what this means. + +For value substitution begin with: ":". + +Same named parameter can be used multiple times within the query. + +```js +var query = 'SELECT ::table.* FROM ::table, ::joined WHERE id = :id AND (name = :name OR name0 = :name0 OR nick = :name) OFFSET ::offset LIMIT ::limit'; +var callback = function(err, data){ ... } +var obj = {id:"245", name0:"Jane", name:"John"}; +var meta = {table:'users', joined:'orders', limit:7, offset:3}; +db.driver.execNamedQuery(query, callback, obj, meta); +``` + +One can test the resulting query and the parameters the function produces by passing in a true value for the fifth argument. + +If the query contains manually written statements that contain a pattern a similar to '":parameter"' and the parameter field exists in the 'obj' or 'meta' variables defined above, then those statements will be replaced as well. + +To fix this issue, custom regexes can be provided as the last and sixth argument. + +```js +var query = 'SELECT !::table.* FROM !::table, !::joined WHERE id = !:id AND (name = !:name OR name0 = !:name0 OR nick = !:name) AND controlField = ":name" AND controlMeta = "::table" OFFSET !::offset LIMIT !::limit'; +``` + +We define regexes that replace "!:" and "!::" instead of the default ":" and "::". +```js +var rgx = {values: /!:(\w+)/g, meta: /!::(\w+)/g}; +var unit_test = true; +``` + +By passing in the unit_test "true" argument as the fifth argument, the query is not executed but the processed query and prepared parameters are returned for inspection. + +If the regexes are passed in, but you want to execute the query, make sure to provide false value for the fifth argument. + +```js +var data = db.driver.execNamedQuery(query, cb, obj, meta, unit_test, rgx); +console.log(data.sql); +``` + +```sql +SELECT users.* FROM users, orders +WHERE id = ? AND (name = ? OR name0 = ? OR nick = ?) AND +controlField = ":name" AND controlMeta = "::table" +OFFSET 3 LIMIT 7 +``` + +```js +console.log(data.sql); +//["245", "John", "Jane", "John"] +``` + +If we did not use custom regexes, then we would have received 'controlField = "?" AND controlMeta = "users"', which is not what we want. + +With the data returned from the test, you can also go ahead and execute it if you want. + +This is very similar to the way the query is executed internally through db.driver.execNamedQuery +```js +db.driver.execQuery(data.sql, data.values, callback); +``` + ### Caching & Integrity Model instances are cached. If multiple different queries will result in the same result, you will diff --git a/lib/Drivers/DML/_shared.js b/lib/Drivers/DML/_shared.js index bf7fd592..e5fabaf4 100644 --- a/lib/Drivers/DML/_shared.js +++ b/lib/Drivers/DML/_shared.js @@ -1,4 +1,5 @@ - +var RGX_COLON_SINGLE = /:(\w+)/g; +var RGX_COLON_DOUBLE = /::(\w+)/g; module.exports = { execQuery: function () { if (arguments.length == 2) { @@ -10,6 +11,33 @@ module.exports = { } return this.execSimpleQuery(query, cb); }, + execNamedQuery: function (query, cb, obj, meta, unit_test, rgx) { + var values = []; + var rgx1 = (rgx && rgx.values) ? rgx.values : RGX_COLON_SINGLE; + var rgx2 = (rgx && rgx.meta) ? rgx.meta : RGX_COLON_DOUBLE; + if(meta){ + query = query.replace(rgx2, function(txt, key){ + if(meta.hasOwnProperty(key)){ + return meta[key]; + } + return txt; + }); + } + if(obj){ + query = query.replace(rgx1, function(txt, key){ + if(obj.hasOwnProperty(key)){ + values.push(obj[key]); + return '?'; + } + return txt; + }); + } + if(unit_test){ + return {sql:query, values:values}; + } + query = this.query.escape(query, values); + return this.execSimpleQuery(query, cb); + }, eagerQuery: function (association, opts, keys, cb) { var desiredKey = Object.keys(association.field); var assocKey = Object.keys(association.mergeAssocId); diff --git a/test/integration/db.js b/test/integration/db.js index 9e9f7114..97e87bf5 100644 --- a/test/integration/db.js +++ b/test/integration/db.js @@ -289,5 +289,34 @@ describe("db.driver", function () { }); }); }); + + describe("#execNamedQuery", function () { + var obj = {id:"245", name0:"Jane", name:"John"}; + var meta = {table:'users', joined:'orders', limit:7, offset:3}; + var unit_test = true; + var cb = function(){}; + it("default regexes work as expected", function (done) { + var query = 'SELECT ::table.* FROM ::table, ::joined WHERE id = :id AND (name = :name OR name0 = :name0 OR nick = :name) OFFSET ::offset LIMIT ::limit'; + var data = db.driver.execNamedQuery(query, cb, obj, meta, unit_test); + should(data.sql === 'SELECT users.* FROM users, orders WHERE id = ? AND (name = ? OR name0 = ? OR nick = ?) OFFSET 3 LIMIT 7'); + should(data.values[0] === "245"); + should(data.values[1] === "John"); + should(data.values[2] === "Jane"); + should(data.values[3] === "John"); + done(); + }); + + it("different symbols can be used with custom regexes to preserve text within quotes", function (done) { + var query = 'SELECT !::table.* FROM !::table, !::joined WHERE id = !:id AND (name = !:name OR name0 = !:name0 OR nick = !:name) AND control_field = ":name" AND control_meta = "::table" OFFSET !::offset LIMIT !::limit'; + var rgx = {values: /!:(\w+)/g, meta: /!::(\w+)/g}; + var data = db.driver.execNamedQuery(query, cb, obj, meta, unit_test, rgx); + should(data.sql === 'SELECT users.* FROM users, orders WHERE id = ? AND (name = ? OR name0 = ? OR nick = ?) AND control_field = ":name" AND control_meta = "::table" OFFSET 3 LIMIT 7'); + should(data.values[0] === "245"); + should(data.values[1] === "John"); + should(data.values[2] === "Jane"); + should(data.values[3] === "John"); + done(); + }); + }); }); });