Skip to content

Commit 6a19f61

Browse files
authored
Merge pull request #466 from hypermedia-app/triple-to-another
feat: converting triple to other patterns
2 parents 49fe2af + 5a1b729 commit 6a19f61

File tree

10 files changed

+169
-18
lines changed

10 files changed

+169
-18
lines changed

.changeset/famous-games-change.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hydrofoil/shape-to-query": minor
3+
"@hydrofoil/sparql-processor": minor
4+
---
5+
6+
The query processor methods now allow returning arrays, which may break extending classes

.changeset/lucky-buckets-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hydrofoil/sparql-processor": patch
3+
---
4+
5+
Allow transforming triple to another type of pattern

packages/processor/index.ts

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import type sparqljs from 'sparqljs'
22
import { match, P } from 'ts-pattern'
33
import type { DataFactory, DefaultGraph, Quad_Predicate } from '@rdfjs/types' // eslint-disable-line camelcase
44

5-
type Term = sparqljs.IriTerm | sparqljs.BlankTerm | sparqljs.LiteralTerm | sparqljs.Variable | sparqljs.QuadTerm | DefaultGraph
5+
type Term =
6+
sparqljs.IriTerm
7+
| sparqljs.BlankTerm
8+
| sparqljs.LiteralTerm
9+
| sparqljs.Variable
10+
| sparqljs.QuadTerm
11+
| DefaultGraph
612

713
export interface Processor {
814
process<Q extends sparqljs.SparqlQuery>(query: Q): Q
@@ -136,11 +142,18 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
136142

137143
processQuads(quads: sparqljs.Quads): sparqljs.Quads {
138144
return match(quads)
139-
.with({ type: 'bgp' }, bgp => this.processBgp(bgp))
145+
.with({ type: 'bgp' }, bgp => {
146+
const processed = this.processBgp(bgp)
147+
const maybeBgp = Array.isArray(processed) ? processed[0] : processed
148+
if (maybeBgp.type !== 'bgp') {
149+
throw new Error('Quads must be transformed to a single bgp pattern')
150+
}
151+
return maybeBgp
152+
})
140153
.with({ type: 'graph' }, (graph): sparqljs.GraphQuads => ({
141154
type: 'graph',
142155
name: this.processTerm(graph.name),
143-
triples: graph.triples.map(triple => this.processTriple(triple)),
156+
triples: graph.triples.map<sparqljs.Triple>(triple => this.processTripleStrict(triple)),
144157
}))
145158
.exhaustive()
146159
}
@@ -174,15 +187,15 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
174187
return {
175188
queryType: 'CONSTRUCT',
176189
...this.processBaseQuery(query),
177-
template: query.template?.map(triple => this.processTriple(triple)),
190+
template: query.template?.map(triple => this.processTripleStrict(triple)),
178191
}
179192
}
180193

181194
processPatterns(where: sparqljs.Pattern[]): sparqljs.Pattern[] {
182-
return where.map((pattern) => this.processPattern(pattern)).filter(Boolean)
195+
return where.flatMap((pattern) => this.processPattern(pattern)).filter(Boolean)
183196
}
184197

185-
processPattern(pattern: sparqljs.Pattern): sparqljs.Pattern {
198+
processPattern(pattern: sparqljs.Pattern): sparqljs.Pattern | sparqljs.Pattern[] {
186199
return match(pattern)
187200
.with({ type: 'bgp' }, (bgp) => this.processBgp(bgp))
188201
.with({ type: 'values' }, values => this.processValues(values))
@@ -291,14 +304,31 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
291304
return this.factory.literal(literal.value, langOrDt)
292305
}
293306

294-
processBgp(bgp: sparqljs.BgpPattern): sparqljs.BgpPattern {
295-
return {
296-
...bgp,
297-
triples: bgp.triples.map(triple => this.processTriple(triple)),
298-
}
307+
processBgp({ triples }: sparqljs.BgpPattern): sparqljs.Pattern | sparqljs.Pattern[] {
308+
let currentBgp: sparqljs.BgpPattern | undefined
309+
310+
return triples.reduce((patterns: sparqljs.Pattern[], triple) => {
311+
const result = this.processTriple(triple)
312+
const processedTriples = Array.isArray(result) ? result : [result]
313+
314+
return processedTriples.reduce((patterns: sparqljs.Pattern[], processedTriple) => {
315+
if ('subject' in processedTriple) {
316+
if (!currentBgp) {
317+
currentBgp = { type: 'bgp', triples: [processedTriple] }
318+
return [...patterns, currentBgp]
319+
}
320+
321+
currentBgp.triples.push(processedTriple)
322+
return patterns
323+
}
324+
325+
currentBgp = undefined
326+
return [...patterns, processedTriple]
327+
}, patterns)
328+
}, [])
299329
}
300330

301-
processTriple(triple: sparqljs.Triple): sparqljs.Triple {
331+
processTriple(triple: sparqljs.Triple): sparqljs.Triple | sparqljs.Triple[] | sparqljs.Pattern | sparqljs.Pattern[] {
302332
return {
303333
subject: this.processTerm(triple.subject),
304334
predicate: match(triple.predicate)
@@ -308,6 +338,14 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
308338
}
309339
}
310340

341+
private processTripleStrict(triple: sparqljs.Triple): sparqljs.Triple {
342+
const processed = this.processTriple(triple)
343+
if (!('subject' in processed)) {
344+
throw new Error('Triple must be transformed to another triple')
345+
}
346+
return processed
347+
}
348+
311349
processPropertyPath(path: sparqljs.PropertyPath): sparqljs.PropertyPath {
312350
if (path.pathType === '!') {
313351
return this.processNegatedPropertySet(path)
@@ -337,7 +375,7 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
337375
}
338376
}
339377

340-
processGroup(group: sparqljs.GroupPattern) : sparqljs.Pattern {
378+
processGroup(group: sparqljs.GroupPattern): sparqljs.Pattern {
341379
return {
342380
...group,
343381
patterns: this.processPatterns(group.patterns),
@@ -373,7 +411,7 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
373411
.with({ type: 'functionCall' }, functionCall => this.processFunctionCall(functionCall))
374412
.with({ type: 'aggregate' }, aggregate => aggregate)
375413
.when(Array.isArray, (tuple: sparqljs.Expression[]) => tuple.map(expression => this.processExpression(expression)))
376-
.with({ equals: P.instanceOf(Function) }, term => this.processTerm(term))
414+
.with({ termType: P.string }, term => this.processTerm(term))
377415
.exhaustive()
378416
}
379417

@@ -392,7 +430,16 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
392430
...operation,
393431
args: operation.args.map(arg =>
394432
match(arg)
395-
.when(isPattern, operation => this.processPattern(operation))
433+
.when(isPattern, operation => {
434+
const processed = this.processPattern(operation)
435+
if (Array.isArray(processed)) {
436+
if (processed.length > 1) {
437+
throw new Error('Operation argument cannot be transformed to an array')
438+
}
439+
return processed[0]
440+
}
441+
return processed
442+
})
396443
.otherwise(() => this.processExpression(arg as sparqljs.Expression)),
397444
),
398445
}

packages/processor/test/index.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path'
33
import rdf from '@zazuko/env/web.js'
44
import { glob } from 'glob'
55
import { expect } from 'chai'
6+
import type { FilterPattern, Triple } from 'sparqljs'
67
import QueryProcessor from '../index.js'
78
import { loadQuery, stringifyQuery } from './lib/query.js'
89

@@ -15,7 +16,8 @@ describe('@hydrofoil/sparql-processor', function () {
1516
const name = path.basename(file)
1617

1718
it(`does not modify the query (${name})`, function () {
18-
const processor = new (class extends QueryProcessor {})(rdf)
19+
const processor = new (class extends QueryProcessor {
20+
})(rdf)
1921

2022
const processed = processor.process(loadQuery(name))
2123

@@ -24,5 +26,63 @@ describe('@hydrofoil/sparql-processor', function () {
2426
})
2527
})
2628
})
29+
30+
context('replacing triples with other patterns', function () {
31+
context('magic property', function () {
32+
class FullTextSearchProcessor extends QueryProcessor {
33+
override processTriple(triple: Triple) {
34+
if ('termType' in triple.predicate && triple.predicate.value === 'http://example.org/fullTextSearch') {
35+
return <FilterPattern>{
36+
type: 'filter',
37+
expression: {
38+
type: 'operation',
39+
operator: 'regex',
40+
args: [
41+
triple.subject,
42+
triple.object,
43+
],
44+
},
45+
}
46+
}
47+
return super.processTriple(triple)
48+
}
49+
}
50+
51+
it('replaces triples with fullTextSearch predicate', function () {
52+
const processor = new FullTextSearchProcessor(rdf)
53+
54+
const query = loadQuery('triple-patterns/magic-property.rq')
55+
const processed = processor.process(query)
56+
57+
expect(stringifyQuery(processed))
58+
.to.deep.equal(stringifyQuery(loadQuery('triple-patterns/magic-property.expected.rq')))
59+
})
60+
})
61+
62+
context('multiple resulting patterns', function () {
63+
class FullTextSearchProcessor extends QueryProcessor {
64+
override processTriple(triple: Triple) {
65+
if ('type' in triple.predicate && triple.predicate.pathType === '/') {
66+
return triple.predicate.items.map((predicate, index, array): Triple => {
67+
const subject = index === 0 ? triple.subject : this.factory.variable(`v${index - 1}`)
68+
const object = index === array.length - 1 ? triple.object : this.factory.variable(`v${index}`)
69+
return { subject, predicate, object }
70+
})
71+
}
72+
return super.processTriple(triple)
73+
}
74+
}
75+
76+
it('replaces one triples with multiple', function () {
77+
const processor = new FullTextSearchProcessor(rdf)
78+
79+
const query = loadQuery('triple-patterns/sequence.rq')
80+
const processed = processor.process(query)
81+
82+
expect(stringifyQuery(processed))
83+
.to.deep.equal(stringifyQuery(loadQuery('triple-patterns/sequence.expected.rq')))
84+
})
85+
})
86+
})
2787
})
2888
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
PREFIX ex: <http://example.org/>
2+
PREFIX schema: <http://schema.org/>
3+
4+
SELECT * {
5+
?s a schema:Person ; schema:name ?name .
6+
FILTER(REGEX(?name, "John*"))
7+
?s schema:knows ex:JaneDoe .
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
PREFIX ex: <http://example.org/>
2+
PREFIX schema: <http://schema.org/>
3+
4+
SELECT * {
5+
?s a schema:Person ; schema:name ?name .
6+
?name ex:fullTextSearch "John*" .
7+
?s schema:knows ex:JaneDoe .
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
PREFIX ex: <http://example.org/>
2+
PREFIX schema: <http://schema.org/>
3+
4+
SELECT * {
5+
?s a schema:Person .
6+
?s schema:knows ?v0 .
7+
?v0 schema:knows ?friend .
8+
?s schema:name ?name .
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
PREFIX ex: <http://example.org/>
2+
PREFIX schema: <http://schema.org/>
3+
4+
SELECT * {
5+
?s a schema:Person .
6+
?s schema:knows/schema:knows ?friend .
7+
?s schema:name ?name .
8+
}

packages/shape-to-query/lib/optimizer/BlankNodeScopeFixer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class BlankNodeScopeFixer extends Processor<Environment<TermMapFactory |
2626
}
2727

2828
processUnion(union: UnionPattern): Pattern {
29-
const patterns = union.patterns.map(pattern => {
29+
const patterns = union.patterns.flatMap(pattern => {
3030
this.incrementScope()
3131
return this.processPattern(pattern)
3232
})

packages/shape-to-query/lib/optimizer/UnionRepeatedPatternsRemover.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class UnionRepeatedPatternsRemover extends Processor {
102102
return processed
103103
}
104104

105-
processBgp(bgp: BgpPattern): BgpPattern {
105+
processBgp(bgp: BgpPattern) {
106106
if (this.union) {
107107
// remove repeated triples from the bgp
108108
bgp.triples = bgp.triples.filter(triple => {

0 commit comments

Comments
 (0)