├── .gitignore ├── test ├── all.js ├── js-array.js ├── intern.js └── query.js ├── .travis.yml ├── util ├── contains.js └── each.js ├── package.js ├── package.json ├── query.js ├── parser.js ├── README.md ├── js-array.js └── specification ├── draft-zyp-rql-00.xml └── draft-zyp-rql-00.html /.gitignore: -------------------------------------------------------------------------------- 1 | downloaded-modules 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | require('./js-array'); 3 | require('./query'); 4 | }); 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | env: 6 | global: 7 | - SAUCE_USERNAME: persvr-ci 8 | - SAUCE_ACCESS_KEY: d8dc90f7-08ca-41a5-8055-c452116b9ef7 9 | 10 | install: 11 | - travis_retry npm install 12 | 13 | script: npm test && npm run test.sauce 14 | -------------------------------------------------------------------------------- /util/contains.js: -------------------------------------------------------------------------------- 1 | ({define:typeof define!=='undefined'?define:function(deps, factory){module.exports = factory(exports);}}). 2 | define([], function(){ 3 | return contains; 4 | 5 | function contains(array, item){ 6 | for(var i = 0, l = array.length; i < l; i++){ 7 | if(array[i] === item){ 8 | return true; 9 | } 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /util/each.js: -------------------------------------------------------------------------------- 1 | ({define:typeof define!=='undefined'?define:function(deps, factory){module.exports = factory(exports);}}). 2 | define([], function(){ 3 | return each; 4 | 5 | function each(array, callback){ 6 | var emit, result; 7 | if (callback.length > 1) { 8 | // can take a second param, emit 9 | result = []; 10 | emit = function(value){ 11 | result.push(value); 12 | }; 13 | } 14 | for(var i = 0, l = array.length; i < l; i++){ 15 | if(callback(array[i], emit)){ 16 | return result || true; 17 | } 18 | } 19 | return result; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var miniExcludes = { 2 | "rql/README.md": 1, 3 | "rql/package": 1 4 | }, 5 | amdExcludes = { 6 | }, 7 | isJsRe = /\.js$/, 8 | isTestRe = /\/test\//, 9 | isSpecificationRe = /\/specification\//; 10 | 11 | var profile = { 12 | resourceTags: { 13 | test: function(filename, mid){ 14 | return isTestRe.test(filename); 15 | }, 16 | 17 | miniExclude: function(filename, mid){ 18 | return isTestRe.test(filename) || isSpecificationRe.test(filename) || mid in miniExcludes; 19 | }, 20 | 21 | amd: function(filename, mid){ 22 | return isJsRe.test(filename) && !(mid in amdExcludes); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rql", 3 | "version": "0.3.3", 4 | "author": "Kris Zyp", 5 | "contributors": [ 6 | "Vladimir Dronnikov " 7 | ], 8 | "keywords": [ 9 | "resource", 10 | "query", 11 | "uri" 12 | ], 13 | "description": "Query language for the web, NoSQL", 14 | "licenses": [ 15 | { 16 | "type": "AFLv2.1", 17 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 18 | }, 19 | { 20 | "type": "BSD", 21 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 22 | } 23 | ], 24 | "directories": { 25 | "lib": "." 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "http://github.com/kriszyp/rql" 30 | }, 31 | "maintainers": [ 32 | { 33 | "name": "Kris Zyp", 34 | "email": "kriszyp@gmail.com" 35 | } 36 | ], 37 | "mappings": { 38 | "patr": "http://github.com/kriszyp/patr/zipball/v0.2.1", 39 | "promised-io": "http://github.com/kriszyp/promised-io/zipball/v0.2.1" 40 | }, 41 | "dependencies": { 42 | "promised-io": ">=0.3.0" 43 | }, 44 | "devDependencies": { 45 | "intern-geezer": "^2.1.1" 46 | }, 47 | "scripts": { 48 | "test": "intern-client config=test/intern", 49 | "test.sauce": "intern-runner config=test/intern", 50 | "test.proxy": "intern-runner config=test/intern --proxyOnly" 51 | }, 52 | "icon": "http://packages.dojofoundation.org/images/persvr.png", 53 | "dojoBuild": "package.js" 54 | } 55 | -------------------------------------------------------------------------------- /test/js-array.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | var test = require('intern!object'), 3 | assert = require('intern/chai!assert'), 4 | Query = require('../query').Query, 5 | executeQuery = require('../js-array').executeQuery; 6 | 7 | var data = [ 8 | { 9 | 'with/slash': 'slashed', 10 | nested: { 11 | property: 'value' 12 | }, 13 | price: 10, 14 | name: 'ten', 15 | tags: [ 'fun', 'even' ] 16 | }, 17 | { 18 | price: 5, 19 | name: 'five', 20 | tags: [ 'fun' ] 21 | } 22 | ]; 23 | 24 | test({ 25 | name: 'rql/test/js-array', 26 | 27 | testFiltering: function () { 28 | assert.equal(executeQuery('price=lt=10', {}, data).length, 1); 29 | assert.equal(executeQuery('price=lt=11', {}, data).length, 2); 30 | assert.equal(executeQuery('nested/property=value', {}, data).length, 1); 31 | assert.equal(executeQuery('with%2Fslash=slashed', {}, data).length, 1); 32 | assert.equal(executeQuery('out(price,(5,10))', {}, data).length, 0); 33 | assert.equal(executeQuery('out(price,(5))', {}, data).length, 1); 34 | assert.equal(executeQuery('contains(tags,even)', {}, data).length, 1); 35 | assert.equal(executeQuery('contains(tags,fun)', {}, data).length, 2); 36 | assert.equal(executeQuery('excludes(tags,fun)', {}, data).length, 0); 37 | assert.equal(executeQuery('excludes(tags,ne(fun))', {}, data).length, 1); 38 | assert.equal(executeQuery('excludes(tags,ne(even))', {}, data).length, 0); 39 | // eq() on re: should trigger .match() 40 | assert.deepEqual(executeQuery('price=match=10', {}, data), [ data[0] ]); 41 | // ne() on re: should trigger .not(.match()) 42 | assert.deepEqual(executeQuery('name=match=f.*', {}, data), [ data[1] ]); 43 | assert.deepEqual(executeQuery('name=match=glob:f*', {}, data), [ data[1] ]); 44 | assert.deepEqual(executeQuery(new Query().match('name', /f.*/), {}, data), [data[1]]); 45 | }, 46 | 47 | testFiltering1: function () { 48 | var data = [ 49 | { 'path.1': [ 1, 2, 3 ] }, 50 | { 'path.1': [ 9, 3, 7 ] } 51 | ]; 52 | 53 | assert.deepEqual(executeQuery('contains(path,3)&sort()', {}, data), []); // path is undefined 54 | assert.deepEqual(executeQuery('contains(path.1,3)&sort()', {}, data), data); // 3 found in both 55 | assert.deepEqual(executeQuery('excludes(path.1,3)&sort()', {}, data), []); // 3 found in both 56 | assert.deepEqual(executeQuery('excludes(path.1,7)&sort()', {}, data), [ data[0] ]); // 7 found in second 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/intern.js: -------------------------------------------------------------------------------- 1 | // Learn more about configuring this file at . 2 | // These default settings work OK for most people. The options that *must* be changed below are the 3 | // packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites. 4 | define({ 5 | // The port on which the instrumenting proxy will listen 6 | proxyPort: 9000, 7 | 8 | // A fully qualified URL to the Intern proxy 9 | proxyUrl: 'http://localhost:9000/', 10 | 11 | // Default desired capabilities for all environments. Individual capabilities can be overridden by any of the 12 | // specified browser environments in the `environments` array below as well. See 13 | // https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and 14 | // https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities. 15 | // Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment 16 | // automatically 17 | capabilities: { 18 | 'selenium-version': '2.42.0', 19 | 'idle-timeout': 30 20 | }, 21 | 22 | // Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce 23 | // OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other 24 | // capabilities options specified for an environment will be copied as-is 25 | environments: [ 26 | { browserName: 'internet explorer', version: '11', platform: 'Windows 8.1' }, 27 | { browserName: 'internet explorer', version: '10', platform: 'Windows 8' }, 28 | { browserName: 'internet explorer', version: '9', platform: 'Windows 7' }, 29 | { browserName: 'internet explorer', version: '8', platform: 'Windows XP' }, 30 | { browserName: 'firefox', version: '31', platform: [ 'OS X 10.9', 'Windows 7', 'Linux' ] }, 31 | { browserName: 'chrome', version: '34', platform: [ 'OS X 10.9', 'Windows 7', 'Linux' ] }, 32 | { browserName: 'safari', version: '7', platform: 'OS X 10.9' } 33 | ], 34 | 35 | // Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service 36 | maxConcurrency: 3, 37 | 38 | // Name of the tunnel class to use for WebDriver tests 39 | tunnel: 'SauceLabsTunnel', 40 | 41 | // Configuration options for the module loader; any AMD configuration options supported by the specified AMD loader 42 | // can be used here 43 | loader: (typeof process === 'undefined' && location.search.indexOf('config=rql') > -1) ? 44 | { 45 | // if we are using the full path to rql, we assume we are running 46 | // in a sibling path configuration 47 | baseUrl: '../..' 48 | } : {}, 49 | 50 | // Non-functional test suite(s) to run in each browser 51 | suites: [ (typeof process === 'undefined' && location.search.indexOf('config=rql') > -1) ? 52 | 'rql/test/all' : 'test/all' ], 53 | 54 | // A regular expression matching URLs to files that should not be included in code coverage analysis 55 | excludeInstrumentation: /^(?:test|node_modules)\// 56 | }); 57 | -------------------------------------------------------------------------------- /query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a Query constructor with chainable capability. For example: 3 | * var Query = require("./query").Query; 4 | * query = Query(); 5 | * query.executor = function(query){ 6 | * require("./js-array").query(query, params, data); // if we want to operate on an array 7 | * }; 8 | * query.eq("a", 3).le("b", 4).forEach(function(object){ 9 | * // for each object that matches the query 10 | * }); 11 | */ 12 | //({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./js-array"));}}). 13 | //define(["exports", "./parser", "./js-array"], function(exports, parser, jsarray){ 14 | ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./util/each"));}}). 15 | define(["exports", "./parser", "./util/each"], function(exports, parser, each){ 16 | 17 | var parseQuery = parser.parseQuery; 18 | try{ 19 | var when = require("promised-io/promise").when; 20 | }catch(e){ 21 | when = function(value, callback){callback(value)}; 22 | } 23 | 24 | parser.Query = function(seed, params){ 25 | if (typeof seed === 'string') 26 | return parseQuery(seed, params); 27 | var q = new Query(); 28 | if (seed && seed.name && seed.args) 29 | q.name = seed.name, q.args = seed.args; 30 | return q; 31 | }; 32 | exports.Query = parser.Query; 33 | //TODO:THE RIGHT WAY IS:exports.knownOperators = Object.keys(jsarray.operators || {}).concat(Object.keys(jsarray.jsOperatorMap || {})); 34 | exports.knownOperators = ["sort", "match", "in", "out", "or", "and", "select", "contains", "excludes", "values", "limit", "distinct", "recurse", "aggregate", "between", "sum", "mean", "max", "min", "count", "first", "one", "eq", "ne", "le", "ge", "lt", "gt"]; 35 | exports.knownScalarOperators = ["mean", "sum", "min", "max", "count", "first", "one"]; 36 | exports.arrayMethods = ["forEach", "reduce", "map", "filter", "indexOf", "some", "every"]; 37 | 38 | function Query(name){ 39 | this.name = name || "and"; 40 | this.args = []; 41 | } 42 | function serializeArgs(array, delimiter){ 43 | var results = []; 44 | for(var i = 0, l = array.length; i < l; i++){ 45 | results.push(queryToString(array[i])); 46 | } 47 | return results.join(delimiter); 48 | } 49 | exports.Query.prototype = Query.prototype; 50 | Query.prototype.toString = function(){ 51 | return this.name === "and" ? 52 | serializeArgs(this.args, "&") : 53 | queryToString(this); 54 | }; 55 | 56 | function queryToString(part) { 57 | if (part instanceof Array) { 58 | return '(' + serializeArgs(part, ",")+')'; 59 | } 60 | if (part && part.name && part.args) { 61 | return [ 62 | part.name, 63 | "(", 64 | serializeArgs(part.args, ","), 65 | ")" 66 | ].join(""); 67 | } 68 | return exports.encodeValue(part); 69 | }; 70 | 71 | function encodeString(s) { 72 | if (typeof s === "string") { 73 | s = encodeURIComponent(s); 74 | if (s.match(/[\(\)]/)) { 75 | s = s.replace("(","%28").replace(")","%29"); 76 | }; 77 | } 78 | return s; 79 | } 80 | 81 | exports.encodeValue = function(val) { 82 | var encoded; 83 | if (val === null) val = 'null'; 84 | if (val !== parser.converters["default"]('' + ( 85 | val.toISOString && val.toISOString() || val.toString() 86 | ))) { 87 | var type = typeof val; 88 | if(val instanceof RegExp){ 89 | // TODO: control whether to we want simpler glob() style 90 | val = val.toString(); 91 | var i = val.lastIndexOf('/'); 92 | type = val.substring(i).indexOf('i') >= 0 ? "re" : "RE"; 93 | val = encodeString(val.substring(1, i)); 94 | encoded = true; 95 | } 96 | if(type === "object"){ 97 | type = "epoch"; 98 | val = val.getTime(); 99 | encoded = true; 100 | } 101 | if(type === "string") { 102 | val = encodeString(val); 103 | encoded = true; 104 | } 105 | val = [type, val].join(":"); 106 | } 107 | if (!encoded && typeof val === "string") val = encodeString(val); 108 | return val; 109 | }; 110 | 111 | exports.updateQueryMethods = function(){ 112 | each(exports.knownOperators, function(name){ 113 | Query.prototype[name] = function(){ 114 | var newQuery = new Query(); 115 | newQuery.executor = this.executor; 116 | var newTerm = new Query(name); 117 | newTerm.args = Array.prototype.slice.call(arguments); 118 | newQuery.args = this.args.concat([newTerm]); 119 | return newQuery; 120 | }; 121 | }); 122 | each(exports.knownScalarOperators, function(name){ 123 | Query.prototype[name] = function(){ 124 | var newQuery = new Query(); 125 | newQuery.executor = this.executor; 126 | var newTerm = new Query(name); 127 | newTerm.args = Array.prototype.slice.call(arguments); 128 | newQuery.args = this.args.concat([newTerm]); 129 | return newQuery.executor(newQuery); 130 | }; 131 | }); 132 | each(exports.arrayMethods, function(name){ 133 | // this makes no guarantee of ensuring that results supports these methods 134 | Query.prototype[name] = function(){ 135 | var args = arguments; 136 | return when(this.executor(this), function(results){ 137 | return results[name].apply(results, args); 138 | }); 139 | }; 140 | }); 141 | 142 | }; 143 | 144 | exports.updateQueryMethods(); 145 | 146 | /* recursively iterate over query terms calling 'fn' for each term */ 147 | Query.prototype.walk = function(fn, options){ 148 | options = options || {}; 149 | function walk(name, terms){ 150 | terms = terms || []; 151 | 152 | var i = 0, 153 | l = terms.length, 154 | term, 155 | args, 156 | func, 157 | newTerm; 158 | 159 | for (; i < l; i++) { 160 | term = terms[i]; 161 | if (term == null) { 162 | term = {}; 163 | } 164 | func = term.name; 165 | args = term.args; 166 | if (!func || !args) { 167 | continue; 168 | } 169 | if (args[0] instanceof Query) { 170 | walk.call(this, func, args); 171 | } 172 | else { 173 | newTerm = fn.call(this, func, args); 174 | if (newTerm && newTerm.name && newTerm.ags) { 175 | terms[i] = newTerm; 176 | } 177 | } 178 | } 179 | } 180 | walk.call(this, this.name, this.args); 181 | }; 182 | 183 | /* append a new term */ 184 | Query.prototype.push = function(term){ 185 | this.args.push(term); 186 | return this; 187 | }; 188 | 189 | /* disambiguate query */ 190 | Query.prototype.normalize = function(options){ 191 | options = options || {}; 192 | options.primaryKey = options.primaryKey || 'id'; 193 | options.map = options.map || {}; 194 | var result = { 195 | original: this, 196 | sort: [], 197 | limit: [Infinity, 0, Infinity], 198 | skip: 0, 199 | limit: Infinity, 200 | select: [], 201 | values: false 202 | }; 203 | var plusMinus = { 204 | // [plus, minus] 205 | sort: [1, -1], 206 | select: [1, 0] 207 | }; 208 | function normal(func, args){ 209 | // cache some parameters 210 | if (func === 'sort' || func === 'select') { 211 | result[func] = args; 212 | var pm = plusMinus[func]; 213 | result[func+'Arr'] = result[func].map(function(x){ 214 | if (x instanceof Array) x = x.join('.'); 215 | var o = {}; 216 | var a = /([-+]*)(.+)/.exec(x); 217 | o[a[2]] = pm[(a[1].charAt(0) === '-')*1]; 218 | return o; 219 | }); 220 | result[func+'Obj'] = {}; 221 | result[func].forEach(function(x){ 222 | if (x instanceof Array) x = x.join('.'); 223 | var a = /([-+]*)(.+)/.exec(x); 224 | result[func+'Obj'][a[2]] = pm[(a[1].charAt(0) === '-')*1]; 225 | }); 226 | } else if (func === 'limit') { 227 | // validate limit() args to be numbers, with sane defaults 228 | var limit = args; 229 | result.skip = +limit[1] || 0; 230 | limit = +limit[0] || 0; 231 | if (options.hardLimit && limit > options.hardLimit) 232 | limit = options.hardLimit; 233 | result.limit = limit; 234 | result.needCount = true; 235 | } else if (func === 'values') { 236 | // N.B. values() just signals we want array of what we select() 237 | result.values = true; 238 | } else if (func === 'eq') { 239 | // cache primary key equality -- useful to distinguish between .get(id) and .query(query) 240 | var t = typeof args[1]; 241 | //if ((args[0] instanceof Array ? args[0][args[0].length-1] : args[0]) === options.primaryKey && ['string','number'].indexOf(t) >= 0) { 242 | if (args[0] === options.primaryKey && ('string' === t || 'number' === t)) { 243 | result.pk = String(args[1]); 244 | } 245 | } 246 | // cache search conditions 247 | //if (options.known[func]) 248 | // map some functions 249 | /*if (options.map[func]) { 250 | func = options.map[func]; 251 | }*/ 252 | } 253 | this.walk(normal); 254 | return result; 255 | }; 256 | 257 | /* FIXME: an example will be welcome 258 | Query.prototype.toMongo = function(options){ 259 | return this.normalize({ 260 | primaryKey: '_id', 261 | map: { 262 | ge: 'gte', 263 | le: 'lte' 264 | }, 265 | known: ['lt','lte','gt','gte','ne','in','nin','not','mod','all','size','exists','type','elemMatch'] 266 | }); 267 | }; 268 | */ 269 | 270 | return exports; 271 | }); 272 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides RQL parsing. For example: 3 | * var parsed = require("./parser").parse("b=3&le(c,5)"); 4 | */ 5 | ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./util/contains"));}}). 6 | define(["exports", "./util/contains"], function(exports, contains){ 7 | 8 | var operatorMap = { 9 | "=": "eq", 10 | "==": "eq", 11 | ">": "gt", 12 | ">=": "ge", 13 | "<": "lt", 14 | "<=": "le", 15 | "!=": "ne" 16 | }; 17 | 18 | 19 | exports.primaryKeyName = 'id'; 20 | exports.lastSeen = ['sort', 'select', 'values', 'limit']; 21 | exports.jsonQueryCompatible = true; 22 | 23 | function parse(/*String|Object*/query, parameters){ 24 | if (typeof query === "undefined" || query === null) 25 | query = ''; 26 | var term = new exports.Query(); 27 | var topTerm = term; 28 | topTerm.cache = {}; // room for lastSeen params 29 | var topTermName = topTerm.name; 30 | topTerm.name = ''; 31 | if(typeof query === "object"){ 32 | if(query instanceof exports.Query){ 33 | return query; 34 | } 35 | for(var i in query){ 36 | var term = new exports.Query(); 37 | topTerm.args.push(term); 38 | term.name = "eq"; 39 | term.args = [i, query[i]]; 40 | } 41 | return topTerm; 42 | } 43 | if(query.charAt(0) == "?"){ 44 | throw new URIError("Query must not start with ?"); 45 | } 46 | if(exports.jsonQueryCompatible){ 47 | query = query.replace(/%3C=/g,"=le=").replace(/%3E=/g,"=ge=").replace(/%3C/g,"=lt=").replace(/%3E/g,"=gt="); 48 | } 49 | if(query.indexOf("/") > -1){ // performance guard 50 | // convert slash delimited text to arrays 51 | query = query.replace(/[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, function(slashed){ 52 | return "(" + slashed.replace(/\//g, ",") + ")"; 53 | }); 54 | } 55 | // convert FIQL to normalized call syntax form 56 | query = query.replace(/(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g, 57 | // <--------- property -----------><------ operator -----><---------------- value ------------------> 58 | function(t, property, operator, value){ 59 | if(operator.length < 3){ 60 | if(!operatorMap[operator]){ 61 | throw new URIError("Illegal operator " + operator); 62 | } 63 | operator = operatorMap[operator]; 64 | } 65 | else{ 66 | operator = operator.substring(1, operator.length - 1); 67 | } 68 | return operator + '(' + property + "," + value + ")"; 69 | }); 70 | if(query.charAt(0)=="?"){ 71 | query = query.substring(1); 72 | } 73 | var leftoverCharacters = query.replace(/(\))|([&\|,])?([\+\*\$\-:\w%\._]*)(\(?)/g, 74 | // <-closedParan->|<-delim-- propertyOrValue -----(> | 75 | function(t, closedParan, delim, propertyOrValue, openParan){ 76 | if(delim){ 77 | if(delim === "&"){ 78 | setConjunction("and"); 79 | } 80 | if(delim === "|"){ 81 | setConjunction("or"); 82 | } 83 | } 84 | if(openParan){ 85 | var newTerm = new exports.Query(); 86 | newTerm.name = propertyOrValue; 87 | newTerm.parent = term; 88 | call(newTerm); 89 | } 90 | else if(closedParan){ 91 | var isArray = !term.name; 92 | term = term.parent; 93 | if(!term){ 94 | throw new URIError("Closing paranthesis without an opening paranthesis"); 95 | } 96 | if(isArray){ 97 | term.args.push(term.args.pop().args); 98 | } 99 | } 100 | else if(propertyOrValue || delim === ','){ 101 | term.args.push(stringToValue(propertyOrValue, parameters)); 102 | 103 | // cache the last seen sort(), select(), values() and limit() 104 | if (contains(exports.lastSeen, term.name)) { 105 | topTerm.cache[term.name] = term.args; 106 | } 107 | // cache the last seen id equality 108 | if (term.name === 'eq' && term.args[0] === exports.primaryKeyName) { 109 | var id = term.args[1]; 110 | if (id && !(id instanceof RegExp)) id = id.toString(); 111 | topTerm.cache[exports.primaryKeyName] = id; 112 | } 113 | } 114 | return ""; 115 | }); 116 | if(term.parent){ 117 | throw new URIError("Opening paranthesis without a closing paranthesis"); 118 | } 119 | if(leftoverCharacters){ 120 | // any extra characters left over from the replace indicates invalid syntax 121 | throw new URIError("Illegal character in query string encountered " + leftoverCharacters); 122 | } 123 | 124 | function call(newTerm){ 125 | term.args.push(newTerm); 126 | term = newTerm; 127 | // cache the last seen sort(), select(), values() and limit() 128 | if (contains(exports.lastSeen, term.name)) { 129 | topTerm.cache[term.name] = term.args; 130 | } 131 | } 132 | function setConjunction(operator){ 133 | if(!term.name){ 134 | term.name = operator; 135 | } 136 | else if(term.name !== operator){ 137 | throw new Error("Can not mix conjunctions within a group, use paranthesis around each set of same conjuctions (& and |)"); 138 | } 139 | } 140 | function removeParentProperty(obj) { 141 | if(obj && obj.args){ 142 | delete obj.parent; 143 | var args = obj.args; 144 | for(var i = 0, l = args.length; i < l; i++){ 145 | removeParentProperty(args[i]); 146 | } 147 | } 148 | return obj; 149 | }; 150 | removeParentProperty(topTerm); 151 | if (!topTerm.name) { 152 | topTerm.name = topTermName; 153 | } 154 | return topTerm; 155 | }; 156 | 157 | exports.parse = exports.parseQuery = parse; 158 | 159 | /* dumps undesirable exceptions to Query().error */ 160 | exports.parseGently = function(){ 161 | var terms; 162 | try { 163 | terms = parse.apply(this, arguments); 164 | } catch(err) { 165 | terms = new exports.Query(); 166 | terms.error = err.message; 167 | } 168 | return terms; 169 | } 170 | 171 | exports.commonOperatorMap = { 172 | "and" : "&", 173 | "or" : "|", 174 | "eq" : "=", 175 | "ne" : "!=", 176 | "le" : "<=", 177 | "ge" : ">=", 178 | "lt" : "<", 179 | "gt" : ">" 180 | } 181 | function stringToValue(string, parameters){ 182 | var converter = exports.converters['default']; 183 | if(string.charAt(0) === "$"){ 184 | var param_index = parseInt(string.substring(1)) - 1; 185 | return param_index >= 0 && parameters ? parameters[param_index] : undefined; 186 | } 187 | if(string.indexOf(":") > -1){ 188 | var parts = string.split(":"); 189 | converter = exports.converters[parts[0]]; 190 | if(!converter){ 191 | throw new URIError("Unknown converter " + parts[0]); 192 | } 193 | string = parts.slice(1).join(':'); 194 | } 195 | return converter(string); 196 | }; 197 | 198 | var autoConverted = exports.autoConverted = { 199 | "true": true, 200 | "false": false, 201 | "null": null, 202 | "undefined": undefined, 203 | "Infinity": Infinity, 204 | "-Infinity": -Infinity 205 | }; 206 | 207 | exports.converters = { 208 | auto: function(string){ 209 | if(autoConverted.hasOwnProperty(string)){ 210 | return autoConverted[string]; 211 | } 212 | var number = +string; 213 | if(isNaN(number) || number.toString() !== string){ 214 | /*var isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(x); 215 | if (isoDate) { 216 | date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6], +isoDate[7] || 0)); 217 | }*/ 218 | string = decodeURIComponent(string); 219 | if(exports.jsonQueryCompatible){ 220 | if(string.charAt(0) == "'" && string.charAt(string.length-1) == "'"){ 221 | return JSON.parse('"' + string.substring(1,string.length-1) + '"'); 222 | } 223 | } 224 | return string; 225 | } 226 | return number; 227 | }, 228 | number: function(x){ 229 | var number = +x; 230 | if(isNaN(number)){ 231 | throw new URIError("Invalid number " + number); 232 | } 233 | return number; 234 | }, 235 | epoch: function(x){ 236 | var date = new Date(+x); 237 | if (isNaN(date.getTime())) { 238 | throw new URIError("Invalid date " + x); 239 | } 240 | return date; 241 | }, 242 | isodate: function(x){ 243 | // four-digit year 244 | var date = '0000'.substr(0,4-x.length)+x; 245 | // pattern for partial dates 246 | date += '0000-01-01T00:00:00Z'.substring(date.length); 247 | return exports.converters.date(date); 248 | }, 249 | date: function(x){ 250 | var isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(x); 251 | var date; 252 | if (isoDate) { 253 | date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6], +isoDate[7] || 0)); 254 | }else{ 255 | date = new Date(x); 256 | } 257 | if (isNaN(date.getTime())){ 258 | throw new URIError("Invalid date " + x); 259 | } 260 | return date; 261 | }, 262 | "boolean": function(x){ 263 | return x === "true"; 264 | }, 265 | string: function(string){ 266 | return decodeURIComponent(string); 267 | }, 268 | re: function(x){ 269 | return new RegExp(decodeURIComponent(x), 'i'); 270 | }, 271 | RE: function(x){ 272 | return new RegExp(decodeURIComponent(x)); 273 | }, 274 | glob: function(x){ 275 | var s = decodeURIComponent(x).replace(/([\\|\||\(|\)|\[|\{|\^|\$|\*|\+|\?|\.|\<|\>])/g, function(x){return '\\'+x;}).replace(/\\\*/g,'.*').replace(/\\\?/g,'.?'); 276 | if (s.substring(0,2) !== '.*') s = '^'+s; else s = s.substring(2); 277 | if (s.substring(s.length-2) !== '.*') s = s+'$'; else s = s.substring(0, s.length-2); 278 | return new RegExp(s, 'i'); 279 | } 280 | }; 281 | 282 | // exports.converters["default"] can be changed to a different converter if you want 283 | // a different default converter, for example: 284 | // RP = require("rql/parser"); 285 | // RP.converters["default"] = RQ.converter.string; 286 | exports.converters["default"] = exports.converters.auto; 287 | 288 | // this can get replaced by the chainable query if query.js is loaded 289 | exports.Query = function(){ 290 | this.name = "and"; 291 | this.args = []; 292 | }; 293 | return exports; 294 | }); 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/persvr/rql.svg?branch=master)](https://travis-ci.org/persvr/rql) 2 | 3 | Resource Query Language (RQL) is a query language designed for use in URIs with object 4 | style data structures. This project includes the RQL specification and 5 | provides a JavaScript implementation of query 6 | parsing and query execution implementation for JavaScript arrays. The JavaScript library 7 | supports AMD and NodeJS/CommonJS module format so it can be run in the browser or 8 | in the server. RQL can be thought as basically a set of 9 | nestable named operators which each have a set of arguments. RQL is designed to 10 | have an extremely simple, but extensible grammar that can be written in a URL friendly query string. A simple RQL 11 | query with a single operator that indicates a search for any resources with a property of 12 | "foo" that has value of 3 could be written: 13 | 14 | eq(foo,3) 15 | 16 | RQL is a compatible superset of standard HTML form URL encoding. The following query 17 | is identical to the query (it is sugar for the query above): 18 | 19 | foo=3 20 | 21 | Such that this can be used in URIs like: 22 | 23 | http://example.org/data?foo=3 24 | 25 | # JavaScript Library 26 | 27 | Using the JavaScript library we can construct queries 28 | using chained operator calls in JavaScript. We could execute the query above like this: 29 | 30 | var Query = require("rql/query").Query; 31 | var fooEq3Query = new Query().eq("foo",3); 32 | 33 | # RQL Rules 34 | 35 | The RQL grammar is based around standard URI delimiters. The standard rules for 36 | encoding strings with URL encoding (%xx) are observed. RQL also supersets FIQL. 37 | Therefore we can write a query that finds resources with a "price" property below 38 | 10 with a "lt" operator using FIQL syntax: 39 | 40 | price=lt=10 41 | 42 | Which is identical (and sugar for call operator syntax known as the normalized form): 43 | 44 | lt(price,10) 45 | 46 | One can combine conditions with multiple operators with "&": 47 | 48 | foo=3&price=lt=10 49 | 50 | Is the same as: 51 | 52 | eq(foo,3)<(price,10) 53 | 54 | Which is also the same as: 55 | 56 | and(eq(foo,3),lt(price,10)) 57 | 58 | We can execute a query against a JavaScript array: 59 | 60 | require("rql/js-array").executeQuery("foo=3&price=lt=10", {}, data)... 61 | 62 | The | operator can be used to indicate an "or" operation. We can also use paranthesis 63 | to group expressions. For example: 64 | 65 | (foo=3|foo=bar)&price=lt=10 66 | 67 | Which is the same as: 68 | 69 | and(or(eq(foo,3),eq(foo,bar)),lt(price,10)) 70 | 71 | Values in queries can be strings (using URL encoding), numbers, booleans, null, undefined, 72 | and dates (in ISO UTC format without colon encoding). We can also denote arrays 73 | with paranthesis enclosed, comma separated values. For example to find the objects 74 | where foo can be the number 3, the string bar, the boolean true, or the date for the 75 | first day of the century we could write an array with the "in" operator: 76 | 77 | foo=in=(3,bar,true,2000-01-01T00:00:00Z) 78 | 79 | We can also explicitly specify primitive types in queries. To explicitly specify a string "3", 80 | we can do: 81 | 82 | foo=string:3 83 | 84 | Any property can be nested by using an array of properties. To search by the bar property of 85 | the object in the foo property we can do: 86 | 87 | (foo,bar)=3 88 | 89 | We can also use slashes as shorthand for arrays, so we could equivalently write the nested 90 | query: 91 | 92 | foo/bar=3 93 | 94 | Another common operator is sort. We can use the sort operator to sort by a specified property. 95 | To sort by foo in ascending order: 96 | 97 | price=lt=10&sort(+foo) 98 | 99 | We can also do multiple property sorts. To sort by price in ascending order and rating in descending order: 100 | 101 | sort(+price,-rating) 102 | 103 | The aggregate function can be used for aggregation. To calculate the sum of sales for 104 | each department: 105 | 106 | aggregate(departmentId,sum(sales)) 107 | 108 | Here is a definition of the common operators (individual stores may have support 109 | for more less operators): 110 | 111 | * sort(<+|-><property) - Sorts by the given property in order specified by the prefix (+ for ascending, - for descending) 112 | * select(<property>,<property>,...) - Trims each object down to the set of properties defined in the arguments 113 | * values(<property>) - Returns an array of the given property value for each object 114 | * aggregate(<property|function>,...) - Aggregates the array, grouping by objects that are distinct for the provided properties, and then reduces the remaining other property values using the provided functions 115 | * distinct() - Returns a result set with duplicates removed 116 | * in(<property>,<array-of-values>) - Filters for objects where the specified property's value is in the provided array 117 | * out(<property>,<array-of-values>) - Filters for objects where the specified property's value is not in the provided array 118 | * contains(<property>,<value | expression>) - Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided expression. 119 | * excludes(<property>,<value | expression>) - Filters for objects where the specified property's value is an array and the array does not contain any of value that equals the provided value or satisfies the provided expression. 120 | * limit(count,start,maxCount) - Returns the given range of objects from the result set 121 | * and(<query>,<query>,...) - Applies all the given queries 122 | * or(<query>,<query>,...) - The union of the given queries 123 | * eq(<property>,<value>) - Filters for objects where the specified property's value is equal to the provided value 124 | * lt(<property>,<value>) - Filters for objects where the specified property's value is less than the provided value 125 | * le(<property>,<value>) - Filters for objects where the specified property's value is less than or equal to the provided value 126 | * gt(<property>,<value>) - Filters for objects where the specified property's value is greater than the provided value 127 | * ge(<property>,<value>) - Filters for objects where the specified property's value is greater than or equal to the provided value 128 | * ne(<property>,<value>) - Filters for objects where the specified property's value is not equal to the provided value 129 | * rel(<relation name?>,<query>) - Applies the provided query against the linked data of the provided relation name. 130 | * sum(<property?>) - Finds the sum of every value in the array or if the property argument is provided, returns the sum of the value of property for every object in the array 131 | * mean(<property?>) - Finds the mean of every value in the array or if the property argument is provided, returns the mean of the value of property for every object in the array 132 | * max(<property?>) - Finds the maximum of every value in the array or if the property argument is provided, returns the maximum of the value of property for every object in the array 133 | * min(<property?>) - Finds the minimum of every value in the array or if the property argument is provided, returns the minimum of the value of property for every object in the array 134 | * recurse(<property?>) - Recursively searches, looking in children of the object as objects in arrays in the given property value 135 | * first() - Returns the first record of the query's result set 136 | * one() - Returns the first and only record of the query's result set, or produces an error if the query's result set has more or less than one record in it. 137 | * count() - Returns the count of the number of records in the query's result set 138 | 139 | # JavaScript Modules 140 | 141 | ## rql/query 142 | 143 | var newQuery = require("rql/query").Query(); 144 | 145 | This module allows us to construct queries. With the query object, we could execute 146 | RQL operators as methods against the query object. For example: 147 | 148 | var Query = require("rql/query").Query; 149 | var fooBetween3And10Query = new Query().lt("foo",3).gt("foo",10); 150 | 151 | ## rql/parser 152 | 153 | var parsedQueryObject = require("rql/parser").parseQuery(rqlString); 154 | 155 | If you are writing an implementation of RQL for a database or other storage endpoint, or want to introspect queries, you can use the parsed query data 156 | structures. You can parse string queries with parser module's parseQuery function. 157 | Query objects have a "name" property and an "args" with an array of the arguments. 158 | For example: 159 | 160 | require("rql/parser").parseQuery("(foo=3|foo=bar)&price=lt=10") -> 161 | { 162 | name: "and", 163 | args: [ 164 | { 165 | name:"or", 166 | args:[ 167 | { 168 | name:"eq", 169 | args:["foo",3] 170 | }, 171 | { 172 | name:"eq", 173 | args:["foo","bar"] 174 | } 175 | ] 176 | }, 177 | { 178 | name:"lt", 179 | args:["price",10] 180 | } 181 | ] 182 | } 183 | 184 | Installation 185 | ======== 186 | 187 | RQL can be installed using any standard package manager, for example with NPM: 188 | 189 | npm install rql 190 | 191 | or CPM: 192 | 193 | cpm install rql 194 | 195 | or RingoJS: 196 | 197 | ringo-admin install persvr/rql 198 | 199 | 200 | Licensing 201 | -------- 202 | 203 | The RQL implementation is part of the Persevere project, and therefore is licensed under the 204 | AFL or BSD license. The Persevere project is administered under the Dojo foundation, 205 | and all contributions require a Dojo CLA. 206 | 207 | Project Links 208 | ------------ 209 | 210 | See the main Persevere project for more information: 211 | 212 | ### Homepage: 213 | 214 | * [http://persvr.org/](http://persvr.org/) 215 | 216 | ### Mailing list: 217 | 218 | * [http://groups.google.com/group/json-query](http://groups.google.com/group/json-query) 219 | 220 | ### IRC: 221 | 222 | * [\#persevere on irc.freenode.net](http://webchat.freenode.net/?channels=persevere) 223 | -------------------------------------------------------------------------------- /test/query.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | var test = require('intern!object'), 3 | assert = require('intern/chai!assert'), 4 | Query = require('../query').Query, 5 | parseQuery = require('../parser').parseQuery, 6 | JSON = require('intern/dojo/json'), 7 | supportsDateString = !isNaN(new Date('2009')), 8 | queryPairs = { 9 | arrays: { 10 | a: { name: 'and', args: [ 'a' ]}, 11 | '(a)': { name: 'and', args: [[ 'a' ]]}, 12 | 'a,b,c': { name: 'and', args: [ 'a', 'b', 'c' ]}, 13 | '(a,b,c)': { name: 'and', args: [[ 'a', 'b', 'c']]}, 14 | 'a(b)': { name: 'and', args: [{ name: 'a', args: [ 'b' ]}]}, 15 | 'a(b,c)': { name: 'and', args: [{ name: 'a', args: [ 'b', 'c' ]}]}, 16 | 'a((b),c)': { name: 'and', args: [{ name: 'a', args: [ [ 'b' ], 'c' ]}]}, 17 | 'a((b,c),d)': { name: 'and', args: [{ name: 'a', args: [ [ 'b', 'c' ], 'd' ]}]}, 18 | 'a(b/c,d)': { name: 'and', args: [{ name: 'a', args: [ [ 'b', 'c' ], 'd' ]}]}, 19 | 'a(b)&c(d(e))': { name: 'and', args:[ 20 | { name: 'a', args: [ 'b' ]}, 21 | { name: 'c', args: [ { name: 'd', args: [ 'e' ]} ]} 22 | ]} 23 | }, 24 | 'dot-comparison': { 25 | 'foo.bar=3': { name: 'and', args: [{ name: 'eq', args: [ 'foo.bar', 3 ]}]}, 26 | 'select(sub.name)': { 27 | name: 'and', 28 | args: [ { name: 'select', args: [ 'sub.name' ]} ], 29 | cache: { select: [ 'sub.name' ]} 30 | } 31 | }, 32 | equality: { 33 | 'eq(a,b)': { name: 'and', args:[{ name: 'eq', args: [ 'a', 'b' ]}]}, 34 | 'a=eq=b': 'eq(a,b)', 35 | 'a=b': 'eq(a,b)' 36 | }, 37 | inequality: { 38 | 'ne(a,b)': { name: 'and', args: [{ name: 'ne', args: [ 'a', 'b' ]}]}, 39 | 'a=ne=b': 'ne(a,b)', 40 | 'a!=b': 'ne(a,b)' 41 | }, 42 | 'less-than': { 43 | 'lt(a,b)': { name: 'and', args: [{ name: 'lt', args: [ 'a', 'b' ]}]}, 44 | 'a=lt=b': 'lt(a,b)', 45 | 'ab': 'gt(a,b)' 56 | }, 57 | 'greater-than-equals': { 58 | 'ge(a,b)': { name: 'and', args: [{ name: 'ge', args: [ 'a', 'b' ]}]}, 59 | 'a=ge=b': 'ge(a,b)', 60 | 'a>=b': 'ge(a,b)' 61 | }, 62 | 'nested comparisons': { 63 | 'a(b(le(c,d)))': { name: 'and', args: [ 64 | { name: 'a', args: [{ name: 'b', args: [{ name: 'le', args: [ 'c', 'd' ]}]}]} 65 | ]}, 66 | 'a(b(c=le=d))': 'a(b(le(c,d)))', 67 | 'a(b(c<=d))': 'a(b(le(c,d)))' 68 | }, 69 | 'arbitrary FIQL desugaring': { 70 | 'a=b=c': { name: 'and', args: [{ name: 'b', args: [ 'a', 'c' ]}]}, 71 | 'a(b=cd=e)': { name: 'and', args: [{ name: 'a', args: [{ name: 'cd', args: [ 'b', 'e' ]}]}]} 72 | }, 73 | 'and grouping': { 74 | 'a&b&c': { name: 'and', args: [ 'a', 'b', 'c' ]}, 75 | 'a(b)&c': { name: 'and', args: [ { name: 'a', args: [ 'b' ] }, 'c' ]}, 76 | 'a&(b&c)': { name: 'and', args: [ 'a', { name: 'and', args: [ 'b', 'c' ]}]} 77 | }, 78 | 'or grouping': { 79 | '(a|b|c)': { name: 'and', args: [{ name: 'or', args: [ 'a', 'b', 'c' ]}]}, 80 | '(a(b)|c)': { name: 'and', args: [{ name: 'or', args: [ { name: 'a', args: [ 'b' ]}, 'c' ]}]} 81 | }, 82 | 'complex grouping': { 83 | 'a&(b|c)': { name: 'and', args: [ 'a', { name: 'or', args: [ 'b', 'c' ]}]}, 84 | 'a|(b&c)': { name: 'or', args: [ 'a', { name: 'and', args: [ 'b', 'c' ]}]}, 85 | 'a(b(c [{a:3}] 4 | * 5 | */ 6 | 7 | ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./query"), require("./util/each"), require("./util/contains"));}}). 8 | define(["exports", "./parser", "./query", "./util/each", "./util/contains"], function(exports, parser, QUERY, each, contains){ 9 | //({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"));}}). 10 | //define(["exports", "./parser"], function(exports, parser){ 11 | 12 | var parseQuery = parser.parseQuery; 13 | var stringify = typeof JSON !== "undefined" && JSON.stringify || function(str){ 14 | return '"' + str.replace(/"/g, "\\\"") + '"'; 15 | }; 16 | var nextId = 1; 17 | exports.jsOperatorMap = { 18 | "eq" : "===", 19 | "ne" : "!==", 20 | "le" : "<=", 21 | "ge" : ">=", 22 | "lt" : "<", 23 | "gt" : ">" 24 | }; 25 | exports.operators = { 26 | sort: function(){ 27 | var terms = []; 28 | for(var i = 0; i < arguments.length; i++){ 29 | var sortAttribute = arguments[i]; 30 | var firstChar = sortAttribute.charAt(0); 31 | var term = {attribute: sortAttribute, ascending: true}; 32 | if (firstChar == "-" || firstChar == "+") { 33 | if(firstChar == "-"){ 34 | term.ascending = false; 35 | } 36 | term.attribute = term.attribute.substring(1); 37 | } 38 | terms.push(term); 39 | } 40 | this.sort(function(a, b){ 41 | for (var term, i = 0; term = terms[i]; i++) { 42 | if (a[term.attribute] != b[term.attribute]) { 43 | return term.ascending == a[term.attribute] > b[term.attribute] ? 1 : -1; 44 | } 45 | } 46 | return 0; 47 | }); 48 | return this; 49 | }, 50 | match: filter(function(value, regex){ 51 | return new RegExp(regex).test(value); 52 | }), 53 | "in": filter(function(value, values){ 54 | return contains(values, value); 55 | }), 56 | out: filter(function(value, values){ 57 | return !contains(values, value); 58 | }), 59 | contains: filter(function(array, value){ 60 | if(typeof value == "function"){ 61 | return array instanceof Array && each(array, function(v){ 62 | return value.call([v]).length; 63 | }); 64 | } 65 | else{ 66 | return array instanceof Array && contains(array, value); 67 | } 68 | }), 69 | excludes: filter(function(array, value){ 70 | if(typeof value == "function"){ 71 | return !each(array, function(v){ 72 | return value.call([v]).length; 73 | }); 74 | } 75 | else{ 76 | return !contains(array, value); 77 | } 78 | }), 79 | or: function(){ 80 | var items = []; 81 | var idProperty = "__rqlId" + nextId++; 82 | try{ 83 | for(var i = 0; i < arguments.length; i++){ 84 | var group = arguments[i].call(this); 85 | for(var j = 0, l = group.length;j < l;j++){ 86 | var item = group[j]; 87 | // use marker to do a union in linear time. 88 | if(!item[idProperty]){ 89 | item[idProperty] = true; 90 | items.push(item); 91 | } 92 | } 93 | } 94 | }finally{ 95 | // cleanup markers 96 | for(var i = 0, l = items.length; i < l; i++){ 97 | delete items[idProperty]; 98 | } 99 | } 100 | return items; 101 | }, 102 | and: function(){ 103 | var items = this; 104 | // TODO: use condition property 105 | for(var i = 0; i < arguments.length; i++){ 106 | items = arguments[i].call(items); 107 | } 108 | return items; 109 | }, 110 | select: function(){ 111 | var args = arguments; 112 | var argc = arguments.length; 113 | return each(this, function(object, emit){ 114 | var selected = {}; 115 | for(var i = 0; i < argc; i++){ 116 | var propertyName = args[i]; 117 | var value = evaluateProperty(object, propertyName); 118 | if(typeof value != "undefined"){ 119 | selected[propertyName] = value; 120 | } 121 | } 122 | emit(selected); 123 | }); 124 | }, 125 | unselect: function(){ 126 | var args = arguments; 127 | var argc = arguments.length; 128 | return each(this, function(object, emit){ 129 | var selected = {}; 130 | for (var i in object) if (object.hasOwnProperty(i)) { 131 | selected[i] = object[i]; 132 | } 133 | for(var i = 0; i < argc; i++) { 134 | delete selected[args[i]]; 135 | } 136 | emit(selected); 137 | }); 138 | }, 139 | values: function(first){ 140 | if(arguments.length == 1){ 141 | return each(this, function(object, emit){ 142 | emit(object[first]); 143 | }); 144 | } 145 | var args = arguments; 146 | var argc = arguments.length; 147 | return each(this, function(object, emit){ 148 | var selected = []; 149 | if (argc === 0) { 150 | for(var i in object) if (object.hasOwnProperty(i)) { 151 | selected.push(object[i]); 152 | } 153 | } else { 154 | for(var i = 0; i < argc; i++){ 155 | var propertyName = args[i]; 156 | selected.push(object[propertyName]); 157 | } 158 | } 159 | emit(selected); 160 | }); 161 | }, 162 | limit: function(limit, start, maxCount){ 163 | var totalCount = this.length; 164 | start = start || 0; 165 | var sliced = this.slice(start, start + limit); 166 | if(maxCount){ 167 | sliced.start = start; 168 | sliced.end = start + sliced.length - 1; 169 | sliced.totalCount = Math.min(totalCount, typeof maxCount === "number" ? maxCount : Infinity); 170 | } 171 | return sliced; 172 | }, 173 | distinct: function(){ 174 | var primitives = {}; 175 | var needCleaning = []; 176 | var newResults = this.filter(function(value){ 177 | if(value && typeof value == "object"){ 178 | if(!value.__found__){ 179 | value.__found__ = function(){};// get ignored by JSON serialization 180 | needCleaning.push(value); 181 | return true; 182 | } 183 | }else{ 184 | if(!primitives[value]){ 185 | primitives[value] = true; 186 | return true; 187 | } 188 | } 189 | }); 190 | each(needCleaning, function(object){ 191 | delete object.__found__; 192 | }); 193 | return newResults; 194 | }, 195 | recurse: function(property){ 196 | // TODO: this needs to use lazy-array 197 | var newResults = []; 198 | function recurse(value){ 199 | if(value instanceof Array){ 200 | each(value, recurse); 201 | }else{ 202 | newResults.push(value); 203 | if(property){ 204 | value = value[property]; 205 | if(value && typeof value == "object"){ 206 | recurse(value); 207 | } 208 | }else{ 209 | for(var i in value){ 210 | if(value[i] && typeof value[i] == "object"){ 211 | recurse(value[i]); 212 | } 213 | } 214 | } 215 | } 216 | } 217 | recurse(this); 218 | return newResults; 219 | }, 220 | aggregate: function(){ 221 | var distinctives = []; 222 | var aggregates = []; 223 | for(var i = 0; i < arguments.length; i++){ 224 | var arg = arguments[i]; 225 | if(typeof arg === "function"){ 226 | aggregates.push(arg); 227 | }else{ 228 | distinctives.push(arg); 229 | } 230 | } 231 | var distinctObjects = {}; 232 | var dl = distinctives.length; 233 | each(this, function(object){ 234 | var key = ""; 235 | for(var i = 0; i < dl;i++){ 236 | key += '/' + object[distinctives[i]]; 237 | } 238 | var arrayForKey = distinctObjects[key]; 239 | if(!arrayForKey){ 240 | arrayForKey = distinctObjects[key] = []; 241 | } 242 | arrayForKey.push(object); 243 | }); 244 | var al = aggregates.length; 245 | var newResults = []; 246 | for(var key in distinctObjects){ 247 | var arrayForKey = distinctObjects[key]; 248 | var newObject = {}; 249 | for(var i = 0; i < dl;i++){ 250 | var property = distinctives[i]; 251 | newObject[property] = arrayForKey[0][property]; 252 | } 253 | for(var i = 0; i < al;i++){ 254 | var aggregate = aggregates[i]; 255 | newObject[i] = aggregate.call(arrayForKey); 256 | } 257 | newResults.push(newObject); 258 | } 259 | return newResults; 260 | }, 261 | between: filter(function(value, range){ 262 | return value >= range[0] && value < range[1]; 263 | }), 264 | sum: reducer(function(a, b){ 265 | return a + b; 266 | }), 267 | mean: function(property){ 268 | return exports.operators.sum.call(this, property)/this.length; 269 | }, 270 | max: reducer(function(a, b){ 271 | return Math.max(a, b); 272 | }), 273 | min: reducer(function(a, b){ 274 | return Math.min(a, b); 275 | }), 276 | count: function(){ 277 | return this.length; 278 | }, 279 | first: function(){ 280 | return this[0]; 281 | }, 282 | one: function(){ 283 | if(this.length > 1){ 284 | throw new TypeError("More than one object found"); 285 | } 286 | return this[0]; 287 | } 288 | }; 289 | exports.filter = filter; 290 | function filter(condition, not){ 291 | // convert to boolean right now 292 | var filter = function(property, second){ 293 | if(typeof second == "undefined"){ 294 | second = property; 295 | property = undefined; 296 | } 297 | var args = arguments; 298 | var filtered = []; 299 | for(var i = 0, length = this.length; i < length; i++){ 300 | var item = this[i]; 301 | if(condition(evaluateProperty(item, property), second)){ 302 | filtered.push(item); 303 | } 304 | } 305 | return filtered; 306 | }; 307 | filter.condition = condition; 308 | return filter; 309 | }; 310 | function reducer(func){ 311 | return function(property){ 312 | var result = this[0]; 313 | if(property){ 314 | result = result && result[property]; 315 | for(var i = 1, l = this.length; i < l; i++) { 316 | result = func(result, this[i][property]); 317 | } 318 | }else{ 319 | for(var i = 1, l = this.length; i < l; i++) { 320 | result = func(result, this[i]); 321 | } 322 | } 323 | return resul;t 324 | } 325 | } 326 | exports.evaluateProperty = evaluateProperty; 327 | function evaluateProperty(object, property){ 328 | if(property instanceof Array){ 329 | each(property, function(part){ 330 | object = object[decodeURIComponent(part)]; 331 | }); 332 | return object; 333 | }else if(typeof property == "undefined"){ 334 | return object; 335 | }else{ 336 | return object[decodeURIComponent(property)]; 337 | } 338 | }; 339 | var conditionEvaluator = exports.conditionEvaluator = function(condition){ 340 | var jsOperator = exports.jsOperatorMap[term.name]; 341 | if(jsOperator){ 342 | js += "(function(item){return item." + term[0] + jsOperator + "parameters[" + (index -1) + "][1];});"; 343 | } 344 | else{ 345 | js += "operators['" + term.name + "']"; 346 | } 347 | return eval(js); 348 | }; 349 | exports.executeQuery = function(query, options, target){ 350 | return exports.query(query, options, target); 351 | } 352 | exports.query = query; 353 | exports.missingOperator = function(operator){ 354 | throw new Error("Operator " + operator + " is not defined"); 355 | } 356 | function query(query, options, target){ 357 | options = options || {}; 358 | query = parseQuery(query, options.parameters); 359 | function t(){} 360 | t.prototype = exports.operators; 361 | var operators = new t; 362 | // inherit from exports.operators 363 | for(var i in options.operators){ 364 | operators[i] = options.operators[i]; 365 | } 366 | function op(name){ 367 | return operators[name]||exports.missingOperator(name); 368 | } 369 | var parameters = options.parameters || []; 370 | var js = ""; 371 | function queryToJS(value){ 372 | if(value && typeof value === "object" && !(value instanceof RegExp)){ 373 | if(value instanceof Array){ 374 | return '[' + each(value, function(value, emit){ 375 | emit(queryToJS(value)); 376 | }) + ']'; 377 | }else{ 378 | var jsOperator = exports.jsOperatorMap[value.name]; 379 | if(jsOperator){ 380 | // item['foo.bar'] ==> (item && item.foo && item.foo.bar && ...) 381 | var path = value.args[0]; 382 | var target = value.args[1]; 383 | if (typeof target == "undefined"){ 384 | var item = "item"; 385 | target = path; 386 | }else if(path instanceof Array){ 387 | var item = "item"; 388 | var escaped = []; 389 | for(var i = 0;i < path.length; i++){ 390 | escaped.push(stringify(path[i])); 391 | item +="&&item[" + escaped.join("][") + ']'; 392 | } 393 | }else{ 394 | var item = "item&&item[" + stringify(path) + "]"; 395 | } 396 | // use native Array.prototype.filter if available 397 | var condition = item + jsOperator + queryToJS(target); 398 | if (typeof Array.prototype.filter === 'function') { 399 | return "(function(){return this.filter(function(item){return " + condition + "})})"; 400 | //???return "this.filter(function(item){return " + condition + "})"; 401 | } else { 402 | return "(function(){var filtered = []; for(var i = 0, length = this.length; i < length; i++){var item = this[i];if(" + condition + "){filtered.push(item);}} return filtered;})"; 403 | } 404 | }else{ 405 | if (value instanceof Date){ 406 | return value.valueOf(); 407 | } 408 | return "(function(){return op('" + value.name + "').call(this" + 409 | (value && value.args && value.args.length > 0 ? (", " + each(value.args, function(value, emit){ 410 | emit(queryToJS(value)); 411 | }).join(",")) : "") + 412 | ")})"; 413 | } 414 | } 415 | }else{ 416 | return typeof value === "string" ? stringify(value) : value; 417 | } 418 | } 419 | var evaluator = eval("(1&&function(target){return " + queryToJS(query) + ".call(target);})"); 420 | return target ? evaluator(target) : evaluator; 421 | } 422 | function throwMaxIterations(){ 423 | throw new Error("Query has taken too much computation, and the user is not allowed to execute resource-intense queries. Increase maxIterations in your config file to allow longer running non-indexed queries to be processed."); 424 | } 425 | exports.maxIterations = 10000; 426 | return exports; 427 | }); 428 | -------------------------------------------------------------------------------- /specification/draft-zyp-rql-00.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | ]> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Resource Query Language 19 | 20 | 21 | 23 | SitePen (USA) 24 | 25 |
26 | 27 | 530 Lytton Avenue 28 | 29 | Palo Alto, CA 94301 30 | 31 | USA 32 | 33 | 34 | +1 650 968 8787 35 | 36 | kris@sitepen.com 37 |
38 |
39 | 40 | 41 | 42 | Internet Engineering Task Force 43 | 44 | resource 45 | 46 | query 47 | 48 | 49 | 50 | 51 | Resource Query Languages (RQL) defines a syntactically simple query language 52 | for querying and retrieving resources. RQL is designed to be URI friendly, 53 | particularly as a query component of a URI, and highly extensible. RQL is a superset 54 | of HTML's URL encoding of form values, and a superset of Feed Item Query Language (FIQL). 55 | RQL basically consists of a set of nestable named operators which each have a set of 56 | arguments and operate on a collection of resources. An example of an RQL for finding 57 | resources with a category of "toy" and sorted by price in ascending order: 58 |
59 | 60 |
63 |
64 | 65 |
66 | 67 |
68 | 69 | 70 |
71 | Resource Query Languages (RQL) defines a syntactically simple query language 72 | for querying and retrieving resources. RQL is designed to be URI friendly, 73 | particularly as a query component of a URI, and highly extensible. RQL is a superset 74 | of HTML's URL encoding of form values, and a superset of Feed Item Query Language (FIQL). 75 | 76 |
77 | 78 | 79 |
80 | The key words "MUST", "MUST 82 | NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", 83 | "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 84 | interpreted as described in RFC 2119. 85 |
86 | 87 | 88 | 89 |
90 | RQL consists of a set of nestable named operators which each have a set of 91 | arguments. RQL is designed to be applied to a collection of resources. Each top-level 92 | operator defines a constraint or modification to the result of the query from the base 93 | collection of resources. Nested operators provide constraint information for the operators within which they are embedded. 94 | 95 |
96 |
97 | 98 | An operator consists of an operator name followed by a list of comma-delimited arguments within paranthesis: 99 |
100 | 101 |
104 | Each argument may be a value (a string of unreserved or URL-encoded characters), an array, or another operator. The name of the operator indicates the type of action the operator will perform on the collection, using the given arguments. This document defines the semantics of a set of operators, but the query language can be extended with additional operators.
105 | 106 | A simple RQL query with a single operator that indicates a search for any 107 | resources with a property of "foo" that has value of 3 could be written: 108 |
109 | 110 |
113 |
114 |
115 |
116 | 117 | Simple values are simply URL-encoded sequences of characters. Unreserved characters do not need to be encoded, other characters should be encoding using standard URL encoding. Simple values can be decoded to determine the intended string of characters. Values can also include arrays or typed values. These are described in the "Typed Values" and "Arrays" section below. 118 | 119 |
120 |
121 | 122 | 123 | One of the allowable arguments for operators is an array. An array is paranthesis-enclosed comma-delimited set of items. Each item in the array can be a value or an operator. A query that finds all the resources where the category can be "toy" or "food" could be written with an array argument: 124 |
125 | 126 |
129 | 130 |
131 |
132 |
133 | 134 | Operators may be nested. For example, a set of operators can be used as the arguments for the "or" operator. Another query that finds all the resources where the category can be "toy" or "food" could be written with nested "eq" operators within an "or" operator: 135 |
136 | 137 |
140 |
141 |
142 |
143 | 144 | 145 | RQL defines the following semantics for these operators: 146 |
147 | sort(<+|-><property) - Sorts the returned query result by the given property. The order of sort is specified by the prefix (+ for ascending, - for descending) to property. 148 | To sort by foo in ascending order: 149 |
150 | 151 |
154 | One can also do multiple property sorts. To sort by price in ascending order and rating in descending order: 155 |
156 | 157 |
160 |
161 |
162 |
163 | select(<property>) - Returns a query result where each item is value of the property specified by the argument 164 | select(<property>,<property>,...) - Trims each object down to the set of properties defined in the arguments 165 |
166 |
167 | aggregate(<property|operator>,...) - The aggregate function can be used for aggregation, it aggregates the result set, grouping by objects that are distinct for the provided properties, and then reduces the remaining other property values using the provided operator. To calculate the sum of sales for each department: 168 |
169 | 170 |
173 |
174 |
175 |
176 | distinct() - Returns a result set with duplicates removed 177 |
178 |
179 | in(<property>,<array-of-values>) - Filters for objects where the specified property's value is in the provided array 180 |
181 |
182 | out(<property>,<array-of-values>) - Filters for objects where the specified property's value is not in the provided array 183 |
184 |
185 | contains(<property>,<value | query>) - Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided query. 186 |
187 |
188 | contains(<property>,<value | query>) - Filters for objects where the specified property's value is an array and the array does not contains any value that equals the provided value or satisfies the provided query. 189 |
190 |
191 | limit(&tl;count>,<start>) - Returns a limited range of records from the result set. The first parameter indicates the number of records to return, and the optional second parameter indicates the starting offset. 192 |
193 |
194 | and(<query>,<query>,...) - Returns a query result set applying all the given operators to constrain the query 195 |
196 |
197 | or(<query>,<query>,...) - Returns a union result set of the given operators 198 |
199 |
200 | eq(<property>,<value>) - Filters for objects where the specified property's value is equal to the provided value 201 |
202 |
203 | lt(<property>,<value>) - Filters for objects where the specified property's value is less than the provided value 204 |
205 |
206 | le(<property>,<value>) - Filters for objects where the specified property's value is less than or equal to the provided value 207 |
208 |
209 | gt(<property>,<value>) - Filters for objects where the specified property's value is greater than the provided value 210 |
211 |
212 | ge(<property>,<value>) - Filters for objects where the specified property's value is greater than or equal to the provided value 213 |
214 |
215 | ne(<property>,<value>) - Filters for objects where the specified property's value is not equal to the provided value 216 |
217 |
218 | rel(<relation name?>,<query>) - Applies the provided query against the linked data of the provided relation name. 219 |
220 |
221 | sum(<property?>) - Finds the sum of every value in the array or if the property argument is provided, returns the sum of the value of property for every object in the array 222 |
223 |
224 | mean(<property?>) - Finds the mean of every value in the array or if the property argument is provided, returns the mean of the value of property for every object in the array 225 |
226 |
227 | max(<property?>) - Finds the maximum of every value in the array or if the property argument is provided, returns the maximum of the value of property for every object in the array 228 |
229 |
230 | min(<property?>) - Finds the minimum of every value in the array or if the property argument is provided, returns the minimum of the value of property for every object in the array 231 |
232 |
233 | first() - Returns the first record of the query's result set 234 |
235 |
236 | one() - Returns the first and only record of the query's result set, or produces an error if the query's result set has more or less than one record in it. 237 |
238 |
239 | count() - Returns the count of the number of records in the query's result set 240 |
241 |
242 | recurse(<property?>) - Recursively searches, looking in children of the object as objects in arrays in the given property value 243 |
244 |
245 |
246 |
247 | 248 | 249 | RQL provides a semantically equivelant syntactic alternate to operator syntax with comparison syntax. A comparison operator may 250 | be written in the form: 251 |
252 | 253 |
256 | As shorthand for: 257 |
258 | 259 |
262 | RQL also supports provides sugar for the "and" operator with ampersand delimited operators. The following form: 263 |
264 | 265 |
268 | As shorthand for: 269 |
270 | 271 |
274 | With these transformations, one can write queries of the form: 275 |
276 | 277 |
280 | This makes the HTML's form url encoding of name value pairs a proper query within RQL.
281 | 282 | Ampersand delimited operators may be grouped by placing them within paranthesis. Top level queries themselves are considered to be implicitly a part of an "and" operator group, and therefore the top level ampersand delimited operators do not need to be enclosed with paranthesis, but "and" groups used within other operators do need to be enclosed in paranthesis. 283 | 284 | Pipe delimited operators may also be placed within paranthesis-enclosed groups as shorthand for the "or" operator. One can write a query: 285 |
286 | 287 |
290 | Also, Feed Item Query Language is a subset of RQL valid. RQL supports named comparisons as shorthand for operators as well. The following form is a named comparison: 291 |
292 | 293 |
296 | Which is shorthand for: 297 |
298 | 299 |
302 | For example, to find resources with a "price" less than 10: 303 |
304 | 305 |
308 |
309 |
310 |
311 | 312 | Basic values in RQL are simply a string of characters and it is up to the recipient of a query to determine how these characters should be interpreted and if they should be coerced to alternate data types understood by the language or database processing the query. However, RQL supports typed values to provide hints to the recipient of the intended data type of the value. The syntax of a typed value is: 313 |
314 | 315 |
318 | RQL suggests the following types to be supported: 319 | 320 | string - Indicates the value string should not be coerced, it should remain a string. 321 | number - Indicates the value string should be coerced to a number. 322 | boolean - Indicates the value string should be coerced to a boolean. A value of "true" should indicate true and a value of "false" should indicate false. 323 | epoch - Indicates the value string should be treated as the milliseconds since the epoch and coerced to a date. 324 | 325 | For example, to query for resources where foo explicitly equals the number 4: 326 |
327 | 328 |
331 | 332 |
333 |
334 |
335 | 336 | The following is the collected ABNF for RQL: 337 |
338 | 339 |
360 | 361 |
362 |
363 |
364 | 365 | If RQL is used as for to define queries in HTTP URLs, there are several considerations. First, servers that allow publicly initiated requests should enforce proper security measures to protect against excessive resource consumption. Many operators may be understood 366 | by the server, but not efficiently executable and servers can therefore reject such queries. Rejections may be indicated by a 403 Forbidden status code response, or if authentication may provide the authorization necessary to perform the query, a 401 Unauthorized status code can be sent. 367 | 368 | 369 | If the query is not syntactically valid, (does not follow the RQL grammar), the server may return a status code of 400 Bad Request. If the query is syntactically valid, but the operator name is not known, the server may also return a status code of 400 Bad Request. 370 | 371 |
372 |
373 | 374 | The proposed MIME media type for Resource Query Language is application/rql 375 | 376 | 377 | Type name: application 378 | 379 | 380 | Subtype name: rql 381 | 382 | 383 | Required parameters: none 384 | 385 | 386 | Optional parameters: none 387 | 388 | 389 | 390 |
391 |
392 | 393 | 394 | 395 | 396 | 397 | 398 | &rfc2119; 399 | &rfc3986; 400 | 401 | 402 | 403 | &html401; 404 | &fiql; 405 | 406 | 407 | 414 | 415 |
416 | -00 417 | 418 | 419 | Initial draft 420 | 421 | 422 | 423 |
424 | 425 |
426 | 427 |
428 | 429 | 435 |
436 |
437 | -------------------------------------------------------------------------------- /specification/draft-zyp-rql-00.html: -------------------------------------------------------------------------------- 1 | 2 | Resource Query Language 3 | 4 | 5 | 6 | 7 | 141 | 142 | 143 |
 TOC 
144 |
145 | 146 | 147 | 148 | 149 |
Internet Engineering Task ForceK. Zyp, Ed.
Internet-DraftSitePen (USA)
Intended status: InformationalJune 30, 2010
Expires: January 1, 2011 
150 |


Resource Query Language
draft-zyp-rql-00

151 | 152 |

Abstract

153 | 154 |

Resource Query Languages (RQL) defines a syntactically simple query language 155 | for querying and retrieving resources. RQL is designed to be URI friendly, 156 | particularly as a query component of a URI, and highly extensible. RQL is a superset 157 | of HTML's URL encoding of form values, and a superset of Feed Item Query Language (FIQL). 158 | RQL basically consists of a set of nestable named operators which each have a set of 159 | arguments and operate on a collection of resources. An example of an RQL for finding 160 | resources with a category of "toy" and sorted by price in ascending order: 161 |

162 |
163 | 
164 | category=toy&sort(+price)
165 | 

166 | 167 | 168 |

169 |

Status of This Memo

170 |

171 | This Internet-Draft is submitted in full 172 | conformance with the provisions of BCP 78 and BCP 79.

173 |

174 | Internet-Drafts are working documents of the Internet Engineering 175 | Task Force (IETF). Note that other groups may also distribute 176 | working documents as Internet-Drafts. The list of current 177 | Internet-Drafts is at http://datatracker.ietf.org/drafts/current/.

178 |

179 | Internet-Drafts are draft documents valid for a maximum of six months 180 | and may be updated, replaced, or obsoleted by other documents at any time. 181 | It is inappropriate to use Internet-Drafts as reference material or to cite 182 | them other than as “work in progress.”

183 |

184 | This Internet-Draft will expire on January 1, 2011.

185 | 186 |

Copyright Notice

187 |

188 | Copyright (c) 2010 IETF Trust and the persons identified as the 189 | document authors. All rights reserved.

190 |

191 | This document is subject to BCP 78 and the IETF Trust's Legal 192 | Provisions Relating to IETF Documents 193 | (http://trustee.ietf.org/license-info) in effect on the date of 194 | publication of this document. Please review these documents 195 | carefully, as they describe your rights and restrictions with respect 196 | to this document. Code Components extracted from this document must 197 | include Simplified BSD License text as described in Section 4.e of 198 | the Trust Legal Provisions and are provided without warranty as 199 | described in the Simplified BSD License.

200 |

201 |

Table of Contents

202 |

203 | 1.  204 | Introduction
205 | 2.  206 | Conventions
207 | 3.  208 | Overview
209 | 4.  210 | Operators
211 | 5.  212 | Values
213 | 6.  214 | Arrays
215 | 7.  216 | Nested Operators
217 | 8.  218 | Defined Operators
219 |     8.1.  220 | sort
221 |     8.2.  222 | select
223 |     8.3.  224 | aggregate
225 |     8.4.  226 | distinct
227 |     8.5.  228 | in
229 |     8.6.  230 | contains
231 |     8.7.  232 | limit
233 |     8.8.  234 | and
235 |     8.9.  236 | or
237 |     8.10.  238 | eq
239 |     8.11.  240 | lt
241 |     8.12.  242 | le
243 |     8.13.  244 | gt
245 |     8.14.  246 | ge
247 |     8.15.  248 | ne
249 |     8.16.  250 | sum
251 |     8.17.  252 | mean
253 |     8.18.  254 | max
255 |     8.19.  256 | min
257 |     8.20.  258 | recurse
259 | 9.  260 | Comparison Syntax
261 | 10.  262 | Typed Values
263 | 11.  264 | ABNF for RQL
265 | 12.  266 | HTTP
267 | 13.  268 | IANA Considerations
269 | 14.  270 | References
271 |     14.1.  272 | Normative References
273 |     14.2.  274 | Informative References
275 | Appendix A.  276 | Change Log
277 | Appendix B.  278 | Open Issues
279 |

280 |
281 | 282 |

283 |
 TOC 
284 |

1.  285 | Introduction

286 | 287 |

Resource Query Languages (RQL) defines a syntactically simple query language 288 | for querying and retrieving resources. RQL is designed to be URI friendly, 289 | particularly as a query component of a URI, and highly extensible. RQL is a superset 290 | of HTML's URL encoding of form values, and a superset of Feed Item Query Language (FIQL). 291 | 292 |

293 |

294 |
 TOC 
295 |

2.  296 | Conventions

297 | 298 |

The key words "MUST", "MUST 299 | NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", 300 | "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 301 | interpreted as described in RFC 2119. 302 |

303 |

304 |
 TOC 
305 |

3.  306 | Overview

307 | 308 |

RQL consists of a set of nestable named operators which each have a set of 309 | arguments. RQL is designed to be applied to a collection of resources. Each top-level 310 | operator defines a constraint or modification to the result of the query from the base 311 | collection of resources. Nested operators provide constraint information for the operators within which they are embedded. 312 | 313 |

314 |

315 |
 TOC 
316 |

4.  317 | Operators

318 | 319 |

320 | An operator consists of an operator name followed by a list of comma-delimited arguments within paranthesis: 321 |

322 |
323 | 
324 | name(args,...)
325 | 

326 | 327 | Each argument may be a value (a string of unreserved or URL-encoded characters), an array, or another operator. The name of the operator indicates the type of action the operator will perform on the collection, using the given arguments. This document defines the semantics of a set of operators, but the query language can be extended with additional operators. 328 |

329 |

330 | A simple RQL query with a single operator that indicates a search for any 331 | resources with a property of "foo" that has value of 3 could be written: 332 |

333 |
334 | 
335 | eq(foo,3)
336 | 

337 | 338 | 339 |

340 |

341 |
 TOC 
342 |

5.  343 | Values

344 | 345 |

346 | Simple values are simply URL-encoded sequences of characters. Unreserved characters do not need to be encoded, other characters should be encoding using standard URL encoding. Simple values can be decoded to determine the intended string of characters. Values can also include arrays or typed values. These are described in the "Typed Values" and "Arrays" section below. 347 | 348 |

349 |

350 |
 TOC 
351 |

6.  352 | Arrays

353 | 354 |

355 | One of the allowable arguments for operators is an array. An array is paranthesis-enclosed comma-delimited set of items. Each item in the array can be a value or an operator. A query that finds all the resources where the category can be "toy" or "food" could be written with an array argument: 356 |

357 |
358 | 
359 | in(category,(toy,food))
360 | 

361 | 362 | 363 | 364 |

365 |

366 |
 TOC 
367 |

7.  368 | Nested Operators

369 | 370 |

371 | Operators may be nested. For example, a set of operators can be used as the arguments for the "or" operator. Another query that finds all the resources where the category can be "toy" or "food" could be written with nested "eq" operators within an "or" operator: 372 |

373 |
374 | 
375 | or(eq(category,toy),eq(category,food))
376 | 

377 | 378 | 379 |

380 |

381 |
 TOC 
382 |

8.  383 | Defined Operators

384 | 385 |

386 | RQL defines the following semantics for these operators: 387 | 388 |


389 |
 TOC 
390 |

8.1.  391 | sort

392 | 393 |

sort(<+|-><property) - Sorts the returned query result by the given property. The order of sort is specified by the prefix (+ for ascending, - for descending) to property. 394 | To sort by foo in ascending order: 395 |

396 |
397 | 
398 | sort(+foo)
399 | 

400 | 401 | One can also do multiple property sorts. To sort by price in ascending order and rating in descending order: 402 |

403 |
404 | 
405 | sort(+price,-rating)
406 | 

407 | 408 | 409 |

410 | 411 |

412 |
 TOC 
413 |

8.2.  414 | select

415 | 416 |

select(<property>) - Returns a query result where each item is value of the property specified by the argument 417 |

418 |

select(<property>,<property>,...) - Trims each object down to the set of properties defined in the arguments 419 |

420 | 421 |

422 |
 TOC 
423 |

8.3.  424 | aggregate

425 | 426 |

aggregate(<property|operator>,...) - The aggregate function can be used for aggregation, it aggregates the result set, grouping by objects that are distinct for the provided properties, and then reduces the remaining other property values using the provided operator. To calculate the sum of sales for each department: 427 |

428 |
429 | 
430 | aggregate(departmentId,sum(sales))
431 | 

432 | 433 | 434 |

435 | 436 |

437 |
 TOC 
438 |

8.4.  439 | distinct

440 | 441 |

distinct() - Returns a result set with duplicates removed 442 |

443 | 444 |

445 |
 TOC 
446 |

8.5.  447 | in

448 | 449 |

in(<property>,<array-of-values>) - Filters for objects where the specified property's value is in the provided array 450 |

451 | 452 |

453 |
 TOC 
454 |

8.6.  455 | contains

456 | 457 |

contains(<property>,<value | array-of-values>) - Filters for objects where the specified property's value is an array and the array contains the provided value or contains a value in the provided array 458 |

459 | 460 |

461 |
 TOC 
462 |

8.7.  463 | limit

464 | 465 |

limit(start,count) - Returns a limited range of records from the result set. The first parameter indicates the starting offset and the second parameter indicates the number of records to return. 466 |

467 | 468 |

469 |
 TOC 
470 |

8.8.  471 | and

472 | 473 |

and(<query>,<query>,...) - Returns a query result set applying all the given operators to constrain the query 474 |

475 | 476 |

477 |
 TOC 
478 |

8.9.  479 | or

480 | 481 |

or(<query>,<query>,...) - Returns a union result set of the given operators 482 |

483 | 484 |

485 |
 TOC 
486 |

8.10.  487 | eq

488 | 489 |

eq(<property>,<value>) - Filters for objects where the specified property's value is equal to the provided value 490 |

491 | 492 |

493 |
 TOC 
494 |

8.11.  495 | lt

496 | 497 |

lt(<property>,<value>) - Filters for objects where the specified property's value is less than the provided value 498 |

499 | 500 |

501 |
 TOC 
502 |

8.12.  503 | le

504 | 505 |

le(<property>,<value>) - Filters for objects where the specified property's value is less than or equal to the provided value 506 |

507 | 508 |

509 |
 TOC 
510 |

8.13.  511 | gt

512 | 513 |

gt(<property>,<value>) - Filters for objects where the specified property's value is greater than the provided value 514 |

515 | 516 |

517 |
 TOC 
518 |

8.14.  519 | ge

520 | 521 |

ge(<property>,<value>) - Filters for objects where the specified property's value is greater than or equal to the provided value 522 |

523 | 524 |

525 |
 TOC 
526 |

8.15.  527 | ne

528 | 529 |

ne(<property>,<value>) - Filters for objects where the specified property's value is not equal to the provided value 530 |

531 | 532 |

533 |
 TOC 
534 |

8.16.  535 | sum

536 | 537 |

sum(<property?>) - Finds the sum of every value in the array or if the property argument is provided, returns the sum of the value of property for every object in the array 538 |

539 | 540 |

541 |
 TOC 
542 |

8.17.  543 | mean

544 | 545 |

mean(<property?>) - Finds the mean of every value in the array or if the property argument is provided, returns the mean of the value of property for every object in the array 546 |

547 | 548 |

549 |
 TOC 
550 |

8.18.  551 | max

552 | 553 |

max(<property?>) - Finds the maximum of every value in the array or if the property argument is provided, returns the maximum of the value of property for every object in the array 554 |

555 | 556 |

557 |
 TOC 
558 |

8.19.  559 | min

560 | 561 |

min(<property?>) - Finds the minimum of every value in the array or if the property argument is provided, returns the minimum of the value of property for every object in the array 562 |

563 | 564 |

565 |
 TOC 
566 |

8.20.  567 | recurse

568 | 569 |

recurse(<property?>) - Recursively searches, looking in children of the object as objects in arrays in the given property value 570 |

571 | 572 | 573 |

574 |
 TOC 
575 |

9.  576 | Comparison Syntax

577 | 578 |

579 | RQL provides a semantically equivelant syntactic alternate to operator syntax with comparison syntax. A comparison operator may 580 | be written in the form: 581 |

582 |
583 | 
584 | name=value
585 | 

586 | 587 | As shorthand for: 588 |

589 |
590 | 
591 | eq(name,value)
592 | 

593 | 594 | RQL also supports provides sugar for the "and" operator with ampersand delimited operators. The following form: 595 |

596 |
597 | 
598 | operator&operator
599 | 

600 | 601 | As shorthand for: 602 |

603 |
604 | 
605 | and(operator,operator)
606 | 

607 | 608 | With these transformations, one can write queries of the form: 609 |

610 |
611 | 
612 | foo=3&bar=text
613 | 

614 | 615 | This makes the HTML's form url encoding of name value pairs a proper query within RQL. 616 |

617 |

618 | Ampersand delimited operators may be grouped by placing them within paranthesis. Top level queries themselves are considered to be implicitly a part of an "and" operator group, and therefore the top level ampersand delimited operators do not need to be enclosed with paranthesis, but "and" groups used within other operators do need to be enclosed in paranthesis. 619 |

620 |

621 | Pipe delimited operators may also be placed within paranthesis-enclosed groups as shorthand for the "or" operator. One can write a query: 622 |

623 |
624 | 
625 | foo=3&(bar=text|bar=string)
626 | 

627 | 628 | Also, Feed Item Query Language is a subset of RQL valid. RQL supports named comparisons as shorthand for operators as well. The following form is a named comparison: 629 |

630 |
631 | 
632 | name=comparator=value
633 | 

634 | 635 | Which is shorthand for: 636 |

637 |
638 | 
639 | comparator(name,value)
640 | 

641 | 642 | For example, to find resources with a "price" less than 10: 643 |

644 |
645 | 
646 | price=lt=10
647 | 

648 | 649 | 650 |

651 |

652 |
 TOC 
653 |

10.  654 | Typed Values

655 | 656 |

657 | Basic values in RQL are simply a string of characters and it is up to the recipient of a query to determine how these characters should be interpreted and if they should be coerced to alternate data types understood by the language or database processing the query. However, RQL supports typed values to provide hints to the recipient of the intended data type of the value. The syntax of a typed value is: 658 |

659 |
660 | 
661 | type:value
662 | 

663 | 664 | RQL suggests the following types to be supported: 665 |

666 |
667 |

string - Indicates the value string should not be coerced, it should remain a string. 668 |

669 |

number - Indicates the value string should be coerced to a number. 670 |

671 |

boolean - Indicates the value string should be coerced to a boolean. A value of "true" should indicate true and a value of "false" should indicate false. 672 |

673 |

epoch - Indicates the value string should be treated as the milliseconds since the epoch and coerced to a date. 674 |

675 |

676 | For example, to query for resources where foo explicitly equals the number 4: 677 |

678 |
679 | 
680 | foo=number:4
681 | 

682 | 683 | 684 | 685 |

686 |

687 |
 TOC 
688 |

11.  689 | ABNF for RQL

690 | 691 |

692 | The following is the collected ABNF for RQL: 693 |

694 |
695 | 
696 | query = and
697 | 
698 | and = operator *( "&" operator )
699 | operator = comparison / call-operator / group
700 | call-operator = name "(" [ argument *( "," argument ) ] ")"
701 | argument = call-operator / value
702 | value = *nchar / typed-value / array
703 | typed-value = 1*nchar ":" *nchar
704 | array = "(" [ value *( "," value ) ] ")"
705 | name = *nchar
706 | 
707 | comparison = name ( "=" [ name "=" ] ) value
708 | group = "(" ( and / or ) ")"
709 | or = operator *( "|" operator )
710 | 
711 | nchar = unreserved / pct-encoded / "*" / "+"
712 | pct-encoded   = "%" HEXDIG HEXDIG
713 | unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
714 | 
715 | 

716 | 717 | 718 | 719 |

720 |

721 |
 TOC 
722 |

12.  723 | HTTP

724 | 725 |

726 | If RQL is used as for to define queries in HTTP URLs, there are several considerations. First, servers that allow publicly initiated requests should enforce proper security measures to protect against excessive resource consumption. Many operators may be understood 727 | by the server, but not efficiently executable and servers can therefore reject such queries. Rejections may be indicated by a 403 Forbidden status code response, or if authentication may provide the authorization necessary to perform the query, a 401 Unauthorized status code can be sent. 728 | 729 |

730 |

731 | If the query is not syntactically valid, (does not follow the RQL grammar), the server may return a status code of 400 Bad Request. If the query is syntactically valid, but the operator name is not known, the server may also return a status code of 400 Bad Request. 732 | 733 |

734 |

735 |
 TOC 
736 |

13.  737 | IANA Considerations

738 | 739 |

740 | The proposed MIME media type for Resource Query Language is application/rql 741 | 742 |

743 |

744 | Type name: application 745 | 746 |

747 |

748 | Subtype name: rql 749 | 750 |

751 |

752 | Required parameters: none 753 | 754 |

755 |

756 | Optional parameters: none 757 | 758 |

759 |

760 |
 TOC 
761 |

14.  762 | References

763 | 764 |

765 |
 TOC 
766 |

14.1. Normative References

767 | 768 | 769 | 770 | 771 | 772 |
[RFC2119]Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels,” BCP 14, RFC 2119, March 1997 (TXT, HTML, XML).
[RFC3986]Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” STD 66, RFC 3986, January 2005 (TXT, HTML, XML).
773 | 774 |

775 |
 TOC 
776 |

14.2. Informative References

777 | 778 | 779 | 780 | 781 | 782 |
[W3C.REC-html401-19991224]Hors, A., Raggett, D., and I. Jacobs, “HTML 4.01 Specification,” World Wide Web Consortium Recommendation REC-html401-19991224, December 1999 (HTML).
[I-D.nottingham-atompub-fiql]Nottingham, M., “FIQL: The Feed Item Query Language,” draft-nottingham-atompub-fiql-00 (work in progress), December 2007 (TXT).
783 | 784 |

785 |
 TOC 
786 |

Appendix A.  787 | Change Log

788 | 789 |

-00 790 |

791 |

792 |

793 |
    794 |
  • Initial draft 795 |
  • 796 |

797 | 798 |

799 |

800 |
 TOC 
801 |

Appendix B.  802 | Open Issues

803 | 804 |

805 |
 TOC 
806 |

Author's Address

807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 |
 Kris Zyp (editor)
 SitePen (USA)
 530 Lytton Avenue
 Palo Alto, CA 94301
 USA
Phone: +1 650 968 8787
EMail: kris@sitepen.com
823 | 824 | 825 | --------------------------------------------------------------------------------