diff --git a/Makefile b/Makefile index 338a30a..99a34d9 100644 --- a/Makefile +++ b/Makefile @@ -153,7 +153,8 @@ ${JIOVERSION}: ${EXTERNALDIR}/URI.js \ ${SRCDIR}/jio.storage/fbstorage.js \ ${SRCDIR}/jio.storage/cloudooostorage.js \ ${SRCDIR}/jio.storage/nocapacitystorage.js \ - ${SRCDIR}/jio.storage/liststorage.js + ${SRCDIR}/jio.storage/liststorage.js \ + ${SRCDIR}/jio.storage/indexstorage2.js @mkdir -p $(@D) cat $^ > $@ diff --git a/examples/index2_benchmark.html b/examples/index2_benchmark.html new file mode 100644 index 0000000..3100f65 --- /dev/null +++ b/examples/index2_benchmark.html @@ -0,0 +1,42 @@ + + + + + + + jIO Query Performance test + + + + + + + + + + + +

Testing index2 query

+ Test +

+
+ + diff --git a/examples/index2_benchmark.js b/examples/index2_benchmark.js new file mode 100644 index 0000000..38b64d3 --- /dev/null +++ b/examples/index2_benchmark.js @@ -0,0 +1,166 @@ +/*global performance, String*/ +(function (window, jIO, rJS) { + "use strict"; + + var test_count = 15; + /*function randomi(limit) { + return Math.floor(Math.random() * Math.floor(limit)); + } + + function randomSentence(length) { + var alphabet = ['a', 'b', 'c', 'd', 'e', ' ', 'f', 'g', 'h', 'i', 'j', 'k', + ' ', 'l', 'm', 'n', 'o', ' ', 'p', 'q', 'r', 's', 't', ' ', + 'u', 'v', 'w', ' ', 'x', 'y', 'z', ' '], sentence = '', z; + for (z = 0; z < length; z += 1) { + sentence += alphabet[randomi(alphabet.length - 1)]; + } + return sentence; + } + + function randomSentenceArray(sentence_length, array_length) { + var y, sentence_array = []; + for (y = 0; y < array_length; y += 1) { + sentence_array.push(randomSentence(sentence_length)); + } + return sentence_array; + }*/ + + function get_fake_data_values2(i) { + if (i === 0 || i === 1 || i === 2) { + return {'url': 'renderjs.com', 'name': 'erp5', 'user': 'preet'}; + } + if (i === 3 || i === 4) { + return {'url': 'erp5.com', 'name': 'erp5', 'user': 'test'}; + } + if (i === 5 || i === 6 || i === 7) { + return {'url': 'nexedi.com', 'name': 'nexedi', 'user': 'prabetcder'}; + } + if (i === 10 || i === 11) { + return {'url': 'vifib.com', 'name': 'renderjs', 'user': 'preetwinder'}; + } + if (i === 12 || i === 13) { + return {'url': 'renderjs.com', 'name': 'jio', 'user': 'obscure'}; + } + return {'url': 'jio.nexedi.com', 'name': 'jio', 'user': 'praounsteter'}; + } + + /*function get_fake_data_values(i) { + var data_value = { + 'id': i, + 'url': 'https://streetsite.com/profiles/' + i, + 'pic_url': 'https://cdn.streetsite.com/pictures/saoteuhcu/' + i, + 'short_description': randomSentence(10 + randomi(40)), + 'description': randomSentence(randomi(250)), + 'comments': randomSentenceArray(randomi(500), randomi(20)) + }; + if (i === 9900) { + data_value.short_description = 'test'; + } + if (i === 7500) { + data_value.short_description = 'preet'; + } + if (i === 5400) { + data_value.short_description = 'obscure'; + } + if (i === 3200) { + data_value.short_description = 'precise'; + } + if (i === 1200) { + data_value.short_description = 'environ'; + } + return data_value; + }*/ + + /* function sequential_test(i, storage) { + if (i < test_count) { + var data_value = { + 'id': i, + 'url': 'https://streetsite.com/profiles/' + i, + 'pic_url': 'https://cdn.streetsite.com/pictures/saoteuhcu/' + i, + 'short_description': randomSentence(10 + randomi(40)), + 'description': randomSentence(randomi(250)), + 'comments': randomSentenceArray(randomi(500), randomi(20)) + }; + if (i === 99000) { + data_value.short_description = 'test'; + } + if (i % 100 === 0) { + data_value.short_description = 'preet'; + } + if (i % 1000 === 0) { + data_value.short_description = 'obscure'; + } + if (i === 32000) { + data_value.short_description = 'precise precise precise'; + } + if (i === 120000) { + data_value.short_description = 'environ'; + } + return storage.put(String(i), data_value) + .then(function () { + if (i % 1000 === 0) { + console.log(i); + } + data_value = null; + return sequential_test(i + 1, storage); + }); + } + return; + }*/ + + + rJS(window) + + .declareService(function () { + var storage = jIO.createJIO({ + type: "index2", + database: "index2test2", + index_keys: ["user", "name", "url"], + sub_storage: { + type: "indexeddb", + database: "index2testdata2", + } + }), promise_list = [], i, time; + console.log('Staring to write ' + test_count + ' documents'); + //sequential_test(0, storage); + for (i = 0; i < test_count; i += 1) { + promise_list.push(storage.put(String(i), get_fake_data_values2(i))); + } + promise_list.push(storage.put('325', get_fake_data_values2(325))); + time = performance.now(); + return RSVP.all(promise_list) + .then(function () { + console.log('Time to write - ', (performance.now() - time)); + console.log('Starting queries'); + console.log('Query 1'); + time = performance.now(); + return storage.allDocs({query: "user:preetwinder"}); + }) + .then(function (result) { + console.log('Time to query 1 - ', (performance.now() - time)); + console.log(result); + console.log('Query 2'); + time = performance.now(); + return storage.allDocs({query: 'user:preet'}); + }) + .then(function (result) { + console.log('Time to query 2 - ', (performance.now() - time)); + console.log(result); + console.log('Query 3'); + time = performance.now(); + return storage.allDocs({query: "(name:jio OR url:nexedi.com" + + ") AND user:obscure"}); + }) + .then(function (result) { + console.log('Time to query 3 - ', performance.now() - time); + console.log(result); + console.log('Query 4'); + time = performance.now(); + return storage.allDocs({query: 'name:not'}); + }) + .then(function (result) { + console.log('Time to query 4 - ', performance.now() - time); + console.log(result); + }); + }); +}(window, jIO, rJS)); \ No newline at end of file diff --git a/src/jio.storage/indexstorage2.js b/src/jio.storage/indexstorage2.js new file mode 100644 index 0000000..1166627 --- /dev/null +++ b/src/jio.storage/indexstorage2.js @@ -0,0 +1,342 @@ +/* + * Copyright 2019, Nexedi SA + * + * This program is free software: you can Use, Study, Modify and Redistribute + * it under the terms of the GNU General Public License version 3, or (at your + * option) any later version, as published by the Free Software Foundation. + * + * You can also Link and Combine this program with other software covered by + * the terms of any of the Free Software licenses or any of the Open Source + * Initiative approved licenses and Convey the resulting work. Corresponding + * source of such a combination shall include the source code for all other + * software used. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See COPYING file for full licensing terms. + * See https://www.nexedi.com/licensing for rationale and options. + */ +/*jslint nomen: true */ +/*global indexedDB, jIO, RSVP, IDBOpenDBRequest, DOMError, Event, + parseStringToObject, Set*/ + +(function (indexedDB, jIO, RSVP, IDBOpenDBRequest, DOMError, + parseStringToObject) { + "use strict"; + + function IndexStorage2(description) { + if (typeof description.database !== "string" || + description.database === "") { + throw new TypeError("IndexStorage2 'database' description property " + + "must be a non-empty string"); + } + this._sub_storage = jIO.createJIO(description.sub_storage); + this._database_name = "jio:" + description.database; + this._index_keys = description.index_keys; + } + + IndexStorage2.prototype.hasCapacity = function (name) { + return ((name === "list") || (name === "query")); + }; + + function handleUpgradeNeeded(evt, index_keys) { + var db = evt.target.result, store, i; + + store = db.createObjectStore("index-store", { + keyPath: "id", + autoIncrement: false + }); + for (i = 0; i < index_keys.length; i += 1) { + store.createIndex("Index-" + index_keys[i], "doc." + index_keys[i], + {unique: false}); + } + } + + function waitForOpenIndexedDB(db_name, index_keys, callback) { + function resolver(resolve, reject) { + // Open DB // + var request = indexedDB.open(db_name); + request.onerror = function (error) { + if (request.result) { + request.result.close(); + } + if ((error !== undefined) && + (error.target instanceof IDBOpenDBRequest) && + (error.target.error instanceof DOMError)) { + reject("Connection to: " + db_name + " failed: " + + error.target.error.message); + } else { + reject(error); + } + }; + + request.onabort = function () { + request.result.close(); + reject("Aborting connection to: " + db_name); + }; + + request.ontimeout = function () { + request.result.close(); + reject("Connection to: " + db_name + " timeout"); + }; + + request.onblocked = function () { + request.result.close(); + reject("Connection to: " + db_name + " was blocked"); + }; + + // Create DB if necessary // + request.onupgradeneeded = function (evt) { + handleUpgradeNeeded(evt, index_keys); + }; + + request.onversionchange = function () { + request.result.close(); + reject(db_name + " was upgraded"); + }; + + request.onsuccess = function () { + return new RSVP.Queue() + .push(function () { + return callback(request.result); + }) + .push(function (result) { + request.result.close(); + resolve(result); + }, function (error) { + request.result.close(); + reject(error); + }); + }; + } + + return new RSVP.Promise(resolver); + } + + function waitForTransaction(db, stores, flag, callback) { + var tx = db.transaction(stores, flag); + function canceller() { + try { + tx.abort(); + } catch (unused) { + // Transaction already finished + return; + } + } + function resolver(resolve, reject) { + var result; + try { + result = callback(tx); + } catch (error) { + reject(error); + } + tx.oncomplete = function () { + return new RSVP.Queue() + .push(function () { + return result; + }) + .push(resolve, function (error) { + canceller(); + reject(error); + }); + }; + tx.onerror = function (error) { + canceller(); + reject(error); + }; + tx.onabort = function (evt) { + reject(evt.target); + }; + return tx; + } + return new RSVP.Promise(resolver, canceller); + } + + function waitForIDBRequest(request) { + return new RSVP.Promise(function (resolve, reject) { + request.onerror = reject; + request.onsuccess = resolve; + }); + } + + IndexStorage2.prototype._runQuery = function (index, value) { + var context = this; + return new RSVP.Queue() + .push(function () { + if ((context._index_keys.indexOf(index) === -1)) { + if (context._sub_storage.hasCapacity("query")) { + return context._sub_storage.buildQuery( + {"query": index + ":" + value} + ) + .then(function (result) { + return result; + }); + } + } + return waitForOpenIndexedDB(context._database_name, + context._index_keys, function (db) { + return waitForTransaction(db, ["index-store"], "readonly", + function (tx) { + return waitForIDBRequest(tx.objectStore("index-store") + .index("Index-" + index).getAll(value)) + .then(function (evt) { + return evt.target.result; + }); + }); + }); + }); + }; + + IndexStorage2.prototype._processQueryObject = function (object) { + var promise_list = [], context = this, i, j, query_result = new Set(); + return RSVP.Queue() + .push(function () { + if (object.type === "simple") { + return context._runQuery(object.key, object.value); + } + if (object.type === "complex") { + for (i = 0; i < object.query_list.length; i += 1) { + promise_list.push(context + ._processQueryObject(object.query_list[i])); + } + return RSVP.all(promise_list) + .then(function (result) { + if (object.operator === "OR") { + for (i = 0; i < result.length; i += 1) { + for (j = 0; j < result[i].length; j += 1) { + query_result.add(result[i][j]); + } + } + return Array.from(query_result); + } + if (object.operator === "AND") { + var temp_set = new Set(); + for (i = 0; i < result[0].length; i += 1) { + query_result.add(result[0][i].id); + } + for (i = 1; i < result.length; i += 1) { + for (j = 0; j < result[i].length; j += 1) { + if (query_result.has(result[i][j].id)) { + temp_set.add(result[i][j]); + } + } + query_result = temp_set; + temp_set = new Set(); + } + return Array.from(query_result); + } + }); + } + }); + }; + + IndexStorage2.prototype.buildQuery = function (options) { + var context = this; + if (options.query) { + return this._processQueryObject(parseStringToObject(options.query)) + .then(function (result) { + return result.map(function (value) { + return {"id": value.id, "value": {} }; + }); + }); + } + return waitForOpenIndexedDB(context._database_name, + context._index_keys, function (db) { + return waitForTransaction(db, ["index-store"], "readonly", + function (tx) { + return waitForIDBRequest(tx.objectStore("index-store").getAll()) + .then(function (evt) { + return evt.target.result.map(function (value) { + return {"id": value.id, "value": {} }; + }); + }); + }); + }); + }; + + IndexStorage2.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, arguments); + }; + + IndexStorage2.prototype._filter_doc_values = function (doc, keys) { + var filtered_doc = {}, i; + for (i = 0; i < keys.length; i += 1) { + filtered_doc[keys[i]] = doc[keys[i]]; + } + return filtered_doc; + }; + + IndexStorage2.prototype.put = function (id, value) { + var context = this; + return context._sub_storage.put(id, value) + .push(function (result) { + return waitForOpenIndexedDB(context._database_name, + context._index_keys, function (db) { + return waitForTransaction(db, ["index-store"], "readwrite", + function (tx) { + return waitForIDBRequest(tx.objectStore("index-store").put({ + "id": id, + "doc": context._filter_doc_values(value, context._index_keys) + })) + .then(function () { + return result; + }); + }); + }); + }); + }; + + IndexStorage2.prototype.post = function (value) { + var context = this; + return context._sub_storage.post(value) + .push(function (id) { + return waitForOpenIndexedDB(context._database_name, + context._index_keys, function (db) { + return waitForTransaction(db, ["index-store"], "readwrite", + function (tx) { + return waitForIDBRequest(tx.objectStore("index-store").put({ + "id": id, + "doc": context._filter_doc_values(value, context._index_keys) + })) + .then(function () { + return id; + }); + }); + }); + }); + }; + + IndexStorage2.prototype.remove = function (id) { + var context = this; + return context._sub_storage.remove(id) + .push(function (result) { + return waitForOpenIndexedDB(context._database_name, context._index_keys, + function (db) { + return waitForTransaction(db, ["index-store"], "readwrite", + function (tx) { + return waitForIDBRequest(tx.objectStore("index-store") + .delete(id)) + .then(function () { + return result; + }); + }); + }); + }); + }; + + IndexStorage2.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + + IndexStorage2.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + + IndexStorage2.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + + jIO.addStorage("index2", IndexStorage2); +}(indexedDB, jIO, RSVP, IDBOpenDBRequest, DOMError, parseStringToObject)); \ No newline at end of file diff --git a/test/jio.storage/indexstorage2.tests.js b/test/jio.storage/indexstorage2.tests.js new file mode 100644 index 0000000..fcd2c16 --- /dev/null +++ b/test/jio.storage/indexstorage2.tests.js @@ -0,0 +1,751 @@ +/* + * Copyright 2019, Nexedi SA + * + * This program is free software: you can Use, Study, Modify and Redistribute + * it under the terms of the GNU General Public License version 3, or (at your + * option) any later version, as published by the Free Software Foundation. + * + * You can also Link and Combine this program with other software covered by + * the terms of any of the Free Software licenses or any of the Open Source + * Initiative approved licenses and Convey the resulting work. Corresponding + * source of such a combination shall include the source code for all other + * software used. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See COPYING file for full licensing terms. + * See https://www.nexedi.com/licensing for rationale and options. + */ +/*jslint nomen: true */ +/*global indexedDB, sinon, IDBDatabase, Blob, + IDBTransaction, IDBIndex, IDBObjectStore, IDBKeyRange*/ +(function (jIO, QUnit, indexedDB, sinon, IDBDatabase, Blob, + IDBTransaction, IDBIndex, IDBObjectStore, IDBKeyRange) { + "use strict"; + var test = QUnit.test, + stop = QUnit.stop, + start = QUnit.start, + ok = QUnit.ok, + expect = QUnit.expect, + deepEqual = QUnit.deepEqual, + equal = QUnit.equal, + module = QUnit.module; + + function deleteIndexedDB(storage) { + return new RSVP.Promise(function resolver(resolve, reject) { + var request = indexedDB.deleteDatabase( + storage.__storage._database_name + ); + request.onerror = reject; + request.onblocked = reject; + request.onsuccess = resolve; + }); + } + + function id_compare(value1, value2) { + if (value1.id > value2.id) { + return 1; + } + if (value1.id < value2.id) { + return -1; + } + return 0; + } + + ///////////////////////////////////////////////////////////////// + // Custom test substorage definition + ///////////////////////////////////////////////////////////////// + function DummyStorage3() { + return this; + } + jIO.addStorage('dummystorage3', DummyStorage3); + + ///////////////////////////////////////////////////////////////// + // indexStorage2.constructor + ///////////////////////////////////////////////////////////////// + module("indexStorage2.constructor", { + teardown: function () { + deleteIndexedDB(this.jio); + } + }); + test("Constructor with empty index_keys", function () { + this.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: [], + sub_storage: { + type: "dummystorage3" + } + }); + + equal(this.jio.__type, "index2"); + equal(this.jio.__storage._sub_storage.__type, "dummystorage3"); + equal(this.jio.__storage._database_name, "jio:index2_test"); + }); + + ///////////////////////////////////////////////////////////////// + // indexStorage2.hasCapacity + ///////////////////////////////////////////////////////////////// + module("indexStorage2.hasCapacity", { + teardown: function () { + deleteIndexedDB(this.jio); + } + }); + test("can list documents", function () { + this.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: [], + sub_storage: { + type: "dummystorage3" + } + }); + + ok(this.jio.hasCapacity("list")); + ok(this.jio.hasCapacity("query")); + }); + + ///////////////////////////////////////////////////////////////// + // indexStorage2.get + ///////////////////////////////////////////////////////////////// + module("indexStorage2.get", { + setup: function () { + this.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: [], + sub_storage: { + type: "dummystorage3" + } + }); + }, + teardown: function () { + deleteIndexedDB(this.jio); + } + }); + test("Simple put get", function () { + var context = this; + stop(); + expect(4); + + DummyStorage3.prototype.put = function (id, value) { + equal(id, "32"); + deepEqual(value, {"a": 3, "b": 2, "c": 8}); + return id; + }; + + DummyStorage3.prototype.get = function (id) { + equal(id, "32"); + return {"a": 3, "b": 2, "c": 8}; + }; + + context.jio.put("32", {"a": 3, "b": 2, "c": 8}) + .then(function () { + return context.jio.get("32"); + }) + .then(function (result) { + deepEqual(result, {"a": 3, "b": 2, "c": 8}); + }) + .fail(function (error) { + console.log(error); + }) + .then(function () { + return deleteIndexedDB(context.jio); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // indexStorage2.buildQuery + ///////////////////////////////////////////////////////////////// + module("indexStorage2.buildQuery", { + teardown: function () { + deleteIndexedDB(this.jio); + } + }); + + test("Simple query matching single object", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["a", "b"], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(3); + + DummyStorage3.prototype.put = function (id, value) { + equal(id, "32"); + deepEqual(value, {a: "3", b: "2"}); + return id; + }; + + context.jio.put("32", {"a": "3", "b": "2"}) + .then(function () { + return context.jio.allDocs({query: 'a: "3"'}); + }) + .then(function (result) { + deepEqual(result.data.rows[0], {"id": "32", "value": {}}); + }) + .fail(function (error) { + console.log(error); + }) + .always(function () { + start(); + }); + }); + + test("Simple query matching multiple objects", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["a", "b"], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(5); + + DummyStorage3.prototype.put = function (id, value) { + if (id === "32") { + deepEqual(value, {a: "3", b: "1"}); + } + if (id === "21") { + deepEqual(value, {a: "8", b: "1"}); + } + if (id === "3") { + deepEqual(value, {a: "5", b: "1"}); + } + return id; + }; + + RSVP.all([ + context.jio.put("32", {a: "3", b: "1"}), + context.jio.put("21", {a: "8", b: "1"}), + context.jio.put("3", {a: "5", b: "1"}) + ]) + .then(function () { + return context.jio.allDocs({query: 'b: "1"'}); + }) + .then(function (result) { + equal(result.data.total_rows, 3); + deepEqual(result.data.rows.sort(id_compare), + [ + {"id": "32", "value": {}}, + {"id": "21", "value": {}}, + {"id": "3", "value": {}} + ].sort(id_compare)); + }) + .fail(function (error) { + console.log(error); + }) + .always(function () { + start(); + }); + }); + + test("No index keys provided", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: [], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(4); + + DummyStorage3.prototype.put = function (id, value) { + equal(id, "32"); + deepEqual(value, {"a": "3", "b": "2"}); + return id; + }; + DummyStorage3.prototype.buildQuery = function (options) { + equal(options.query, 'a:3'); + }; + DummyStorage3.prototype.hasCapacity = function (capacity) { + equal(capacity, "query"); + return false; + }; + + context.jio.put("32", {"a": "3", "b": "2"}) + .then(function () { + return context.jio.allDocs({query: 'a:"3"'}); + }) + .fail(function (error) { + equal(error.message, + "Capacity 'query' is not implemented on 'dummystorage3'"); + }) + .always(function () { + start(); + }); + }); + + test("No index keys provided but substorage supports querying", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: [], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(4); + + DummyStorage3.prototype.put = function (id) { + return id; + }; + DummyStorage3.prototype.hasCapacity = function (capacity) { + equal(capacity, "query"); + if (capacity === "query") { return true; } + }; + DummyStorage3.prototype.buildQuery = function (options) { + equal(options.query, "a:5"); + return [{id: "3", value: {}}]; + }; + + RSVP.all([ + context.jio.put("32", {a: "3", b: "1"}), + context.jio.put("21", {a: "8", b: "1"}), + context.jio.put("3", {a: "5", b: "1"}) + ]) + .then(function () { + return context.jio.allDocs({query: 'a: "5"'}); + }) + .then(function (result) { + equal(result.data.total_rows, 1); + deepEqual(result.data.rows, [{"id": "3", "value": {}}]); + }) + .fail(function (error) { + console.log(error); + }) + .always(function () { + start(); + }); + }); + + test("Index is provided for some keys only", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["a", "b"], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(4); + + DummyStorage3.prototype.put = function (id) { + return id; + }; + DummyStorage3.prototype.hasCapacity = function (capacity) { + equal(capacity, "query"); + if (capacity === "query") { return true; } + }; + DummyStorage3.prototype.buildQuery = function (options) { + equal(options.query, 'c:linear'); + return [{id: "32", value: {}}]; + }; + + RSVP.all([ + context.jio.put("32", {a: "3", b: "1", c: "linear"}), + context.jio.put("21", {a: "8", b: "1", c: "obscure"}), + context.jio.put("3", {a: "5", b: "1", c: "imminent"}) + ]) + .then(function () { + return context.jio.allDocs({query: 'a: "5" OR c: "linear"'}); + }) + .then(function (result) { + equal(result.data.total_rows, 2); + deepEqual(result.data.rows.sort(id_compare), + [{"id": "32", "value": {}}, {"id": "3", "value": {}}] + .sort(id_compare)); + }) + .fail(function (error) { + console.log(error); + }) + .always(function () { + start(); + }); + }); + + test("No Query", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["a"], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(1); + + DummyStorage3.prototype.put = function (id) { + return id; + }; + + RSVP.all([ + context.jio.put("32", {"a": "3", "b": "2"}), + context.jio.put("21", {"a": "6", "b": "9"}), + context.jio.put("3", {"a": "8", "b": "5"}) + ]) + .then(function () { + return context.jio.allDocs(); + }) + .then(function (result) { + equal(result.data.total_rows, 3); + }) + .fail(function (error) { + console.log(error); + }) + .always(function () { + start(); + }); + }); + + test("Complex queries", function () { + var context = this; + context.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["name", "user"], + sub_storage: { + type: "dummystorage3" + } + }); + stop(); + expect(10); + + DummyStorage3.prototype.put = function (id) { + return id; + }; + + RSVP.all([ + context.jio.put("1", {"name": "envision", "url": "jio.nexedi.com", + "user": "Mann"}), + context.jio.put("23", {"name": "obscure", "url": "jio.nexedi.com", + "user": "Hesse"}), + context.jio.put("5", {"name": "envelope", "url": "renderjs.nexedi.com", + "user": "Mann"}), + context.jio.put("34", {"name": "censure", "url": "nexedi.com", + "user": "Brahms"}), + context.jio.put("38", {"name": "observe", "url": "erp5.com", + "user": "Hesse"}), + context.jio.put("76", {"name": "linear", "url": "vifib.com", + "user": "J Evol"}), + context.jio.put("14", {"name": "obscure", "url": "re6st.nexedi.com", + "user": "Lietz"}), + context.jio.put("19", {"name": "razor", "url": "erp5.com", + "user": "Karajan"}), + context.jio.put("59", {"name": "envision", "url": "nexedi.com", + "user": "Handel"}), + context.jio.put("31", {"name": "obtuse", "url": "officejs.com", + "user": "Johann"}), + context.jio.put("45", {"name": "repeat", "url": "slapos.com", + "user": "Specter"}), + context.jio.put("48", {"name": "sever", "url": "neo.nexedi.com", + "user": "Rienzi"}), + context.jio.put("72", {"name": "organisers", "url": "vifib.net", + "user": "Parzival"}) + ]) + .then(function () { + return context.jio.allDocs({"query": "name:razor"}); + }) + .then(function (result) { + equal(result.data.total_rows, 1); + deepEqual(result.data.rows, [{"id": "19", "value": {}}]); + }) + .then(function () { + return context.jio.allDocs({"query": "name:obscure"}); + }) + .then(function (result) { + equal(result.data.total_rows, 2); + deepEqual(result.data.rows.sort(), [{"id": "23", "value": {}}, + {"id": "14", "value": {}}].sort(id_compare)); + }) + .then(function () { + return context.jio.allDocs({"query": "name:envision AND user:Mann"}); + }) + .then(function (result) { + equal(result.data.total_rows, 1); + deepEqual(result.data.rows, [{"id": "1", "value": {}}]); + }) + .then(function () { + return context.jio.allDocs({"query": "name:repeat OR user:Hesse"}); + }) + .then(function (result) { + equal(result.data.total_rows, 3); + deepEqual(result.data.rows.sort(id_compare), + [{"id": "23", "value": {}}, {"id": "38", "value": {}}, + {"id": "45", "value": {}}].sort(id_compare)); + }) + .then(function () { + return context.jio.allDocs( + {"query": "(user:Mann OR user:Hesse) AND name:envelope"} + ); + }) + .then(function (result) { + equal(result.data.total_rows, 1); + deepEqual(result.data.rows, [{"id": "5", "value": {}}]); + }) + .fail(function (error) { + console.error(error); + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // indexStorage2.put + ///////////////////////////////////////////////////////////////// + module("indexStorage2.put", { + setup: function () { + this.jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["name", "user"], + sub_storage: { + type: "dummystorage3" + } + }); + }, + teardown: function () { + deleteIndexedDB(this.jio); + } + }); + + test("spy index usage", function () { + var context = this; + stop(); + expect(22); + + DummyStorage3.prototype.put = function (id) { + return id; + }; + + deleteIndexedDB(context.jio) + .then(function () { + context.spy_open = sinon.spy(indexedDB, "open"); + context.spy_create_store = sinon.spy(IDBDatabase.prototype, + "createObjectStore"); + context.spy_transaction = sinon.spy(IDBDatabase.prototype, + "transaction"); + context.spy_store = sinon.spy(IDBTransaction.prototype, "objectStore"); + context.spy_put = sinon.spy(IDBObjectStore.prototype, "put"); + context.spy_index = sinon.spy(IDBObjectStore.prototype, "index"); + context.spy_create_index = sinon.spy(IDBObjectStore.prototype, + "createIndex"); + context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); + context.spy_key_range = sinon.spy(IDBKeyRange, "only"); + + return context.jio.put("foo", {"name": "foo", "user": "bar"}); + }) + .then(function () { + + ok(context.spy_open.calledOnce, "open count " + + context.spy_open.callCount); + equal(context.spy_open.firstCall.args[0], "jio:index2_test", + "open first argument"); + + equal(context.spy_create_store.callCount, 1, + "createObjectStore count"); + + equal(context.spy_create_store.firstCall.args[0], "index-store", + "first createObjectStore first argument"); + deepEqual(context.spy_create_store.firstCall.args[1], + {keyPath: "id", autoIncrement: false}, + "first createObjectStore second argument"); + + equal(context.spy_create_index.callCount, 2, "createIndex count"); + + equal(context.spy_create_index.firstCall.args[0], "Index-name", + "first createIndex first argument"); + equal(context.spy_create_index.firstCall.args[1], "doc.name", + "first createIndex second argument"); + deepEqual(context.spy_create_index.firstCall.args[2], {unique: false}, + "first createIndex third argument"); + + equal(context.spy_create_index.secondCall.args[0], "Index-user", + "second createIndex first argument"); + equal(context.spy_create_index.secondCall.args[1], "doc.user", + "second createIndex second argument"); + deepEqual(context.spy_create_index.secondCall.args[2], + {unique: false}, + "second createIndex third argument"); + + ok(context.spy_transaction.calledOnce, "transaction count " + + context.spy_transaction.callCount); + deepEqual(context.spy_transaction.firstCall.args[0], ["index-store"], + "transaction first argument"); + equal(context.spy_transaction.firstCall.args[1], "readwrite", + "transaction second argument"); + + ok(context.spy_store.calledOnce, "store count " + + context.spy_store.callCount); + deepEqual(context.spy_store.firstCall.args[0], "index-store", + "store first argument"); + + ok(context.spy_put.calledOnce, "put count " + + context.spy_put.callCount); + deepEqual(context.spy_put.firstCall.args[0], + {"id": "foo", doc: {name: "foo", user: "bar"}}, + "put first argument"); + + ok(!context.spy_index.called, "index count " + + context.spy_index.callCount); + + ok(!context.spy_cursor.called, "cursor count " + + context.spy_cursor.callCount); + + ok(!context.spy_key_range.called, "key range count " + + context.spy_key_range.callCount); + + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + var i, + spy_list = ['spy_open', 'spy_create_store', 'spy_transaction', + 'spy_store', 'spy_put', 'spy_index', 'spy_create_index', + 'spy_cursor', 'spy_key_range']; + for (i = 0; i < spy_list.length; i += 1) { + if (context.hasOwnProperty(spy_list[i])) { + context[spy_list[i]].restore(); + delete context[spy_list[i]]; + } + } + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // IndexStorage2.getAttachment + ///////////////////////////////////////////////////////////////// + module("IndexStorage2.getAttachment"); + test("getAttachment called substorage getAttachment", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["name", "user"], + sub_storage: { + type: "dummystorage3" + }, + }), + blob = new Blob([""]); + + DummyStorage3.prototype.getAttachment = function (id, name) { + equal(id, "1"); + equal(name, "test_name"); + return blob; + }; + + jio.getAttachment("1", "test_name") + .then(function (result) { + equal(result, blob); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // IndexStorage2.putAttachment + ///////////////////////////////////////////////////////////////// + module("IndexStorage2.putAttachment"); + test("putAttachment called substorage putAttachment", function () { + stop(); + expect(4); + + var jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["name", "user"], + sub_storage: { + type: "dummystorage3" + }, + }), + blob = new Blob([""]); + + DummyStorage3.prototype.putAttachment = function (id, name, blob2) { + equal(id, "1"); + equal(name, "test_name"); + deepEqual(blob2, blob); + return "OK"; + }; + + jio.putAttachment("1", "test_name", blob) + .then(function (result) { + equal(result, "OK"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // IndexStorage3.removeAttachment + ///////////////////////////////////////////////////////////////// + module("IndexStorage3.removeAttachment"); + test("removeAttachment called substorage removeAttachment", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "index2", + database: "index2_test", + index_keys: ["name", "user"], + sub_storage: { + type: "dummystorage3" + }, + }); + + DummyStorage3.prototype.removeAttachment = function (id, name) { + equal(id, "1"); + equal(name, "test_name"); + return "removed"; + }; + + jio.removeAttachment("1", "test_name") + .then(function (result) { + equal(result, "removed"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + +}(jIO, QUnit, indexedDB, sinon, IDBDatabase, Blob, + IDBTransaction, IDBIndex, IDBObjectStore, IDBKeyRange)); \ No newline at end of file diff --git a/test/node.js b/test/node.js index cc12794..f5db7ee 100644 --- a/test/node.js +++ b/test/node.js @@ -64,6 +64,7 @@ 'test/jio.storage/erp5storage.tests.js', 'test/jio.storage/fbstorage.tests.js', 'test/jio.storage/gdrivestorage.tests.js', + 'test/jio.storage/indexstorage2.tests.js', 'test/jio.storage/liststorage.tests.js', 'test/jio.storage/memorystorage.tests.js', 'test/jio.storage/nocapacitystorage.tests.js', diff --git a/test/tests.html b/test/tests.html index 7d97a92..df48945 100644 --- a/test/tests.html +++ b/test/tests.html @@ -79,6 +79,7 @@ See https://www.nexedi.com/licensing for rationale and options. +