Skip to content
Browse files

Added support for N1QL query parameter substitution.

Borrowed query formatting code from node-mysql.

Change-Id: Ib0fb2803c2b7c1a0e60cd4b63f0be61e61bd05bd
Reviewed-on: http://review.couchbase.org/33180
Reviewed-by: Mark Nunberg <mnunberg@haskalah.org>
Tested-by: Brett Lawson <brett19@gmail.com>
  • Loading branch information...
1 parent 28fcdc8 commit 73511abb7a4528a244c5a155db6b704019c65ef1 @brett19 brett19 committed Feb 3, 2014
Showing with 194 additions and 4 deletions.
  1. +9 −1 lib/connection.js
  2. +9 −0 lib/couchbase.js
  3. +155 −0 lib/qstring.js
  4. +8 −3 test/query.js
  5. +12 −0 test/queryfmt.js
  6. +1 −0 test_harness.js
View
10 lib/connection.js
@@ -5,6 +5,7 @@ var fs = require('fs');
var path = require('path');
var binding = require('./binding');
var ViewQuery = require('./viewQuery');
+var qstring = require('./qstring');
var qs = require('querystring');
var request = require('request');
@@ -337,11 +338,18 @@ Connection.prototype.shutdown = function() {
* @param {string} query
* @param {Function} callback
*/
-Connection.prototype.query = function(query, callback) {
+Connection.prototype.query = function(query, values, callback) {
if (this.queryhosts === null) {
return callback(new Error('no available query nodes'));
}
+ if (arguments.length < 3) {
+ callback = values;
+ values = null;
+ }
+
+ query = qstring.format(query, values);
+
var qhosts = this.queryhosts;
var host = qhosts[Math.floor(Math.random()*qhosts.length)];
var uri = 'http://' + host + '/query';
View
9 lib/couchbase.js
@@ -1,8 +1,17 @@
'use strict';
+var qstring = require('./qstring');
+
module.exports.Connection = require('./connection');
module.exports.Mock = require('./mock');
+module.exports.formatQuery =
+ module.exports.Mock.formatQuery =
+ function(query, values) {
+ return qstring.format(query, values);
+};
+
+
var CONST = require('./binding').Constants;
/**
View
155 lib/qstring.js
@@ -0,0 +1,155 @@
+/*
+ Borrowed from node-mysql, see:
+ https://github.com/felixge/node-mysql/
+ */
+var SqlString = exports;
+
+SqlString.escapeId = function (val, forbidQualified) {
+ if (Array.isArray(val)) {
+ return val.map(function(v) {
+ return SqlString.escapeId(v, forbidQualified);
+ }).join(', ');
+ }
+
+ if (forbidQualified) {
+ return '`' + val.replace(/`/g, '``') + '`';
+ }
+ return '`' + val.replace(/`/g, '``').replace(/\./g, '`.`') + '`';
+};
+
+SqlString.escape = function(val, stringifyObjects, timeZone) {
+ if (val === undefined || val === null) {
+ return 'NULL';
+ }
+
+ switch (typeof val) {
+ case 'boolean': return (val) ? 'true' : 'false';
+ case 'number': return val+'';
+ }
+
+ if (val instanceof Date) {
+ val = SqlString.dateToString(val, timeZone || 'local');
+ }
+
+ if (Buffer.isBuffer(val)) {
+ return SqlString.bufferToString(val);
+ }
+
+ if (Array.isArray(val)) {
+ return SqlString.arrayToList(val, timeZone);
+ }
+
+ if (typeof val === 'object') {
+ if (stringifyObjects) {
+ val = val.toString();
+ } else {
+ return SqlString.objectToValues(val, 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;
+ }
+ });
+ return "'"+val+"'";
+};
+
+SqlString.arrayToList = function(array, timeZone) {
+ return array.map(function(v) {
+ if (Array.isArray(v)) return '(' + SqlString.arrayToList(v, timeZone) + ')';
+ return SqlString.escape(v, true, timeZone);
+ }).join(', ');
+};
+
+SqlString.format = function(sql, values, stringifyObjects, timeZone) {
+ values = [].concat(values);
+
+ return sql.replace(/\?\??/g, function(match) {
+ if (!values.length) {
+ return match;
+ }
+
+ if (match == "??") {
+ return SqlString.escapeId(values.shift());
+ }
+ return SqlString.escape(values.shift(), stringifyObjects, timeZone);
+ });
+};
+
+SqlString.dateToString = function(date, timeZone) {
+ var dt = new Date(date);
+
+ if (timeZone != 'local') {
+ var tz = convertTimezone(timeZone);
+
+ dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000));
+ if (tz !== false) {
+ dt.setTime(dt.getTime() + (tz * 60000));
+ }
+ }
+
+ var year = dt.getFullYear();
+ var month = zeroPad(dt.getMonth() + 1, 2);
+ var day = zeroPad(dt.getDate(), 2);
+ var hour = zeroPad(dt.getHours(), 2);
+ var minute = zeroPad(dt.getMinutes(), 2);
+ var second = zeroPad(dt.getSeconds(), 2);
+ var millisecond = zeroPad(dt.getMilliseconds(), 3);
+
+ return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + '.' + millisecond;
+};
+
+SqlString.bufferToString = function(buffer) {
+ var hex = '';
+ try {
+ hex = buffer.toString('hex');
+ } catch (err) {
+ // node v0.4.x does not support hex / throws unknown encoding error
+ for (var i = 0; i < buffer.length; i++) {
+ var byte = buffer[i];
+ hex += zeroPad(byte.toString(16));
+ }
+ }
+
+ return "X'" + hex+ "'";
+};
+
+SqlString.objectToValues = function(object, timeZone) {
+ var values = [];
+ for (var key in object) {
+ var value = object[key];
+ if(typeof value === 'function') {
+ continue;
+ }
+
+ values.push(this.escapeId(key) + ' = ' + SqlString.escape(value, true, timeZone));
+ }
+
+ return values.join(', ');
+};
+
+function zeroPad(number, length) {
+ number = number.toString();
+ while (number.length < length) {
+ number = '0' + number;
+ }
+
+ return number;
+}
+
+function convertTimezone(tz) {
+ if (tz == "Z") return 0;
+
+ var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/);
+ if (m) {
+ return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60;
+ }
+ return false;
+}
View
11 test/query.js
@@ -20,7 +20,12 @@ describe.skip('#query', function() {
});
});
+ it('should execute formated query strings correctly', function(done) {
+ cb.query('SELECT * FROM ??', ['default'], function(err, res) {
+ assert(!err, 'Failed to execute query.');
+ assert(res, 'No results returned.');
+ done();
+ });
+ });
+
});
-/**
- * Created by brettlawson on 1/7/2014.
- */
View
12 test/queryfmt.js
@@ -0,0 +1,12 @@
+var assert = require('assert');
+var H = require('../test_harness.js');
+
+describe('#query formatting', function() {
+
+ it('should format query strings correctly', function(done) {
+ var str = H.lib.formatQuery('SELECT ?? FROM ?? WHERE ??=?', ['name', 'default', 'id', 'frank']);
+ assert(str === "SELECT `name` FROM `default` WHERE `id`='frank'", 'query should be correct');
+ done();
+ });
+
+});
View
1 test_harness.js
@@ -43,6 +43,7 @@ delete config.mock;
function Harness(callback) {
this.client = this.newClient();
+ this.lib = couchbase;
this.errors = couchbase.errors;
this.format = couchbase.format;
this.keySerial = 0;

0 comments on commit 73511ab

Please sign in to comment.
Something went wrong with that request. Please try again.