ydn.db
Version:
Javascript database library for IndexedDB, WebDatabase (WebSQL) and WebStorage (localStorage) storage mechanisms supporting version migration, advanced query and transaction workflow.
804 lines (714 loc) • 24.6 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 Database operator providing index and table scan query.
*
* @author kyawtun@yathit.com (Kyaw Tun)
*/
goog.provide('ydn.db.core.DbOperator');
goog.require('ydn.db.Iterator');
goog.require('ydn.db.algo.AbstractSolver');
goog.require('ydn.db.core.IOperator');
goog.require('ydn.db.core.req.IRequestExecutor');
goog.require('ydn.db.crud.DbOperator');
goog.require('ydn.debug.error.ArgumentException');
/**
* Construct storage to execute CRUD database operations.
*
* Execution database operation is atomic, if a new transaction require,
* otherwise existing transaction is used and the operation become part of
* the existing transaction. A new transaction is required if the transaction
* is not active or locked. Active transaction can be locked by using
* mutex.
*
* @param {!ydn.db.crud.Storage} storage base storage object.
* @param {!ydn.db.schema.Database} schema schema.
* @param {ydn.db.tr.Thread} thread execution thread.
* @param {ydn.db.tr.Thread} sync_thread synchronization thread.
* @implements {ydn.db.core.IOperator}
* @constructor
* @extends {ydn.db.crud.DbOperator}
* @struct
*/
ydn.db.core.DbOperator = function(storage, schema, thread,
sync_thread) {
goog.base(this, storage, schema, thread, sync_thread);
};
goog.inherits(ydn.db.core.DbOperator, ydn.db.crud.DbOperator);
/**
* @protected
* @type {goog.debug.Logger} logger.
*/
ydn.db.core.DbOperator.prototype.logger =
goog.log.getLogger('ydn.db.core.DbOperator');
/**
* @define {boolean} debug flag.
*/
ydn.db.core.DbOperator.DEBUG = false;
/**
* @inheritDoc
*/
ydn.db.core.DbOperator.prototype.get = function(arg1, arg2) {
var me = this;
if (arg1 instanceof ydn.db.Iterator) {
/**
* @type {!ydn.db.Iterator}
*/
var q = arg1;
var q_store_name = q.getStoreName();
var store = this.schema.getStore(q_store_name);
if (!store) {
throw new ydn.debug.error.ArgumentException('store "' +
q_store_name + '" not found.');
}
var index_name = q.getIndexName();
if (goog.isDef(index_name) && !store.hasIndex(index_name)) {
throw new ydn.debug.error.ArgumentException('index "' +
index_name + '" not found in store "' + q_store_name + '".');
}
goog.log.finer(this.logger, 'getByIterator:' + q);
var df = this.tx_thread.request(ydn.db.Request.Method.GET_ITER,
[q_store_name]);
df.addTxback(function() {
this.iterate(ydn.db.base.QueryMethod.GET, df, q, 1);
}, this);
return df;
} else {
return goog.base(this, 'get', arg1, arg2);
}
};
/**
* @param {ydn.db.Iterator} q the iterator.
* @param {number=} arg2 limit.
*/
ydn.db.core.DbOperator.prototype.keysOf = function (q, arg2) {
/**
* @type {number}
*/
var limit = ydn.db.base.DEFAULT_RESULT_LIMIT;
if (goog.isNumber(arg2)) {
limit = /** @type {number} */ (arg2);
if (limit < 1) {
throw new ydn.debug.error.ArgumentException('limit must be ' +
'a positive value, but ' + arg2);
}
} else if (goog.isDef(arg2)) {
throw new ydn.debug.error.ArgumentException('limit must be a number, ' +
' but ' + arg2);
}
goog.log.finer(this.logger, 'keysOf:' + q);
var df = this.tx_thread.request(ydn.db.Request.Method.KEYS_ITER,
[q.getStoreName()]);
df.addTxback(function () {
if (q.isIndexIterator()) {
this.iterate(ydn.db.base.QueryMethod.LIST_KEY, df, q, limit);
} else {
this.iterate(ydn.db.base.QueryMethod.LIST_PRIMARY_KEY, df, q, limit);
}
}, this);
return df;
};
/**
* @inheritDoc
*/
ydn.db.core.DbOperator.prototype.keys = function(arg1, arg2, arg3, arg4, arg5,
arg6, arg7) {
if (arg1 instanceof ydn.db.Iterator) {
return this.keysOf(arg1, /** @type {number} */(arg2));
} else {
return goog.base(this, 'keys', arg1, arg2, arg3, arg4, arg5, arg6, arg7);
}
};
/**
* @inheritDoc
*/
ydn.db.core.DbOperator.prototype.countOf = function(arg1) {
/**
*
* @type {!ydn.db.Iterator}
*/
var q = arg1;
goog.log.finer(this.logger, 'countIterator:' + q);
var df = this.tx_thread.request(ydn.db.Request.Method.COUNT,
[q.getStoreName()]);
df.addTxback(function () {
this.iterate(ydn.db.base.QueryMethod.COUNT, df, q);
}, this);
return df;
};
/**
* @inheritDoc
*/
ydn.db.core.DbOperator.prototype.count = function(arg1, arg2, arg3, arg4) {
if (arg1 instanceof ydn.db.Iterator) {
if (goog.isDef(arg2) || goog.isDef(arg3)) {
throw new ydn.debug.error.ArgumentException('too many arguments.');
}
return this.countOf(arg1);
} else {
return goog.base(this, 'count', arg1, arg2, arg3, arg4);
}
};
/**
* @param {!ydn.db.Iterator} q the iterator.
* @param {number=} arg2 limit.
* @return {!ydn.db.Request}
*/
ydn.db.core.DbOperator.prototype.valuesOf = function (q, arg2) {
var me = this;
/**
* @type {number}
*/
var limit;
if (goog.isNumber(arg2)) {
limit = /** @type {number} */ (arg2);
if (limit < 1) {
throw new ydn.debug.error.ArgumentException('limit must be ' +
'a positive value, but ' + limit);
}
} else if (goog.isDef(arg2)) {
throw new ydn.debug.error.ArgumentException('limit must be a number, ' +
'but ' + arg2);
}
goog.log.finer(this.logger, 'listByIterator:' + q);
var df = this.tx_thread.request(ydn.db.Request.Method.VALUES_ITER,
[q.getStoreName()]);
df.addTxback(function () {
if (q.isKeyIterator()) {
this.iterate(ydn.db.base.QueryMethod.LIST_PRIMARY_KEY, df, q, limit);
} else {
this.iterate(ydn.db.base.QueryMethod.LIST_VALUE, df, q, limit);
}
}, this);
return df;
};
/**
* @inheritDoc
*/
ydn.db.core.DbOperator.prototype.values = function(arg1, arg2, arg3, arg4,
arg5, arg6) {
var me = this;
if (arg1 instanceof ydn.db.Iterator) {
return this.valuesOf(arg1, /** @type {number} */(arg2));
} else {
return goog.base(this, 'values', arg1, arg2, arg3, arg4, arg5, arg6);
}
};
/**
* Cursor scan iteration.
* @param {!ydn.db.algo.AbstractSolver|function(!Array, !Array): (Array|undefined)} solver
* solver.
* @param {!Array.<!ydn.db.Iterator>} iterators the cursor.
* @param {ydn.db.base.TransactionMode=} opt_mode mode. Expose for friendly
* use. Scanning is always READ_ONLY mode, but query class may need to open
* transaction in READ_WRITE mode.
* @return {!ydn.db.Request} promise on completed.
*/
ydn.db.core.DbOperator.prototype.scan = function(solver, iterators,
opt_mode) {
if (goog.DEBUG) {
if (!goog.isArray(iterators)) {
throw new ydn.debug.error.ArgumentException('iterators argument must' +
' be an array, but ' + iterators + ' of type ' + typeof iterators +
' found');
}
for (var i = 0; i < iterators.length; i++) {
var is_iter = iterators[i] instanceof ydn.db.Iterator;
if (!is_iter) {
throw new ydn.debug.error.ArgumentException('Iterator at ' + i +
' must be cursor range iterator.');
}
}
}
var tr_mode = opt_mode || ydn.db.base.TransactionMode.READ_ONLY;
var scopes = [];
for (var i = 0; i < iterators.length; i++) {
var stores = iterators[i].stores();
for (var j = 0; j < stores.length; j++) {
if (!goog.array.contains(scopes, stores[j])) {
scopes.push(stores[j]);
}
}
}
goog.log.finer(this.logger, this + ': scan for ' + iterators.length +
' iterators on ' + scopes);
var me = this;
var df = this.tx_thread.request(ydn.db.Request.Method.SCAN, scopes);
this.tx_thread.exec(df, function(tx, tx_no, cb) {
var lbl = tx_no + ' ' + me + ' scanning';
goog.log.finest(me.logger, lbl);
var done = false;
var total;
var idx2iterator = []; // convert main index to iterator index
var keys = [];
var values = [];
/**
*
* @type {Array.<!ydn.db.core.req.ICursor>}
*/
var cursors = [];
var do_exit = function() {
for (var k = 0; k < cursors.length; k++) {
cursors[k].exit();
}
done = true;
goog.array.clear(cursors);
// console.log('existing');
goog.log.finer(me.logger, 'success ' + lbl);
cb(undefined);
};
var result_count = 0;
var streamer_result_count = 0;
var has_key_count = 0;
/**
* All results collected. Now invoke solver and do advancement.
*/
var on_result_ready = function() {
// all cursor has results, than sent to join algorithm callback.
var out;
if (solver instanceof ydn.db.algo.AbstractSolver) {
out = solver.solver(keys, values);
} else {
out = solver(keys, values);
}
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log(me + ' received result from solver ' +
out + ' for keys ' + ydn.json.stringify(keys));
}
var next_primary_keys = [];
var next_effective_keys = [];
var advance = [];
var restart = [];
if (goog.isArray(out)) {
// adv vector is given
for (var i = 0; i < out.length; i++) {
if (out[i] === true) {
advance[i] = 1;
} else if (out[i] === false) {
restart[i] = true;
} else {
next_effective_keys[i] = out[i];
}
}
} else if (goog.isNull(out)) {
// all stop
next_primary_keys = [];
} else if (!goog.isDef(out)) {
// all continue;
next_primary_keys = [];
for (var i = 0; i < iterators.length; i++) {
if (goog.isDef(idx2iterator[i])) {
advance[i] = 1;
}
}
} else if (goog.isObject(out)) {
if (goog.DEBUG) {
var valid_att = ['advance', 'continue', 'continuePrimary', 'restart'];
for (var key in out) {
if (!goog.array.contains(valid_att, key)) {
throw new ydn.debug.error.InvalidOperationException(
'Unknown attribute "' + key +
'" in cursor advancement object');
}
}
}
next_primary_keys = out['continuePrimary'] || [];
next_effective_keys = out['continue'] || [];
advance = out['advance'] || [];
restart = out['restart'] || [];
} else {
throw new ydn.debug.error.InvalidOperationException(
'scan callback output');
}
var move_count = 0;
result_count = 0;
for (var i = 0; i < iterators.length; i++) {
if (goog.isDefAndNotNull(next_primary_keys[i]) ||
goog.isDef(next_effective_keys[i]) ||
goog.isDefAndNotNull(restart[i]) ||
goog.isDefAndNotNull(advance[i])) {
// by marking non moving iterator first, both async and sync callback
// work.
} else {
// take non advancing iterator as already moved.
result_count++;
}
}
for (var i = 0; i < iterators.length; i++) {
if (goog.isDefAndNotNull(next_primary_keys[i]) ||
goog.isDef(next_effective_keys[i]) ||
goog.isDefAndNotNull(restart[i]) ||
goog.isDefAndNotNull(advance[i])) {
var idx = idx2iterator[i];
if (!goog.isDef(idx)) {
throw new ydn.error.InvalidOperationException(i +
' is not an iterator.');
}
var iterator = iterators[idx];
var cursor = cursors[i];
if (goog.DEBUG && !goog.isDefAndNotNull(keys[i])) {
var at = i + '/' + iterators.length;
if (goog.isDefAndNotNull(advance[i])) {
throw new ydn.error.InvalidOperationError(cursor + ' ' + at +
' must not advance ' + advance[i] + ' steps');
} else if (goog.isDef(next_effective_keys[i])) {
throw new ydn.error.InvalidOperationError(cursor + ' ' + at +
' must not continue to key ' + next_effective_keys[i]);
} else if (goog.isDefAndNotNull(next_primary_keys[i])) {
throw new ydn.error.InvalidOperationError(cursor + ' ' + at +
' must not continue to primary key ' + next_primary_keys[i]);
}
}
keys[i] = undefined;
values[i] = undefined;
if (goog.isDefAndNotNull(restart[i])) {
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log('cursor ' + cursor + ' of iterator ' +
iterator + ': restarting.');
}
goog.asserts.assert(restart[i] === true, i +
' restart must be true');
cursor.restart();
} else if (goog.isDef(next_effective_keys[i])) {
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log(iterator + ': continuing to ' +
next_effective_keys[i]);
}
cursor.continueEffectiveKey(next_effective_keys[i]);
} else if (goog.isDefAndNotNull(next_primary_keys[i])) {
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log(cursor + ': continuing to primary key ' +
next_primary_keys[i]);
}
cursor.continuePrimaryKey(next_primary_keys[i]);
} else if (goog.isDefAndNotNull(advance[i])) {
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log(iterator + ': advancing ' + advance[i] +
' steps.');
}
goog.asserts.assert(advance[i] === 1, i +
' advance value must be 1');
cursor.advance(1);
} else {
throw new ydn.error.InternalError(iterator + ': has no action');
}
move_count++;
}
}
// console.log(['on_result_ready', move_count, keys, adv]);
if (move_count == 0) {
do_exit();
}
};
/**
* Received iterator result. When all iterators result are collected,
* begin to send request to collect streamers results.
* @param {number} i index.
* @param {IDBKey=} opt_key effective key.
*/
var on_iterator_next = function(i, opt_key) {
if (done) {
if (ydn.db.core.DbOperator.DEBUG) {
goog.global.console.log('iterator ' + i + ' done');
}
// calling next to a terminated iterator
throw new ydn.error.InternalError();
}
result_count++;
var is_result_ready = result_count === total;
var idx = idx2iterator[i];
/**
* @type {!ydn.db.Iterator}
*/
var iterator = iterators[idx];
/**
* @type {!ydn.db.core.req.ICursor}
*/
var cursor = cursors[idx];
var primary_key = cursor.getPrimaryKey();
var value = cursor.getValue();
if (ydn.db.core.DbOperator.DEBUG) {
var key_str = opt_key +
(goog.isDefAndNotNull(primary_key) ? ', ' + primary_key : '');
var ready_str = is_result_ready ? ' (all result done)' : '';
goog.global.console.log(cursor + ' new position ' + key_str + ready_str);
}
keys[i] = opt_key;
if (iterator.isIndexIterator()) {
if (iterator.isKeyIterator()) {
values[i] = primary_key;
} else {
values[i] = value;
}
} else {
if (iterator.isKeyIterator()) {
values[i] = opt_key;
} else {
values[i] = value;
}
}
if (is_result_ready) { // receive all results
on_result_ready();
}
};
var on_error = function(e) {
for (var k = 0; k < cursors.length; k++) {
cursors[k].exit();
}
goog.array.clear(cursors);
goog.log.finer(me.logger, lbl + ' error');
cb(e, true);
};
var open_iterators = function() {
var idx = 0;
for (var i = 0; i < iterators.length; i++) {
/**
* @type {!ydn.db.Iterator}
*/
var iterator = iterators[i];
var crs = [me.getIndexExecutor().getCursor(tx, tx_no,
iterator.getStoreName())];
var cursor = iterator.load(crs);
cursor.onFail = on_error;
cursor.onNext = goog.partial(on_iterator_next, idx);
cursors[i] = cursor;
idx2iterator[idx] = i;
idx++;
}
total = iterators.length;
};
if (solver instanceof ydn.db.algo.AbstractSolver) {
var wait = solver.begin(tx, iterators, function() {
open_iterators();
});
if (!wait) {
open_iterators();
}
} else {
open_iterators();
}
}, scopes, tr_mode);
return df;
};
/**
* @return {ydn.db.core.req.IRequestExecutor} executor.
*/
ydn.db.core.DbOperator.prototype.getIndexExecutor = function() {
return /** @type {ydn.db.core.req.IRequestExecutor} */ (this.getExecutor());
};
/**
*
* @param {function(this: T, !ydn.db.core.req.ICursor)} callback icursor
* handler.
* @param {!ydn.db.core.AbstractIterator} iter the cursor.
* @param {ydn.db.base.TransactionMode=} opt_mode mode.
* @param {T=} opt_scope optional callback scope.
* @return {!ydn.db.Request} promise on completed.
* @template T
*/
ydn.db.core.DbOperator.prototype.open = function(callback, iter, opt_mode,
opt_scope) {
if (goog.DEBUG) {
if (!(iter instanceof ydn.db.Iterator)) {
throw new ydn.debug.error.ArgumentException(
'Second argument must be cursor range iterator.');
}
var store_names = iter.stores();
for (var i = 0; i < store_names.length; i++) {
var store = this.schema.getStore(store_names[i]);
if (!store) {
throw new ydn.debug.error.ArgumentException('Store "' +
store_names[i] + '" not found.');
}
}
}
var tr_mode = opt_mode || ydn.db.base.TransactionMode.READ_ONLY;
var me = this;
var df = this.tx_thread.request(ydn.db.Request.Method.OPEN, iter.stores(),
tr_mode);
goog.log.finer(this.logger, 'open:' + tr_mode + ' ' + iter);
df.addTxback(function(tx) {
var tx_no = df.getLabel();
var lbl = tx_no + ' iterating ' + iter;
goog.log.finer(me.logger, lbl);
var names = iter.stores();
var crs = [];
for (var ni = 0; ni < names.length; ni++) {
crs[ni] = me.getIndexExecutor().getCursor(tx, tx_no, names[ni]);
}
var cursor = iter.load(crs);
cursor.onFail = function(e) {
df.setDbValue(e, true);
};
/**
* callback.
* @param {IDBKey=} opt_key effective key.
*/
cursor.onNext = function(opt_key) {
if (goog.isDefAndNotNull(opt_key)) {
var adv = callback.call(opt_scope, cursor);
if (adv === true) {
cursor.restart();
} else if (goog.isObject(adv)) {
if (adv['restart'] === true) {
cursor.restart(adv['continue'], adv['continuePrimary']);
} else if (goog.isDefAndNotNull(adv['continue'])) {
cursor.continueEffectiveKey(adv['continue']);
} else if (goog.isDefAndNotNull(adv['continuePrimary'])) {
cursor.continuePrimaryKey(adv['continuePrimary']);
} else {
cursor.exit();
df.setDbValue(undefined); // break the loop
}
} else if (goog.isNull(adv)) {
cursor.exit();
df.setDbValue(undefined);
} else if (goog.isDefAndNotNull(adv)) {
cursor.continueEffectiveKey(adv);
} else {
cursor.advance(1);
}
} else {
cursor.exit();
df.setDbValue(undefined);
}
};
}, this);
return df;
};
/**
* List record in a store.
* @param {ydn.db.base.QueryMethod} mth keys method.
* @param {!ydn.db.Iterator} iter iterator.
* @param {number=} opt_limit limit.
* @param {number=} opt_offset limit.
* @return {!ydn.db.Request} request.
*/
ydn.db.core.DbOperator.prototype.listIter = function(mth, iter,
opt_limit, opt_offset) {
var offset = opt_offset || 0;
var store_name = iter.getStoreName();
var index_name = iter.getIndexName() || null;
var limit = opt_limit || ydn.db.base.DEFAULT_RESULT_LIMIT;
goog.log.finer(this.logger, 'listIter:' + mth + ' ' + iter +
(opt_limit ? ' limit=' + limit : '') +
(opt_offset ? ' offset=' + offset : ''));
var method = ydn.db.Request.Method.VALUES_INDEX;
var req = this.tx_thread.request(method, [store_name]);
// store.hook(req, arguments);
var cursor_position =
(iter.getState() == ydn.db.Iterator.State.COMPLETED ||
iter.getState() == ydn.db.Iterator.State.INITIAL) ?
[] : [iter.getKey(), iter.getPrimaryKey()];
req.addTxback(function() {
var e_key = iter.getKey();
this.getCrudExecutor().list(req, mth, store_name,
index_name, iter.getKeyRange(), limit,
offset, iter.isReversed(), iter.isUnique(), cursor_position);
}, this);
req.addCallback(function() {
if (goog.isDefAndNotNull(cursor_position[0])) {
iter.reset(ydn.db.Iterator.State.RESTING,
cursor_position[0], cursor_position[1]);
} else {
iter.reset();
}
});
return req;
};
/**
* List record in a store.
* @param {ydn.db.base.QueryMethod} mth keys method.
* @param {ydn.db.Request} rq request.
* @param {!ydn.db.core.AbstractIterator} iter iterator.
* @param {number=} opt_limit limit.
* @param {number=} opt_offset limit.
* @protected
*/
ydn.db.core.DbOperator.prototype.iterate = function(mth, rq, iter,
opt_limit, opt_offset) {
var arr = [];
var tx = rq.getTx();
var tx_no = rq.getLabel();
var msg = tx_no + ' ' + mth + 'ByIterator ' + iter;
if (opt_limit > 0) {
msg += ' limit ' + opt_limit;
}
var me = this;
goog.log.finer(this.logger, msg);
var executor = this.getIndexExecutor();
var cursors = [];
var store_names = iter.stores();
for (var i = 0; i < store_names.length; i++) {
cursors[i] = executor.getCursor(tx, tx_no, store_names[i]);
}
var cursor = iter.load(cursors);
cursor.onFail = function(e) {
cursor.exit();
rq.setDbValue(e, true);
};
var count = 0;
var cued = false;
var displayed = false;
/**
* @param {IDBKey=} opt_key
*/
cursor.onNext = function(opt_key) {
if (!displayed) {
goog.log.finest(me.logger, msg + ' starting');
displayed = true;
}
if (goog.isDefAndNotNull(opt_key)) {
var primary_key = cursor.getPrimaryKey();
if (!cued && opt_offset > 0) {
cursor.advance(opt_offset);
cued = true;
return;
}
count++;
if (mth == ydn.db.base.QueryMethod.LIST_KEY) {
arr.push(opt_key);
} else if (mth == ydn.db.base.QueryMethod.LIST_PRIMARY_KEY) {
arr.push(cursor.getPrimaryKey());
} else if (mth == ydn.db.base.QueryMethod.LIST_KEYS) {
arr.push([opt_key, cursor.getPrimaryKey()]);
} else if (mth == ydn.db.base.QueryMethod.COUNT) {
// no result needed.
} else {
// LIST_VALUE
arr.push(cursor.getValue());
}
// console.log(count, cursor);
if (mth == ydn.db.base.QueryMethod.GET) {
cursor.exit();
rq.setDbValue(arr[0]);
} else if (mth == ydn.db.base.QueryMethod.COUNT ||
!goog.isDef(opt_limit) || count < opt_limit) {
cursor.continueEffectiveKey();
} else {
goog.log.finer(me.logger, 'success:' + msg + ' yields ' + arr.length +
' records');
cursor.exit();
rq.setDbValue(arr);
}
} else {
goog.log.finer(me.logger, 'success:' + msg + ' yields ' + arr.length + ' records');
cursor.exit();
var result =
mth == ydn.db.base.QueryMethod.GET ? arr[0] :
mth == ydn.db.base.QueryMethod.COUNT ? count : arr;
rq.setDbValue(result);
}
};
};