Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-cherries-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/sparql-processor": patch
---

Added overridable method `processSubquery`
5 changes: 5 additions & 0 deletions .changeset/three-donkeys-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shape-to-query": patch
---

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.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions packages/processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,15 @@ export default abstract class ProcessorImpl<F extends DataFactory = DataFactory>
.with({ type: 'minus' }, minus => this.processMinus(minus))
.with({ type: 'filter' }, filter => this.processFilter(filter))
.with({ type: 'bind' }, bind => this.processBind(bind))
.with({ type: 'query', queryType: 'SELECT' }, query => {
// clones this instance to provide a separate context for the subquery
return this.clone().processQuery(query)
})
.with({ type: 'query', queryType: 'SELECT' }, query => this.processSubquery(query))
.otherwise(p => p)
}

processSubquery(query: sparqljs.SelectQuery): sparqljs.SelectQuery {
// clones this instance to provide a separate context for the subquery
return this.clone().processQuery(query)
}

processBind(bind: sparqljs.BindPattern): sparqljs.Pattern {
return {
...bind,
Expand Down
55 changes: 55 additions & 0 deletions packages/shape-to-query/lib/optimizer/BlankNodeScopeFixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Processor from '@hydrofoil/sparql-processor'
import type { BlankNode, DataFactory } from '@rdfjs/types'
import type { GroupPattern, Pattern, UnionPattern } from 'sparqljs'
import type { Environment } from '@rdfjs/environment/Environment.js'
import type { TermMapFactory } from '@rdfjs/term-map/Factory.js'

/**
* Ensures that blank nodes are renamed when necessary
* to avoid using the same labels in different scopes
*/
export class BlankNodeScopeFixer extends Processor<Environment<TermMapFactory | DataFactory>> {
constructor(
factory: Environment<TermMapFactory | DataFactory>,
private scopeCounter = 0,
private blankNodes = factory.termMap()) {
super(factory)
}

clone() {
return new BlankNodeScopeFixer(this.factory, this.scopeCounter, this.blankNodes)
}

processGroup(group: GroupPattern): Pattern {
this.incrementScope()
return super.processGroup(group)
}

processUnion(union: UnionPattern): Pattern {
const patterns = union.patterns.map(pattern => {
this.incrementScope()
return this.processPattern(pattern)
})

return {
...union,
patterns,
}
}

processBlankNode(blankNode: BlankNode): BlankNode {
if (!this.blankNodes.has(blankNode)) {
this.blankNodes.set(blankNode, this.scopeCounter)
}

if (this.blankNodes.get(blankNode) === this.scopeCounter) {
return blankNode
}

return this.factory.blankNode(`s${this.scopeCounter}_${blankNode.value}`)
}

private incrementScope() {
this.scopeCounter++
}
}
2 changes: 2 additions & 0 deletions packages/shape-to-query/lib/shapeToQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import type { Processor } from '@hydrofoil/sparql-processor'
import type { Options } from './shapeToPatterns.js'
import { shapeToPatterns } from './shapeToPatterns.js'
import { UnionRepeatedPatternsRemover } from './optimizer/UnionRepeatedPatternsRemover.js'
import { BlankNodeScopeFixer } from './optimizer/BlankNodeScopeFixer.js'

const generator = new sparqljs.Generator()

const defaultOptimizers = (): Processor[] => [
new DuplicatePatternRemover(rdf),
new UnionRepeatedPatternsRemover(rdf),
new BlankNodeScopeFixer(rdf),
new PrefixExtractor(rdf),
]

Expand Down
1 change: 1 addition & 0 deletions packages/shape-to-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"chai": "^5.1.2",
"chai-string": "^1.5.0",
"debug": "^4.4.0",
"docker-compose": "^1.1.0",
"glob": "^11.0.0",
"mocha-chai-jest-snapshot": "^1.1.6",
"n3": "^1.16.3",
Expand Down
68 changes: 68 additions & 0 deletions packages/shape-to-query/test/example/full-text-search.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@prefix sparql: <http://datashapes.org/sparql#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix schema: <http://schema.org/> .
@prefix meta: <https://cube.link/meta/> .
@prefix s2q: <https://hypermedia.app/shape-to-query#> .

<>
a sh:NodeShape ;
sh:property
[
sh:path schema:hasDefinedTerm ;
sh:node
[
sh:property
[
sh:path rdf:type ;
] ;
sh:property
[
sh:path schema:name ;
hydra:freetextQuery "foo"
]
] ;
sh:values
[
sh:distinct
[
sh:limit 10 ;
sh:nodes
[
sh:offset 0 ;
sh:nodes
[
sh:orderBy
[
sh:path schema:name ;
] ;
sh:nodes
[
sh:orderBy
[
sh:path [ sh:inversePath schema:inDefinedTermSet ] ;
] ;
sh:nodes
[
sh:nodes
[
sh:path [ sh:inversePath schema:inDefinedTermSet ] ;
] ;
sh:filterShape
[
sh:property
[
sh:path rdf:type ;
sh:hasValue schema:DefinedTerm ;
] ;
] ;
] ;
] ;
]
]
]
] ;
] ;
.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
PREFIX schema: <http://schema.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
CONSTRUCT {
?resource1 schema:hasDefinedTerm ?resource2.
?resource2 rdf:type ?resource4.
?resource2 schema:name ?resource7.
}
WHERE {
{
SELECT ?resource1 ?resource2 WHERE {
{
SELECT DISTINCT ?resource1 ?resource2 WHERE {
?resource1 ^schema:inDefinedTermSet ?resource2.
?resource2 rdf:type ?resource4.
VALUES ?resource4 {
schema:DefinedTerm
}
OPTIONAL { ?resource2 ^schema:inDefinedTermSet ?resource5. }
OPTIONAL { ?resource2 schema:name ?resource6. }
}
ORDER BY (?resource5) (?resource6)
LIMIT 10
}
?resource1 schema:hasDefinedTerm ?resource10.
{
?resource10 schema:name ?resource12.
{
?resource10 <http://jena.apache.org/text#query> _:b261;
schema:name ?resource12.
_:b261 rdf:first schema:name;
rdf:rest _:b262.
_:b262 rdf:first "foo*";
rdf:rest rdf:nil.
FILTER(REGEX(?resource12, "^foo", "i"))
}
}
}
}
UNION
{
SELECT ?resource2 ?resource4 ?resource7 WHERE {
{
SELECT DISTINCT ?resource1 ?resource2 WHERE {
?resource1 ^schema:inDefinedTermSet ?resource2.
?resource2 rdf:type ?resource4.
VALUES ?resource4 {
schema:DefinedTerm
}
OPTIONAL { ?resource2 ^schema:inDefinedTermSet ?resource5. }
OPTIONAL { ?resource2 schema:name ?resource6. }
}
ORDER BY (?resource5) (?resource6)
LIMIT 10
}
{ ?resource2 rdf:type ?resource4. }
UNION
{ ?resource2 schema:name ?resource7. }
?resource1 schema:hasDefinedTerm ?resource10.
{
?resource10 schema:name ?resource12.
{
?resource10 <http://jena.apache.org/text#query> _:b261;
schema:name ?resource12.
_:b261 rdf:first schema:name;
rdf:rest _:b262.
_:b262 rdf:first "foo*";
rdf:rest rdf:nil.
FILTER(REGEX(?resource12, "^foo", "i"))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
PREFIX ex: <http://example.org/>

ASK {
_:foo a ex:Foo .
{
_:foo a ex:Bar .
{
_:foo a ex:Baz .
{
_:foo a ex:Qux .
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
PREFIX ex: <http://example.org/>

ASK {
{
SELECT * {
_:foo a ex:Foo .
}
}
UNION
{
SELECT * {
_:foo a ex:Bar .
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
PREFIX ex: <http://example.org/>

ASK {
{
_:foo a ex:Bar .
}
UNION
{
_:foo a ex:Foo .
}
UNION
{
_:foo a ex:Baz .
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PREFIX ex: <http://example.org/>

ASK {
_:foo a ex:Foo .
{
_:foo a ex:Bar .
_:foo a ex:Baz .
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>

ASK {
_:foo a ex:Foo .
_:foo a ex:Bar .
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PREFIX ex: <http://example.org/>

ASK {
_:foo a ex:Foo .
{
_:foo a ex:Bar .
}
}

This file was deleted.

Loading
Loading