Callidon / sparql-engine

🚂 A framework for building SPARQL query engines in Javascript/Typescript
https://callidon.github.io/sparql-engine
MIT License
101 stars 14 forks source link

BIND operator broken since 7.0 #78

Open lucafabbian opened 2 years ago

lucafabbian commented 2 years ago

Describe the bug Bind operator working wrong since 7.0

To Reproduce Just use a query with BIND inside one of the examples (here I chose the one about using a storage based on N3.js).

'use strict'

const { Parser, Store } = require('n3')
const { HashMapDataset, Graph, PlanBuilder } = require('sparql-engine')

// Format a triple pattern according to N3 API:
// SPARQL variables must be replaced by `null` values
function formatTriplePattern (triple) {
  let subject = null
  let predicate = null
  let object = null
  if (!triple.subject.startsWith('?')) {
    subject = triple.subject
  }
  if (!triple.predicate.startsWith('?')) {
    predicate = triple.predicate
  }
  if (!triple.object.startsWith('?')) {
    object = triple.object
  }
  return { subject, predicate, object }
}

class N3Graph extends Graph {
  constructor () {
    super()
    this._store = Store()
  }

  insert (triple) {
    return new Promise((resolve, reject) => {
      try {
        this._store.addTriple(triple.subject, triple.predicate, triple.object)
        resolve()
      } catch (e) {
        reject(e)
      }
    })
  }

  delete (triple) {
    return new Promise((resolve, reject) => {
      try {
        this._store.removeTriple(triple.subject, triple.predicate, triple.object)
        resolve()
      } catch (e) {
        reject(e)
      }
    })
  }

  find (triple) {
    const { subject, predicate, object } = formatTriplePattern(triple)
    return this._store.getTriples(subject, predicate, object)
  }

  estimateCardinality (triple) {
    const { subject, predicate, object } = formatTriplePattern(triple)
    return Promise.resolve(this._store.countTriples(subject, predicate, object))
  }
}

const graph = new N3Graph()
const dataset = new HashMapDataset('http://example.org#default', graph)

// Load some RDF data into the graph
const parser = new Parser()
parser.parse(`
  @prefix foaf: <http://xmlns.com/foaf/0.1/> .
  @prefix : <http://example.org#> .
  :a foaf:name "a" .
  :b foaf:name "b" .
`).forEach(t => {
  graph._store.addTriple(t)
})

const query = `
  PREFIX foaf: <http://xmlns.com/foaf/0.1/>
  SELECT ?s ?name
  WHERE {
    BIND(<http://example.org#a> as ?s)
    ?s foaf:name ?name .
  }`

// Creates a plan builder for the RDF dataset
const builder = new PlanBuilder(dataset)

// Get an iterator to evaluate the query
const iterator = builder.build(query)

// Read results
iterator.subscribe(bindings => {
  console.log('Found solution:', bindings.toObject())
}, err => {
  console.error('error', err)
}, () => {
  console.log('Query evaluation complete!')
})

Expected behavior On 6.0 and below, I get the expected result:

Found solution: { '?s': 'http://example.org#a', '?name': '"a"' }
Query evaluation complete!

While on 7.0 and above, I get:

Found solution: { '?s': 'http://example.org#a', '?name': '"a"' }
Found solution: { '?s': 'http://example.org#a', '?name': '"b"' }
Query evaluation complete!
Callidon commented 2 years ago

Hi,

Thank you for using sparql-engine 👍 It is in deed a regression: in v0.7.0, we introduced a query plan optimizer, to, optimize the execution plan regardless of the syntax of the query. However, we didn't account for the very special case your issue highlight: a BIND clause sued to feed data into the res tof the query. The plan optimizer doesn't recognize the dependency between the BIND clause and the triple patterns, and pushes the BIND evaluation after the triple pattern evaluation. This pattern is evaluated first and then the BIND overrides ?s with http://example.org#a, leading to this results.

I will look into this if there's a quick fix, thank you for sharing it!