UNPKG

foam-framework

Version:
1,350 lines (1,112 loc) 36.5 kB
/** * @license * Copyright 2012 Google Inc. 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. */ /* * Index Interface: * put(state, value) -> new state * remove(state, value) -> new state * removeAll(state) -> new state // TODO * plan(state, sink, options) -> {cost: int, toString: fn, execute: fn} * size(state) -> int * Add: * get(key) -> obj * update(oldValue, newValue) * * TODO: * reuse plans * add ability for indices to pre-populate data */ /** Plan indicating that there are no matching records. **/ var NOT_FOUND = { cost: 0, execute: function(_, sink, __) { return anop; }, toString: function() { return "no-match(cost=0)"; } }; /** Plan indicating that an index has no plan for executing a query. **/ var NO_PLAN = { cost: Number.MAX_VALUE, execute: function() { return anop; }, toString: function() { return "no-plan"; } }; function dump(o) { if ( Array.isArray(o) ) return '[' + o.map(dump).join(',') + ']'; return o ? o.toString() : '<undefined>'; } /** An Index which holds only a single value. **/ var ValueIndex = { put: function(s, newValue) { return newValue; }, remove: function() { return undefined; }, plan: (function() { var plan = { cost: 1, execute: function(s, sink) { sink.put(s); return anop; }, toString: function() { return 'unique'; } }; return function() { return plan; }; })(), get: function(value, key) { return value; }, select: function(value, sink, options) { if ( options ) { if ( options.query && ! options.query.f(value) ) return; if ( 'skip' in options && options.skip-- > 0 ) return; if ( 'limit' in options && options.limit-- < 1 ) return; } sink.put(value); }, selectReverse: function(value, sink, options) { this.select(value, sink, options); }, size: function(obj) { return 1; }, toString: function() { return 'value'; } }; var KEY = 0; var VALUE = 1; var SIZE = 2; var LEVEL = 3; var LEFT = 4; var RIGHT = 5; // TODO: investigate how well V8 optimizes static classes // [0 key, 1 value, 2 size, 3 level, 4 left, 5 right] /** An AATree (balanced binary search tree) Index. **/ var TreeIndex = { create: function(prop, tail) { tail = tail || ValueIndex; return { __proto__: this, prop: prop, tail: tail, selectCount: 0 }; }, /** * Bulk load an unsorted array of objects. * Faster than loading individually, and produces a balanced tree. **/ bulkLoad: function(a) { // Only safe if children aren't themselves trees if ( this.tail === ValueIndex ) { a.sort(toCompare(this.prop)); return this.bulkLoad_(a, 0, a.length-1); } var s = undefined; for ( var i = 0 ; i < a.length ; i++ ) { s = this.put(s, a[i]); } return s; }, bulkLoad_: function(a, start, end) { if ( end < start ) return undefined; var m = start + Math.floor((end-start+1) / 2); var tree = this.put(undefined, a[m]); tree[LEFT] = this.bulkLoad_(a, start, m-1); tree[RIGHT] = this.bulkLoad_(a, m+1, end); tree[SIZE] += this.size(tree[LEFT]) + this.size(tree[RIGHT]); return tree; }, // Set the value's property to be the same as the key in the index. // This saves memory by sharing objects. dedup: function(obj, value) { obj[this.prop.name] = value; }, maybeClone: function(s) { if ( s && this.selectCount > 0 ) return s.clone(); return s; }, put: function(s, newValue) { return this.putKeyValue(s, this.prop.f(newValue), newValue); }, putKeyValue: function(s, key, value) { if ( ! s ) { return [key, this.tail.put(null, value), 1, 1]; } s = this.maybeClone(s); var r = this.compare(s[KEY], key); if ( r === 0 ) { this.dedup(value, s[KEY]); s[SIZE] -= this.tail.size(s[VALUE]); s[VALUE] = this.tail.put(s[VALUE], value); s[SIZE] += this.tail.size(s[VALUE]); } else { var side = r > 0 ? LEFT : RIGHT; if ( s[side] ) s[SIZE] -= s[side][SIZE]; s[side] = this.putKeyValue(s[side], key, value); s[SIZE] += s[side][SIZE]; } return this.split(this.skew(s)); }, // input: T, a node representing an AA tree that needs to be rebalanced. // output: Another node representing the rebalanced AA tree. skew: function(s) { if ( s && s[LEFT] && s[LEFT][LEVEL] === s[LEVEL] ) { // Swap the pointers of horizontal left links. var l = this.maybeClone(s[LEFT]); s[LEFT] = l[RIGHT]; l[RIGHT] = s; this.updateSize(s); this.updateSize(l); return l; } return s; }, updateSize: function(s) { s[SIZE] = this.size(s[LEFT]) + this.size(s[RIGHT]) + this.tail.size(s[VALUE]); }, // input: T, a node representing an AA tree that needs to be rebalanced. // output: Another node representing the rebalanced AA tree. split: function(s) { if ( s && s[RIGHT] && s[RIGHT][RIGHT] && s[LEVEL] === s[RIGHT][RIGHT][LEVEL] ) { // We have two horizontal right links. Take the middle node, elevate it, and return it. var r = this.maybeClone(s[RIGHT]); s[RIGHT] = r[LEFT]; r[LEFT] = s; r[LEVEL]++; this.updateSize(s); this.updateSize(r); return r; } return s; }, remove: function(s, value) { return this.removeKeyValue(s, this.prop.f(value), value); }, removeKeyValue: function(s, key, value) { if ( ! s ) return s; s = this.maybeClone(s); var r = this.compare(s[KEY], key); if ( r === 0 ) { s[SIZE] -= this.tail.size(s[VALUE]); s[VALUE] = this.tail.remove(s[VALUE], value); // If the sub-Index still has values, then don't // delete this node. if ( s[VALUE] ) { s[SIZE] += this.tail.size(s[VALUE]); return s; } // If we're a leaf, easy, otherwise reduce to leaf case. if ( ! s[LEFT] && ! s[RIGHT] ) return undefined; var side = s[LEFT] ? LEFT : RIGHT; // TODO: it would be faster if successor and predecessor also deleted // the entry at the same time in order to prevent two traversals. // But, this would also duplicate the delete logic. var l = side === LEFT ? this.predecessor(s) : this.successor(s) ; s[KEY] = l[KEY]; s[VALUE] = l[VALUE]; s[side] = this.removeNode(s[side], l[KEY]); } else { var side = r > 0 ? LEFT : RIGHT; s[SIZE] -= this.size(s[side]); s[side] = this.removeKeyValue(s[side], key, value); s[SIZE] += this.size(s[side]); } // Rebalance the tree. Decrease the level of all nodes in this level if // necessary, and then skew and split all nodes in the new level. s = this.skew(this.decreaseLevel(s)); if ( s[RIGHT] ) { s[RIGHT] = this.skew(this.maybeClone(s[RIGHT])); if ( s[RIGHT][RIGHT] ) s[RIGHT][RIGHT] = this.skew(this.maybeClone(s[RIGHT][RIGHT])); } s = this.split(s); s[RIGHT] = this.split(this.maybeClone(s[RIGHT])); return s; }, removeNode: function(s, key) { if ( ! s ) return s; s = this.maybeClone(s); var r = this.compare(s[KEY], key); if ( r === 0 ) return s[LEFT] ? s[LEFT] : s[RIGHT]; var side = r > 0 ? LEFT : RIGHT; s[SIZE] -= this.size(s[side]); s[side] = this.removeNode(s[side], key); s[SIZE] += this.size(s[side]); return s; }, predecessor: function(s) { if ( ! s[LEFT] ) return s; for ( s = s[LEFT] ; s[RIGHT] ; s = s[RIGHT] ); return s; }, successor: function(s) { if ( ! s[RIGHT] ) return s; for ( s = s[RIGHT] ; s[LEFT] ; s = s[LEFT] ); return s; }, // input: T, a tree for which we want to remove links that skip levels. // output: T with its level decreased. decreaseLevel: function(s) { var expectedLevel = Math.min(s[LEFT] ? s[LEFT][LEVEL] : 0, s[RIGHT] ? s[RIGHT][LEVEL] : 0) + 1; if ( expectedLevel < s[LEVEL] ) { s[LEVEL] = expectedLevel; if ( s[RIGHT] && expectedLevel < s[RIGHT][LEVEL] ) { s[RIGHT] = this.maybeClone(s[RIGHT]); s[RIGHT][LEVEL] = expectedLevel; } } return s; }, get: function(s, key) { if ( ! s ) return undefined; var r = this.compare(s[KEY], key); if ( r === 0 ) return s[VALUE]; return this.get(r > 0 ? s[LEFT] : s[RIGHT], key); }, select: function(s, sink, options) { if ( ! s ) return; if ( options ) { if ( 'limit' in options && options.limit <= 0 ) return; var size = this.size(s); if ( options.skip >= size && ! options.query ) { options.skip -= size; return; } } this.select(s[LEFT], sink, options); this.tail.select(s[VALUE], sink, options); this.select(s[RIGHT], sink, options); }, selectReverse: function(s, sink, options) { if ( ! s ) return; if ( options ) { if ( 'limit' in options && options.limit <= 0 ) return; var size = this.size(s); if ( options.skip >= size ) { options.skip -= size; return; } } this.selectReverse(s[RIGHT], sink, options); this.tail.selectReverse(s[VALUE], sink, options); this.selectReverse(s[LEFT], sink, options); }, findPos: function(s, key, incl) { if ( ! s ) return 0; var r = this.compare(s[KEY], key); if ( r === 0 ) { return incl ? this.size(s[LEFT]) : this.size(s) - this.size(s[RIGHT]); } return r > 0 ? this.findPos(s[LEFT], key, incl) : this.findPos(s[RIGHT], key, incl) + this.size(s) - this.size(s[RIGHT]); }, size: function(s) { return s ? s[SIZE] : 0; }, compare: function(o1, o2) { return this.prop.compareProperty(o1, o2); }, plan: function(s, sink, options) { var query = options && options.query; if ( query === FALSE ) return NOT_FOUND; if ( ! query && CountExpr.isInstance(sink) ) { var count = this.size(s); // console.log('**************** COUNT SHORT-CIRCUIT ****************', count, this.toString()); return { cost: 0, execute: function(unused, sink, options) { sink.count += count; return anop; }, toString: function() { return 'short-circuit-count(' + count + ')'; } }; } // if ( options && options.limit != null && options.skip != null && options.skip + options.limit > this.size(s) ) return NO_PLAN; var prop = this.prop; var isExprMatch = function(model) { if ( ! model ) return undefined; if ( query ) { if ( model.isInstance(query) && query.arg1 === prop ) { var arg2 = query.arg2; query = undefined; return arg2; } if ( AndExpr.isInstance(query) ) { for ( var i = 0 ; i < query.args.length ; i++ ) { var q = query.args[i]; if ( model.isInstance(q) && q.arg1 === prop ) { query = query.clone(); query.args[i] = TRUE; query = query.partialEval(); if ( query === TRUE ) query = null; return q.arg2; } } } } return undefined; }; // if ( sink.model_ === GroupByExpr && sink.arg1 === prop ) { // console.log('**************** GROUP-BY SHORT-CIRCUIT ****************'); // TODO: // } var index = this; var arg2 = isExprMatch(GLOBAL.InExpr); if ( arg2 && // Just scan if that would be faster. Math.log(this.size(s))/Math.log(2) * arg2.length < this.size(s) ) { var keys = arg2; var subPlans = []; var results = []; var cost = 1; var newOptions = {}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = options.limit; if ( 'skip' in options ) newOptions.skip = options.skip; if ( 'order' in options ) newOptions.order = options.order; for ( var i = 0 ; i < keys.length ; i++) { var result = this.get(s, keys[i]); if ( result ) { var subPlan = this.tail.plan(result, sink, newOptions); cost += subPlan.cost; subPlans.push(subPlan); results.push(result); } } if ( subPlans.length == 0 ) return NOT_FOUND; return { cost: 1 + cost, execute: function(s2, sink, options) { var pars = []; for ( var i = 0 ; i < subPlans.length ; i++ ) { pars.push(subPlans[i].execute(results[i], sink, newOptions)); } return apar.apply(null, pars); }, toString: function() { return 'IN(key=' + prop.name + ', size=' + results.length + ')'; } }; } arg2 = isExprMatch(GLOBAL.EqExpr); if ( arg2 != undefined ) { var key = arg2.f(); var result = this.get(s, key); if ( ! result ) return NOT_FOUND; // var newOptions = {__proto__: options, query: query}; var newOptions = {}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = options.limit; if ( 'skip' in options ) newOptions.skip = options.skip; if ( 'order' in options ) newOptions.order = options.order; var subPlan = this.tail.plan(result, sink, newOptions); return { cost: 1 + subPlan.cost, execute: function(s2, sink, options) { return subPlan.execute(result, sink, newOptions); }, toString: function() { return 'lookup(key=' + prop.name + ', cost=' + this.cost + (query && query.toSQL ? ', query: ' + query.toSQL() : '') + ') ' + subPlan.toString(); } }; } arg2 = isExprMatch(GLOBAL.GtExpr); if ( arg2 != undefined ) { var key = arg2.f(); var pos = this.findPos(s, key, false); var newOptions = {skip: ((options && options.skip) || 0) + pos}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = options.limit; if ( 'order' in options ) newOptions.order = options.order; options = newOptions; } arg2 = isExprMatch(GLOBAL.GteExpr); if ( arg2 != undefined ) { var key = arg2.f(); var pos = this.findPos(s, key, true); var newOptions = {skip: ((options && options.skip) || 0) + pos}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = options.limit; if ( 'order' in options ) newOptions.order = options.order; options = newOptions; } arg2 = isExprMatch(GLOBAL.LtExpr); if ( arg2 != undefined ) { var key = arg2.f(); var pos = this.findPos(s, key, true); var newOptions = {limit: (pos - (options && options.skip) || 0)}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = Math.min(options.limit, newOptions.limit); if ( 'skip' in options ) newOptions.skip = options.skip; if ( 'order' in options ) newOptions.order = options.order; options = newOptions; } arg2 = isExprMatch(GLOBAL.LteExpr); if ( arg2 != undefined ) { var key = arg2.f(); var pos = this.findPos(s, key, false); var newOptions = {limit: (pos - (options && options.skip) || 0)}; if ( query ) newOptions.query = query; if ( 'limit' in options ) newOptions.limit = Math.min(options.limit, newOptions.limit); if ( 'skip' in options ) newOptions.skip = options.skip; if ( 'order' in options ) newOptions.order = options.order; options = newOptions; } var cost = this.size(s); var sortRequired = false; var reverseSort = false; if ( options && options.order ) { if ( options.order === prop ) { // sort not required } else if ( GLOBAL.DescExpr && DescExpr.isInstance(options.order) && options.order.arg1 === prop ) { // reverse-sort, sort not required reverseSort = true; } else { sortRequired = true; if ( cost != 0 ) cost *= Math.log(cost) / Math.log(2); } } if ( options && ! sortRequired ) { if ( options.skip ) cost -= options.skip; if ( options.limit ) cost = Math.min(cost, options.limit); } return { cost: cost, execute: function() { /* var o = options && (options.skip || options.limit) ? {skip: options.skip || 0, limit: options.limit || Number.MAX_VALUE} : undefined; */ if ( sortRequired ) { var a = []; index.selectCount++; index.select(s, a, {query: options.query}); index.selectCount--; a.sort(toCompare(options.order)); var skip = options.skip || 0; var limit = Number.isFinite(options.limit) ? options.limit : a.length; limit += skip; limit = Math.min(a.length, limit); for ( var i = skip; i < limit; i++ ) sink.put(a[i]); } else { // What did this do? It appears to break sorting in saturn mail /* if ( reverseSort && options && options.skip ) // TODO: temporary fix, should include range in select and selectReverse calls instead. options = { __proto__: options, skip: index.size(s) - options.skip - (options.limit || index.size(s)-options.skip) };*/ index.selectCount++; reverseSort ? index.selectReverse(s, sink, options) : index.select(s, sink, options) ; index.selectCount--; } return anop; }, toString: function() { return 'scan(key=' + prop.name + ', cost=' + this.cost + (query && query.toSQL ? ', query: ' + query.toSQL() : '') + ')'; } }; }, toString: function() { return 'TreeIndex(' + this.prop.name + ', ' + this.tail + ')'; } }; /** Case-Insensitive TreeIndex **/ var CITreeIndex = { __proto__: TreeIndex, create: function(prop, tail) { tail = tail || ValueIndex; return { __proto__: this, prop: prop, tail: tail }; }, put: function(s, newValue) { return this.putKeyValue(s, this.prop.f(newValue).toLowerCase(), newValue); }, remove: function(s, value) { return this.removeKeyValue(s, this.prop.f(value).toLowerCase(), value); } }; /** An Index for storing multi-valued properties. **/ var SetIndex = { __proto__: TreeIndex, create: function(prop, tail) { tail = tail || ValueIndex; return { __proto__: this, prop: prop, tail: tail }; }, // TODO: see if this can be done some other way dedup: function(obj, value) { // NOP, not safe to do here }, put: function(s, newValue) { var a = this.prop.f(newValue); if ( a.length ) { for ( var i = 0 ; i < a.length ; i++ ) { s = this.putKeyValue(s, a[i], newValue); } } else { s = this.putKeyValue(s, '', newValue); } return s; }, remove: function(s, value) { var a = this.prop.f(value); if ( a.length ) { for ( var i = 0 ; i < a.length ; i++ ) { s = this.removeKeyValue(s, a[i], value); } } else { s = this.removeKeyValue(s, '', value); } return s; } }; var PositionQuery = { create: function(args) { return { __proto__: this, skip: args.skip, limit: args.limit, s: args.s }; }, reduce: function(other) { var otherFinish = other.skip + other.limit; var myFinish = this.skip + this.limit; if ( other.skip > myFinish ) return null; if ( other.skip >= this.skip ) { return PositionQuery.create({ skip: this.skip, limit: Math.max(myFinish, otherFinish) - this.skip, s: this.s }); } return other.reduce(this); }, equals: function(other) { return this.skip === other.skip && this.limit === other.limit; } }; var AutoPositionIndex = { create: function(factory, mdao, networkdao, maxage) { var obj = { __proto__: this, factory: factory, maxage: maxage, dao: mdao, networkdao: networkdao, sets: [], alt: AltIndex.create() }; return obj; }, put: function(s, value) { return this.alt.put(s, value); }, remove: function(s, value) { return this.alt.remove(s, value); }, bulkLoad: function(a) { return []; }, addIndex: function(s, index) { return this; }, addPosIndex: function(s, options) { var index = PositionIndex.create( options && options.order, options && options.query, this.factory, this.dao, this.networkdao, this.queue, this.maxage); this.alt.delegates.push(index); s.push(index.bulkLoad([])); }, hasIndex: function(options) { for ( var i = 0; i < this.sets.length; i++ ) { var set = this.sets[i]; if ( set[0].equals((options && options.query) || '') && set[1].equals((options && options.order) || '') ) return true; } return false; }, plan: function(s, sink, options) { var subPlan = this.alt.plan(s, sink, options); if ( subPlan != NO_PLAN ) return subPlan; if ( ( options && options.skip != null && options.limit != null ) || CountExpr.isInstance(sink) ) { if ( this.hasIndex(options) ) return NO_PLAN; this.sets.push([(options && options.query) || '', (options && options.order) || '']); this.addPosIndex(s, options); return this.alt.plan(s, sink, options); } return NO_PLAN; } }; var PositionIndex = { create: function(order, query, factory, dao, networkdao, queue, maxage) { var obj = { __proto__: this, order: order || '', query: query || '', factory: factory, dao: dao, networkdao: networkdao.where(query).orderBy(order), maxage: maxage, queue: arequestqueue(function(ret, request) { var s = request.s; obj.networkdao .skip(request.skip) .limit(request.limit) .select()(function(objs) { var now = Date.now(); for ( var i = 0; i < objs.length; i++ ) { s[request.skip + i] = { obj: objs[i].id, timestamp: now }; s.feedback = objs[i].id; obj.dao.put(objs[i]); s.feedback = null; } ret(); }); }, undefined, 1) }; return obj; }, put: function(s, newValue) { if ( s.feedback === newValue.id ) return s; if ( this.query && ! this.query.f(newValue) ) return s; var compare = toCompare(this.order); for ( var i = 0; i < s.length; i++ ) { var entry = s[i] if ( ! entry ) continue; // TODO: This abuses the fact that find is synchronous. this.dao.find(entry.obj, { put: function(o) { entry = o; } }); // Only happens when things are put into the dao from a select on this index. // otherwise objects are removed() first from the MDAO. if ( entry.id === newValue.id ) { break; } if ( compare(entry, newValue) > 0 ) { for ( var j = s.length; j > i; j-- ) { s[j] = s[j-1]; } // If we have objects on both sides, put this one here. if ( i == 0 || s[i-1] ) s[i] = { obj: newValue.id, timestamp: Date.now() }; break; } } return s; }, remove: function(s, obj) { if ( s.feedback === obj.id ) return s; for ( var i = 0; i < s.length; i++ ) { if ( s[i] && s[i].obj === obj.id ) { for ( var j = i; j < s.length - 1; j++ ) { s[j] = s[j+1]; } break; } } return s; }, bulkLoad: function(a) { return []; }, plan: function(s, sink, options) { var order = ( options && options.order ) || ''; var query = ( options && options.query ) || ''; var skip = options && options.skip; var limit = options && options.limit; var self = this; if ( ! order.equals(this.order) || ! query.equals(this.query) ) return NO_PLAN; if ( CountExpr.isInstance(sink) ) { return { cost: 0, execute: function(s, sink, options) { if ( ! s.count ) { s.count = amemo(function(ret) { self.networkdao.select(COUNT())(function(c) { ret(c); }); }, self.maxage); } return (function(ret, count) { sink.copyFrom(count); ret(); }).ao(s.count); }, toString: function() { return 'position-index(cost=' + this.cost + ', count)'; } } } else if ( skip == undefined || limit == undefined ) { return NO_PLAN; } var threshold = Date.now() - this.maxage; return { cost: 0, toString: function() { return 'position-index(cost=' + this.cost + ')'; }, execute: function(s, sink, options) { var objs = []; var min; var max; for ( var i = 0 ; i < limit; i++ ) { var o = s[i + skip]; if ( ! o || o.timestamp < threshold ) { if ( min == undefined ) min = i + skip; max = i + skip; } if ( o ) { // TODO: Works because find is actually synchronous. // this will need to fixed if find starts using an async function. self.dao.find(o.obj, { put: function(obj) { objs[i] = obj; } }); } else { objs[i] = self.factory(); } if ( ! objs[i] ) debugger; } if ( min != undefined ) { self.queue(PositionQuery.create({ skip: min, limit: (max - min) + 1, s: s })); } for ( var i = 0; i < objs.length; i++ ) { sink.put(objs[i]); } return anop; } }; } }; var AltIndex = { // Maximum cost for a plan which is good enough to not bother looking at the rest. GOOD_ENOUGH_PLAN: 10, // put to 10 or more when not testing create: function() { return { __proto__: this, delegates: argsToArray(arguments) }; }, addIndex: function(s, index) { // Populate the index var a = [].sink; this.plan(s, a).execute(s, a); s.push(index.bulkLoad(a)); this.delegates.push(index); return this; }, bulkLoad: function(a) { var root = [].sink; for ( var i = 0 ; i < this.delegates.length ; i++ ) { root[i] = this.delegates[i].bulkLoad(a); } return root; }, get: function(s, key) { return this.delegates[0].get(s[0], key); }, put: function(s, newValue) { s = s || [].sink; for ( var i = 0 ; i < this.delegates.length ; i++ ) { s[i] = this.delegates[i].put(s[i], newValue); } return s; }, remove: function(s, obj) { s = s || [].sink; for ( var i = 0 ; i < this.delegates.length ; i++ ) { s[i] = this.delegates[i].remove(s[i], obj); } return s; }, plan: function(s, sink, options) { var bestPlan; var bestPlanI = 0; // console.log('Planning: ' + (options && options.query && options.query.toSQL && options.query.toSQL())); for ( var i = 0 ; i < this.delegates.length ; i++ ) { var plan = this.delegates[i].plan(s[i], sink, options); // console.log(' plan ' + i + ': ' + plan); if ( plan.cost <= AltIndex.GOOD_ENOUGH_PLAN ) { bestPlanI = i; bestPlan = plan; break; } if ( ! bestPlan || plan.cost < bestPlan.cost ) { bestPlanI = i; bestPlan = plan; } } // console.log('Best Plan: ' + bestPlan); if ( bestPlan == undefined || bestPlan == NO_PLAN ) return NO_PLAN; return { __proto__: bestPlan, execute: function(unused, sink, options) { return bestPlan.execute(s[bestPlanI], sink, options); } }; }, size: function(obj) { return this.delegates[0].size(obj[0]); }, toString: function() { return 'Alt(' + this.delegates.join(',') + ')'; } }; var mLangIndex = { create: function(mlang) { return { __proto__: this, mlang: mlang, PLAN: { cost: 0, execute: function(s, sink, options) { sink.copyFrom(s); return anop; }, toString: function() { return 'mLangIndex(' + this.s + ')'; } } }; }, bulkLoad: function(a) { a.select(this.mlang); return this.mlang; }, put: function(s, newValue) { // TODO: Should we clone s here? That would be more // correct in terms of the purely functional interface // but maybe we can get away with it. s = s || this.mlang.clone(); s.put(newValue); return s; }, remove: function(s, obj) { // TODO: Should we clone s here? That would be more // correct in terms of the purely functional interface // but maybe we can get away with it. s = s || this.mlang.clone(); s.remove && s.remove(obj); return s; }, size: function(s) { return Number.MAX_VALUE; }, plan: function(s, sink, options) { // console.log('s'); if ( options && options.query ) return NO_PLAN; if ( sink.model_ && sink.model_.isInstance(s) && s.arg1 === sink.arg1 ) { this.PLAN.s = s; return this.PLAN; } return NO_PLAN; }, toString: function() { return 'mLangIndex(' + this.mlang + ')'; } }; /** An Index which adds other indices as needed. **/ var AutoIndex = { create: function(mdao) { return { __proto__: this, properties: { id: true }, mdao: mdao }; }, put: function(s, newValue) { return s; }, remove: function(s, obj) { return s; }, bulkLoad: function(a) { return 'auto'; }, addIndex: function(prop) { if ( GLOBAL.DescExpr && DescExpr.isInstance(prop) ) prop = prop.arg1; console.log('Adding AutoIndex : ', prop.id); this.properties[prop.name] = true; this.mdao.addIndex(prop); }, plan: function(s, sink, options) { if ( options ) { if ( options.order && Property.isInstance(options.order) && ! this.properties[options.order.name] ) { this.addIndex(options.order); } else if ( options.query ) { // TODO: check for property in query } } return NO_PLAN; }, toString: function() { return 'AutoIndex()'; } }; var MDAO = Model.create({ extends: 'AbstractDAO', name: 'MDAO', label: 'Indexed DAO', properties: [ { name: 'model', type: 'Model', required: true }, { model_: 'BooleanProperty', name: 'autoIndex', defaultValue: false } ], methods: { init: function() { this.SUPER(); this.map = {}; // TODO(kgr): this doesn't support multi-part keys, but should this.index = TreeIndex.create(this.model.getProperty(this.model.ids[0])); if ( this.autoIndex ) this.addRawIndex(AutoIndex.create(this)); }, /** * Add a non-unique index * args: one or more properties **/ addIndex: function() { var props = argsToArray(arguments); // Add on the primary key(s) to make the index unique. for ( var i = 0 ; i < this.model.ids.length ; i++ ) { props.push(this.model.getProperty(this.model.ids[i])); if ( ! props[props.length - 1] ) throw "Undefined index property"; } return this.addUniqueIndex.apply(this, props); }, /** * Add a unique index * args: one or more properties **/ addUniqueIndex: function() { var index = ValueIndex; for ( var i = arguments.length-1 ; i >= 0 ; i-- ) { var prop = arguments[i]; // TODO: the index prototype should be in the property var proto = prop.type == 'Array[]' ? SetIndex : TreeIndex; index = proto.create(prop, index); } return this.addRawIndex(index); }, // TODO: name 'addIndex' and renamed addIndex addRawIndex: function(index) { // Upgrade single Index to an AltIndex if required. if ( ! /*AltIndex.isInstance(this.index)*/ this.index.delegates ) { this.index = AltIndex.create(this.index); this.root = [this.root]; } this.index.addIndex(this.root, index); return this; }, /** * Bulk load data from another DAO. * Any data already loaded into this DAO will be lost. * @arg sink (optional) eof is called when loading is complete. **/ bulkLoad: function(dao, sink) { var self = this; dao.select({ __proto__: [].sink, eof: function() { self.root = self.index.bulkLoad(this); sink && sink.eof && sink.eof(); }}); }, put: function(obj, sink) { var oldValue = this.map[obj.id]; if ( oldValue ) { this.root = this.index.put(this.index.remove(this.root, oldValue), obj); } else { this.root = this.index.put(this.root, obj); } this.map[obj.id] = obj; this.notify_('put', [obj]); sink && sink.put && sink.put(obj); }, findObj_: function(key, sink) { var obj = this.map[key]; // var obj = this.index.get(this.root, key); if ( obj ) { sink.put && sink.put(obj); } else { sink.error && sink.error('find', key); } }, find: function(key, sink) { if ( key == undefined ) { sink && sink.error && sink.error('missing key'); return; } if ( ! key.f ) { // TODO: make better test, use model this.findObj_(key, sink); return; } // How to handle multi value primary keys? var found = false; this.where(key).limit(1).select({ // ???: Is 'put' needed? put: function(obj) { found = true; sink && sink.put && sink.put(obj); }, eof: function() { if ( ! found ) sink && sink.error && sink.error('find', key); } }); }, remove: function(obj, sink) { if ( ! obj ) { sink && sink.error && sink.error('missing key'); return; } var id = (obj.id !== undefined && obj.id !== '') ? obj.id : obj; var self = this; this.find(id, { put: function(obj) { self.root = self.index.remove(self.root, obj); delete self.map[obj.id]; self.notify_('remove', [obj]); sink && sink.remove && sink.remove(obj); }, error: function() { sink && sink.error && sink.error('remove', obj); } }); }, removeAll: function(sink, options) { if (!options) options = {}; if (!options.query) options.query = TRUE; var future = afuture(); this.where(options.query).select()(function(a) { for ( var i = 0 ; i < a.length ; i++ ) { this.root = this.index.remove(this.root, a[i]); delete this.map[a[i].id]; this.notify_('remove', [a[i]]); sink && sink.remove && sink.remove(a[i]); } sink && sink.eof && sink.eof(); future.set(sink); }.bind(this)); return future.get; }, select: function(sink, options) { sink = sink || [].sink; // Clone the options to prevent 'limit' from being mutated in the original. if ( options ) options = {__proto__: options}; if ( GLOBAL.ExplainExpr && GLOBAL.ExplainExpr.isInstance(sink) ) { var plan = this.index.plan(this.root, sink.arg1, options); sink.plan = 'cost: ' + plan.cost + ', ' + plan.toString(); sink && sink.eof && sink.eof(); return aconstant(sink); } var plan = this.index.plan(this.root, sink, options); var future = afuture(); plan.execute(this.root, sink, options)( function(ret) { sink && sink.eof && sink.eof(); future.set(sink) }); return future.get; }, toString: function() { return 'MDAO(' + this.model.name + ',' + this.index + ')'; } } });