lokijs
Version:
Fast document oriented javascript in-memory database
610 lines (512 loc) • 15.7 kB
JavaScript
// to be run in node
var gordian = require('gordian'),
suite = new gordian('LokiTests'),
loki = require('../src/lokijs.js'),
db,
users;
function docCompare(a, b) {
if (a.$loki < b.$loki) return -1;
if (a.$loki > b.$loki) return 1;
return 0;
}
db = new loki('test.json');
users = db.addCollection('user');
var jonas = null;
function populateTestData() {
users.insert({
name: 'dave',
age: 25,
lang: 'English'
});
users.insert({
name: 'joe',
age: 39,
lang: 'Italian'
});
jonas = users.insert({
name: 'jonas',
age: 30,
lang: 'Swedish'
});
}
function testCoreMethods() {
// findOne()
var j = users.findOne({
'name': 'jonas'
});
suite.assertStrictEqual("findOne", j.name, 'jonas');
// find()
var result = users.find({
'age': {
'$gt': 29
}
});
suite.assertStrictEqual("find", result.length, 2);
// $regex test
suite.assertStrictEqual('$regex', users.find({
"name": {
'$regex': /o/
}
}).length, 2);
// insert() : try inserting existing document (should fail), try adding doc with legacy id column
var collectionLength = users.data.length;
var objDave = users.findOne({
'name': 'dave'
});
var wasAdded = true;
try {
users.insert(objDave);
} catch (err) {
wasAdded = false;
}
suite.assertStrictEqual('insert existing document caught', wasAdded, false);
// our collections are not strongly typed so lets invent some object that has its 'own' id column
var legacyObject = {
id: 999,
first: 'aaa',
last: 'bbb',
city: 'pasadena',
state: 'ca'
}
wasAdded = true;
try {
users.insert(legacyObject);
} catch (err) {
wasAdded = false;
}
suite.assertStrictEqual('insert legacy document allowed', wasAdded, true);
// remove object so later queries access valid properties on all objects
if (wasAdded) {
users.remove(legacyObject); // the object itself should have been modified
}
// update()
legacyObject = {
id: 998,
first: 'aaa',
last: 'bbb',
city: 'pasadena',
state: 'ca'
}
var wasUpdated = true;
try {
users.update(legacyObject);
} catch (err) {
wasUpdated = false;
}
suite.assertStrictEqual('updating object not in collection should fail', wasUpdated, false);
// remove() - add some bogus object to remove
var userCount1 = users.data.length;
testObject = {
first: 'aaa',
last: 'bbb',
city: 'pasadena',
state: 'ca'
}
users.insert(testObject);
suite.assertStrictEqual('delete test : insert obj to delete', userCount1 + 1, users.data.length);
users.remove(testObject);
suite.assertStrictEqual('delete test : delete', userCount1, users.data.length);
}
function testCalculateRange() {
var eic = db.addCollection("eic");
eic.ensureIndex("testid");
eic.insert({
'testid': 1,
'testString': 'hhh',
'testFloat': 5.2
}); //0
eic.insert({
'testid': 1,
'testString': 'aaa',
'testFloat': 6.2
}); //1
eic.insert({
'testid': 5,
'testString': 'zzz',
'testFloat': 7.2
}); //2
eic.insert({
'testid': 6,
'testString': 'ggg',
'testFloat': 1.2
}); //3
eic.insert({
'testid': 9,
'testString': 'www',
'testFloat': 8.2
}); //4
eic.insert({
'testid': 11,
'testString': 'yyy',
'testFloat': 4.2
}); //5
eic.insert({
'testid': 22,
'testString': 'yyz',
'testFloat': 9.2
}); //6
eic.insert({
'testid': 23,
'testString': 'm',
'testFloat': 2.2
}); //7
var rset = eic.chain();
rset.find({
'testid': 1
}); // force index to be built
// ranges are order of sequence in index not data array positions
var range = rset.calculateRange('$eq', 'testid', 22);
suite.assertEqual('calculateRange $eq', range, [6, 6]);
range = rset.calculateRange('$eq', 'testid', 1);
suite.assertEqual('calculateRange $eq multiple', range, [0, 1]);
range = rset.calculateRange('$eq', 'testid', 7);
suite.assertEqual('calculateRange $eq not found', range, [0, -1]);
range = rset.calculateRange('$gte', 'testid', 23);
suite.assertEqual('calculateRange $gte', range, [7, 7]);
// reference this new record for future evaluations
eic.insert({
'testid': 23,
'testString': 'bbb',
'testFloat': 1.9
});
range = rset.calculateRange('$gte', 'testid', 23);
suite.assertEqual('calculateRange $gte', range, [7, 8]);
range = rset.calculateRange('$gte', 'testid', 24);
suite.assertEqual('calculateRange $gte out of range', range, [0, -1]);
range = rset.calculateRange('$lte', 'testid', 5);
suite.assertEqual('calculateRange $lte', range, [0, 2]);
range = rset.calculateRange('$lte', 'testid', 1);
suite.assertEqual('calculateRange $lte', range, [0, 1]);
range = rset.calculateRange('$lte', 'testid', -1);
suite.assertEqual('calculateRange $lte out of range', range, [0, -1]);
// add another index on string property
eic.ensureIndex('testString');
rset.find({
'testString': 'asdf'
}); // force index to be built
range = rset.calculateRange('$lte', 'testString', 'ggg');
suite.assertEqual('calculateRange $lte string', range, [0, 2]); // includes record added in middle
range = rset.calculateRange('$gte', 'testString', 'm');
suite.assertEqual('calculateRange $gte string', range, [4, 8]); // offset by 1 because of record in middle
// add some float range evaluations
eic.ensureIndex('testFloat');
rset.find({
'testFloat': '1.1'
}); // force index to be built
range = rset.calculateRange('$lte', 'testFloat', 1.2);
suite.assertEqual('calculateRange $lte float', range, [0, 0]);
range = rset.calculateRange('$eq', 'testFloat', 1.111);
suite.assertEqual('calculateRange $eq not found', range, [0, -1]);
range = rset.calculateRange('$eq', 'testFloat', 8.2);
suite.assertEqual('calculateRange $eq found', range, [7, 7]); // 8th pos
range = rset.calculateRange('$gte', 'testFloat', 1.0);
suite.assertEqual('calculateRange $gt all', range, [0, 8]); // 8th pos
}
function testIndexLifecycle() {
var ilc = db.addCollection('ilc');
var hasIdx = ilc.binaryIndices.hasOwnProperty('testid');
suite.assertEqual('index lifecycle before', hasIdx, false);
ilc.ensureIndex('testid');
hasIdx = ilc.binaryIndices.hasOwnProperty('testid');
suite.assertEqual('index lifecycle created', hasIdx, true);
suite.assertEqual('index lifecycle created', ilc.binaryIndices.testid.dirty, false);
suite.assertEqual('index lifecycle created', ilc.binaryIndices.testid.values, []);
ilc.insert({
'testid': 5
});
suite.assertEqual('index lifecycle dirty', ilc.binaryIndices.testid.dirty, true);
ilc.insert({
'testid': 8
});
suite.assertEqual('index lifecycle still lazy', ilc.binaryIndices.testid.values, []);
suite.assertEqual('index lifecycle still dirty', ilc.binaryIndices.testid.dirty, true);
ilc.find({
'testid': 8
}); // should force index build
suite.assertEqual('index lifecycle built', ilc.binaryIndices.testid.dirty, false);
suite.assertEqual('index lifecycle still lazy', ilc.binaryIndices.testid.values.length, 2);
}
function testIndexes() {
var itc = db.addCollection('test', {
indices: ['testid']
});
itc.insert({
'testid': 1
});
itc.insert({
'testid': 2
});
itc.insert({
'testid': 5
});
itc.insert({
'testid': 5
});
itc.insert({
'testid': 9
});
itc.insert({
'testid': 11
});
itc.insert({
'testid': 22
});
itc.insert({
'testid': 22
});
// lte
var results = itc.find({
'testid': {
'$lte': 1
}
});
suite.assertStrictEqual('find using index $lte', results.length, 1);
results = itc.find({
'testid': {
'$lte': 22
}
});
suite.assertStrictEqual('find using index $lte', results.length, 8);
// lt
results = itc.find({
'testid': {
'$lt': 1
}
});
suite.assertStrictEqual('find using index $lt', results.length, 0);
results = itc.find({
'testid': {
'$lt': 22
}
});
suite.assertStrictEqual('find using index $lt', results.length, 6);
// eq
results = itc.find({
'testid': {
'$eq': 22
}
});
suite.assertStrictEqual('find using index $eq', results.length, 2);
// gt
results = itc.find({
'testid': {
'$gt': 22
}
});
suite.assertStrictEqual('find using index $eq', results.length, 0);
results = itc.find({
'testid': {
'$gt': 5
}
});
suite.assertStrictEqual('find using index $eq', results.length, 4);
// gte
results = itc.find({
'testid': {
'$gte': 5
}
});
suite.assertStrictEqual('find using index $gte', results.length, 6);
results = itc.find({
'testid': {
'$gte': 10
}
});
suite.assertStrictEqual('find using index $gte', results.length, 3);
}
function testResultset() {
// Resultset find
suite.assertStrictEqual('Resultset (chained) find', users.chain().find({
'age': {
'$gte': 30
}
}).where(function (obj) {
return obj.lang === 'Swedish';
}).data().length, 1);
// Resultset offset
suite.assertStrictEqual('Resultset (chained) offset', users.chain().offset(1).data().length, users.data.length - 1);
// Resultset limit
suite.assertStrictEqual('Resultset (chained) limit', users.chain().limit(2).data().length, 2);
}
/* Dynamic View Tests */
function stepEvaluateDocument() {
var view = users.addDynamicView('test');
var query = {
'age': {
'$gt': 24
}
};
view.applyFind(query);
// churn evaluateDocuments() to make sure it works right
jonas.age = 23;
users.update(jonas);
suite.assertStrictEqual("evalDoc1", view.data().length, users.data.length - 1);
jonas.age = 30;
users.update(jonas);
suite.assertStrictEqual("evalDoc2", view.data().length, users.data.length);
jonas.age = 23;
users.update(jonas);
suite.assertStrictEqual("evalDoc3", view.data().length, users.data.length - 1);
jonas.age = 30;
users.update(jonas);
suite.assertStrictEqual("evalDoc4", view.data().length, users.data.length);
// assert set equality of docArrays irrelevant of sort/sequence
var result1 = users.find(query).sort(docCompare);
var result2 = view.data().sort(docCompare);
result1.forEach(function (obj) {
delete obj.meta
});
result2.forEach(function (obj) {
delete obj.meta
});
suite.assertEqual('Result data Equality', result1, result2);
suite.assertNotStrictEqual('Strict Equality', users.find(query), view.data());
suite.assertEqual('View data equality', view.resultset, view.resultset.copy());
suite.assertNotStrictEqual('View data copy strict equality', view.resultset, view.resultset.copy());
return view;
}
// make sure view persistence works as expected
function stepDynamicViewPersistence() {
var query = {
'age': {
'$gt': 24
}
};
// set up a persistent dynamic view with sort
var pview = users.addDynamicView('test2', true);
pview.applyFind(query);
pview.applySimpleSort("age");
// the dynamic view depends on an internal resultset
// the persistent dynamic view also depends on an internal resultdata data array
// filteredrows should be applied immediately to resultset will be lazily built into resultdata later when data() is called
suite.assertStrictEqual("dynamic view initialization 1", pview.resultset.filteredrows.length, 3);
suite.assertStrictEqual("dynamic view initialization 2", pview.resultdata.length, 0);
// compare how many documents are in results before adding new ones
var pviewResultsetLenBefore = pview.resultset.filteredrows.length;
users.insert({
name: 'abc',
age: 21,
lang: 'English'
});
users.insert({
name: 'def',
age: 25,
lang: 'English'
});
// now see how many are in resultset (without rebuilding persistent view)
var pviewResultsetLenAfter = pview.resultset.filteredrows.length;
// only one document should have been added to resultset (1 was filtered out)
suite.assertStrictEqual("dv resultset is 'set' valid", pviewResultsetLenBefore + 1, pviewResultsetLenAfter);
// Test sorting and lazy build of resultdata
// retain copy of internal resultset's filteredrows before lazy sort
var frcopy = pview.resultset.filteredrows.slice();
pview.data();
// now make a copy of internal result's filteredrows after lazy sort
var frcopy2 = pview.resultset.filteredrows.slice();
// verify filteredrows logically matches resultdata (irrelevant of sort)
for (var idxFR = 0; idxFR < frcopy2.length; idxFR++) {
suite.assertEqual("dynamic view resultset/resultdata consistency", pview.resultdata[idxFR], pview.collection.data[frcopy2[idxFR]]);
}
// now verify they are not exactly equal (verify sort moved stuff)
suite.assertNotEqual('dynamic view sort', frcopy, frcopy2);
}
function testDynamicView() {
var view = stepEvaluateDocument();
stepDynamicViewPersistence();
}
function duplicateItemFoundOnIndex() {
var test = db.addCollection('nodupes', ['index']);
var item = test.insert({
index: 'key',
a: 1
});
var results = test.find({
index: 'key'
});
suite.assertStrictEqual('one result exists', results.length, 1);
suite.assertStrictEqual('the correct result is returned', results[0].a, 1);
item.a = 2;
test.update(item);
results = test.find({
index: 'key'
});
suite.assertStrictEqual('one result exists', results.length, 1);
suite.assertStrictEqual('the correct result is returned', results[0].a, 2);
}
function testEmptyTableWithIndex() {
var itc = db.addCollection('test', ['testindex']);
var resultsNoIndex = itc.find({
'testid': 2
});
suite.assertStrictEqual('no results found', resultsNoIndex.length, 0);
var resultsWithIndex = itc.find({
'testindex': 4
});
suite.assertStrictEqual('no results found', resultsWithIndex.length, 0);
}
function testAnonym() {
var coll = db.anonym([{
name: 'joe'
}, {
name: 'jack'
}], ['name']);
suite.assertEqual('Anonym collection', coll.data.length, 2);
suite.assertEqual('Collection not found', db.getCollection('anonym'), null);
coll.name = 'anonym';
db.loadCollection(coll);
suite.assertEqual('Anonym collection loaded', !!db.getCollection('anonym'), true);
coll.clear();
suite.assertEqual('No data after coll.clear()', 0, coll.data.length);
}
function testCollections() {
var db = new loki('testCollections');
db.name = 'testCollections';
suite.assertEqual('DB name', db.getName(), 'testCollections');
var t = db.addCollection('test1', {
transactional: true
});
db.addCollection('test2');
suite.assertThrows('Throw error on wrong remove', function () {
t.remove('foo');
}, Error);
suite.assertThrows('Throw error on non-synced doc', function () {
t.remove({
name: 'joe'
});
}, Error);
suite.assertEqual('List collections', db.listCollections().length, 2);
t.clear();
var users = [{
name: 'joe'
}, {
name: 'dave'
}];
t.insert(users);
suite.assertEqual('2 docs after array insert', 2, t.data.length);
t.remove(users);
suite.assertEqual('0 docs after array remove', 0, t.data.length);
function TestError() {}
TestError.prototype = new Error;
db.autosaveEnable();
db.on('close', function () {
throw new TestError;
});
suite.assertThrows('Throw error on purpose on close', function () {
db.close(function () {
return;
});
}, TestError);
}
/* Main Test */
populateTestData();
testCoreMethods();
testCalculateRange();
testIndexes();
testIndexLifecycle();
testResultset();
testDynamicView();
duplicateItemFoundOnIndex();
testEmptyTableWithIndex();
testAnonym();
testCollections();
suite.report();