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
JavaScript
// 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();
};
}