Skip to content

Commit 6b83456

Browse files
authored
fix: blank node conflicts (#380)
* fix: blank node conflicts * test: missing snapshot
1 parent 7bc7278 commit 6b83456

29 files changed

+605
-48
lines changed

.changeset/poor-cherries-reply.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+
Added overridable method `processSubquery`

.changeset/three-donkeys-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hydrofoil/shape-to-query": patch
3+
---
4+
5+
When SPARQL constraints generated blank nodes, the exact same blank nodes could be used in the query multiple times, causing invalid SPARQL queries. This change ensures that blank nodes are unique within the query, preventing such issues.

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/processor/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,15 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
194194
.with({ type: 'minus' }, minus => this.processMinus(minus))
195195
.with({ type: 'filter' }, filter => this.processFilter(filter))
196196
.with({ type: 'bind' }, bind => this.processBind(bind))
197-
.with({ type: 'query', queryType: 'SELECT' }, query => {
198-
// clones this instance to provide a separate context for the subquery
199-
return this.clone().processQuery(query)
200-
})
197+
.with({ type: 'query', queryType: 'SELECT' }, query => this.processSubquery(query))
201198
.otherwise(p => p)
202199
}
203200

201+
processSubquery(query: sparqljs.SelectQuery): sparqljs.SelectQuery {
202+
// clones this instance to provide a separate context for the subquery
203+
return this.clone().processQuery(query)
204+
}
205+
204206
processBind(bind: sparqljs.BindPattern): sparqljs.Pattern {
205207
return {
206208
...bind,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Processor from '@hydrofoil/sparql-processor'
2+
import type { BlankNode, DataFactory } from '@rdfjs/types'
3+
import type { GroupPattern, Pattern, UnionPattern } from 'sparqljs'
4+
import type { Environment } from '@rdfjs/environment/Environment.js'
5+
import type { TermMapFactory } from '@rdfjs/term-map/Factory.js'
6+
7+
/**
8+
* Ensures that blank nodes are renamed when necessary
9+
* to avoid using the same labels in different scopes
10+
*/
11+
export class BlankNodeScopeFixer extends Processor<Environment<TermMapFactory | DataFactory>> {
12+
constructor(
13+
factory: Environment<TermMapFactory | DataFactory>,
14+
private scopeCounter = 0,
15+
private blankNodes = factory.termMap()) {
16+
super(factory)
17+
}
18+
19+
clone() {
20+
return new BlankNodeScopeFixer(this.factory, this.scopeCounter, this.blankNodes)
21+
}
22+
23+
processGroup(group: GroupPattern): Pattern {
24+
this.incrementScope()
25+
return super.processGroup(group)
26+
}
27+
28+
processUnion(union: UnionPattern): Pattern {
29+
const patterns = union.patterns.map(pattern => {
30+
this.incrementScope()
31+
return this.processPattern(pattern)
32+
})
33+
34+
return {
35+
...union,
36+
patterns,
37+
}
38+
}
39+
40+
processBlankNode(blankNode: BlankNode): BlankNode {
41+
if (!this.blankNodes.has(blankNode)) {
42+
this.blankNodes.set(blankNode, this.scopeCounter)
43+
}
44+
45+
if (this.blankNodes.get(blankNode) === this.scopeCounter) {
46+
return blankNode
47+
}
48+
49+
return this.factory.blankNode(`s${this.scopeCounter}_${blankNode.value}`)
50+
}
51+
52+
private incrementScope() {
53+
this.scopeCounter++
54+
}
55+
}

packages/shape-to-query/lib/shapeToQuery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import type { Processor } from '@hydrofoil/sparql-processor'
88
import type { Options } from './shapeToPatterns.js'
99
import { shapeToPatterns } from './shapeToPatterns.js'
1010
import { UnionRepeatedPatternsRemover } from './optimizer/UnionRepeatedPatternsRemover.js'
11+
import { BlankNodeScopeFixer } from './optimizer/BlankNodeScopeFixer.js'
1112

1213
const generator = new sparqljs.Generator()
1314

1415
const defaultOptimizers = (): Processor[] => [
1516
new DuplicatePatternRemover(rdf),
1617
new UnionRepeatedPatternsRemover(rdf),
18+
new BlankNodeScopeFixer(rdf),
1719
new PrefixExtractor(rdf),
1820
]
1921

packages/shape-to-query/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"chai": "^5.1.2",
5555
"chai-string": "^1.5.0",
5656
"debug": "^4.4.0",
57+
"docker-compose": "^1.1.0",
5758
"glob": "^11.0.0",
5859
"mocha-chai-jest-snapshot": "^1.1.6",
5960
"n3": "^1.16.3",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@prefix sparql: <http://datashapes.org/sparql#> .
2+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
3+
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .
4+
@prefix sh: <http://www.w3.org/ns/shacl#> .
5+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
6+
@prefix schema: <http://schema.org/> .
7+
@prefix meta: <https://cube.link/meta/> .
8+
@prefix s2q: <https://hypermedia.app/shape-to-query#> .
9+
10+
<>
11+
a sh:NodeShape ;
12+
sh:property
13+
[
14+
sh:path schema:hasDefinedTerm ;
15+
sh:node
16+
[
17+
sh:property
18+
[
19+
sh:path rdf:type ;
20+
] ;
21+
sh:property
22+
[
23+
sh:path schema:name ;
24+
hydra:freetextQuery "foo"
25+
]
26+
] ;
27+
sh:values
28+
[
29+
sh:distinct
30+
[
31+
sh:limit 10 ;
32+
sh:nodes
33+
[
34+
sh:offset 0 ;
35+
sh:nodes
36+
[
37+
sh:orderBy
38+
[
39+
sh:path schema:name ;
40+
] ;
41+
sh:nodes
42+
[
43+
sh:orderBy
44+
[
45+
sh:path [ sh:inversePath schema:inDefinedTermSet ] ;
46+
] ;
47+
sh:nodes
48+
[
49+
sh:nodes
50+
[
51+
sh:path [ sh:inversePath schema:inDefinedTermSet ] ;
52+
] ;
53+
sh:filterShape
54+
[
55+
sh:property
56+
[
57+
sh:path rdf:type ;
58+
sh:hasValue schema:DefinedTerm ;
59+
] ;
60+
] ;
61+
] ;
62+
] ;
63+
]
64+
]
65+
]
66+
] ;
67+
] ;
68+
.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
PREFIX schema: <http://schema.org/>
2+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
3+
CONSTRUCT {
4+
?resource1 schema:hasDefinedTerm ?resource2.
5+
?resource2 rdf:type ?resource4.
6+
?resource2 schema:name ?resource7.
7+
}
8+
WHERE {
9+
{
10+
SELECT ?resource1 ?resource2 WHERE {
11+
{
12+
SELECT DISTINCT ?resource1 ?resource2 WHERE {
13+
?resource1 ^schema:inDefinedTermSet ?resource2.
14+
?resource2 rdf:type ?resource4.
15+
VALUES ?resource4 {
16+
schema:DefinedTerm
17+
}
18+
OPTIONAL { ?resource2 ^schema:inDefinedTermSet ?resource5. }
19+
OPTIONAL { ?resource2 schema:name ?resource6. }
20+
}
21+
ORDER BY (?resource5) (?resource6)
22+
LIMIT 10
23+
}
24+
?resource1 schema:hasDefinedTerm ?resource10.
25+
{
26+
?resource10 schema:name ?resource12.
27+
{
28+
?resource10 <http://jena.apache.org/text#query> _:b261;
29+
schema:name ?resource12.
30+
_:b261 rdf:first schema:name;
31+
rdf:rest _:b262.
32+
_:b262 rdf:first "foo*";
33+
rdf:rest rdf:nil.
34+
FILTER(REGEX(?resource12, "^foo", "i"))
35+
}
36+
}
37+
}
38+
}
39+
UNION
40+
{
41+
SELECT ?resource2 ?resource4 ?resource7 WHERE {
42+
{
43+
SELECT DISTINCT ?resource1 ?resource2 WHERE {
44+
?resource1 ^schema:inDefinedTermSet ?resource2.
45+
?resource2 rdf:type ?resource4.
46+
VALUES ?resource4 {
47+
schema:DefinedTerm
48+
}
49+
OPTIONAL { ?resource2 ^schema:inDefinedTermSet ?resource5. }
50+
OPTIONAL { ?resource2 schema:name ?resource6. }
51+
}
52+
ORDER BY (?resource5) (?resource6)
53+
LIMIT 10
54+
}
55+
{ ?resource2 rdf:type ?resource4. }
56+
UNION
57+
{ ?resource2 schema:name ?resource7. }
58+
?resource1 schema:hasDefinedTerm ?resource10.
59+
{
60+
?resource10 schema:name ?resource12.
61+
{
62+
?resource10 <http://jena.apache.org/text#query> _:b261;
63+
schema:name ?resource12.
64+
_:b261 rdf:first schema:name;
65+
rdf:rest _:b262.
66+
_:b262 rdf:first "foo*";
67+
rdf:rest rdf:nil.
68+
FILTER(REGEX(?resource12, "^foo", "i"))
69+
}
70+
}
71+
}
72+
}
73+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
PREFIX ex: <http://example.org/>
2+
3+
ASK {
4+
_:foo a ex:Foo .
5+
{
6+
_:foo a ex:Bar .
7+
{
8+
_:foo a ex:Baz .
9+
{
10+
_:foo a ex:Qux .
11+
}
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)