nedb
Advanced tools
Comparing version 1.6.2 to 1.7.1
@@ -106,5 +106,4 @@ /** | ||
*/ | ||
Cursor.prototype._exec = function(callback) { | ||
var candidates = this.db.getCandidates(this.query) | ||
, res = [], added = 0, skipped = 0, self = this | ||
Cursor.prototype._exec = function(_callback) { | ||
var res = [], added = 0, skipped = 0, self = this | ||
, error = null | ||
@@ -114,65 +113,73 @@ , i, keys, key | ||
try { | ||
for (i = 0; i < candidates.length; i += 1) { | ||
if (model.match(candidates[i], this.query)) { | ||
// If a sort is defined, wait for the results to be sorted before applying limit and skip | ||
if (!this._sort) { | ||
if (this._skip && this._skip > skipped) { | ||
skipped += 1; | ||
function callback (error, res) { | ||
if (self.execFn) { | ||
return self.execFn(error, res, _callback); | ||
} else { | ||
return _callback(error, res); | ||
} | ||
} | ||
this.db.getCandidates(this.query, function (err, candidates) { | ||
if (err) { return callback(err); } | ||
try { | ||
for (i = 0; i < candidates.length; i += 1) { | ||
if (model.match(candidates[i], self.query)) { | ||
// If a sort is defined, wait for the results to be sorted before applying limit and skip | ||
if (!self._sort) { | ||
if (self._skip && self._skip > skipped) { | ||
skipped += 1; | ||
} else { | ||
res.push(candidates[i]); | ||
added += 1; | ||
if (self._limit && self._limit <= added) { break; } | ||
} | ||
} else { | ||
res.push(candidates[i]); | ||
added += 1; | ||
if (this._limit && this._limit <= added) { break; } | ||
} | ||
} else { | ||
res.push(candidates[i]); | ||
} | ||
} | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
// Apply all sorts | ||
if (this._sort) { | ||
keys = Object.keys(this._sort); | ||
// Apply all sorts | ||
if (self._sort) { | ||
keys = Object.keys(self._sort); | ||
// Sorting | ||
var criteria = []; | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
criteria.push({ key: key, direction: self._sort[key] }); | ||
} | ||
res.sort(function(a, b) { | ||
var criterion, compare, i; | ||
for (i = 0; i < criteria.length; i++) { | ||
criterion = criteria[i]; | ||
compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings); | ||
if (compare !== 0) { | ||
return compare; | ||
// Sorting | ||
var criteria = []; | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
criteria.push({ key: key, direction: self._sort[key] }); | ||
} | ||
res.sort(function(a, b) { | ||
var criterion, compare, i; | ||
for (i = 0; i < criteria.length; i++) { | ||
criterion = criteria[i]; | ||
compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings); | ||
if (compare !== 0) { | ||
return compare; | ||
} | ||
} | ||
} | ||
return 0; | ||
}); | ||
return 0; | ||
}); | ||
// Applying limit and skip | ||
var limit = this._limit || res.length | ||
, skip = this._skip || 0; | ||
// Applying limit and skip | ||
var limit = self._limit || res.length | ||
, skip = self._skip || 0; | ||
res = res.slice(skip, skip + limit); | ||
} | ||
res = res.slice(skip, skip + limit); | ||
} | ||
// Apply projection | ||
try { | ||
res = this.project(res); | ||
} catch (e) { | ||
error = e; | ||
res = undefined; | ||
} | ||
// Apply projection | ||
try { | ||
res = self.project(res); | ||
} catch (e) { | ||
error = e; | ||
res = undefined; | ||
} | ||
if (this.execFn) { | ||
return this.execFn(error, res, callback); | ||
} else { | ||
return callback(error, res); | ||
} | ||
}); | ||
}; | ||
@@ -179,0 +186,0 @@ |
@@ -72,2 +72,3 @@ var customUtils = require('./customUtils') | ||
this.indexes._id = new Index({ fieldName: '_id', unique: true }); | ||
this.ttlIndexes = {}; | ||
@@ -81,3 +82,3 @@ // Queue a load of the database right away and call the onload handler | ||
util.inherits(Datastore, require('events')); | ||
util.inherits(Datastore, require('events').EventEmitter); | ||
@@ -120,2 +121,3 @@ | ||
* @param {Boolean} options.sparse | ||
* @param {Number} options.expireAfterSeconds - Optional, if set this index becomes a TTL index (only works on Date fields, not arrays of Date) | ||
* @param {Function} cb Optional callback, signature: err | ||
@@ -137,2 +139,3 @@ */ | ||
this.indexes[options.fieldName] = new Index(options); | ||
if (options.expireAfterSeconds !== undefined) { this.ttlIndexes[options.fieldName] = options.expireAfterSeconds; } // With this implementation index creation is not necessary to ensure TTL but we stick with MongoDB's API here | ||
@@ -250,46 +253,85 @@ try { | ||
* | ||
* TODO: needs to be moved to the Cursor module | ||
* Returned candidates will be scanned to find and remove all expired documents | ||
* | ||
* @param {Query} query | ||
* @param {Boolean} dontExpireStaleDocs Optional, defaults to false, if true don't remove stale docs. Useful for the remove function which shouldn't be impacted by expirations | ||
* @param {Function} callback Signature err, docs | ||
*/ | ||
Datastore.prototype.getCandidates = function (query) { | ||
Datastore.prototype.getCandidates = function (query, dontExpireStaleDocs, callback) { | ||
var indexNames = Object.keys(this.indexes) | ||
, self = this | ||
, usableQueryKeys; | ||
// For a basic match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.isDate(query[k]) || query[k] === null) { | ||
usableQueryKeys.push(k); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return this.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]]); | ||
if (typeof dontExpireStaleDocs === 'function') { | ||
callback = dontExpireStaleDocs; | ||
dontExpireStaleDocs = false; | ||
} | ||
// For a $in match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (query[k] && query[k].hasOwnProperty('$in')) { | ||
usableQueryKeys.push(k); | ||
async.waterfall([ | ||
// STEP 1: get candidates list by checking indexes from most to least frequent usecase | ||
function (cb) { | ||
// For a basic match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.isDate(query[k]) || query[k] === null) { | ||
usableQueryKeys.push(k); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]])); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return this.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in); | ||
} | ||
// For a comparison match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) { | ||
usableQueryKeys.push(k); | ||
// For a $in match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (query[k] && query[k].hasOwnProperty('$in')) { | ||
usableQueryKeys.push(k); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in)); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return this.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]]); | ||
// For a comparison match | ||
usableQueryKeys = []; | ||
Object.keys(query).forEach(function (k) { | ||
if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) { | ||
usableQueryKeys.push(k); | ||
} | ||
}); | ||
usableQueryKeys = _.intersection(usableQueryKeys, indexNames); | ||
if (usableQueryKeys.length > 0) { | ||
return cb(null, self.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]])); | ||
} | ||
// By default, return all the DB data | ||
return cb(null, self.getAllData()); | ||
} | ||
// STEP 2: remove all expired documents | ||
, function (docs) { | ||
if (dontExpireStaleDocs) { return callback(null, docs); } | ||
// By default, return all the DB data | ||
return this.getAllData(); | ||
var expiredDocsIds = [], validDocs = [], ttlIndexesFieldNames = Object.keys(self.ttlIndexes); | ||
docs.forEach(function (doc) { | ||
var valid = true; | ||
ttlIndexesFieldNames.forEach(function (i) { | ||
if (doc[i] !== undefined && util.isDate(doc[i]) && Date.now() > doc[i].getTime() + self.ttlIndexes[i] * 1000) { | ||
valid = false; | ||
} | ||
}); | ||
if (valid) { validDocs.push(doc); } else { expiredDocsIds.push(doc._id); } | ||
}); | ||
async.eachSeries(expiredDocsIds, function (_id, cb) { | ||
self._remove({ _id: _id }, {}, function (err) { | ||
if (err) { return callback(err); } | ||
return cb(); | ||
}); | ||
}, function (err) { | ||
return callback(null, validDocs); | ||
}); | ||
}]); | ||
}; | ||
@@ -556,44 +598,45 @@ | ||
, function () { // Perform the update | ||
var modifiedDoc | ||
, candidates = self.getCandidates(query) | ||
, modifications = [] | ||
; | ||
var modifiedDoc , modifications = []; | ||
// Preparing update (if an error is thrown here neither the datafile nor | ||
// the in-memory indexes are affected) | ||
try { | ||
for (i = 0; i < candidates.length; i += 1) { | ||
if (model.match(candidates[i], query) && (multi || numReplaced === 0)) { | ||
numReplaced += 1; | ||
modifiedDoc = model.modify(candidates[i], updateQuery); | ||
if (self.timestampData) { modifiedDoc.updatedAt = new Date(); } | ||
modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc }); | ||
self.getCandidates(query, function (err, candidates) { | ||
if (err) { return callback(err); } | ||
// Preparing update (if an error is thrown here neither the datafile nor | ||
// the in-memory indexes are affected) | ||
try { | ||
for (i = 0; i < candidates.length; i += 1) { | ||
if (model.match(candidates[i], query) && (multi || numReplaced === 0)) { | ||
numReplaced += 1; | ||
modifiedDoc = model.modify(candidates[i], updateQuery); | ||
if (self.timestampData) { modifiedDoc.updatedAt = new Date(); } | ||
modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc }); | ||
} | ||
} | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
// Change the docs in memory | ||
try { | ||
// Change the docs in memory | ||
try { | ||
self.updateIndexes(modifications); | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
// Update the datafile | ||
var updatedDocs = _.pluck(modifications, 'newDoc'); | ||
self.persistence.persistNewState(updatedDocs, function (err) { | ||
if (err) { return callback(err); } | ||
if (!options.returnUpdatedDocs) { | ||
return callback(null, numReplaced); | ||
} else { | ||
var updatedDocsDC = []; | ||
updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); }); | ||
return callback(null, numReplaced, updatedDocsDC); | ||
} | ||
// Update the datafile | ||
var updatedDocs = _.pluck(modifications, 'newDoc'); | ||
self.persistence.persistNewState(updatedDocs, function (err) { | ||
if (err) { return callback(err); } | ||
if (!options.returnUpdatedDocs) { | ||
return callback(null, numReplaced); | ||
} else { | ||
var updatedDocsDC = []; | ||
updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); }); | ||
return callback(null, numReplaced, updatedDocsDC); | ||
} | ||
}); | ||
}); | ||
} | ||
]); | ||
}]); | ||
}; | ||
Datastore.prototype.update = function () { | ||
@@ -616,7 +659,3 @@ this.executor.push({ this: this, fn: this._update, arguments: arguments }); | ||
var callback | ||
, self = this | ||
, numRemoved = 0 | ||
, multi | ||
, removedDocs = [] | ||
, candidates = this.getCandidates(query) | ||
, self = this, numRemoved = 0, removedDocs = [], multi | ||
; | ||
@@ -628,17 +667,22 @@ | ||
try { | ||
candidates.forEach(function (d) { | ||
if (model.match(d, query) && (multi || numRemoved === 0)) { | ||
numRemoved += 1; | ||
removedDocs.push({ $$deleted: true, _id: d._id }); | ||
self.removeFromIndexes(d); | ||
} | ||
this.getCandidates(query, true, function (err, candidates) { | ||
if (err) { return callback(err); } | ||
try { | ||
candidates.forEach(function (d) { | ||
if (model.match(d, query) && (multi || numRemoved === 0)) { | ||
numRemoved += 1; | ||
removedDocs.push({ $$deleted: true, _id: d._id }); | ||
self.removeFromIndexes(d); | ||
} | ||
}); | ||
} catch (err) { return callback(err); } | ||
self.persistence.persistNewState(removedDocs, function (err) { | ||
if (err) { return callback(err); } | ||
return callback(null, numRemoved); | ||
}); | ||
} catch (err) { return callback(err); } | ||
self.persistence.persistNewState(removedDocs, function (err) { | ||
if (err) { return callback(err); } | ||
return callback(null, numRemoved); | ||
}); | ||
}; | ||
Datastore.prototype.remove = function () { | ||
@@ -650,5 +694,2 @@ this.executor.push({ this: this, fn: this._remove, arguments: arguments }); | ||
module.exports = Datastore; |
@@ -91,3 +91,3 @@ var BinarySearchTree = require('binary-search-tree').AVLTree | ||
} | ||
if (error) { | ||
@@ -97,3 +97,3 @@ for (i = 0; i < failingI; i += 1) { | ||
} | ||
throw error; | ||
@@ -100,0 +100,0 @@ } |
{ | ||
"name": "nedb", | ||
"version": "1.6.2", | ||
"version": "1.7.1", | ||
"author": { | ||
@@ -5,0 +5,0 @@ "name": "Louis Chatriot", |
@@ -556,2 +556,3 @@ <img src="http://i.imgur.com/9O1xHFb.png" style="width: 25%; height: 25%; float: left;"> | ||
* **sparse** (optional, defaults to `false`): don't index documents for which the field is not defined. Use this option along with "unique" if you want to accept multiple documents for which it is not defined. | ||
* **expireAfterSeconds** (number of seconds, optional): if set, the created index is a TTL (time to live) index, that will automatically remove documents when the system date becomes larger than the date on the indexed field plus `expireAfterSeconds`. Documents where the indexed field is not specified or not a `Date` object are ignored | ||
@@ -591,2 +592,14 @@ Note: the `_id` is automatically indexed with a unique constraint, no need to call `ensureIndex` on it. | ||
}); | ||
// Example of using expireAfterSeconds to remove documents 1 hour | ||
// after their creation (db's timestampData option is true here) | ||
db.ensureIndex({ fieldName: 'createdAt', expireAfterSeconds: 3600 }, function (err) { | ||
}); | ||
// You can also use the option to set an expiration date like so | ||
db.ensureIndex({ fieldName: 'expirationDate', expireAfterSeconds: 0 }, function (err) { | ||
// Now all documents will expire when system time reaches the date in their | ||
// expirationDate field | ||
}); | ||
``` | ||
@@ -593,0 +606,0 @@ |
@@ -100,3 +100,3 @@ var should = require('chai').should() | ||
}); | ||
it('With an empty collection', function (done) { | ||
@@ -117,3 +117,3 @@ async.waterfall([ | ||
}); | ||
it('With a limit', function (done) { | ||
@@ -294,3 +294,3 @@ var cursor = new Cursor(d); | ||
it('Using a limit higher than total number of docs shouldnt cause an error', function (done) { | ||
var i; | ||
var i; | ||
async.waterfall([ | ||
@@ -314,3 +314,3 @@ function (cb) { | ||
it('Using limit and skip with sort', function (done) { | ||
var i; | ||
var i; | ||
async.waterfall([ | ||
@@ -317,0 +317,0 @@ function (cb) { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1277364
28720
676