var BinarySearchTree = require('binary-search-tree').BinarySearchTree , model = require('./model') , _ = require('underscore') , util = require('util') ; /** * Two indexed pointers are equal iif they point to the same place */ function checkValueEquality (a, b) { return a === b; } /** * Create a new index * @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields) * @param {Datastore} options.datastore Datastore on which the index is created * @param {Boolean} options.unique Optional, enforce a unique constraint (default: false) * @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false) * TODO: for now the sparse option doesn't work fully * don't use it. I will implement it in the future * in the meantime you can use non-unique, non-sparse indexes * for approx. the same behaviour */ function Index (options) { this.fieldName = options.fieldName; this.datastore = options.datastore; this.unique = options.unique || false; this.sparse = options.sparse || false; this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality }; this.reset(); // No data in the beginning } /** * Reset an index * @param {Document or Array of documents} newData Optional, data to initialize the index with * If an error is thrown during insertion, the index is not modified */ Index.prototype.reset = function (newData) { this.tree = new BinarySearchTree(this.treeOptions); if (this.sparse) { this.nonindexedDocs = []; } if (newData) { this.insert(newData); } }; /** * Insert a new document in the index * If an array is passed, we insert all its elements (if one insertion fails the index is not modified) * O(log(n)) */ Index.prototype.insert = function (doc) { var key, self = this; if (util.isArray(doc)) { this.insertMultipleDocs(doc); return; } key = model.getDotValue(doc, this.fieldName); // We don't index documents that don't contain the field if the index is sparse if (key === undefined && this.sparse) { this.nonindexedDocs.push(doc); return; } this.tree.insert(key, doc); }; /** * Insert an array of documents in the index * If a constraint is violated, an error should be thrown and the changes rolled back */ Index.prototype.insertMultipleDocs = function (docs) { var i, error, failingI; for (i = 0; i < docs.length; i += 1) { try { this.insert(docs[i]); } catch (e) { error = e; failingI = i; break; } } if (error) { for (i = 0; i < failingI; i += 1) { this.remove(docs[i]); } throw error; } }; /** * Remove a document from the index * If an array is passed, we remove all its elements * The remove operation is safe with regards to the 'unique' constraint * O(log(n)) */ Index.prototype.remove = function (doc) { var key, self = this if (util.isArray(doc)) { doc.forEach(function (d) { self.remove(d); }); return; } key = model.getDotValue(doc, this.fieldName); if (key === undefined && this.sparse) { this.nonindexedDocs = _.without(this.nonindexedDocs, doc); return; } this.tree.delete(key, doc); }; /** * Update a document in the index * Naive implementation, still in O(log(n)) */ Index.prototype.update = function (oldDoc, newDoc) { if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); } this.remove(oldDoc); this.insert(newDoc); }; /** * Revert an update */ Index.prototype.revertUpdate = function (oldDoc, newDoc) { var revert = []; if (!util.isArray(oldDoc)) { this.update(newDoc, oldDoc); } else { oldDoc.forEach(function (pair) { revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc }); }); this.update(revert); } }; /** * Update multiple documents in the index * If a constraint is violated, the changes need to be rolled back * and an error thrown * @param {Array of oldDoc, newDoc pairs} pairs */ Index.prototype.updateMultipleDocs = function (pairs) { var i, failingI, error; for (i = 0; i < pairs.length; i += 1) { this.remove(pairs[i].oldDoc); } for (i = 0; i < pairs.length; i += 1) { try { this.insert(pairs[i].newDoc); } catch (e) { error = e; failingI = i; break; } } // If an error was raised, roll back changes in the inverse order if (error) { for (i = 0; i < failingI; i += 1) { this.remove(pairs[i].newDoc); } for (i = 0; i < pairs.length; i += 1) { this.insert(pairs[i].oldDoc); } throw error; } }; /** * Get all documents in index that match the query on fieldName * For now only works with field equality (i.e. can't use the index for $lt query for example) * And doesn't return non indexed docs * @param {Thing} value Value to match the key against * @return {Array of documents} */ Index.prototype.getMatching = function (value) { return this.tree.search(value); }; /** * Get all elements in the index * @return {Array of documents} */ Index.prototype.getAll = function () { var res = []; this.tree.executeOnEveryNode(function (node) { var i; for (i = 0; i < node.data.length; i += 1) { res.push(node.data[i]); } }); return res; }; // Interface module.exports = Index;