From 8308ddaf74b60ec56902b511b8285291e90b8c2a Mon Sep 17 00:00:00 2001 From: maartenvandenbrande Date: Tue, 23 Apr 2024 12:17:55 +0200 Subject: [PATCH] Added incremental optional operator --- .../config/query-operation/actors.json | 1 + .../config/rdf-join/actors-optional.json | 10 +- .../.npmignore | 0 .../README.md | 31 + .../ActorRdfJoinIncrementalOptionalHash.ts | 54 ++ .../lib/IncrementalOptionalHash.ts | 160 +++ .../lib/index.ts | 1 + .../package.json | 47 + ...ctorRdfJoinIncrementalOptionalHash-test.ts | 916 ++++++++++++++++++ 9 files changed, 1218 insertions(+), 2 deletions(-) create mode 100644 packages/actor-rdf-join-incremental-optional-hash/.npmignore create mode 100644 packages/actor-rdf-join-incremental-optional-hash/README.md create mode 100644 packages/actor-rdf-join-incremental-optional-hash/lib/ActorRdfJoinIncrementalOptionalHash.ts create mode 100644 packages/actor-rdf-join-incremental-optional-hash/lib/IncrementalOptionalHash.ts create mode 100644 packages/actor-rdf-join-incremental-optional-hash/lib/index.ts create mode 100644 packages/actor-rdf-join-incremental-optional-hash/package.json create mode 100644 packages/actor-rdf-join-incremental-optional-hash/test/ActorRdfJoinIncrementalOptionalHash-test.ts diff --git a/engines/config-query-sparql-incremental/config/query-operation/actors.json b/engines/config-query-sparql-incremental/config/query-operation/actors.json index d970a073..9f114fef 100644 --- a/engines/config-query-sparql-incremental/config/query-operation/actors.json +++ b/engines/config-query-sparql-incremental/config/query-operation/actors.json @@ -9,6 +9,7 @@ "icqsi:config/query-operation/actors/query/quadpattern.json", "ccqs:config/query-operation/actors/query/union.json", "ccqs:config/query-operation/actors/query/minus.json", + "ccqs:config/query-operation/actors/query/leftjoin.json", "ccqs:config/query-operation/actors/query/values.json", "ccqs:config/query-operation/actors/query/path-alt.json", diff --git a/engines/config-query-sparql-incremental/config/rdf-join/actors-optional.json b/engines/config-query-sparql-incremental/config/rdf-join/actors-optional.json index b7de3988..88171214 100644 --- a/engines/config-query-sparql-incremental/config/rdf-join/actors-optional.json +++ b/engines/config-query-sparql-incremental/config/rdf-join/actors-optional.json @@ -1,10 +1,16 @@ { "@context": [ - "https://linkedsoftwaredependencies.org/bundles/npm/@comunica/runner/^2.0.0/components/context.jsonld" + "https://linkedsoftwaredependencies.org/bundles/npm/@comunica/runner/^2.0.0/components/context.jsonld", + + "https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-rdf-join-incremental-optional-hash/^1.0.0/components/context.jsonld" ], "@id": "urn:comunica:default:Runner", "@type": "Runner", "actors": [ - + { + "@id": "urn:comunica:default:rdf-join/actors#incremental-optional-hash", + "@type": "ActorRdfJoinIncrementalOptionalHash", + "mediatorJoinSelectivity": { "@id": "urn:comunica:default:rdf-join-selectivity/mediators#main" } + } ] } diff --git a/packages/actor-rdf-join-incremental-optional-hash/.npmignore b/packages/actor-rdf-join-incremental-optional-hash/.npmignore new file mode 100644 index 00000000..e69de29b diff --git a/packages/actor-rdf-join-incremental-optional-hash/README.md b/packages/actor-rdf-join-incremental-optional-hash/README.md new file mode 100644 index 00000000..d1dd8bb9 --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/README.md @@ -0,0 +1,31 @@ +# Incremunica Incremental Optional Hash RDF Join Actor + +[![npm version](https://badge.fury.io/js/@incremunica%2Factor-rdf-join-incremental-optional-hash.svg)](https://badge.fury.io/js/@incremunica%2Factor-rdf-join-incremental-optional-hash) + +An Incremunica Optional Hash RDF Join Actor. + +## Install + +```bash +$ yarn add @incremunica/actor-rdf-join-incremental-optional-hash +``` + +## Configure + +After installing, this package can be added to your engine's configuration as follows: +```text +{ + "@context": [ + ... + "https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-rdf-join-incremental-optional-hash/^1.0.0/components/context.jsonld" + ], + "actors": [ + ... + { + "@id": "urn:comunica:default:rdf-join/actors#incremental-optional-hash", + "@type": "ActorRdfJoinIncrementalOptionalHash", + "mediatorJoinSelectivity": { "@id": "urn:comunica:default:rdf-join-selectivity/mediators#main" } + } + ] +} +``` diff --git a/packages/actor-rdf-join-incremental-optional-hash/lib/ActorRdfJoinIncrementalOptionalHash.ts b/packages/actor-rdf-join-incremental-optional-hash/lib/ActorRdfJoinIncrementalOptionalHash.ts new file mode 100644 index 00000000..882b7c94 --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/lib/ActorRdfJoinIncrementalOptionalHash.ts @@ -0,0 +1,54 @@ +import type { IActionRdfJoin, IActorRdfJoinArgs, IActorRdfJoinOutputInner } from '@comunica/bus-rdf-join'; +import { ActorRdfJoin } from '@comunica/bus-rdf-join'; +import type { IMediatorTypeJoinCoefficients } from '@comunica/mediatortype-join-coefficients'; +import type { MetadataBindings } from '@comunica/types'; +import type { BindingsStream } from '@incremunica/incremental-types'; +import { IncrementalOptionalHash } from './IncrementalOptionalHash'; + +/** + * An Incremunica Optional Hash RDF Join Actor. + */ +export class ActorRdfJoinIncrementalOptionalHash extends ActorRdfJoin { + public constructor(args: IActorRdfJoinArgs) { + super(args, { + logicalType: 'optional', + physicalName: 'hash', + limitEntries: 2, + canHandleUndefs: false, + }); + } + + protected async getOutput(action: IActionRdfJoin): Promise { + const metadatas = await ActorRdfJoin.getMetadatas(action.entries); + const variables = ActorRdfJoin.overlappingVariables(metadatas); + const join = new IncrementalOptionalHash( + action.entries[0].output.bindingsStream, + action.entries[1].output.bindingsStream, + entry => ActorRdfJoinIncrementalOptionalHash.hash(entry, variables), + ActorRdfJoin.joinBindings, + ); + return { + result: { + type: 'bindings', + bindingsStream: join, + metadata: async() => await this.constructResultMetadata( + action.entries, + await ActorRdfJoin.getMetadatas(action.entries), + action.context, + ), + }, + }; + } + + protected async getJoinCoefficients( + action: IActionRdfJoin, + metadatas: MetadataBindings[], + ): Promise { + return { + iterations: 0, + persistedItems: 0, + blockingItems: 0, + requestTime: 0, + }; + } +} diff --git a/packages/actor-rdf-join-incremental-optional-hash/lib/IncrementalOptionalHash.ts b/packages/actor-rdf-join-incremental-optional-hash/lib/IncrementalOptionalHash.ts new file mode 100644 index 00000000..ee122646 --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/lib/IncrementalOptionalHash.ts @@ -0,0 +1,160 @@ +import type { Bindings as BindingsFactoryBindings } from '@incremunica/incremental-bindings-factory'; +import { BindingsFactory } from '@incremunica/incremental-bindings-factory'; +import { IncrementalInnerJoin } from '@incremunica/incremental-inner-join'; +import type { Bindings, BindingsStream } from '@incremunica/incremental-types'; + +export class IncrementalOptionalHash extends IncrementalInnerJoin { + private readonly rightMemory: Map = new Map(); + private readonly leftMemory: Map = new Map(); + private activeElement: Bindings | null = null; + private otherArray: Bindings[] = []; + private index = 0; + private readonly funHash: (entry: Bindings) => string; + private prependArray: boolean; + private appendArray: boolean; + private readonly bindingsFactory = new BindingsFactory(); + + public constructor( + left: BindingsStream, + right: BindingsStream, + funHash: (entry: Bindings) => string, + funJoin: (...bindings: Bindings[]) => Bindings | null, + ) { + super(left, right, funJoin); + this.funHash = funHash; + } + + protected _cleanup(): void { + this.leftMemory.clear(); + this.rightMemory.clear(); + this.activeElement = null; + } + + protected hasResults(): boolean { + return !this.leftIterator.done || + !this.rightIterator.done || + this.activeElement !== null; + } + + private addOrDeleteFromMemory(item: Bindings, hash: string, memory: Map): boolean { + let array = memory.get(hash); + if (item.diff) { + if (array === undefined) { + array = []; + memory.set(hash, array); + } + array.push(item); + return true; + } + + if (array === undefined) { + return false; + } + + if (array.length < 2 && array[0].equals(item)) { + memory.delete(hash); + return true; + } + + const index = array.findIndex((bindings: Bindings) => item.equals(bindings)); + if (index !== -1) { + array[index] = array[array.length - 1]; + array.pop(); + return true; + } + return false; + } + + public read(): Bindings | null { + // eslint-disable-next-line no-constant-condition + while (true) { + if (this.ended) { + return null; + } + + // There is an active element + if (this.activeElement !== null || this.appendArray) { + if (this.index === this.otherArray.length) { + if (this.prependArray) { + this.prependArray = false; + this.index = 0; + continue; + } + if (this.appendArray && this.activeElement !== null) { + this.index = 0; + this.activeElement = null; + continue; + } + this.appendArray = false; + this.index = 0; + this.activeElement = null; + continue; + } + + let resultingBindings = null; + if (this.prependArray) { + // We need to delete the bindings with no optional bindings + resultingBindings = this.bindingsFactory.fromBindings( this.otherArray[this.index]); + resultingBindings.diff = false; + } else if (this.activeElement === null) { + // If this.activeElement is null, then appendArray is true + // we need to add the bindings with no optional bindings + resultingBindings = this.bindingsFactory.fromBindings( this.otherArray[this.index]); + resultingBindings.diff = true; + } else { + // Otherwise merge bindings + resultingBindings = this.funJoin(this.activeElement, this.otherArray[this.index]); + } + + this.index++; + + if (resultingBindings !== null) { + return resultingBindings; + } + continue; + } + + if (!this.hasResults()) { + this._end(); + } + + let item = this.rightIterator.read(); + if (item !== null) { + const hash = this.funHash(item); + const rightMemEl = this.rightMemory.get(hash); + if (this.addOrDeleteFromMemory(item, hash, this.rightMemory)) { + const otherArray = this.leftMemory.get(hash); + if (otherArray !== undefined) { + if (item.diff && (rightMemEl === undefined || rightMemEl.length === 0)) { + this.prependArray = true; + } + if (!item.diff && this.rightMemory.get(hash)?.length === 1) { + this.appendArray = true; + } + this.activeElement = item; + this.otherArray = otherArray; + } + } + continue; + } + + item = this.leftIterator.read(); + if (item !== null) { + const hash = this.funHash(item); + if (this.addOrDeleteFromMemory(item, hash, this.leftMemory)) { + const otherArray = this.rightMemory.get(hash); + if (otherArray !== undefined) { + this.activeElement = item; + this.otherArray = otherArray; + } else { + return item; + } + } + continue; + } + + this.readable = false; + return null; + } + } +} diff --git a/packages/actor-rdf-join-incremental-optional-hash/lib/index.ts b/packages/actor-rdf-join-incremental-optional-hash/lib/index.ts new file mode 100644 index 00000000..36a54704 --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/lib/index.ts @@ -0,0 +1 @@ +export * from './ActorRdfJoinIncrementalOptionalHash'; diff --git a/packages/actor-rdf-join-incremental-optional-hash/package.json b/packages/actor-rdf-join-incremental-optional-hash/package.json new file mode 100644 index 00000000..25d88e0e --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/package.json @@ -0,0 +1,47 @@ +{ + "name": "@incremunica/actor-rdf-join-incremental-optional-hash", + "version": "1.2.2", + "description": "An incremental-optional-hash rdf-join actor", + "lsd:module": true, + "main": "lib/index.js", + "typings": "lib/index", + "repository": { + "type": "git", + "url": "https://github.com/maartyman/incremunica.git", + "directory": "packages/actor-rdf-join-incremental-optional-hash" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "keywords": [ + "comunica", + "actor", + "rdf-join", + "incremental-optional-hash" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/maartyman/incremunica/issues" + }, + "homepage": "https://maartyman.github.io/incremunica/", + "files": [ + "components", + "lib/**/*.d.ts", + "lib/**/*.js", + "lib/**/*.js.map" + ], + "dependencies": { + "@comunica/bus-rdf-join": "^2.10.1", + "@comunica/mediatortype-join-coefficients": "^2.10.0", + "@comunica/types": "^2.10.0", + "@incremunica/incremental-bindings-factory": "^1.2.2", + "@incremunica/incremental-inner-join": "^1.2.2", + "@incremunica/incremental-types": "^1.2.2" + }, + "scripts": { + "build": "npm run build:ts && npm run build:components", + "build:ts": "node \"../../node_modules/typescript/bin/tsc\"", + "build:components": "componentsjs-generator" + } +} diff --git a/packages/actor-rdf-join-incremental-optional-hash/test/ActorRdfJoinIncrementalOptionalHash-test.ts b/packages/actor-rdf-join-incremental-optional-hash/test/ActorRdfJoinIncrementalOptionalHash-test.ts new file mode 100644 index 00000000..41cd1326 --- /dev/null +++ b/packages/actor-rdf-join-incremental-optional-hash/test/ActorRdfJoinIncrementalOptionalHash-test.ts @@ -0,0 +1,916 @@ +import { BindingsFactory } from '@incremunica/incremental-bindings-factory'; +import type { IActionRdfJoin } from '@comunica/bus-rdf-join'; +import { ActorRdfJoin } from '@comunica/bus-rdf-join'; +import type { IActionRdfJoinSelectivity, IActorRdfJoinSelectivityOutput } from '@comunica/bus-rdf-join-selectivity'; +import type { Actor, IActorTest, Mediator } from '@comunica/core'; +import { ActionContext, Bus } from '@comunica/core'; +import type { IQueryOperationResultBindings, Bindings, IActionContext } from '@comunica/types'; +import type * as RDF from '@rdfjs/types'; +import arrayifyStream from 'arrayify-stream'; +import {ArrayIterator} from 'asynciterator'; +import { DataFactory } from 'rdf-data-factory'; +import '@incremunica/incremental-jest'; +import { MetadataValidationState } from '@comunica/metadata'; +import {ActorRdfJoinIncrementalOptionalHash} from "../lib/ActorRdfJoinIncrementalOptionalHash"; + +const DF = new DataFactory(); +const BF = new BindingsFactory(); + +describe('ActorRdfJoinIncrementalOptionalHash', () => { + let bus: any; + let context: IActionContext; + + beforeEach(() => { + bus = new Bus({ name: 'bus' }); + context = new ActionContext(); + }); + + describe('The ActorRdfJoinIncrementalOptionalHash module', () => { + it('should be a function', () => { + expect(ActorRdfJoinIncrementalOptionalHash).toBeInstanceOf(Function); + }); + + it('should be a ActorRdfJoinIncrementalOptionalHash constructor', () => { + expect(new ( ActorRdfJoinIncrementalOptionalHash)({ name: 'actor', bus })).toBeInstanceOf(ActorRdfJoinIncrementalOptionalHash); + expect(new ( ActorRdfJoinIncrementalOptionalHash)({ name: 'actor', bus })).toBeInstanceOf(ActorRdfJoin); + }); + + it('should not be able to create new ActorRdfJoinIncrementalOptionalHash objects without \'new\'', () => { + expect(() => { ( ActorRdfJoinIncrementalOptionalHash)(); }).toThrow(); + }); + }); + + describe('An ActorRdfJoinIncrementalOptionalHash instance', () => { + let mediatorJoinSelectivity: Mediator< + Actor, + IActionRdfJoinSelectivity, IActorTest, IActorRdfJoinSelectivityOutput>; + let actor: ActorRdfJoinIncrementalOptionalHash; + let action: IActionRdfJoin; + let variables0: RDF.Variable[]; + let variables1: RDF.Variable[]; + + beforeEach(() => { + mediatorJoinSelectivity = { + mediate: async() => ({ selectivity: 1 }), + }; + actor = new ActorRdfJoinIncrementalOptionalHash({ name: 'actor', bus, mediatorJoinSelectivity }); + variables0 = []; + variables1 = []; + action = { + type: 'optional', + entries: [ + { + output: { + bindingsStream: new ArrayIterator([], { autoStart: false }), + metadata: async() => ({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 4 }, + pageSize: 100, + requestTime: 10, + canContainUndefs: false, + variables: variables0, + }), + type: 'bindings', + }, + operation: {}, + }, + { + output: { + bindingsStream: new ArrayIterator([], { autoStart: false }), + metadata: async() => ({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 5 }, + pageSize: 100, + requestTime: 20, + canContainUndefs: false, + variables: variables1, + }), + type: 'bindings', + }, + operation: {}, + }, + ], + context, + }; + }); + + describe('should test', () => { + afterEach(() => { + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + }); + + it('should only handle 2 streams', () => { + action.entries.push( {}); + return expect(actor.test(action)).rejects.toBeTruthy(); + }); + + it('should fail on undefs in left stream', () => { + action.entries[0].output.metadata = () => Promise.resolve({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 4 }, + canContainUndefs: true, + variables: [], + }); + return expect(actor.test(action)).rejects + .toThrow(new Error('Actor actor can not join streams containing undefs')); + }); + + it('should fail on undefs in right stream', () => { + action.entries[1].output.metadata = () => Promise.resolve({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 4 }, + canContainUndefs: true, + variables: [], + }); + return expect(actor.test(action)).rejects + .toThrow(new Error('Actor actor can not join streams containing undefs')); + }); + + it('should fail on undefs in left and right stream', () => { + action.entries[0].output.metadata = () => Promise.resolve({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 4 }, + canContainUndefs: true, + variables: [], + }); + action.entries[1].output.metadata = () => Promise.resolve({ + state: new MetadataValidationState(), + cardinality: { type: 'estimate', value: 4 }, + canContainUndefs: true, + variables: [], + }); + return expect(actor.test(action)).rejects + .toThrow(new Error('Actor actor can not join streams containing undefs')); + }); + + it('should generate correct test metadata', async() => { + await expect(actor.test(action)).resolves.toHaveProperty('iterations', 0); + }); + }); + + it('should generate correct metadata', async() => { + await actor.run(action).then(async(result: IQueryOperationResultBindings) => { + await expect(( result).metadata()).resolves.toHaveProperty('cardinality', + { type: 'estimate', + value: (await ( action.entries[0].output).metadata()).cardinality.value * + (await ( action.entries[1].output).metadata()).cardinality.value }); + + await expect(result.bindingsStream).toEqualBindingsStream([]); + }); + }); + + it('should return an empty stream for empty input', () => { + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + expect((await output.metadata()).variables).toEqual([]); + await expect(output.bindingsStream).toEqualBindingsStream([]); + }); + }); + + it('should return null on read if join has ended', () => { + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + expect((await output.metadata()).variables).toEqual([]); + await expect(output.bindingsStream).toEqualBindingsStream([]); + expect(output.bindingsStream.ended).toBeTruthy(); + expect(output.bindingsStream.read()).toBeNull(); + }); + }); + + it('should end after both streams are ended and no new elements can be generated', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('3') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('4') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + output.bindingsStream.read() + await new Promise((resolve) => setTimeout(() => resolve(), 100)); + expect(action.entries[0].output.bindingsStream.ended).toBeTruthy(); + expect(action.entries[1].output.bindingsStream.ended).toBeTruthy(); + expect(output.bindingsStream.ended).toBeFalsy(); + await arrayifyStream(output.bindingsStream) + await new Promise((resolve) => setTimeout(() => resolve(), 100)); + expect(output.bindingsStream.ended).toBeTruthy(); + }); + }); + + it('should join bindings with matching values', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('b'), DF.literal('b') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('c'), DF.literal('c') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + await expect(output.bindingsStream).toEqualBindingsStream([ + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('b'), DF.literal('b') ], + [ DF.variable('c'), DF.literal('c') ], + ]), + ]); + }); + }); + + it('should return bindings from input 1 if incompatible values', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('b'), DF.literal('b') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('d') ], + [ DF.variable('c'), DF.literal('c') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + await expect(output.bindingsStream).toEqualBindingsStream([ + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('b'), DF.literal('b') ], + ]), + ]); + }); + }); + + it('should join multiple bindings', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('3') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('3') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('3') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('4') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('5') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('0') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('0') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('c'), DF.literal('7') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('5') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('3') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('3') ], + [ DF.variable('c'), DF.literal('5') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('3') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('3') ], + [ DF.variable('c'), DF.literal('7') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('4') ], + [ DF.variable('c'), DF.literal('7') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + expect((await arrayifyStream(output.bindingsStream))).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings (left) right first', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings (left) left first', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + null, + null, + null, + null, + null, + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('3') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings (right) right first', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings (right) left first', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + null, + null, + null, + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ], false), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings that are not in the result set (left)', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('b') ], + ], false), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('c'), DF.literal('c') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should join multiple bindings with negative bindings that are not in the result set (right)', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('b') ], + ]), + ]); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('a') ], + [ DF.variable('c'), DF.literal('c') ], + ], false), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('b') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('2') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('6') ], + ]), + ]; + expect((await output.metadata()).variables).toEqual([ DF.variable('a'), DF.variable('b'), DF.variable('c') ]); + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + expect(await arrayifyStream(output.bindingsStream)).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + it('should be symmetric', () => { + // Clean up the old bindings + action.entries.forEach(output => output.output?.bindingsStream?.destroy()); + + action.entries[0].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + ]), + ]).transform({ + transform: (item: Bindings, done: () => void, push: (i: RDF.Bindings) => void) => { + push(item); + setTimeout(() => { + push(item); + done(); + }, 100) + } + }); + variables0 = [ DF.variable('a'), DF.variable('b') ]; + action.entries[1].output.bindingsStream = new ArrayIterator([ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + ]); + variables1 = [ DF.variable('a'), DF.variable('c') ]; + return actor.run(action).then(async(output: IQueryOperationResultBindings) => { + const expected = [ + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + BF.bindings([ + [ DF.variable('a'), DF.literal('1') ], + [ DF.variable('b'), DF.literal('2') ], + [ DF.variable('c'), DF.literal('4') ], + ]), + ]; + // Mapping to string and sorting since we don't know order (well, we sort of know, but we might not!) + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + expect((await arrayifyStream(output.bindingsStream))).toBeIsomorphicBindingsArray( + expected + ); + }); + }); + + }); +});