syncbase/exec: Expose support for parameterized queries.

Allow Syncbase to accept parameterized queries, with '?' placeholders
in the where clause.

MultiPart: 4/5
Change-Id: Icde606faf4806bcb2da9879376e0eecdc9f92861
diff --git a/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js b/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
index ca80fa4..b0fd022 100644
--- a/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
+++ b/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
@@ -1022,7 +1022,7 @@
 };
     
       
-Database.prototype.exec = function(ctx, serverCall, schemaVersion, query) {
+Database.prototype.exec = function(ctx, serverCall, schemaVersion, query, params) {
   throw new Error('Method Exec not implemented');
 };
     
@@ -1297,7 +1297,7 @@
       
     {
     name: 'Exec',
-    doc: "// Exec executes a syncQL query and returns all results as specified by in the\n// query's select/delete statement. Concurrency semantics are documented in model.go.",
+    doc: "// Exec executes a syncQL query with positional parameters and returns all\n// results as specified by the query's select/delete statement.\n// Concurrency semantics are documented in model.go.",
     inArgs: [{
       name: 'schemaVersion',
       doc: "",
@@ -1308,6 +1308,11 @@
       doc: "",
       type: vdl.types.STRING
     },
+    {
+      name: 'params',
+      doc: "",
+      type: _type9
+    },
     ],
     outArgs: [],
     inStream: null,
diff --git a/src/nosql/abstract-database.js b/src/nosql/abstract-database.js
index 5a15028..50bca14 100644
--- a/src/nosql/abstract-database.js
+++ b/src/nosql/abstract-database.js
@@ -111,6 +111,11 @@
 /**
  * Executes a syncQL query.
  *
+ * If the query is parameterized, paramValues must contain a value for each '?'
+ * placeholder in the query. If there are no placeholders, paramValues must be
+ * empty or omitted. paramTypes should be provided in addition to paramValues
+ * for paramValue elements that lack VOM types, including JS primitives.
+ *
  * Returns a stream of rows.  The first row contains an array of headers (i.e.
  * column names).  Subsequent rows contain an array of values for each row that
  * matches the query.  The number of values returned in each row will match the
@@ -120,7 +125,7 @@
  * time of the RPC, and will not reflect subsequent writes to keys not yet
  * reached by the stream.
  *
- * NOTE(nlacasse): The Go client library returns the headers seperately from
+ * NOTE(nlacasse): The Go client library returns the headers separately from
  * the stream.  We could potentially do something similar in JavaScript, by
  * pulling the headers off the stream and passing them to the callback.
  * However, by Vanadium JS convention the callback gets called at the *end* of
@@ -136,17 +141,47 @@
  *
  * @param {module:vanadium.context.Context} ctx Vanadium context.
  * @param {string} query Query string.
+ * @param {Object[]} [paramValues] Query parameters, one per '?' placeholder in
+ * the query.
+ * @param {module:vanadium.vdl.Type[]} [paramTypes] Query parameter types, one
+ * per value in paramValues. Not required if paramValues are VDL typed or if
+ * values being queried are JSValues.
  * @param {function} cb Callback.
  * @returns {stream} Stream of rows.
  */
-AbstractDatabase.prototype.exec = function(ctx, query, cb) {
+AbstractDatabase.prototype.exec = function(ctx, query, paramValues, paramTypes,
+                                           cb) {
+  if (typeof cb === 'undefined' && typeof paramValues === 'function') {
+    cb = paramValues;
+    paramValues = undefined;
+    paramTypes = undefined;
+  }
+  if (typeof cb === 'undefined' && typeof paramTypes === 'function') {
+    cb = paramTypes;
+    paramTypes = undefined;
+  }
+
+  var params;
+  if (typeof paramValues !== 'undefined') {
+    paramTypes = paramTypes || [];
+    try {
+      params = paramValues.map(function(param, i) {
+        var type = paramTypes[i] || vanadium.vdl.types.ANY;
+        return vanadium.vdl.canonicalize.value(param, type);
+      });
+    } catch (e) {
+      return cb(e);
+    }
+  }
+
   var streamUnwrapper = through2({
     objectMode: true
   }, function(res, enc, cb) {
     return cb(null, res.map(unwrap));
   });
 
-  var stream = this._wire(ctx).exec(ctx, this.schemaVersion, query, cb).stream;
+  var stream = this._wire(ctx).exec(ctx, this.schemaVersion,
+                                    query, params, cb).stream;
 
   var decodedStream = stream.pipe(streamUnwrapper);
   stream.on('error', function(err) {
diff --git a/test/integration/test-database.js b/test/integration/test-database.js
index d69910c..48076cc 100644
--- a/test/integration/test-database.js
+++ b/test/integration/test-database.js
@@ -414,6 +414,22 @@
           ['Moe', moe],
         ]
       },
+      {
+        q: 'select k, v from %s where k = ? or v.Age = ?',
+        paramValues: [
+          'Moe',
+          38,
+        ],
+        paramTypes: [
+          vdl.types.STRING,
+          vdl.types.INT32,
+        ],
+        want: [
+          ['k', 'v'],
+          ['Homer', homer],
+          ['Moe', moe],
+        ]
+      },
     ];
 
     putPeople();
@@ -443,7 +459,9 @@
       }
 
       async.forEachSeries(testCases, function(testCase, cb) {
-        assertExec(format(testCase.q, table.name), testCase.want, cb);
+        assertExec(format(testCase.q, table.name),
+                   testCase.paramValues, testCase.paramTypes,
+                   testCase.want, cb);
       }, end);
     }
 
@@ -453,11 +471,14 @@
     }
 
     // Assert that query 'q' returns the rows in 'want'.
-    function assertExec(q, want, cb) {
-      var stream = db.exec(ctx, q, function(err) {
+    function assertExec(q, paramValues, paramTypes, want, cb) {
+      var testCb = function(err) {
         t.error(err);
         cb();
-      });
+      };
+      var stream = paramValues === undefined ?
+        db.exec(ctx, q, testCb) :
+        db.exec(ctx, q, paramValues, paramTypes, testCb);
       stream.on('error', t.error);
       toArray(stream, function(err, got) {
         t.error(err);