Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 1214754c08cf87ed52a496ccf7fe4af11288ab5b Francisco Treacy committed Jun 19, 2011
Showing with 676 additions and 0 deletions.
  1. +72 −0 README.md
  2. +6 −0 example.js
  3. +598 −0 riak-ql.js
72 README.md
@@ -0,0 +1,72 @@
+## riak-ql
+
+A small query language atop the excellent JSONSelect for Riak Map phases.
+
+### Sample usage with riak-js
+
+ var db = require('riak-js').getClient();
+
+ db
+ .add('users')
+ .map('query', ':root where .admin && !.email:val("justin@gmail.com")')
+ .run()
+
+### Install
+
+Copy `riak-ql.js` to your `js_source_dir`. This will make `query` available in the global scope.
+
+### Query Language
+
+It is basically composed by a main selector, and an array of selectors exclusively used for conditions.
+
+The main selector (the value you want to get out) is separated from the condition selectors by a `where` clause.
+
+Boolean operators `&&` and `!` can be used as expected. There is no `||` as JSONSelect already allows this by separating expressions with a comma (`,`) - use that.
+
+Everything is optional, which means that even an empty query will also work, returning `:root`.
+
+### More examples
+
+ .email
+
+ .email where !.expired
+
+ :root where .name("Justin"), .name("Mark")
+
+ where .admin
+
+ :root where .admin && .email:val("justin@gmail.com") && !.expired
+
+
+### Warning
+
+It is a very quick hack, not a proper parser.
+
+Using the `where ` or `&&` literals for other than the indeded use will cause trouble.
+
+Not recommended for production use.
+
+### License
+
+MIT License. JSONSelect licensed under ISC.
+
+Copyright (c) 2010 Francisco Treacy, <francisco.treacy@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6 example.js
@@ -0,0 +1,6 @@
+var db = require('riak-js').getClient();
+
+db
+ .add('users')
+ .map('query', ':root where .email:val("justin@gmail.com"), .login:expr(x=0)')
+ .run()
598 riak-ql.js
@@ -0,0 +1,598 @@
+/**
+ * riak-ql: Riak Query Language
+ *
+ * A small query language atop the excellent JSONSelect for Riak Map phases.
+ *
+ */
+(function() {
+
+ var selector = {};
+
+ /*! Copyright (c) 2011, Lloyd Hilaiel, ISC License */
+ /*
+ * This is the JSONSelect reference implementation, in javascript.
+ */
+ (function(exports) {
+
+ var // localize references
+ toString = Object.prototype.toString;
+
+ function jsonParse(str) {
+ try {
+ if(JSON && JSON.parse){
+ return JSON.parse(str);
+ }
+ return (new Function("return " + str))();
+ } catch(e) {
+ te("ijs", e.message);
+ }
+ }
+
+ // emitted error codes.
+ var errorCodes = {
+ "bop": "binary operator expected",
+ "ee": "expression expected",
+ "epex": "closing paren expected ')'",
+ "ijs": "invalid json string",
+ "mcp": "missing closing paren",
+ "mepf": "malformed expression in pseudo-function",
+ "mexp": "multiple expressions not allowed",
+ "mpc": "multiple pseudo classes (:xxx) not allowed",
+ "nmi": "multiple ids not allowed",
+ "pex": "opening paren expected '('",
+ "se": "selector expected",
+ "sex": "string expected",
+ "sra": "string required after '.'",
+ "uc": "unrecognized char",
+ "ucp": "unexpected closing paren",
+ "ujs": "unclosed json string",
+ "upc": "unrecognized pseudo class"
+ };
+
+ // throw an error message
+ function te(ec, context) {
+ throw new Error(errorCodes[ec] + ( context && " in '" + context + "'"));
+ }
+
+ // THE LEXER
+ var toks = {
+ psc: 1, // pseudo class
+ psf: 2, // pseudo class function
+ typ: 3, // type
+ str: 4, // string
+ ide: 5 // identifiers (or "classes", stuff after a dot)
+ };
+
+ // The primary lexing regular expression in jsonselect
+ var pat = new RegExp(
+ "^(?:" +
+ // (1) whitespace
+ "([\\r\\n\\t\\ ]+)|" +
+ // (2) one-char ops
+ "([~*,>\\)\\(])|" +
+ // (3) types names
+ "(string|boolean|null|array|object|number)|" +
+ // (4) pseudo classes
+ "(:(?:root|first-child|last-child|only-child))|" +
+ // (5) pseudo functions
+ "(:(?:nth-child|nth-last-child|has|expr|val|contains))|" +
+ // (6) bogusly named pseudo something or others
+ "(:\\w+)|" +
+ // (7 & 8) identifiers and JSON strings
+ "(?:(\\.)?(\\\"(?:[^\\\\\\\"]|\\\\[^\\\"])*\\\"))|" +
+ // (8) bogus JSON strings missing a trailing quote
+ "(\\\")|" +
+ // (9) identifiers (unquoted)
+ "\\.((?:[_a-zA-Z]|[^\\0-\\0177]|\\\\[^\\r\\n\\f0-9a-fA-F])(?:[_a-zA-Z0-9\\-]|[^\\u0000-\\u0177]|(?:\\\\[^\\r\\n\\f0-9a-fA-F]))*)" +
+ ")"
+ );
+
+ // A regular expression for matching "nth expressions" (see grammar, what :nth-child() eats)
+ var nthPat = /^\s*\(\s*(?:([+\-]?)([0-9]*)n\s*(?:([+\-])\s*([0-9]))?|(odd|even)|([+\-]?[0-9]+))\s*\)/;
+ function lex(str, off) {
+ if (!off) off = 0;
+ var m = pat.exec(str.substr(off));
+ if (!m) return undefined;
+ off+=m[0].length;
+ var a;
+ if (m[1]) a = [off, " "];
+ else if (m[2]) a = [off, m[0]];
+ else if (m[3]) a = [off, toks.typ, m[0]];
+ else if (m[4]) a = [off, toks.psc, m[0]];
+ else if (m[5]) a = [off, toks.psf, m[0]];
+ else if (m[6]) te("upc", str);
+ else if (m[8]) a = [off, m[7] ? toks.ide : toks.str, jsonParse(m[8])];
+ else if (m[9]) te("ujs", str);
+ else if (m[10]) a = [off, toks.ide, m[10].replace(/\\([^\r\n\f0-9a-fA-F])/g,"$1")];
+ return a;
+ }
+
+ // THE EXPRESSION SUBSYSTEM
+
+ var exprPat = new RegExp(
+ // skip and don't capture leading whitespace
+ "^\\s*(?:" +
+ // (1) simple vals
+ "(true|false|null)|" +
+ // (2) numbers
+ "(-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)|" +
+ // (3) strings
+ "(\"(?:[^\\]|\\[^\"])*\")|" +
+ // (4) the 'x' value placeholder
+ "(x)|" +
+ // (5) binops
+ "(&&|\\|\\||[\\$\\^<>!\\*]=|[=+\\-*/%<>])|" +
+ // (6) parens
+ "([\\(\\)])" +
+ ")"
+ );
+
+ function is(o, t) { return typeof o === t; }
+ var operators = {
+ '*': [ 9, function(lhs, rhs) { return lhs * rhs; } ],
+ '/': [ 9, function(lhs, rhs) { return lhs / rhs; } ],
+ '%': [ 9, function(lhs, rhs) { return lhs % rhs; } ],
+ '+': [ 7, function(lhs, rhs) { return lhs + rhs; } ],
+ '-': [ 7, function(lhs, rhs) { return lhs - rhs; } ],
+ '<=': [ 5, function(lhs, rhs) { return is(lhs, 'number') && is(rhs, 'number') && lhs <= rhs; } ],
+ '>=': [ 5, function(lhs, rhs) { return is(lhs, 'number') && is(rhs, 'number') && lhs >= rhs; } ],
+ '$=': [ 5, function(lhs, rhs) { return is(lhs, 'string') && is(rhs, 'string') && lhs.lastIndexOf(rhs) === lhs.length - rhs.length; } ],
+ '^=': [ 5, function(lhs, rhs) { return is(lhs, 'string') && is(rhs, 'string') && lhs.indexOf(rhs) === 0; } ],
+ '*=': [ 5, function(lhs, rhs) { return is(lhs, 'string') && is(rhs, 'string') && lhs.indexOf(rhs) !== -1; } ],
+ '>': [ 5, function(lhs, rhs) { return is(lhs, 'number') && is(rhs, 'number') && lhs > rhs; } ],
+ '<': [ 5, function(lhs, rhs) { return is(lhs, 'number') && is(rhs, 'number') && lhs < rhs; } ],
+ '=': [ 3, function(lhs, rhs) { return lhs === rhs; } ],
+ '!=': [ 3, function(lhs, rhs) { return lhs !== rhs; } ],
+ '&&': [ 2, function(lhs, rhs) { return lhs && rhs; } ],
+ '||': [ 1, function(lhs, rhs) { return lhs || rhs; } ]
+ };
+
+ function exprLex(str, off) {
+ var v, m = exprPat.exec(str.substr(off));
+ if (m) {
+ off += m[0].length;
+ v = m[1] || m[2] || m[3] || m[5] || m[6];
+ if (m[1] || m[2] || m[3]) return [off, 0, jsonParse(v)];
+ else if (m[4]) return [off, 0, undefined];
+ return [off, v];
+ }
+ }
+
+ function exprParse2(str, off) {
+ if (!off) off = 0;
+ // first we expect a value or a '('
+ var l = exprLex(str, off),
+ lhs;
+ if (l && l[1] === '(') {
+ lhs = exprParse2(str, l[0]);
+ var p = exprLex(str, lhs[0]);
+ if (!p || p[1] !== ')') te('epex', str);
+ off = p[0];
+ lhs = [ '(', lhs[1] ];
+ } else if (!l || (l[1] && l[1] != 'x')) {
+ te("ee", str + " - " + ( l[1] && l[1] ));
+ } else {
+ lhs = ((l[1] === 'x') ? undefined : l[2]);
+ off = l[0];
+ }
+
+ // now we expect a binary operator or a ')'
+ var op = exprLex(str, off);
+ if (!op || op[1] == ')') return [off, lhs];
+ else if (op[1] == 'x' || !op[1]) {
+ te('bop', str + " - " + ( op[1] && op[1] ));
+ }
+
+ // tail recursion to fetch the rhs expression
+ var rhs = exprParse2(str, op[0]);
+ off = rhs[0];
+ rhs = rhs[1];
+
+ // and now precedence! how shall we put everything together?
+ var v;
+ if (typeof rhs !== 'object' || rhs[0] === '(' || operators[op[1]][0] < operators[rhs[1]][0] ) {
+ v = [lhs, op[1], rhs];
+ }
+ else {
+ v = rhs;
+ while (typeof rhs[0] === 'object' && rhs[0][0] != '(' && operators[op[1]][0] >= operators[rhs[0][1]][0]) {
+ rhs = rhs[0];
+ }
+ rhs[0] = [lhs, op[1], rhs[0]];
+ }
+ return [off, v];
+ }
+
+ function exprParse(str, off) {
+ function deparen(v) {
+ if (typeof v !== 'object' || v === null) return v;
+ else if (v[0] === '(') return deparen(v[1]);
+ else return [deparen(v[0]), v[1], deparen(v[2])];
+ }
+ var e = exprParse2(str, off ? off : 0);
+ return [e[0], deparen(e[1])];
+ }
+
+ function exprEval(expr, x) {
+ if (expr === undefined) return x;
+ else if (expr === null || typeof expr !== 'object') {
+ return expr;
+ }
+ var lhs = exprEval(expr[0], x),
+ rhs = exprEval(expr[2], x);
+ return operators[expr[1]][1](lhs, rhs);
+ }
+
+ // THE PARSER
+
+ function parse(str, off, nested, hints) {
+ if (!nested) hints = {};
+
+ var a = [], am, readParen;
+ if (!off) off = 0;
+
+ while (true) {
+ var s = parse_selector(str, off, hints);
+ a.push(s[1]);
+ s = lex(str, off = s[0]);
+ if (s && s[1] === " ") s = lex(str, off = s[0]);
+ if (!s) break;
+ // now we've parsed a selector, and have something else...
+ if (s[1] === ">" || s[1] === "~") {
+ if (s[1] === "~") hints.usesSiblingOp = true;
+ a.push(s[1]);
+ off = s[0];
+ } else if (s[1] === ",") {
+ if (am === undefined) am = [ ",", a ];
+ else am.push(a);
+ a = [];
+ off = s[0];
+ } else if (s[1] === ")") {
+ if (!nested) te("ucp", s[1]);
+ readParen = 1;
+ off = s[0];
+ break;
+ }
+ }
+ if (nested && !readParen) te("mcp", str);
+ if (am) am.push(a);
+ var rv;
+ if (!nested && hints.usesSiblingOp) {
+ rv = normalize(am ? am : a);
+ } else {
+ rv = am ? am : a;
+ }
+ return [off, rv];
+ }
+
+ function normalizeOne(sel) {
+ var sels = [], s;
+ for (var i = 0; i < sel.length; i++) {
+ if (sel[i] === '~') {
+ // `A ~ B` maps to `:has(:root > A) > B`
+ // `Z A ~ B` maps to `Z :has(:root > A) > B, Z:has(:root > A) > B`
+ // This first clause, takes care of the first case, and the first half of the latter case.
+ if (i < 2 || sel[i-2] != '>') {
+ s = sel.slice(0,i-1);
+ s = s.concat([{has:[[{pc: ":root"}, ">", sel[i-1]]]}, ">"]);
+ s = s.concat(sel.slice(i+1));
+ sels.push(s);
+ }
+ // here we take care of the second half of above:
+ // (`Z A ~ B` maps to `Z :has(:root > A) > B, Z :has(:root > A) > B`)
+ // and a new case:
+ // Z > A ~ B maps to Z:has(:root > A) > B
+ if (i > 1) {
+ var at = sel[i-2] === '>' ? i-3 : i-2;
+ s = sel.slice(0,at);
+ var z = {};
+ for (var k in sel[at]) if (sel[at].hasOwnProperty(k)) z[k] = sel[at][k];
+ if (!z.has) z.has = [];
+ z.has.push([{pc: ":root"}, ">", sel[i-1]]);
+ s = s.concat(z, '>', sel.slice(i+1));
+ sels.push(s);
+ }
+ break;
+ }
+ }
+ if (i == sel.length) return sel;
+ return sels.length > 1 ? [','].concat(sels) : sels[0];
+ }
+
+ function normalize(sels) {
+ if (sels[0] === ',') {
+ var r = [","];
+ for (var i = i; i < sels.length; i++) {
+ var s = normalizeOne(s[i]);
+ r = r.concat(s[0] === "," ? s.slice(1) : s);
+ }
+ return r;
+ } else {
+ return normalizeOne(sels);
+ }
+ }
+
+ function parse_selector(str, off, hints) {
+ var soff = off;
+ var s = { };
+ var l = lex(str, off);
+ // skip space
+ if (l && l[1] === " ") { soff = off = l[0]; l = lex(str, off); }
+ if (l && l[1] === toks.typ) {
+ s.type = l[2];
+ l = lex(str, (off = l[0]));
+ } else if (l && l[1] === "*") {
+ // don't bother representing the universal sel, '*' in the
+ // parse tree, cause it's the default
+ l = lex(str, (off = l[0]));
+ }
+
+ // now support either an id or a pc
+ while (true) {
+ if (l === undefined) {
+ break;
+ } else if (l[1] === toks.ide) {
+ if (s.id) te("nmi", l[1]);
+ s.id = l[2];
+ } else if (l[1] === toks.psc) {
+ if (s.pc || s.pf) te("mpc", l[1]);
+ // collapse first-child and last-child into nth-child expressions
+ if (l[2] === ":first-child") {
+ s.pf = ":nth-child";
+ s.a = 0;
+ s.b = 1;
+ } else if (l[2] === ":last-child") {
+ s.pf = ":nth-last-child";
+ s.a = 0;
+ s.b = 1;
+ } else {
+ s.pc = l[2];
+ }
+ } else if (l[1] === toks.psf) {
+ if (l[2] === ":val" || l[2] === ":contains") {
+ s.expr = [ undefined, l[2] === ":val" ? "=" : "*=", undefined];
+ // any amount of whitespace, followed by paren, string, paren
+ l = lex(str, (off = l[0]));
+ if (l && l[1] === " ") l = lex(str, off = l[0]);
+ if (!l || l[1] !== "(") te("pex", str);
+ l = lex(str, (off = l[0]));
+ if (l && l[1] === " ") l = lex(str, off = l[0]);
+ if (!l || l[1] !== toks.str) te("sex", str);
+ s.expr[2] = l[2];
+ l = lex(str, (off = l[0]));
+ if (l && l[1] === " ") l = lex(str, off = l[0]);
+ if (!l || l[1] !== ")") te("epex", str);
+ } else if (l[2] === ":has") {
+ // any amount of whitespace, followed by paren
+ l = lex(str, (off = l[0]));
+ if (l && l[1] === " ") l = lex(str, off = l[0]);
+ if (!l || l[1] !== "(") te("pex", str);
+ var h = parse(str, l[0], true);
+ l[0] = h[0];
+ if (!s.has) s.has = [];
+ s.has.push(h[1]);
+ } else if (l[2] === ":expr") {
+ if (s.expr) te("mexp", str);
+ var e = exprParse(str, l[0]);
+ l[0] = e[0];
+ s.expr = e[1];
+ } else {
+ if (s.pc || s.pf ) te("mpc", str);
+ s.pf = l[2];
+ var m = nthPat.exec(str.substr(l[0]));
+ if (!m) te("mepf", str);
+ if (m[5]) {
+ s.a = 2;
+ s.b = (m[5] === "odd") ? 1 : 0;
+ } else if (m[6]) {
+ s.a = 0;
+ s.b = parseInt(m[6], 10);
+ } else {
+ s.a = parseInt((m[1] ? m[1] : "+") + (m[2] ? m[2] : "1"),10);
+ s.b = m[3] ? parseInt(m[3] + m[4],10) : 0;
+ }
+ l[0] += m[0].length;
+ }
+ } else {
+ break;
+ }
+ l = lex(str, (off = l[0]));
+ }
+
+ // now if we didn't actually parse anything it's an error
+ if (soff === off) te("se", str);
+
+ return [off, s];
+ }
+
+ // THE EVALUATOR
+
+ function isArray(o) {
+ return Array.isArray ? Array.isArray(o) :
+ toString.call(o) === "[object Array]";
+ }
+
+ function mytypeof(o) {
+ if (o === null) return "null";
+ var to = typeof o;
+ if (to === "object" && isArray(o)) to = "array";
+ return to;
+ }
+
+ function mn(node, sel, id, num, tot) {
+ var sels = [];
+ var cs = (sel[0] === ">") ? sel[1] : sel[0];
+ var m = true, mod;
+ if (cs.type) m = m && (cs.type === mytypeof(node));
+ if (cs.id) m = m && (cs.id === id);
+ if (m && cs.pf) {
+ if (cs.pf === ":nth-last-child") num = tot - num;
+ else num++;
+ if (cs.a === 0) {
+ m = cs.b === num;
+ } else {
+ mod = ((num - cs.b) % cs.a);
+
+ m = (!mod && ((num*cs.a + cs.b) >= 0));
+ }
+ }
+ if (m && cs.has) {
+ // perhaps we should augment forEach to handle a return value
+ // that indicates "client cancels traversal"?
+ var bail = function() { throw 42; };
+ for (var i = 0; i < cs.has.length; i++) {
+ try {
+ forEach(cs.has[i], node, bail);
+ } catch (e) {
+ if (e === 42) continue;
+ }
+ m = false;
+ break;
+ }
+ }
+ if (m && cs.expr) {
+ m = exprEval(cs.expr, node);
+ }
+ // should we repeat this selector for descendants?
+ if (sel[0] !== ">" && sel[0].pc !== ":root") sels.push(sel);
+
+ if (m) {
+ // is there a fragment that we should pass down?
+ if (sel[0] === ">") { if (sel.length > 2) { m = false; sels.push(sel.slice(2)); } }
+ else if (sel.length > 1) { m = false; sels.push(sel.slice(1)); }
+ }
+
+ return [m, sels];
+ }
+
+ function forEach(sel, obj, fun, id, num, tot) {
+ var a = (sel[0] === ",") ? sel.slice(1) : [sel],
+ a0 = [],
+ call = false,
+ i = 0, j = 0, k, x;
+ for (i = 0; i < a.length; i++) {
+ x = mn(obj, a[i], id, num, tot);
+ if (x[0]) {
+ call = true;
+ }
+ for (j = 0; j < x[1].length; j++) {
+ a0.push(x[1][j]);
+ }
+ }
+ if (a0.length && typeof obj === "object") {
+ if (a0.length >= 1) {
+ a0.unshift(",");
+ }
+ if (isArray(obj)) {
+ for (i = 0; i < obj.length; i++) {
+ forEach(a0, obj[i], fun, undefined, i, obj.length);
+ }
+ } else {
+ for (k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ forEach(a0, obj[k], fun, k);
+ }
+ }
+ }
+ }
+ if (call && fun) {
+ fun(obj);
+ }
+ }
+
+ function match(sel, obj) {
+ var a = [];
+ forEach(sel, obj, function(x) {
+ a.push(x);
+ });
+ return a;
+ }
+
+ function compile(sel) {
+ return {
+ sel: parse(sel)[1],
+ match: function(obj){
+ return match(this.sel, obj);
+ },
+ forEach: function(obj, fun) {
+ return forEach(this.sel, obj, fun);
+ }
+ };
+ }
+
+ exports._lex = lex;
+ exports._parse = parse;
+ exports.match = function (sel, obj) {
+ return compile(sel).match(obj);
+ };
+ exports.forEach = function(sel, obj, fun) {
+ return compile(sel).forEach(obj, fun);
+ };
+ exports.compile = compile;
+ })(selector);
+
+ // add trim to String's prototype, missing in spidermonkey
+ String.prototype.trim = function () {
+ return this.replace(/^\s*/, "").replace(/\s*$/, "");
+ }
+
+ var _rql = function(expr, obj) {
+
+ var parts = expr.split('where ');
+
+ if (parts.length > 2) throw new Error("Syntax Error: Too many 'where's in: " + expr);
+
+ var main = parts[0].trim() || ':root',
+ result = true;
+
+ if (parts[1]) {
+
+ parts[1] = parts[1].trim();
+
+ var handle = function(elem) {
+ if (typeof elem == 'string') {
+ var negate = elem[0] === '!';
+ if (negate) elem = elem.slice(1);
+
+ elem = selector.match(elem, obj).filter(function(result) {
+ return result !== null && result !== undefined;
+ }).length;
+
+ if (negate) elem = !elem;
+ }
+
+ return elem;
+ }
+
+ var selectors = parts[1].split('&&');
+
+ result = selectors
+ .map(function(t) {
+ return t.trim()
+ })
+ .reduce(function(a, b) {
+ return handle(a) && handle(b);
+ }, 1);
+ }
+
+ if (!!result) {
+ return selector.match(main, obj);
+ } else {
+ return [];
+ }
+
+ }
+
+ query = function(values, keydata, arg) {
+
+ var data = Riak.mapValuesJson(values)[0];
+
+ if (!typeof arg == "string") {
+ throw new Error("Argument must be a string");
+ }
+
+ return _rql(arg, data);
+
+ }
+
+})();

0 comments on commit 1214754

Please sign in to comment.