diff --git a/engines/query-sparql/test/QuerySparql-test.ts b/engines/query-sparql/test/QuerySparql-test.ts index d3cfa9844e..b4634fca43 100644 --- a/engines/query-sparql/test/QuerySparql-test.ts +++ b/engines/query-sparql/test/QuerySparql-test.ts @@ -341,147 +341,6 @@ describe('System test: QuerySparql', () => { }); }); - describe('extension function', () => { - let funcAllow: string; - let store: Store; - let baseFunctions: Record Promise>; - let baseFunctionCreator: (functionName: RDF.NamedNode) => ((args: RDF.Term[]) => Promise) | undefined; - let quads: RDF.Quad[]; - let stringType: RDF.NamedNode; - let booleanType: RDF.NamedNode; - let integerType: RDF.NamedNode; - beforeEach(() => { - stringType = DF.namedNode('http://www.w3.org/2001/XMLSchema#string'); - booleanType = DF.namedNode('http://www.w3.org/2001/XMLSchema#boolean'); - integerType = DF.namedNode('http://www.w3.org/2001/XMLSchema#integer'); - funcAllow = 'allowAll'; - baseFunctions = { - 'http://example.org/functions#allowAll': async(args: RDF.Term[]) => DF.literal('true', booleanType), - }; - baseFunctionCreator = (functionName: RDF.NamedNode) => - async(args: RDF.Term[]) => DF.literal('true', booleanType); - store = new Store(); - quads = [ - DF.quad(DF.namedNode('ex:a'), DF.namedNode('ex:p1'), DF.literal('apple', stringType)), - DF.quad(DF.namedNode('ex:a'), DF.namedNode('ex:p2'), DF.literal('APPLE', stringType)), - DF.quad(DF.namedNode('ex:a'), DF.namedNode('ex:p3'), DF.literal('Apple', stringType)), - DF.quad(DF.namedNode('ex:a'), DF.namedNode('ex:p4'), DF.literal('aPPLE', stringType)), - ]; - store.addQuads(quads); - }); - const baseQuery = (funcName: string) => `PREFIX func: - SELECT * WHERE { - ?s ?p ?o. - FILTER (func:${funcName}(?o)) - }`; - - it('rejects when record does not match', async() => { - const context = { sources: [ store ]}; - context.extensionFunctions = baseFunctions; - await expect(engine.query(baseQuery('nonExist'), context)).rejects.toThrow('Unknown named operator'); - }); - - it('rejects when creator returns null', async() => { - const context = { sources: [ store ]}; - context.extensionFunctionCreator = () => null; - await expect(engine.query(baseQuery('nonExist'), context)).rejects.toThrow('Unknown named operator'); - }); - - it('with results and pointless custom filter given by creator', async() => { - const context = { sources: [ store ]}; - context.extensionFunctionCreator = baseFunctionCreator; - const result = await engine.query(baseQuery(funcAllow), context); - expect((await arrayifyStream(await result.execute())).length).toEqual(store.size); - }); - - it('with results and pointless custom filter given by record', async() => { - const context = { sources: [ store ]}; - context.extensionFunctions = baseFunctions; - const result = await engine.query(baseQuery(funcAllow), context); - expect((await arrayifyStream(await result.execute())).length).toEqual(4); - }); - - it('with results but all filtered away', async() => { - const context = { sources: [ store ]}; - context.extensionFunctionCreator = () => () => - DF.literal('false', booleanType); - const result = await engine.query(baseQuery('rejectAll'), context); - expect(await arrayifyStream(await result.execute())).toEqual([]); - }); - - it('throws error when supplying both record and creator', async() => { - const context = { sources: [ store ]}; - context.extensionFunctions = baseFunctions; - context.extensionFunctionCreator = baseFunctionCreator; - await expect(engine.query(baseQuery(funcAllow), context)).rejects - .toThrow('Illegal simultaneous usage of extensionFunctionCreator and extensionFunctions in context'); - }); - - it('handles complex queries with BIND to', async() => { - const context = { sources: [ store ]}; - const complexQuery = `PREFIX func: - SELECT ?caps WHERE { - ?s ?p ?o. - BIND (func:to-upper-case(?o) AS ?caps) - } - `; - context.extensionFunctions = { - async 'http://example.org/functions#to-upper-case'(args: RDF.Term[]) { - const arg = args[0]; - if (arg.termType === 'Literal' && arg.datatype.equals(DF.literal('', stringType).datatype)) { - return DF.literal(arg.value.toUpperCase(), stringType); - } - return arg; - }, - }; - const bindingsStream = await engine.queryBindings(complexQuery, context); - expect((await bindingsStream.toArray()).map(res => res.get(DF.variable('caps'))!.value)).toEqual( - quads.map(q => q.object.value.toUpperCase()), - ); - }); - - describe('handles complex queries with groupBy', () => { - let context: any; - let complexQuery: string; - let extensionBuilder: (timout: boolean) => (args: RDF.Term[]) => Promise; - - beforeEach(() => { - context = { sources: [ store ]}; - complexQuery = `PREFIX func: - SELECT (SUM(func:count-chars(?o)) AS ?sum) WHERE { - ?s ?p ?o. - } - `; - extensionBuilder = (timout: boolean) => async(args: RDF.Term[]) => { - const arg = args[0]; - if (arg.termType === 'Literal' && arg.datatype.equals(DF.literal('', stringType).datatype)) { - if (timout) { - await new Promise(resolve => setTimeout(resolve, 1)); - } - return DF.literal(String(arg.value.length), integerType); - } - return arg; - }; - }); - - it('can be evaluated', async() => { - context.extensionFunctions = { - 'http://example.org/functions#count-chars': extensionBuilder(false), - }; - const bindingsStream = await engine.queryBindings(complexQuery, context); - expect((await bindingsStream.toArray()).map(res => res.get(DF.variable('sum'))!.value)).toEqual([ '20' ]); - }); - - it('can be truly async', async() => { - context.extensionFunctions = { - 'http://example.org/functions#count-chars': extensionBuilder(true), - }; - const bindingsStream = await engine.queryBindings(complexQuery, context); - expect((await bindingsStream.toArray()).map(res => res.get(DF.variable('sum'))!.value)).toEqual([ '20' ]); - }); - }); - }); - describe('functionArgumentsCache', () => { let query: string; let stringType: RDF.NamedNode; diff --git a/packages/actor-query-operation-filter-sparqlee/test/ActorQueryOperationFilterSparqlee-test.ts b/packages/actor-query-operation-filter-sparqlee/test/ActorQueryOperationFilterSparqlee-test.ts index b93150b56e..02c9410b86 100644 --- a/packages/actor-query-operation-filter-sparqlee/test/ActorQueryOperationFilterSparqlee-test.ts +++ b/packages/actor-query-operation-filter-sparqlee/test/ActorQueryOperationFilterSparqlee-test.ts @@ -3,7 +3,7 @@ import { ActorQueryOperation } from '@comunica/bus-query-operation'; import { KeysInitQuery } from '@comunica/context-entries'; import { ActionContext, Bus } from '@comunica/core'; import * as sparqlee from '@comunica/expression-evaluator'; -import { isExpressionError } from '@comunica/expression-evaluator'; +import { ExpressionEvaluatorFactory, isExpressionError } from '@comunica/expression-evaluator'; import type { IQueryOperationResultBindings, Bindings } from '@comunica/types'; import { ArrayIterator } from 'asynciterator'; import { DataFactory } from 'rdf-data-factory'; @@ -85,9 +85,23 @@ describe('ActorQueryOperationFilterSparqlee', () => { describe('An ActorQueryOperationFilterSparqlee instance', () => { let actor: ActorQueryOperationFilterSparqlee; let factory: Factory; + let expressionEvaluatorFactory: ExpressionEvaluatorFactory; beforeEach(() => { - actor = new ActorQueryOperationFilterSparqlee({ name: 'actor', bus, mediatorQueryOperation }); + expressionEvaluatorFactory = new ExpressionEvaluatorFactory({ + mediatorQueryOperation, + mediatorBindingsAggregatorFactory: { + mediate(arg: any) { + throw new Error('Not implemented'); + }, + }, + }); + actor = new ActorQueryOperationFilterSparqlee({ + name: 'actor', + bus, + mediatorQueryOperation, + expressionEvaluatorFactory, + }); factory = new Factory(); }); diff --git a/packages/actor-query-operation-group/test/ActorQueryOperationGroup-test.ts b/packages/actor-query-operation-group/test/ActorQueryOperationGroup-test.ts index b3841b6637..07e1f69f56 100644 --- a/packages/actor-query-operation-group/test/ActorQueryOperationGroup-test.ts +++ b/packages/actor-query-operation-group/test/ActorQueryOperationGroup-test.ts @@ -11,6 +11,22 @@ import { Algebra } from 'sparqlalgebrajs'; import { ActorQueryOperationGroup } from '../lib'; import { GroupsState } from '../lib/GroupsState'; import '@comunica/jest'; +import type { + IActionBindingsAggregatorFactory, + IActorBindingsAggregatorFactoryOutput, +} from '@comunica/bus-bindings-aggeregator-factory'; +import { + WildcardCountAggregator, +} from '@comunica/actor-bindings-aggregator-factory-wildcard-count/lib/WildcardCountAggregator'; +import { CountAggregator } from '@comunica/actor-bindings-aggregator-factory-count/lib/CountAggregator'; +import { SumAggregator } from '@comunica/actor-bindings-aggregator-factory-sum/lib/SumAggregator'; +import { AverageAggregator } from '@comunica/actor-bindings-aggregator-factory-average/lib/AverageAggregator'; +import { MinAggregator } from '@comunica/actor-bindings-aggregator-factory-min/lib/MinAggregator'; +import { MaxAggregator } from '@comunica/actor-bindings-aggregator-factory-max/lib/MaxAggregator'; +import { SampleAggregator } from '@comunica/actor-bindings-aggregator-factory-sample/lib/SampleAggregator'; +import { + GroupConcatAggregator, +} from '@comunica/actor-bindings-aggregator-factory-group-concat/lib/GroupConcatAggregator'; const DF = new DataFactory(); const BF = new BindingsFactory(); @@ -89,6 +105,45 @@ interface ICaseOutput { actor: ActorQueryOperationGroup; bus: any; mediatorQueryOperation: any; op: IActionQueryOperation; } +function aggregatorFactory({ expr, factory, context }: IActionBindingsAggregatorFactory): +IActorBindingsAggregatorFactoryOutput { + if (expr.aggregator === 'count') { + if (expr.expression.wildcard) { + return { + aggregator: new WildcardCountAggregator(expr, factory, context), + }; + } + return { + aggregator: new CountAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'sum') { + return { + aggregator: new SumAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'avg') { + return { + aggregator: new AverageAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'min') { + return { + aggregator: new MinAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'max') { + return { + aggregator: new MaxAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'sample') { + return { + aggregator: new SampleAggregator(expr, factory, context), + }; + } if (expr.aggregator === 'group_concat') { + return { + aggregator: new GroupConcatAggregator(expr, factory, context), + }; + } + throw new Error(`Unsupported aggregator ${expr.aggregator}`); +} + function constructCase( { inputBindings, inputVariables = [], groupVariables = [], aggregates = [], inputOp }: ICaseOptions, ): ICaseOutput { @@ -116,8 +171,9 @@ function constructCase( const expressionEvaluatorFactory = new ExpressionEvaluatorFactory({ mediatorQueryOperation, mediatorBindingsAggregatorFactory: { - mediate(arg: any) { - throw new Error('Not implemented'); + async mediate({ expr, factory, context }: IActionBindingsAggregatorFactory): + Promise { + return aggregatorFactory({ expr, factory, context }); }, }, }); @@ -166,8 +222,9 @@ describe('ActorQueryOperationGroup', () => { expressionEvaluatorFactory = new ExpressionEvaluatorFactory({ mediatorQueryOperation, mediatorBindingsAggregatorFactory: { - mediate(arg: any) { - throw new Error('Not implemented'); + async mediate({ expr, factory, context }: IActionBindingsAggregatorFactory): + Promise { + return aggregatorFactory({ expr, factory, context }); }, }, }); diff --git a/packages/bus-query-operation/test/ActorQueryOperation-test.ts b/packages/bus-query-operation/test/ActorQueryOperation-test.ts index 34d7703293..1be2858cd0 100644 --- a/packages/bus-query-operation/test/ActorQueryOperation-test.ts +++ b/packages/bus-query-operation/test/ActorQueryOperation-test.ts @@ -1,7 +1,6 @@ import { BindingsFactory } from '@comunica/bindings-factory'; import { Bus } from '@comunica/core'; import { MetadataValidationState } from '@comunica/metadata'; -import { ArrayIterator } from 'asynciterator'; import { ActorQueryOperation } from '..'; const BF = new BindingsFactory(); @@ -89,20 +88,4 @@ describe('ActorQueryOperation', () => { expect(() => ActorQueryOperation.validateQueryOutput({ type: 'no-boolean' }, 'boolean')).toThrow(); }); }); - - describe('#getAsyncExpressionContext', () => { - let mediatorQueryOperation: any; - - beforeEach(() => { - mediatorQueryOperation = { - mediate: (arg: any) => Promise.resolve({ - bindingsStream: new ArrayIterator([], { autoStart: false }), - metadata: () => Promise.resolve({ cardinality: 0 }), - operated: arg, - type: 'bindings', - variables: [ 'a' ], - }), - }; - }); - }); }); diff --git a/packages/expression-evaluator/lib/evaluators/evaluatorHelpers/AsyncRecursiveEvaluator.ts b/packages/expression-evaluator/lib/evaluators/evaluatorHelpers/AsyncRecursiveEvaluator.ts index 4a0a2985cf..c67f8c1c7a 100644 --- a/packages/expression-evaluator/lib/evaluators/evaluatorHelpers/AsyncRecursiveEvaluator.ts +++ b/packages/expression-evaluator/lib/evaluators/evaluatorHelpers/AsyncRecursiveEvaluator.ts @@ -100,10 +100,6 @@ export class AsyncRecursiveEvaluator { } private async evalExistence(expr: E.Existence, mapping: RDF.Bindings): Promise { - if (!this.context.exists) { - throw new Err.NoExistenceHook(); - } - return new E.BooleanLiteral(await this.context.exists(expr.expression, mapping)); } diff --git a/packages/expression-evaluator/lib/util/Errors.ts b/packages/expression-evaluator/lib/util/Errors.ts index 5274a6e9ca..d3a281fea1 100644 --- a/packages/expression-evaluator/lib/util/Errors.ts +++ b/packages/expression-evaluator/lib/util/Errors.ts @@ -210,12 +210,6 @@ export class NoAggregator extends Error { } } -export class NoExistenceHook extends Error { - public constructor() { - super('EXISTS found, but no existence hook provided.'); - } -} - function pp(object: T): string { return JSON.stringify(object); } diff --git a/packages/expression-evaluator/test/integration/evaluators/RecursiveEvaluator-test.ts b/packages/expression-evaluator/test/integration/evaluators/RecursiveEvaluator-test.ts index c7113c4671..352f405121 100644 --- a/packages/expression-evaluator/test/integration/evaluators/RecursiveEvaluator-test.ts +++ b/packages/expression-evaluator/test/integration/evaluators/RecursiveEvaluator-test.ts @@ -26,8 +26,8 @@ describe('recursive evaluators', () => { expect(await evaluator.evaluate(new E.IntegerLiteral(1), BF.bindings())).toEqual(new E.IntegerLiteral(1)); }); - it('is not able to evaluate existence by default', async() => { - await expect(evaluator.evaluate(new E.Existence({ + it('is able to evaluate existence by default', async() => { + expect(await evaluator.evaluate(new E.Existence({ type: types.EXPRESSION, expressionType: expressionTypes.EXISTENCE, not: false, @@ -36,17 +36,23 @@ describe('recursive evaluators', () => { variables: [], bindings: [], }, - }), BF.bindings())).rejects.toThrow(Err.NoExistenceHook); + }), BF.bindings())).toEqual(new E.BooleanLiteral(false)); }); it('is able to evaluate existence if configured', async() => { - const customEvaluator = new AsyncRecursiveEvaluator( - getDefaultCompleteEEContext(getMockEEActionContext()), - getMockEEFactory().createEvaluator(translate('SELECT * WHERE { ?s ?p ?o FILTER (1 + 1)}').input.expression, - getMockEEActionContext()), + const customEvaluator = getMockEEFactory().createEvaluator( + translate('SELECT * WHERE { ?s ?p ?o FILTER (1 + 1)}').input.expression, + getMockEEActionContext(), + { + exists: async() => true, + }, + ); + const customAsyncRecursiveEvaluator = new AsyncRecursiveEvaluator( + customEvaluator.context, + customEvaluator, ); - expect(await customEvaluator.evaluate(new E.Existence({ + expect(await customAsyncRecursiveEvaluator.evaluate(new E.Existence({ type: types.EXPRESSION, expressionType: expressionTypes.EXISTENCE, not: false, @@ -73,13 +79,19 @@ describe('recursive evaluators', () => { }); it('is able to evaluate aggregates if configured', async() => { - const customEvaluator = new AsyncRecursiveEvaluator( - getDefaultCompleteEEContext(getMockEEActionContext()), - getMockEEFactory().createEvaluator(translate('SELECT * WHERE { ?s ?p ?o FILTER (1 + 1)}').input.expression, - getMockEEActionContext()), + const customEvaluator = getMockEEFactory().createEvaluator( + translate('SELECT * WHERE { ?s ?p ?o FILTER (1 + 1)}').input.expression, + getMockEEActionContext(), + { + aggregate: async() => DF.literal('42'), + }, + ); + const customAsyncRecursiveEvaluator = new AsyncRecursiveEvaluator( + customEvaluator.context, + customEvaluator, ); - expect(await customEvaluator.evaluate(new E.Aggregate('count', { + expect(await customAsyncRecursiveEvaluator.evaluate(new E.Aggregate('count', { type: types.EXPRESSION, expressionType: expressionTypes.AGGREGATE, aggregator: 'count', diff --git a/packages/expression-evaluator/test/unit/evaluators/ExpressionEvaluatorFactory-test.ts b/packages/expression-evaluator/test/unit/evaluators/ExpressionEvaluatorFactory-test.ts index 7c6131db60..78929b2ba6 100644 --- a/packages/expression-evaluator/test/unit/evaluators/ExpressionEvaluatorFactory-test.ts +++ b/packages/expression-evaluator/test/unit/evaluators/ExpressionEvaluatorFactory-test.ts @@ -2,19 +2,46 @@ import { BindingsFactory } from '@comunica/bindings-factory'; import { KeysInitQuery } from '@comunica/context-entries'; import { ActionContext } from '@comunica/core'; import type { FunctionArgumentsCache } from '@comunica/types'; +import { ArrayIterator } from 'asynciterator'; import type { Algebra } from 'sparqlalgebrajs'; import { Factory } from 'sparqlalgebrajs'; import type { ExpressionEvaluator } from '../../../lib'; -import { getMockEEFactory, getMockExpression } from '../../util/utils'; +import { ExpressionEvaluatorFactory } from '../../../lib'; +import { getMockExpression } from '../../util/utils'; const BF = new BindingsFactory(); describe('The ExpressionEvaluatorFactory', () => { + let mediatorQueryOperation: any; + let mediatorBindingsAggregatorFactory: any; + let expressionEvaluatorFactory: ExpressionEvaluatorFactory; + + beforeEach(() => { + mediatorQueryOperation = { + mediate: (arg: any) => Promise.resolve({ + bindingsStream: new ArrayIterator([], { autoStart: false }), + metadata: () => Promise.resolve({ cardinality: 0 }), + operated: arg, + type: 'bindings', + variables: [ 'a' ], + }), + }; + + mediatorBindingsAggregatorFactory = { + mediate: (arg: any) => Promise.reject(new Error('Not implemented')), + }; + + expressionEvaluatorFactory = new ExpressionEvaluatorFactory({ + mediatorBindingsAggregatorFactory, + mediatorQueryOperation, + }); + }); + describe('creates an evaluator with good defaults', () => { let evaluator: ExpressionEvaluator; beforeEach(() => { - evaluator = getMockEEFactory().createEvaluator(getMockExpression('1+1'), new ActionContext({})); + evaluator = expressionEvaluatorFactory.createEvaluator(getMockExpression('1+1'), new ActionContext({})); }); it('empty contexts save for the bnode function', () => { @@ -56,7 +83,7 @@ describe('The ExpressionEvaluatorFactory', () => { [KeysInitQuery.baseIRI.name]: 'http://base.org/', [KeysInitQuery.functionArgumentsCache.name]: functionArgumentsCache, }); - const evaluator = getMockEEFactory().createEvaluator(getMockExpression('1+1'), actionContext); + const evaluator = expressionEvaluatorFactory.createEvaluator(getMockExpression('1+1'), actionContext); expect(evaluator.context).toMatchObject({ now: date, bnode: expect.any(Function),