UNPKG

ydn.db

Version:

Javascript database library for IndexedDB, WebDatabase (WebSQL) and WebStorage (localStorage) storage mechanisms supporting version migration, advanced query and transaction workflow.

794 lines (708 loc) 20.9 kB
// Copyright 2012 YDN Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Object store for simple storage. * * @author kyawtun@yathit.com (Kyaw Tun) */ goog.provide('ydn.db.con.simple.Store'); goog.require('ydn.db.base'); goog.require('ydn.db.con.simple'); goog.require('ydn.db.con.simple.Node'); goog.require('ydn.structs.Buffer'); /** * * @param {string} db_name database name. * @param {!Storage} storage * @param {!ydn.db.schema.Store} store_schema * @constructor * @struct */ ydn.db.con.simple.Store = function(db_name, storage, store_schema) { /** * @final */ this.db_name = db_name; /** * @final */ this.storage = storage; /** * @final */ this.schema = store_schema; /** * @final */ this.key_indexes = {}; var kp = this.schema.getKeyPath(); /** * @final * @type {string} */ this.primary_index = goog.isArray(kp) ? kp.join(',') : kp || ydn.db.base.SQLITE_SPECIAL_COLUNM_NAME; this.key_indexes[this.primary_index] = null; /** * @final * @type {string} */ this.key_prefix = ydn.db.con.simple.makeKey(this.db_name, this.schema.getName(), this.primary_index) + ydn.db.con.simple.SEP; }; /** * * @define {boolean} debug flag. */ ydn.db.con.simple.Store.DEBUG = false; /** * @type {!Storage} * @private */ ydn.db.con.simple.Store.prototype.storage; /** * @private * @type {string} */ ydn.db.con.simple.Store.prototype.db_name; /** * @private * @type {!ydn.db.schema.Store} */ ydn.db.con.simple.Store.prototype.schema; /** * @private * @type {string} */ ydn.db.con.simple.Store.prototype.primary_index; /** * List of ascending ordered key for each index and primary key. * @type {!Object.<!ydn.structs.Buffer>} * @private */ ydn.db.con.simple.Store.prototype.key_indexes; /** * Use store name and id to form a key to use in setting key to storage. * @protected * @final * @param {IDBKey=} opt_id id. If not given, key for store return. * @return {string} canonical key name. */ ydn.db.con.simple.Store.prototype.makeKey = function(opt_id) { return this.key_prefix + ydn.db.utils.encodeKey(opt_id); }; /** * @type {string} * @private */ ydn.db.con.simple.Store.prototype.key_prefix; /** * Extract key from encoded form. * @final * @protected * @param {string} eKey key as it stored in the storage. * @return {!IDBKey} the key. */ ydn.db.con.simple.Store.prototype.extractKey = function(eKey) { var key = ydn.db.utils.decodeKey(eKey.substr(this.key_prefix.length)); return /** @type {!IDBKey} */ (key); }; /** * Key generator for autoIncrement key. * @see http://www.w3.org/TR/IndexedDB/#key-generator-concept * @return {!IDBKey} */ ydn.db.con.simple.Store.prototype.generateKey = function() { var store_key = this.makeKey(); var sch = ydn.json.parse(this.storage.getItem(store_key)); if (!sch['key_count']) { sch['key_count'] = 0; } sch['key_count']++; this.storage.setItem(store_key, ydn.json.stringify(sch)); return sch['key_count']; }; /** * * @param {string=} opt_index_name index name, default to primary key index. * @return {!ydn.structs.Buffer} */ ydn.db.con.simple.Store.prototype.getIndexCache = function(opt_index_name) { var index_name = opt_index_name || this.primary_index; if (!this.key_indexes[index_name]) { this.key_indexes[index_name] = new ydn.structs.Buffer(ydn.db.con.simple.Node.cmp); var n = this.storage.length; for (var i = 0; i < n; i++) { var key_str = this.storage.key(i); if (!goog.isNull(key_str)) { if (goog.string.startsWith(key_str, this.key_prefix)) { var key = this.extractKey(key_str); if (index_name == this.primary_index) { var node = new ydn.db.con.simple.Node(key); this.key_indexes[index_name].add(node); } else { var obj_str = this.storage.getItem(key_str); if (!goog.isNull(obj_str)) { var index = this.schema.getIndex(index_name); var obj = ydn.json.parse(obj_str); var index_key = /** @type {IDBKey} */ (index.extractKey(obj)); if (index.isMultiEntry()) { if (goog.isArray(index_key)) { for (var k = 0; k < index_key.length; k++) { var i_node = new ydn.db.con.simple.Node(index_key[k], key); this.key_indexes[index_name].add(i_node); } } } else { var index_node = new ydn.db.con.simple.Node(index_key, key); this.key_indexes[index_name].add(index_node); } } } } } } if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log('index ' + index_name + ' of ' + this.schema.getName() + ' for ' + this.key_indexes[index_name].getCount() + ' records.'); } } return this.key_indexes[index_name]; }; /** * * @param {!IDBKey} key primary key. * @param {!Object} value record value. */ ydn.db.con.simple.Store.prototype.updateIndex = function(key, value) { for (var idx in this.key_indexes) { var cache = this.key_indexes[idx]; if (cache) { if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log('updating ' + key + ' in index ' + idx + ' of ' + this.schema.getName()); } if (idx == this.primary_index) { cache.add(new ydn.db.con.simple.Node(key)); } else { var index = this.schema.getIndex(idx); var index_key = ydn.db.utils.getValueByKeys(value, index.getKeyPath()); if (goog.isDefAndNotNull(index_key)) { var node = new ydn.db.con.simple.Node(key, index_key); cache.add(node); } } } } }; /** * * @param {!IDBKey} key primary key. * @param {!Object} value record value. */ ydn.db.con.simple.Store.prototype.removeIndex = function(key, value) { for (var idx in this.key_indexes) { var cache = this.key_indexes[idx]; if (cache) { if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log('removing ' + key + ' in index ' + idx + ' of ' + this.schema.getName()); } if (idx == this.primary_index) { cache.remove(new ydn.db.con.simple.Node(key)); } else { var index = this.schema.getIndex(idx); var index_key = ydn.db.utils.getValueByKeys(value, index.getKeyPath()); var node = new ydn.db.con.simple.Node(key, index_key); cache.remove(node); } } } }; /** * @protected */ ydn.db.con.simple.Store.prototype.clearIndexCache = function() { for (var idx in this.key_indexes) { var cache = this.key_indexes[idx]; if (cache) { cache.clear(); if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log('index ' + idx + ' of ' + this.schema.getName() + ' cleared.'); } } } this.key_indexes = {}; }; /** * * @param {IDBKey|undefined} key * @param {!Object} value * @param {boolean=} opt_is_add for add method, the key must not already exist. * @return {IDBKey?} key in case of unique key constraint, return null. */ ydn.db.con.simple.Store.prototype.addRecord = function(key, value, opt_is_add) { if (!goog.isDefAndNotNull(key)) { if (this.schema.usedInlineKey()) { key = this.schema.extractKey(value); } if (this.schema.isAutoIncrement() && !goog.isDefAndNotNull(key)) { key = this.generateKey(); } } goog.asserts.assert(goog.isDefAndNotNull(key), this + 'primary key not provided in ' + ydn.json.toShortString(value)); if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log('add ' + key); } if (opt_is_add) { /* if (this.key_indexes[this.primary_index]) { var cache = this.key_indexes[this.primary_index]; var node = new ydn.db.con.simple.Node(key); if (cache.contains(node)) { return null; // primary key constraint } } else { */ if (!goog.isNull(this.storage.getItem(this.makeKey(key)))) { return null; } // } } this.storage.setItem(this.makeKey(key), ydn.json.stringify(value)); this.updateIndex(key, value); return key; }; /** * * @param {IDBKey} key * @return {number} number deleted. */ ydn.db.con.simple.Store.prototype.removeRecord = function(key) { var eKey = this.makeKey(key); var obj = this.storage.getItem(eKey); if (goog.isNull(obj)) { return 0; } else { this.storage.removeItem(eKey); var value = ydn.json.parse(obj); this.removeIndex(key, value); return 1; } }; /** * Clear all record in stores. */ ydn.db.con.simple.Store.prototype.clear = function() { this.clearIndexCache(); this.removeRecords(); }; /** * * @param {string?} index_name * @param {!IDBKey} key * @return {*} */ ydn.db.con.simple.Store.prototype.getRecord = function(index_name, key) { if (!index_name || index_name == this.primary_index) { var v_str = this.storage.getItem(this.makeKey(key)); var v = undefined; if (!goog.isNull(v_str)) { v = /** @type {!Object} */ (ydn.json.parse(v_str)); for (var i = 0, n = this.schema.countIndex(); i < n; i++) { var index = this.schema.index(i); if (index.getType() == ydn.db.schema.DataType.DATE) { // restore date type, because after deserialization, it is a string. var d_str = index.extractKey(v); if (d_str) { var d = new Date(d_str); index.applyValue(v, d); } } } } return v; } else { goog.asserts.assert(this.schema.hasIndex(index_name), 'index "' + index_name + '" not found in ' + this); throw 'impl'; } }; /** * * @return {string} return store name. */ ydn.db.con.simple.Store.prototype.getName = function() { return this.schema.getName(); }; /** * * @param {string?=} opt_index_name index name. * @param {IDBKeyRange=} opt_key_range key range. * @return {number} */ ydn.db.con.simple.Store.prototype.countRecords = function(opt_index_name, opt_key_range) { opt_index_name = opt_index_name || this.primary_index; var me = this; var cache = this.getIndexCache(opt_index_name); /** * @type {ydn.db.con.simple.Node} */ var start = null; /** * @type {ydn.db.con.simple.Node} */ var end = null; var count = 0; var lowerOpen = false; var upperOpen = false; if (goog.isDefAndNotNull(opt_key_range)) { if (goog.isDefAndNotNull(opt_key_range.lower)) { start = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.lower)); } if (goog.isDefAndNotNull(opt_key_range.upper)) { end = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.upper)); } lowerOpen = opt_key_range.lowerOpen; upperOpen = opt_key_range.upperOpen; } /** * * @param {goog.structs.AvlTree.Node} node * @return {boolean|undefined} */ var tr_fn = function(node) { if (!goog.isDefAndNotNull(node)) { return; } var x = /** @type {ydn.db.con.simple.Node} */ (node.value); if (lowerOpen && goog.isDefAndNotNull(start) && ydn.db.cmp(x.getKey(), start.getKey()) == 0) { return; } if (goog.isDefAndNotNull(end)) { var cmp = ydn.db.cmp(x.getKey(), end.getKey()); if (cmp === 1) { return true; } if (cmp === 0 && upperOpen) { return true; } } count++; }; cache.traverse(tr_fn, start); return count; }; /** * * @param {IDBKeyRange=} opt_key_range * @return {number} */ ydn.db.con.simple.Store.prototype.removeRecords = function(opt_key_range) { var me = this; var cache = this.getIndexCache(this.primary_index); /** * @type {ydn.db.con.simple.Node} */ var start = null; /** * @type {ydn.db.con.simple.Node} */ var end = null; var count = 0; var removed_ids = []; var removed_objs = []; var lowerOpen = false; var upperOpen = false; if (goog.isDefAndNotNull(opt_key_range)) { if (goog.isDefAndNotNull(opt_key_range.lower)) { start = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.lower)); } if (goog.isDefAndNotNull(opt_key_range.upper)) { end = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.upper)); } lowerOpen = opt_key_range.lowerOpen; upperOpen = opt_key_range.upperOpen; } /** * * @param {goog.structs.AvlTree.Node} node * @return {boolean|undefined} */ var tr_fn = function(node) { if (!goog.isDefAndNotNull(node)) { return; } var x = /** @type {ydn.db.con.simple.Node} */ (node.value); if (lowerOpen && goog.isDefAndNotNull(start) && ydn.db.con.simple.Node.cmp(x, start) == 0) { return; } if (goog.isDefAndNotNull(end)) { var cmp = ydn.db.con.simple.Node.cmp(x, end); if (cmp === 1) { return true; } if (cmp === 0 && upperOpen) { return true; } } var key = me.makeKey(x.getKey()); var obj = me.storage.getItem(key); if (!goog.isNull(obj)) { me.storage.removeItem(key); count++; if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log(count + '. remove ' + x.getKey() + ' ' + key); } if (removed_ids.length < 10) { removed_ids.push(x.getKey()); removed_objs.push(ydn.json.parse(obj)); } } }; cache.traverse(tr_fn, start); // update tree if (removed_ids.length < 10) { for (var i = 0; i < removed_ids.length; i++) { this.removeIndex(removed_ids[i], removed_objs[i]); } } else { // to many node removed, just clear the tree. this.clearIndexCache(); } return count; }; /** * * @param {ydn.db.base.QueryMethod} mth * @param {string?=} opt_index_name * @param {IDBKeyRange=} opt_key_range * @param {boolean=} opt_reverse * @param {number=} opt_limit * @param {number=} opt_offset * @param {boolean=} opt_unique * @param {Array.<IDBKey|undefined>=} opt_position last cursor position. * @return {!Array} results. */ ydn.db.con.simple.Store.prototype.getItems = function(mth, opt_index_name, opt_key_range, opt_reverse, opt_limit, opt_offset, opt_unique, opt_position) { var results = []; var prev_key; var resume = !!opt_position && goog.isDefAndNotNull(opt_position[0]); opt_index_name = opt_index_name || this.primary_index; var is_index = opt_index_name != this.primary_index; var cache = this.getIndexCache(opt_index_name); /** * @type {ydn.db.con.simple.Node} */ var start = null; /** * @type {ydn.db.con.simple.Node} */ var end = null; if (!goog.isDef(opt_offset)) { opt_offset = 0; } var offsetted = -1; var lowerOpen = false; var upperOpen = false; if (goog.isDefAndNotNull(opt_key_range)) { if (goog.isDefAndNotNull(opt_key_range.lower)) { if (is_index && opt_reverse) { start = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.lower), '\uffff'); } else { start = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.lower)); } } if (goog.isDefAndNotNull(opt_key_range.upper)) { if (is_index && !opt_reverse) { end = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.upper), '\uffff'); } else { end = new ydn.db.con.simple.Node( /** @type {!IDBKey} */ (opt_key_range.upper)); } } lowerOpen = !!opt_key_range.lowerOpen; upperOpen = !!opt_key_range.upperOpen; } if (resume) { if (opt_reverse) { upperOpen = true; } else { lowerOpen = true; } var e_key = /** @type {IDBKey} */ (opt_position[0]); var p_key = goog.isDef(opt_position[1]) ? opt_position[1] : '\uffff'; if (opt_reverse) { if (is_index) { end = new ydn.db.con.simple.Node(e_key, p_key); } else { end = new ydn.db.con.simple.Node(e_key); } } else { if (is_index) { start = new ydn.db.con.simple.Node(e_key, p_key); } else { start = new ydn.db.con.simple.Node(e_key); } } } if (ydn.db.con.simple.Store.DEBUG) { goog.global.console.log(this + ' ' + (opt_reverse ? ' rev' : '') + ' from ' + start + ' to ' + end); } // console.log(opt_reverse, start, end) var me = this; /** * @param {goog.structs.AvlTree.Node} node * @return {boolean|undefined} */ var tr_fn = function(node) { if (!node) { return; } offsetted++; if (offsetted < opt_offset) { return; } var x = /** @type {ydn.db.con.simple.Node} */ (node.value); // console.log(x + ' ' + start + ' ' + end) if (opt_reverse) { if (upperOpen && goog.isDefAndNotNull(end)) { var cmp = resume ? ydn.db.con.simple.Node.cmp(x, end) : ydn.db.cmp(x.getKey(), end.getKey()); if (cmp == 0) { return; } } if (goog.isDefAndNotNull(start)) { var cmp = resume ? ydn.db.con.simple.Node.cmp(x, start) : ydn.db.cmp(x.getKey(), start.getKey()); if (cmp == -1 || (cmp == 0 && lowerOpen)) { if (opt_position) { opt_position[0] = undefined; opt_position[1] = undefined; } return true; } } } else { if (lowerOpen && goog.isDefAndNotNull(start)) { var cmp = resume ? ydn.db.con.simple.Node.cmp(x, start) : ydn.db.cmp(x.getKey(), start.getKey()); if (cmp == 0) { return; } } if (goog.isDefAndNotNull(end)) { var cmp = resume ? ydn.db.con.simple.Node.cmp(x, end) : ydn.db.cmp(x.getKey(), end.getKey()); if (cmp == 1 || (cmp == 0 && upperOpen)) { if (opt_position) { opt_position[0] = undefined; opt_position[1] = undefined; } return true; } } } var key = x.getKey(); // console.log(prev_key, key) if (!opt_unique || !is_index || !goog.isDefAndNotNull(prev_key) || ydn.db.cmp(prev_key, key) != 0) { // console.log('take') var primary_key = /** @type {!IDBKey} */ (is_index ? x.getPrimaryKey() : key); if (mth == ydn.db.base.QueryMethod.LIST_PRIMARY_KEY) { results.push(primary_key); } else if (mth == ydn.db.base.QueryMethod.LIST_KEY) { results.push(key); } else if (mth == ydn.db.base.QueryMethod.LIST_KEYS) { results.push([key, primary_key]); } else if (mth == ydn.db.base.QueryMethod.LIST_VALUE) { var v = me.getRecord(null, primary_key); results.push(v); } else { results.push([key, primary_key, me.getRecord(null, primary_key)]); } if (opt_position) { opt_position[0] = key; opt_position[1] = primary_key; } } prev_key = key; if (goog.isDef(opt_limit) && results.length >= opt_limit) { return true; } }; if (opt_reverse) { cache.reverseTraverse(tr_fn, end); } else { cache.traverse(tr_fn, start); } return results; }; /** * * @param {string?=} opt_index_name * @param {IDBKeyRange=} opt_key_range * @param {boolean=} opt_reverse * @param {number=} opt_limit * @param {number=} opt_offset * @return {!Array} results. */ ydn.db.con.simple.Store.prototype.getRecords = function(opt_index_name, opt_key_range, opt_reverse, opt_limit, opt_offset) { return this.getItems(ydn.db.base.QueryMethod.LIST_VALUE, opt_index_name, opt_key_range, opt_reverse, opt_limit, opt_offset); }; /** * * @param {string?=} opt_index_name * @param {IDBKeyRange=} opt_key_range * @param {boolean=} opt_reverse * @param {number=} opt_limit * @param {number=} opt_offset * @return {!Array} results. */ ydn.db.con.simple.Store.prototype.getKeys = function(opt_index_name, opt_key_range, opt_reverse, opt_limit, opt_offset) { return this.getItems(ydn.db.base.QueryMethod.LIST_PRIMARY_KEY, opt_index_name, opt_key_range, opt_reverse, opt_limit, opt_offset); }; if (goog.DEBUG) { /** * @inheritDoc */ ydn.db.con.simple.Store.prototype.toString = function() { return 'ydn.db.con.simple.Store:' + this.db_name + ':' + this.schema.getName(); }; }