test/generic/Index.spec.js
describe('Index', () => {
const backends = [
TestRunner.nativeRunner('indexTest', 1, jdb => jdb.createObjectStore('testStore')),
TestRunner.volatileRunner(() => JungleDB.createVolatileObjectStore())
];
backends.forEach(/** @type {TestRunner} */ runner => {
it(`can access longer keypaths (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('testIndex', 'val', {keyEncoding: JungleDB.NUMBER_ENCODING});
st.createIndex('testIndex2', ['a', 'b'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
await st.put('test', {'val': 123, 'a': {'b': 1}});
expect((await st.get('test')).val).toBe(123);
expect(await st.keys(Query.eq('testIndex', 123))).toEqual(new Set(['test']));
expect(await st.keys(Query.eq('testIndex2', 1))).toEqual(new Set(['test']));
expect(await st.values(Query.eq('testIndex', 123))).toEqual([{'val': 123, 'a': {'b': 1}}]);
expect(await st.values(Query.eq('testIndex2', 1))).toEqual([{'val': 123, 'a': {'b': 1}}]);
expect(await st.index('testIndex').maxKeys()).toEqual(new Set(['test']));
expect(await st.index('testIndex').maxValues()).toEqual([{'val': 123, 'a': {'b': 1}}]);
expect(await st.index('testIndex').minKeys()).toEqual(new Set(['test']));
expect(await st.index('testIndex').minValues()).toEqual([{'val': 123, 'a': {'b': 1}}]);
await runner.destroy();
})().then(done, done.fail);
});
it(`can handle values not conforming to index (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['treeInfo', 'depth'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
// Fill object store.
const expectedJs = new Set();
for (let i = 0; i < 10; ++i) {
for (let j = 0; j < 5; ++j) {
expectedJs.add(j);
await st.put(`${i}-${j}`, {'val': i * 10 + j, 'treeInfo': {'depth': i, 'j': j}});
await st.put('head', `${i * 10 + j}`);
}
}
const tx = st.transaction();
for (let i = 10; i < 50; ++i) {
for (let j = 0; j < 5; ++j) {
expectedJs.add(j);
await tx.put(`${i}-${j}`, {'val': i * 10 + j, 'treeInfo': {'depth': i, 'j': j}});
await tx.put('head', `${i * 10 + j}`);
}
}
await tx.commit();
// Retrieve values at specific depth.
expect(await st.get('head')).toBe(`${49 * 10 + 4}`);
const atDepth5 = await st.values(Query.eq('depth', 5));
const atDepth15 = await st.values(Query.eq('depth', 15));
let actualJs = new Set();
for (const obj of atDepth5) {
expect(obj['treeInfo']['depth']).toBe(5);
actualJs.add(obj['treeInfo']['j']);
}
expect(actualJs).toEqual(expectedJs);
actualJs = new Set();
for (const obj of atDepth15) {
expect(obj['treeInfo']['depth']).toBe(15);
actualJs.add(obj['treeInfo']['j']);
}
expect(actualJs).toEqual(expectedJs);
// Retrieve values in depth range for given j.
const range = await st.values(Query.within('depth', 5, 15));
const expectedIs = Set.from([5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
for (let j = 0; j < 5; ++j) {
const actualIs = new Set();
for (const obj of range) {
if (obj['treeInfo']['j'] === j) {
actualIs.add(obj['treeInfo']['depth']);
}
}
expect(actualIs).toEqual(expectedIs);
}
await runner.destroy();
})().then(done, done.fail);
});
it(`can handle values not conforming to index (using direct index access) (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['treeInfo', 'depth'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
// Fill object store.
const expectedJs = new Set();
for (let i = 0; i < 10; ++i) {
for (let j = 0; j < 5; ++j) {
expectedJs.add(j);
await st.put(`${i}-${j}`, {'val': i * 10 + j, 'treeInfo': {'depth': i, 'j': j}});
await st.put('head', `${i * 10 + j}`);
}
}
const tx = st.transaction();
for (let i = 10; i < 50; ++i) {
for (let j = 0; j < 5; ++j) {
expectedJs.add(j);
await tx.put(`${i}-${j}`, {'val': i * 10 + j, 'treeInfo': {'depth': i, 'j': j}});
await tx.put('head', `${i * 10 + j}`);
}
}
await tx.commit();
// Retrieve values at specific depth.
expect(await st.get('head')).toBe(`${49 * 10 + 4}`);
const atDepth5 = await st.index('depth').values(KeyRange.only(5));
const atDepth15 = await st.index('depth').values(KeyRange.only(15));
let actualJs = new Set();
for (const obj of atDepth5) {
expect(obj['treeInfo']['depth']).toBe(5);
actualJs.add(obj['treeInfo']['j']);
}
expect(actualJs).toEqual(expectedJs);
actualJs = new Set();
for (const obj of atDepth15) {
expect(obj['treeInfo']['depth']).toBe(15);
actualJs.add(obj['treeInfo']['j']);
}
expect(actualJs).toEqual(expectedJs);
// Retrieve values in depth range for given j.
const range = await st.index('depth').values(KeyRange.bound(5, 15));
const expectedIs = Set.from([5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
for (let j = 0; j < 5; ++j) {
const actualIs = new Set();
for (const obj of range) {
if (obj['treeInfo']['j'] === j) {
actualIs.add(obj['treeInfo']['depth']);
}
}
expect(actualIs).toEqual(expectedIs);
}
await runner.destroy();
})().then(done, done.fail);
});
it(`provides unique indices (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['a', 'b'], {keyEncoding: JungleDB.NUMBER_ENCODING, unique: true});
});
await st.put('test', {'val': 123, 'a': {'b': 1}});
let threw = false;
try {
await st.put('test2', {'val': 123, 'a': {'b': 1}});
} catch (e) {
threw = true;
}
expect(threw).toBe(true);
threw = false;
let tx = st.transaction();
try {
await tx.put('test2', {'val': 123, 'a': {'b': 1}});
} catch (e) {
threw = true;
}
await tx.abort();
expect(threw).toBe(true);
threw = false;
tx = st.transaction();
try {
tx.putSync('test2', {'val': 123, 'a': {'b': 1}});
await tx.commit();
} catch (e) {
threw = true;
}
expect(threw).toBe(true);
expect(tx.state).toBe(Transaction.STATE.ABORTED);
threw = false;
tx = st.transaction();
tx.putSync('test2', {'val': 124, 'a': {'b': 1}});
try {
tx.putSync('test3', {'val': 124, 'a': {'b': 1}});
} catch (e) {
threw = true;
}
await tx.abort();
expect(threw).toBe(true);
// Test that we can remove values and put them into the store again
// TODO: Fix behaviour when keys are replaced
await st.put('test2', {'val': 124, 'a': {'b': 2}});
await st.put('test3', {'val': 125, 'a': {'b': 3}});
threw = false;
tx = st.transaction();
tx.removeSync('test');
tx.removeSync('test2');
tx.removeSync('test3');
try {
tx.putSync('test4', {'val': 123, 'a': {'b': 1}});
tx.putSync('test5', {'val': 125, 'a': {'b': 3}});
} catch (e) {
threw = true;
}
await tx.abort();
expect(threw).toBe(false);
await runner.destroy();
})().then(done, done.fail);
});
it(`returns ordered results (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['a'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
await st.put('test1', {'v': 1, 'a': 123});
await st.put('test3', {'v': 3, 'a': 123});
await st.put('test2', {'v': 2, 'a': 123});
await st.put('test0', {'v': 0, 'a': 124});
let keys = await st.index('depth').keys();
let i = 1;
for (const key of keys) {
if (i === 4) i = 0;
expect(key).toBe(`test${i}`);
i++;
}
let values = await st.index('depth').values();
i = 1;
for (const value of values) {
if (i === 4) i = 0;
expect(value.v).toBe(i);
i++;
}
keys = await st.index('depth').keys(KeyRange.only(123));
i = 1;
for (const key of keys) {
expect(key).toBe(`test${i}`);
i++;
}
values = await st.index('depth').values(KeyRange.only(123));
i = 1;
for (const value of values) {
expect(value.v).toBe(i);
i++;
}
await runner.destroy();
})().then(done, done.fail);
});
it(`correctly processes limited queries (${runner.type})`, (done) => {
(async function () {
const st = await runner.init(st => {
st.createIndex('depth', ['a'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
function expectLimited(given, expected, limit) {
const size = Array.isArray(given) ? given.length : given.size;
expect(size).toBe(limit);
expect(new Set(given)).toEqual(new Set(given).intersection(new Set(expected)));
}
await st.put('test1', {'v': 1, 'a': 123});
await st.put('test3', {'v': 3, 'a': 123});
await st.put('test2', {'v': 2, 'a': 123});
await st.put('test0', {'v': 0, 'a': 124});
let keys = await st.index('depth').keys(/*query*/ null, 2);
expectLimited(keys, new Set(['test1', 'test2']), 2);
let values = await st.index('depth').values(/*query*/ null, 3);
values = values.map(o => o.v);
expectLimited(values, [1, 2, 3], 3);
keys = await st.index('depth').keys(KeyRange.only(123), 1);
expectLimited(keys, new Set(['test1']), 1);
values = await st.index('depth').values(KeyRange.only(123), 0);
values = values.map(o => o.v);
expectLimited(values, [], 0);
await runner.destroy();
})().then(done, done.fail);
});
it(`correctly constructs key streams (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['a'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
await st.put('test1', {'v': 1, 'a': 123});
await st.put('test3', {'v': 3, 'a': 123});
await st.put('test2', {'v': 2, 'a': 123});
await st.put('test0', {'v': 0, 'a': 124});
const index = st.index('depth');
let i = 1;
await index.keyStream(key => {
if (i === 4) i = 0;
expect(key).toBe(`test${i}`);
i++;
return true;
});
expect(i).toBe(1);
i = 0;
await index.keyStream(key => {
expect(key).toBe(`test${i}`);
if (i === 0) i = 4;
i--;
return true;
}, false);
expect(i).toBe(0);
i = 3;
await index.keyStream(key => {
expect(key).toBe(`test${i}`);
i--;
return true;
}, false, KeyRange.only(123));
expect(i).toBe(0);
i = 1;
await index.keyStream(key => {
expect(key).toBe(`test${i}`);
i++;
return key !== 'test3';
}, true, KeyRange.lowerBound(121, true));
expect(i).toBe(4);
await runner.destroy();
})().then(done, done.fail);
});
it(`correctly constructs value streams (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('depth', ['a'], {keyEncoding: JungleDB.NUMBER_ENCODING});
});
const index = st.index('depth');
// Value streams currently do not work reliably in LevelDB
if (typeof LevelDBBackend !== 'undefined' && runner.type === 'native') {
let threw = false;
try {
await index.valueStream(() => {});
} catch (e) {
threw = true;
}
expect(threw).toBe(true);
await runner.destroy();
return;
}
let i = 0;
await index.valueStream((value, key) => {
i++;
return true;
});
expect(i).toBe(0);
i = 0;
await index.valueStream((value, key) => {
i++;
return true;
}, false, KeyRange.only(123));
expect(i).toBe(0);
await st.put('test1', {'v': 1, 'a': 123});
await st.put('test3', {'v': 3, 'a': 123});
await st.put('test2', {'v': 2, 'a': 123});
await st.put('test0', {'v': 0, 'a': 124});
await st.get('test3');
i = 1;
await index.valueStream((value, key) => {
if (i === 4) i = 0;
expect(key).toBe(`test${i}`);
expect(value.v).toBe(i);
i++;
return true;
});
expect(i).toBe(1);
i = 0;
await index.valueStream((value, key) => {
expect(key).toBe(`test${i}`);
expect(value.v).toBe(i);
if (i === 0) i = 4;
i--;
return true;
}, false);
expect(i).toBe(0);
i = 3;
await index.valueStream((value, key) => {
expect(key).toBe(`test${i}`);
expect(value.v).toBe(i);
i--;
return true;
}, false, KeyRange.only(123));
expect(i).toBe(0);
i = 1;
await index.valueStream((value, key) => {
expect(key).toBe(`test${i}`);
expect(value.v).toBe(i);
i++;
return key !== 'test3';
}, true, KeyRange.lowerBound(121, true));
expect(i).toBe(4);
await runner.destroy();
})().then(done, done.fail);
});
});
it('only fills the index once', (done) => {
(async function () {
// Write something into an object store.
let db = new JungleDB('indexTest', 1);
let st = db.createObjectStore('testStore');
st.createIndex('depth', ['a', 'b'], {keyEncoding: JungleDB.NUMBER_ENCODING});
await db.connect();
await st.put('test', {'val': 123, 'a': {'b': 1}});
await st.put('test2', 'other');
expect(await st.index('depth').count()).toBe(1);
await db.close();
// 2nd connection
db = new JungleDB('indexTest', 1);
st = db.createObjectStore('testStore');
st.createIndex('depth', ['a', 'b']);
await db.connect();
await st.put('test', {'val': 123, 'a': {'b': 1}});
await st.put('test2', 'other');
expect(await st.index('depth').count()).toBe(1);
await db.destroy();
})().then(done, done.fail);
});
it('can fill the index on implicit upgrade', (done) => {
(async function () {
// Write something into an object store.
let db = new JungleDB('indexTest', 1);
let st = db.createObjectStore('testStore');
await db.connect();
await st.put('test', {'val': 123, 'a': {'b': 1}});
await st.put('test2', 'other');
await db.close();
// 2nd connection
db = new JungleDB('indexTest', 2);
st = db.createObjectStore('testStore', {upgradeCondition: false});
st.createIndex('depth', ['a', 'b'], {keyEncoding: JungleDB.NUMBER_ENCODING});
await db.connect();
expect(await st.index('depth').count()).toBe(1);
await db.destroy();
})().then(done, done.fail);
});
it('can fill the index on explicit upgrade', (done) => {
(async function () {
// Write something into an object store.
let db = new JungleDB('indexTest', 1);
let st = db.createObjectStore('testStore');
await db.connect();
await st.put('test', {'val': 123, 'a': {'b': 1}});
await st.put('test2', 'other');
await db.close();
// 2nd connection
db = new JungleDB('indexTest', 2);
st = db.createObjectStore('testStore', {upgradeCondition: false});
st.createIndex('depth', ['a', 'b'], {upgradeCondition: true, keyEncoding: JungleDB.NUMBER_ENCODING});
await db.connect();
expect(await st.index('depth').count()).toBe(1);
await db.destroy();
})().then(done, done.fail);
});
it('provides unique indices on combined transactions', (done) => {
(async function () {
// Write something into an object store.
let db = new JungleDB('indexTest', 1);
let st1 = db.createObjectStore('testStore');
st1.createIndex('depth', ['a', 'b'], {unique: true, keyEncoding: JungleDB.NUMBER_ENCODING});
const st2 = db.createObjectStore('testStore2');
await db.connect();
await st1.put('test', {'val': 123, 'a': {'b': 1}});
let threw = false;
const tx1 = st1.transaction();
const tx2 = st2.transaction();
try {
tx1.putSync('test2', {'val': 123, 'a': {'b': 1}});
tx2.putSync('test2', 'ok');
await JungleDB.commitCombined(tx1, tx2);
} catch (e) {
threw = true;
}
expect(threw).toBe(true);
expect(tx1.state).toBe(Transaction.STATE.ABORTED);
expect(tx2.state).toBe(Transaction.STATE.ABORTED);
await db.destroy();
})().then(done, done.fail);
});
it('supports binary key encoding', (done) => {
// Edge does not fully support binary keys (DataError in IDBKeyRange)
if (typeof window !== 'undefined' && window.navigator.userAgent.indexOf("Edge") > -1) {
done();
return;
}
// Karma breaks array buffers
spyOn(ComparisonUtils, 'isUint8Array').and.callFake(function (obj) {
if (typeof Buffer !== 'undefined' && typeof window === 'undefined' && obj instanceof Buffer) return true;
return ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer || (obj && obj.constructor && obj.constructor.name === 'ArrayBuffer');
});
(async function () {
// Write something into an object store.
let db = new JungleDB('indexTest', 1);
/** @type {ObjectStore} */
let st = db.createObjectStore('testStore');
st.createIndex('testIndex', 'val', {
lmdbKeyEncoding: JungleDB.BINARY_ENCODING,
leveldbKeyEncoding: JungleDB.BINARY_ENCODING
});
await db.connect();
await st.put('test1', {'val': new Uint8Array([1, 2, 3, 4]), 'a': {'b': 1}});
await st.put('test2', {'val': new Uint8Array([1, 2, 3, 4]), 'a': {'b': 5}});
await st.put('test3', {'val': new Uint8Array([1, 2, 3, 5]), 'a': {'b': 5}});
await st.put('test4', {'val': new Uint8Array([1, 2, 4, 4]), 'a': {'b': 6}});
expect((await st.get('test1')).val).toEqual(new Uint8Array([1, 2, 3, 4]));
expect(await st.keys(Query.eq('testIndex', new Uint8Array([1, 2, 3, 4])))).toEqual(new Set(['test1', 'test2']));
expect(await st.keys(Query.ge('testIndex', new Uint8Array([1, 2, 3, 5])))).toEqual(new Set(['test3', 'test4']));
expect(await st.index('testIndex').maxKeys()).toEqual(new Set(['test4']));
expect(await st.index('testIndex').minKeys()).toEqual(new Set(['test1', 'test2']));
await db.destroy();
})().then(done, done.fail);
});
it('supports binary key encoding (InMemory)', (done) => {
(async function () {
// Write something into an object store.
/** @type {ObjectStore} */
let st = JungleDB.createVolatileObjectStore();
st.createIndex('testIndex', 'val');
await st.put('test1', {'val': new Uint8Array([1, 2, 3, 4]), 'a': {'b': 1}});
await st.put('test2', {'val': new Uint8Array([1, 2, 3, 4]), 'a': {'b': 5}});
await st.put('test3', {'val': new Uint8Array([1, 2, 3, 5]), 'a': {'b': 5}});
await st.put('test4', {'val': new Uint8Array([1, 2, 4, 4]), 'a': {'b': 6}});
expect((await st.get('test1')).val).toEqual(new Uint8Array([1, 2, 3, 4]));
expect(await st.keys(Query.eq('testIndex', new Uint8Array([1, 2, 3, 4])))).toEqual(new Set(['test1', 'test2']));
expect(await st.keys(Query.ge('testIndex', new Uint8Array([1, 2, 3, 5])))).toEqual(new Set(['test3', 'test4']));
expect(await st.index('testIndex').maxKeys()).toEqual(new Set(['test4']));
expect(await st.index('testIndex').minKeys()).toEqual(new Set(['test1', 'test2']));
})().then(done, done.fail);
});
const backends2 = [
TestRunner.nativeRunner('indexTest', 1, jdb => jdb.createObjectStore('testStore', { keyEncoding: JDB.JungleDB.NUMBER_ENCODING })),
TestRunner.volatileRunner(() => JungleDB.createVolatileObjectStore())
];
backends2.forEach(/** @type {TestRunner} */ runner => {
it(`returns ordered results advanced (${runner.type})`, (done) => {
(async function () {
// Write something into an object store.
let st = await runner.init(st => {
st.createIndex('sender', ['sender'], { keyEncoding: JungleDB.NUMBER_ENCODING });
});
await st.put(101, {'sender': 123});
await st.put(10136, {'sender': 123});
await st.put(11315, {'sender': 123});
await st.put(1133, {'sender': 123});
await st.put(12788, {'sender': 123});
await st.put(14331, {'sender': 123});
await st.put(15861, {'sender': 123});
await st.put(2303, {'sender': 123});
await st.put(3763, {'sender': 123});
await st.put(5678, {'sender': 123});
await st.put(7311, {'sender': 123});
await st.put(8715, {'sender': 123});
// Value streams currently do not work reliably in LevelDB
if (typeof LevelDBBackend !== 'undefined' && runner.type === 'native') {
let threw = false;
try {
await st.index('sender').valueStream(() => {});
} catch (e) {
threw = true;
}
expect(threw).toBe(true);
await runner.destroy();
return;
}
let lastKey = 0;
const keys = await st.index('sender').valueStream((value, key) => {
expect(key).toBeGreaterThanOrEqual(lastKey);
lastKey = key;
return true;
}, /*ascending*/ true, JDB.KeyRange.only(123));
await runner.destroy();
})().then(done, done.fail);
});
});
});