Skip to content

Commit

Permalink
Added support for N1QL query parameter substitution.
Browse files Browse the repository at this point in the history
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
brett19 committed Feb 4, 2014
1 parent 28fcdc8 commit 73511ab
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 4 deletions.
10 changes: 9 additions & 1 deletion lib/connection.js
Expand Up @@ -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');

Expand Down Expand Up @@ -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';
Expand Down
9 changes: 9 additions & 0 deletions 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;

/**
Expand Down
155 changes: 155 additions & 0 deletions 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;
}
11 changes: 8 additions & 3 deletions test/query.js
Expand Up @@ -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.
*/
12 changes: 12 additions & 0 deletions 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();
});

});
1 change: 1 addition & 0 deletions test_harness.js
Expand Up @@ -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;
Expand Down

0 comments on commit 73511ab

Please sign in to comment.