├── README ├── TODO ├── query.js ├── test.html ├── test.js └── testglue.js /README: -------------------------------------------------------------------------------- 1 | A small query library for IndexedDB 2 | 3 | 4 | Query construction 5 | ------------------ 6 | 7 | A query consists of the name of an index, an operation, and a 8 | comparison value. This is how you construct a query: 9 | 10 | Index("make").oneof("BMW", "Volkswagen") 11 | 12 | This will return all objects whose "make" index value is 13 | either "BMW" or "Volkswagen". Available operations are: 14 | 15 | * eq 16 | * lt, lteq 17 | * gt, gteq 18 | * between, betweeq 19 | * oneof 20 | 21 | It is possible to link queries with boolean operations, e.g.: 22 | 23 | Index("make").eq("BMW") 24 | .and(Index("model").eq("325i")) 25 | .and(Index("year").lteq(1991)) 26 | 27 | 28 | Getting results 29 | --------------- 30 | 31 | Getting results from a query works very much like getting results from 32 | a single index in IndexedDB. You have the option of a cursor, e.g.: 33 | 34 | let cars = []; 35 | let store = transaction.objectStore("cars"); 36 | let request = query.openCursor(store); 37 | request.onsuccess = function (event) { 38 | let cursor = request.result; 39 | if (cursor) { 40 | cars.push(cursor.value); 41 | cursor.continue(); 42 | } 43 | } 44 | 45 | or simply getting all values at once: 46 | 47 | let cars; 48 | let request = query.getAll(store); 49 | request.onsuccess = function (event) { 50 | cars = request.result; 51 | } 52 | 53 | `query.openKeyCursor` and `query.getAllKeys` are also available if 54 | just the keys are of interest. 55 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | * error handling 4 | * limits 5 | * ordered results 6 | * test perf 7 | * investigate different strategies for filtering 8 | * investigate using (or at least supporting) workers for CPU-bound tasks 9 | * 'startswith' comparison for strings 10 | 11 | Separate/additional library: 12 | 13 | * indexing helpers (e.g. for full text searching, case folding, etc.) 14 | * query plan optimizer 15 | -------------------------------------------------------------------------------- /query.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Represents a queriable index. Call methods on the returned object 7 | * to create queries. 8 | * 9 | * Example: 10 | * 11 | * let query = Index("make").eq("BMW"); 12 | * 13 | */ 14 | function Index(name) { 15 | function queryMaker(op) { 16 | return function () { 17 | return IndexQuery(name, op, arguments); 18 | }; 19 | } 20 | return { 21 | eq: queryMaker("eq"), 22 | neq: queryMaker("neq"), 23 | gt: queryMaker("gt"), 24 | gteq: queryMaker("gteq"), 25 | lt: queryMaker("lt"), 26 | lteq: queryMaker("lteq"), 27 | between: queryMaker("between"), 28 | betweeq: queryMaker("betweeq"), 29 | oneof: function oneof() { 30 | let values = Array.slice(arguments); 31 | let query = IndexQuery(name, "eq", [values.shift()]); 32 | while (values.length) { 33 | query = query.or(IndexQuery(name, "eq", [values.shift()])); 34 | } 35 | return query; 36 | } 37 | }; 38 | } 39 | 40 | /** 41 | * Helper that notifies a 'success' event on a request, with a given 42 | * result object. This is typically either a cursor or a result array. 43 | */ 44 | function notifySuccess(request, result) { 45 | let event = {type: "success", 46 | target: request}; //TODO complete event interface 47 | request.readyState = IDBRequest.DONE; 48 | request.result = result; 49 | if (typeof request.onsuccess == "function") { 50 | request.onsuccess(event); 51 | } 52 | }; 53 | 54 | /** 55 | * Create a cursor object. 56 | */ 57 | function Cursor(store, request, keys, keyOnly) { 58 | let cursor = { 59 | continue: function continue_() { 60 | if (!keys.length) { 61 | notifySuccess(request, undefined); 62 | return; 63 | } 64 | let key = keys.shift(); 65 | if (keyOnly) { 66 | cursor.key = key; 67 | notifySuccess(request, cursor); 68 | return; 69 | } 70 | let r = store.get(key); 71 | r.onsuccess = function onsuccess() { 72 | cursor.key = key; 73 | cursor.value = r.result; 74 | notifySuccess(request, cursor); 75 | }; 76 | } 77 | //TODO complete cursor interface 78 | }; 79 | return cursor; 80 | } 81 | 82 | /** 83 | * Create a request object. 84 | */ 85 | function Request() { 86 | return { 87 | result: undefined, 88 | onsuccess: null, 89 | onerror: null, 90 | readyState: IDBRequest.LOADING 91 | // TODO complete request interface 92 | }; 93 | } 94 | 95 | /** 96 | * Create a request that will receive a cursor. 97 | * 98 | * This will also kick off the query, instantiate the Cursor when the 99 | * results are available, and notify the first 'success' event. 100 | */ 101 | function CursorRequest(store, queryFunc, keyOnly) { 102 | let request = Request(); 103 | queryFunc(store, function (keys) { 104 | let cursor = Cursor(store, request, keys, keyOnly); 105 | cursor.continue(); 106 | }); 107 | return request; 108 | } 109 | 110 | /** 111 | * Create a request that will receive a result array. 112 | * 113 | * This will also kick off the query, build up the result array, and 114 | * notify the 'success' event. 115 | */ 116 | function ResultRequest(store, queryFunc, keyOnly) { 117 | let request = Request(); 118 | queryFunc(store, function (keys) { 119 | if (keyOnly || !keys.length) { 120 | notifySuccess(request, keys); 121 | return; 122 | } 123 | let results = []; 124 | function getNext() { 125 | let r = store.get(keys.shift()); 126 | r.onsuccess = function onsuccess() { 127 | results.push(r.result); 128 | if (!keys.length) { 129 | notifySuccess(request, results); 130 | return; 131 | } 132 | getNext(); 133 | }; 134 | } 135 | getNext(); 136 | }); 137 | return request; 138 | } 139 | 140 | /** 141 | * Provide a generic way to create a query object from a query function. 142 | * Depending on the implementation of that query function, the query could 143 | * produce results from an index, combine results from other queries, etc. 144 | */ 145 | function Query(queryFunc, toString) { 146 | 147 | let query = { 148 | 149 | // Sadly we need to expose this to make Intersection and Union work :( 150 | _queryFunc: queryFunc, 151 | 152 | and: function and(query2) { 153 | return Intersection(query, query2); 154 | }, 155 | 156 | or: function or(query2) { 157 | return Union(query, query2); 158 | }, 159 | 160 | openCursor: function openCursor(store) { 161 | return CursorRequest(store, queryFunc, false); 162 | }, 163 | 164 | openKeyCursor: function openKeyCursor(store) { 165 | return CursorRequest(store, queryFunc, true); 166 | }, 167 | 168 | getAll: function getAll(store) { 169 | return ResultRequest(store, queryFunc, false); 170 | }, 171 | 172 | getAllKeys: function getAllKeys(store) { 173 | return ResultRequest(store, queryFunc, true); 174 | }, 175 | 176 | toString: toString 177 | }; 178 | return query; 179 | }; 180 | 181 | /** 182 | * Create a query object that queries an index. 183 | */ 184 | function IndexQuery(indexName, operation, values) { 185 | let negate = false; 186 | let op = operation; 187 | if (op == "neq") { 188 | op = "eq"; 189 | negate = true; 190 | } 191 | 192 | function makeRange() { 193 | let range; 194 | switch (op) { 195 | case "eq": 196 | range = IDBKeyRange.only(values[0]); 197 | break; 198 | case "lt": 199 | range = IDBKeyRange.upperBound(values[0], true); 200 | break; 201 | case "lteq": 202 | range = IDBKeyRange.upperBound(values[0]); 203 | break; 204 | case "gt": 205 | range = IDBKeyRange.lowerBound(values[0], true); 206 | break; 207 | case "gteq": 208 | range = IDBKeyRange.lowerBound(values[0]); 209 | range.upperOpen = true; 210 | break; 211 | case "between": 212 | range = IDBKeyRange.bound(values[0], values[1], true, true); 213 | break; 214 | case "betweeq": 215 | range = IDBKeyRange.bound(values[0], values[1]); 216 | break; 217 | } 218 | return range; 219 | } 220 | 221 | function queryKeys(store, callback) { 222 | let index = store.index(indexName); 223 | let range = makeRange(); 224 | let request = index.getAllKeys(range); 225 | request.onsuccess = function onsuccess(event) { 226 | let result = request.result; 227 | if (!negate) { 228 | callback(result); 229 | return; 230 | } 231 | 232 | // Deal with the negation case. This means we fetch all keys and then 233 | // subtract the original result from it. 234 | request = index.getAllKeys(); 235 | request.onsuccess = function onsuccess(event) { 236 | let all = request.result; 237 | callback(arraySub(all, result)); 238 | }; 239 | }; 240 | } 241 | 242 | let args = arguments; 243 | function toString() { 244 | return "IndexQuery(" + Array.slice(args).toSource().slice(1, -1) + ")"; 245 | } 246 | 247 | return Query(queryKeys, toString); 248 | } 249 | 250 | /** 251 | * Create a query object that performs the intersection of two given queries. 252 | */ 253 | function Intersection(query1, query2) { 254 | function queryKeys(store, callback) { 255 | query1._queryFunc(store, function (keys1) { 256 | query2._queryFunc(store, function (keys2) { 257 | callback(arrayIntersect(keys1, keys2)); 258 | }); 259 | }); 260 | } 261 | 262 | function toString() { 263 | return "Intersection(" + query1.toString() + ", " + query2.toString() + ")"; 264 | } 265 | 266 | return Query(queryKeys, toString); 267 | } 268 | 269 | /** 270 | * Create a query object that performs the union of two given queries. 271 | */ 272 | function Union(query1, query2) { 273 | function queryKeys(store, callback) { 274 | query1._queryFunc(store, function (keys1) { 275 | query2._queryFunc(store, function (keys2) { 276 | callback(arrayUnion(keys1, keys2)); 277 | }); 278 | }); 279 | } 280 | 281 | function toString() { 282 | return "Union(" + query1.toString() + ", " + query2.toString() + ")"; 283 | } 284 | 285 | return Query(queryKeys, toString); 286 | } 287 | 288 | 289 | function arraySub(minuend, subtrahend) { 290 | if (!minuend.length || !subtrahend.length) { 291 | return minuend; 292 | } 293 | return minuend.filter(function(item) { 294 | return subtrahend.indexOf(item) == -1; 295 | }); 296 | } 297 | 298 | function arrayUnion(foo, bar) { 299 | if (!foo.length) { 300 | return bar; 301 | } 302 | if (!bar.length) { 303 | return foo; 304 | } 305 | return foo.concat(arraySub(bar, foo)); 306 | } 307 | 308 | function arrayIntersect(foo, bar) { 309 | if (!foo.length) { 310 | return foo; 311 | } 312 | if (!bar.length) { 313 | return bar; 314 | } 315 | return foo.filter(function(item) { 316 | return bar.indexOf(item) != -1; 317 | }); 318 | } 319 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |