Skip to content

Commit 8ab8a90

Browse files
authored
Merge pull request #136 from maartyman/feat/order-by
Feat/order by
2 parents 526d1d1 + eadb2c3 commit 8ab8a90

24 files changed

Lines changed: 3972 additions & 7 deletions

File tree

engines/config-query-sparql-incremental/config/query-operation/actors.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"icqsi:config/query-operation/actors/query/filter.json",
1111
"ccqs:config/query-operation/actors/query/from.json",
1212
"icqsi:config/query-operation/actors/query/group.json",
13+
"icqsi:config/query-operation/actors/query/orderby.json",
1314
"ccqs:config/query-operation/actors/query/join.json",
1415
"ccqs:config/query-operation/actors/query/leftjoin.json",
1516
"ccqs:config/query-operation/actors/query/minus.json",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"@context": [
3+
"https://linkedsoftwaredependencies.org/bundles/npm/@comunica/runner/^4.0.0/components/context.jsonld",
4+
5+
"https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-query-operation-orderby/^2.0.0/components/context.jsonld"
6+
],
7+
"@id": "urn:comunica:default:Runner",
8+
"@type": "Runner",
9+
"actors": [
10+
{
11+
"@id": "urn:comunica:default:query-operation/actors#orderby",
12+
"@type": "ActorQueryOperationOrderBy",
13+
"mediatorQueryOperation": { "@id": "urn:comunica:default:query-operation/mediators#main" },
14+
"mediatorExpressionEvaluatorFactory": { "@id": "urn:comunica:default:expression-evaluator-factory/mediators#main" },
15+
"mediatorTermComparatorFactory": { "@id": "urn:comunica:default:term-comparator-factory/mediators#main" }
16+
}
17+
]
18+
}

engines/config-query-sparql-incremental/config/query-operation/actors/query/slice.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"@context": [
33
"https://linkedsoftwaredependencies.org/bundles/npm/@comunica/runner/^4.0.0/components/context.jsonld",
44

5-
"https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-query-operation-slice/^2.0.0/components/context.jsonld"
5+
"https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-query-operation-slice/^2.0.0/components/context.jsonld",
6+
"https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-query-operation-slice-ordered/^2.0.0/components/context.jsonld"
67
],
78
"@id": "urn:comunica:default:Runner",
89
"@type": "Runner",
@@ -11,6 +12,11 @@
1112
"@id": "urn:comunica:default:query-operation/actors#slice",
1213
"@type": "ActorQueryOperationSlice",
1314
"mediatorQueryOperation": { "@id": "urn:comunica:default:query-operation/mediators#main" }
15+
},
16+
{
17+
"@id": "urn:comunica:default:query-operation/actors#slice-ordered",
18+
"@type": "ActorQueryOperationSliceOrdered",
19+
"mediatorQueryOperation": { "@id": "urn:comunica:default:query-operation/mediators#main" }
1420
}
1521
]
1622
}

engines/query-sparql-incremental/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,10 @@
269269
"@incremunica/actor-query-operation-distinct-hash": "^2.1.0",
270270
"@incremunica/actor-query-operation-filter": "^2.1.0",
271271
"@incremunica/actor-query-operation-group": "^2.1.0",
272+
"@incremunica/actor-query-operation-orderby": "^2.1.0",
272273
"@incremunica/actor-query-operation-reduced-hash": "^2.1.0",
273274
"@incremunica/actor-query-operation-slice": "^2.1.0",
275+
"@incremunica/actor-query-operation-slice-ordered": "^2.1.0",
274276
"@incremunica/actor-query-source-identify-hypermedia-none": "^2.1.0",
275277
"@incremunica/actor-query-source-identify-stream": "^2.1.0",
276278
"@incremunica/actor-query-source-identify-streaming-rdfjs": "^2.1.0",

engines/query-sparql-incremental/test/QuerySparql-test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { KeysBindings } from '@incremunica/context-entries';
99
import { createTestBindingsFactory, partialArrayifyAsyncIterator } from '@incremunica/dev-tools';
1010
import { StreamingStore } from '@incremunica/streaming-store';
1111
import type { ContextQuerySourceStream, Quad } from '@incremunica/types';
12+
import { getBindingsIndex } from '@incremunica/user-tools';
1213
import { ArrayIterator } from 'asynciterator';
1314
import { DataFactory } from 'rdf-data-factory';
1415
import { PassThrough } from 'readable-stream';
@@ -81,6 +82,54 @@ describe('System test: QuerySparql (without external network)', () => {
8182
streamingStore.end();
8283
});
8384

85+
it('simple query with ORDERBY', async() => {
86+
streamingStore.addQuad(quad('Alice', 'http://test/hasInterest', 'Cooking'));
87+
streamingStore.addQuad(quad('Bob', 'http://test/hasInterest', 'Sports'));
88+
streamingStore.addQuad(quad('Alice', 'http://test/hasInterest', 'Reading'));
89+
90+
const bindingStream = await engine.queryBindings(`PREFIX test: <http://test/>
91+
SELECT ?subject ?interest
92+
WHERE {
93+
?subject test:hasInterest ?interest .
94+
}
95+
ORDER BY ?subject ?interest`, {
96+
sources: [ streamingStore ],
97+
});
98+
99+
const result1 = await partialArrayifyAsyncIterator(bindingStream, 3);
100+
const expectedResult1 = [
101+
BF.bindings([
102+
[ DF.variable('subject'), DF.namedNode('Alice') ],
103+
[ DF.variable('interest'), DF.namedNode('Cooking') ],
104+
]).setContextEntry(KeysBindings.isAddition, true),
105+
BF.bindings([
106+
[ DF.variable('subject'), DF.namedNode('Alice') ],
107+
[ DF.variable('interest'), DF.namedNode('Reading') ],
108+
]).setContextEntry(KeysBindings.isAddition, true),
109+
BF.bindings([
110+
[ DF.variable('subject'), DF.namedNode('Bob') ],
111+
[ DF.variable('interest'), DF.namedNode('Sports') ],
112+
]).setContextEntry(KeysBindings.isAddition, true),
113+
];
114+
expect(result1).toBeIsomorphicBindingsArray(expectedResult1);
115+
for (const result of result1) {
116+
expect(result).toEqualBindings(expectedResult1[getBindingsIndex(result)]);
117+
}
118+
119+
streamingStore.removeQuad(quad('Alice', 'http://test/hasInterest', 'Cooking'));
120+
121+
const result2 = await partialArrayifyAsyncIterator(bindingStream, 1);
122+
expect(result2).toBeIsomorphicBindingsArray([
123+
BF.bindings([
124+
[ DF.variable('subject'), DF.namedNode('Alice') ],
125+
[ DF.variable('interest'), DF.namedNode('Cooking') ],
126+
]).setContextEntry(KeysBindings.isAddition, false),
127+
]);
128+
expect(getBindingsIndex(result2[0])).toBe(0);
129+
130+
streamingStore.end();
131+
});
132+
84133
it('simple query with an EXTEND', async() => {
85134
streamingStore.addQuad(quad('http://test/Alice', 'http://test/hasInterest', 1));
86135
streamingStore.addQuad(quad('http://test/Alice', 'http://test/hasInterest', 2));
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Incremunica OrderBy Query Operation Actor
2+
3+
[![npm version](https://badge.fury.io/js/%40incremunica%2Factor-query-operation-orderby.svg)](https://www.npmjs.com/package/@incremunica/actor-query-operation-orderby)
4+
5+
A [Query Operation](https://github.com/comunica/comunica/tree/master/packages/bus-query-operation) actor that handles [SPARQL `ORDER BY`](https://www.w3.org/TR/sparql11-query/#sparqlOrderBy) operations.
6+
7+
## Install
8+
9+
```bash
10+
$ yarn add @incremunica/actor-query-operation-orderby
11+
```
12+
13+
## Configure
14+
15+
After installing, this package can be added to your engine's configuration as follows:
16+
```text
17+
{
18+
"@context": [
19+
...
20+
"https://linkedsoftwaredependencies.org/bundles/npm/@incremunica/actor-query-operation-orderby/^2.0.0/components/context.jsonld"
21+
],
22+
"actors": [
23+
...
24+
{
25+
"@id": "urn:comunica:default:query-operation/actors#orderby",
26+
"@type": "ActorQueryOperationOrderBy",
27+
"mediatorQueryOperation": { "@id": "urn:comunica:default:query-operation/mediators#main" },
28+
"mediatorExpressionEvaluatorFactory": { "@id": "urn:comunica:default:expression-evaluator-factory/mediators#main" },
29+
"mediatorTermComparatorFactory": { "@id": "urn:comunica:default:term-comparator-factory/mediators#main" }
30+
}
31+
]
32+
}
33+
```
34+
35+
### Config Parameters
36+
37+
* `mediatorQueryOperation`: A mediator over the [Query Operation bus](https://github.com/comunica/comunica/tree/master/packages/bus-query-operation).
38+
* `mediatorExpressionEvaluatorFactory`: A mediator over the [Expression Evaluator Factory bus](https://github.com/comunica/comunica/tree/master/packages/bus-expression-evaluator-factory).
39+
* `mediatorTermComparatorFactory`: A factory to create a [Term Comparator Factory bus](https://github.com/comunica/comunica/tree/master/packages/bus-term-comparator-factory).
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { MediatorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory';
2+
import type { IActorQueryOperationTypedMediatedArgs } from '@comunica/bus-query-operation';
3+
import { ActorQueryOperationTypedMediated } from '@comunica/bus-query-operation';
4+
import type { MediatorTermComparatorFactory } from '@comunica/bus-term-comparator-factory';
5+
import type { IActorTest, TestResult } from '@comunica/core';
6+
import { passTestVoid } from '@comunica/core';
7+
import type { BindingsStream, IActionContext, IQueryOperationResult } from '@comunica/types';
8+
import type { Bindings } from '@comunica/utils-bindings-factory';
9+
import { bindingsToCompactString } from '@comunica/utils-bindings-factory';
10+
import { isExpressionError } from '@comunica/utils-expression-evaluator';
11+
import { getSafeBindings } from '@comunica/utils-query-operation';
12+
import { KeysBindings } from '@incremunica/context-entries';
13+
import type * as RDF from '@rdfjs/types';
14+
import type { AsyncIterator } from 'asynciterator';
15+
import { Algebra } from 'sparqlalgebrajs';
16+
import { IndexedSortTree } from './IndexedSortTree';
17+
18+
export interface IAnnotatedBinding {
19+
bindings: Bindings;
20+
result: (RDF.Term | undefined)[];
21+
hash: string;
22+
}
23+
24+
/**
25+
* An incremunica OrderBy Query Operation Actor.
26+
*/
27+
export class ActorQueryOperationOrderBy extends ActorQueryOperationTypedMediated<Algebra.OrderBy> {
28+
private readonly mediatorExpressionEvaluatorFactory: MediatorExpressionEvaluatorFactory;
29+
private readonly mediatorTermComparatorFactory: MediatorTermComparatorFactory;
30+
31+
public constructor(args: IActorQueryOperationOrderBySparqleeArgs) {
32+
super(args, 'orderby');
33+
this.mediatorExpressionEvaluatorFactory = args.mediatorExpressionEvaluatorFactory;
34+
this.mediatorTermComparatorFactory = args.mediatorTermComparatorFactory;
35+
}
36+
37+
public async testOperation(): Promise<TestResult<IActorTest>> {
38+
return passTestVoid();
39+
}
40+
41+
public async runOperation(operation: Algebra.OrderBy, context: IActionContext):
42+
Promise<IQueryOperationResult> {
43+
const outputRaw = await this.mediatorQueryOperation.mediate({ operation: operation.input, context });
44+
const output = getSafeBindings(outputRaw);
45+
const variables = (await output.metadata()).variables.map(v => v.variable);
46+
47+
let bindingsStream = <AsyncIterator<Bindings>><any>output.bindingsStream;
48+
49+
// Sorting backwards since the first one is the most important therefore should be ordered last.
50+
const orderByEvaluator = await this.mediatorTermComparatorFactory.mediate({ context });
51+
52+
let annotatedBindingsStream = bindingsStream.map<IAnnotatedBinding>((bindings: Bindings) =>
53+
({ bindings, result: [], hash: bindingsToCompactString(bindings, variables) }));
54+
const isAscending = [];
55+
for (let expr of operation.expressions) {
56+
isAscending.push(this.isAscending(expr));
57+
expr = this.extractSortExpression(expr);
58+
// Transform the stream by annotating it with the expr result
59+
const evaluator = await this.mediatorExpressionEvaluatorFactory
60+
.mediate({ algExpr: expr, context });
61+
62+
const transform = async(
63+
annotatedBinding: IAnnotatedBinding,
64+
next: () => void,
65+
push: (result: IAnnotatedBinding) => void,
66+
): Promise<void> => {
67+
try {
68+
const result = await evaluator.evaluate(annotatedBinding.bindings);
69+
annotatedBinding.result.push(result);
70+
push(annotatedBinding);
71+
} catch (error: unknown) {
72+
// We ignore all Expression errors.
73+
// Other errors (likely programming mistakes) are still propagated.
74+
if (!isExpressionError(<Error> error)) {
75+
bindingsStream.destroy(<Error> error);
76+
next();
77+
return;
78+
}
79+
annotatedBinding.result.push(undefined);
80+
push(annotatedBinding);
81+
}
82+
next();
83+
};
84+
// eslint-disable-next-line ts/no-misused-promises
85+
annotatedBindingsStream = annotatedBindingsStream.transform<IAnnotatedBinding>({ transform, autoStart: false });
86+
}
87+
88+
const index = new IndexedSortTree(orderByEvaluator, isAscending);
89+
bindingsStream = annotatedBindingsStream.map((annotatedBindings) => {
90+
try {
91+
const bindingsOrderData =
92+
(annotatedBindings.bindings.getContextEntry(KeysBindings.isAddition) ?? true) ?
93+
index.insert(annotatedBindings).data :
94+
index.remove(annotatedBindings).data;
95+
return annotatedBindings.bindings.setContextEntry(KeysBindings.order, bindingsOrderData);
96+
} catch (error) {
97+
bindingsStream.destroy(<any>error);
98+
return null;
99+
}
100+
});
101+
102+
return {
103+
type: 'bindings',
104+
bindingsStream: <BindingsStream><any>bindingsStream,
105+
metadata: output.metadata,
106+
};
107+
}
108+
109+
// Remove descending operator if necessary
110+
private extractSortExpression(expr: Algebra.Expression): Algebra.Expression {
111+
const { expressionType, operator } = expr;
112+
if (expressionType !== Algebra.expressionTypes.OPERATOR) {
113+
return expr;
114+
}
115+
return operator === 'desc' ?
116+
expr.args[0] :
117+
expr;
118+
}
119+
120+
private isAscending(expr: Algebra.Expression): boolean {
121+
const { expressionType, operator } = expr;
122+
if (expressionType !== Algebra.expressionTypes.OPERATOR) {
123+
return true;
124+
}
125+
return operator !== 'desc';
126+
}
127+
}
128+
129+
export interface IActorQueryOperationOrderBySparqleeArgs extends IActorQueryOperationTypedMediatedArgs {
130+
mediatorExpressionEvaluatorFactory: MediatorExpressionEvaluatorFactory;
131+
mediatorTermComparatorFactory: MediatorTermComparatorFactory;
132+
}

0 commit comments

Comments
 (0)