From 9eea4b675c4197bcd1c2351b835682ddd04b0228 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 8 Sep 2024 20:34:00 +1000 Subject: [PATCH] perf: compute intersection over indices (#438) * perf: compute intersection over indices * chore: dont use ||= syntax * chore: fix lint errors * fix: dont set _graphs = false * chore: explicitly add truthiness test for graphs * chore: add docs * chore: fix lint errors * perf: remove unecessary lookups --- src/N3Store.js | 39 +++++++++++++++++++++++++++++++++++++++ test/N3Store-test.js | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/N3Store.js b/src/N3Store.js index 46236cba..10df07a6 100644 --- a/src/N3Store.js +++ b/src/N3Store.js @@ -17,6 +17,34 @@ function merge(target, source, depth = 4) { return target; } +/** + * Determines the intersection of the `_graphs` index s1 and s2. + * s1 and s2 *must* belong to Stores that share an `_entityIndex`. + * + * False is returned when there is no intersection; this should + * *not* be set as the value for an index. + */ +function intersect(s1, s2, depth = 4) { + let target = false; + + for (const key in s1) { + if (key in s2) { + const intersection = depth === 0 ? null : intersect(s1[key], s2[key], depth - 1); + if (intersection !== false) { + target = target || Object.create(null); + target[key] = intersection; + } + // Depth 3 is the 'subjects', 'predicates' and 'objects' keys. + // If the 'subjects' index is empty, so will the 'predicates' and 'objects' index. + else if (depth === 3) { + return false; + } + } + } + + return target; +} + // ## Constructor export class N3EntityIndex { constructor(options = {}) { @@ -901,7 +929,18 @@ export default class N3Store { const store = new N3Store({ entityIndex: this._entityIndex }); store._graphs = merge(Object.create(null), this._graphs); store._size = this._size; + return store; } + else if ((other instanceof N3Store) && this._entityIndex === other._entityIndex) { + const store = new N3Store({ entityIndex: this._entityIndex }); + const graphs = intersect(other._graphs, this._graphs); + if (graphs) { + store._graphs = graphs; + store._size = null; + } + return store; + } + return this.filter(quad => other.has(quad)); } diff --git a/test/N3Store-test.js b/test/N3Store-test.js index 5d9403db..fc750745 100644 --- a/test/N3Store-test.js +++ b/test/N3Store-test.js @@ -2030,7 +2030,7 @@ describe('Store', () => { const matrix = [true, false, 'instantiated'].flatMap(match => [true, false].map(share => [match, share])); describe.each(matrix)('RDF/JS Dataset Methods [DatasetCoreAndReadableStream: %s] [sharedIndex: %s]', (match, shareIndex) => { - let q, store, store1, store2, store3, storeg, storeb, empty, options; + let q, store, store1, store2, store3, store4, storeg, storeb, empty, options; beforeEach(() => { options = shareIndex ? { entityIndex: new EntityIndex() } : {}; @@ -2046,6 +2046,7 @@ describe('Store', () => { store1 = new Store([q[0], q[1]], options); store2 = new Store([q[0], q[2]], options); store3 = new Store([q[0], q[3]], options); + store4 = new Store([new Quad(new NamedNode('a'), new NamedNode('b'), new NamedNode('c'))], options); if (match) { empty = store2.match(new NamedNode('sn')); @@ -2137,8 +2138,40 @@ describe('Store', () => { expect(store2.size).toEqual(2); expect(store.size).toEqual(1); + const stores = [store, store1, store2, store3, store4, storeb, storeg, empty]; + for (const s1 of stores) { + for (const s2 of stores) { + expect(s1.intersection(s2).size).toBeLessThanOrEqual(s1.size); + expect(s1.intersection(s2).size).toBeLessThanOrEqual(s2.size); + expect(s1.intersection(s2)._graphs).toBeTruthy(); + expect(s1.intersection(s2).equals(s2.intersection(s1))); + expect(s1.union(s2).intersection(s1).equals(s1)); + expect(s1.intersection(s2).union(s1).equals(s1)); + expect(new Store([...s1.union(s2).intersection(s1)]).equals(new Store([...s1]))); + expect(new Store([...s1.intersection(s2).union(s1)]).equals(new Store([...s2]))); + + const newStore = s1.intersection(s2); + const size = newStore.size; + newStore.add(new Quad(new NamedNode('mys1'), new NamedNode('myp1'), new NamedNode('myo1'))); + expect(newStore.size).toBe(size + 1); + } + } + expect(store.intersection(store).size).toEqual(1); expect(store2.intersection(store2).size).toEqual(2); + expect(storeg.intersection(store).size).toBe(0); + expect(store.intersection(storeg).size).toBe(0); + expect(storeg.intersection(storeb).size).toBe(1); + expect(store.intersection(storeb).size).toBe(1); + expect(store.intersection(store1).size).toBe(1); + expect(store.intersection(store3).size).toBe(1); + expect(store.intersection(store2).size).toBe(1); + expect(empty.intersection(store1).size).toBe(0); + expect(empty.intersection(store2).size).toBe(0); + expect(store2.intersection(store1).size).toBe(1); + expect(store1.intersection(store2).size).toBe(1); + expect(store1.intersection(storeb).size).toBe(1); + expect(storeb.intersection(store1).size).toBe(1); }); });