Skip to content
Open
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
26 changes: 20 additions & 6 deletions lib/query_processing/src/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,19 @@ pub fn variable(
context: &Context,
) -> Result<SolutionMappings, QueryProcessingError> {
if !solution_mappings.rdf_node_types.contains_key(v.as_str()) {
return Err(QueryProcessingError::VariableNotFound(
v.as_str().to_string(),
// A variable that does not occur in the pattern is unbound in every solution
// (SPARQL 1.1, section 18.6), e.g. ORDER BY ?x or COALESCE(?x, ...) where ?x is
// never bound. Evaluate it as unbound instead of failing the query.
solution_mappings.mappings = solution_mappings.mappings.with_column(
lit(LiteralValue::untyped_null())
.cast(BaseRDFNodeType::None.default_input_polars_data_type())
.alias(context.as_str()),
);
solution_mappings.rdf_node_types.insert(
context.as_str().to_string(),
));
BaseRDFNodeType::None.into_default_input_rdf_node_state(),
);
return Ok(solution_mappings);
}
solution_mappings.mappings = solution_mappings
.mappings
Expand Down Expand Up @@ -427,10 +436,15 @@ pub fn bound(
outer_context: &Context,
) -> Result<SolutionMappings, QueryProcessingError> {
if !solution_mappings.rdf_node_types.contains_key(v.as_str()) {
return Err(QueryProcessingError::VariableNotFound(
v.as_str().to_string(),
// A variable that does not occur in the pattern is never bound.
solution_mappings.mappings = solution_mappings
.mappings
.with_column(lit(false).alias(outer_context.as_str()));
solution_mappings.rdf_node_types.insert(
outer_context.as_str().to_string(),
));
BaseRDFNodeType::Literal(xsd::BOOLEAN.into_owned()).into_default_input_rdf_node_state(),
);
return Ok(solution_mappings);
}
let expr = expr_is_null_workaround(
col(v.as_str()),
Expand Down
57 changes: 57 additions & 0 deletions py_maplib/tests/test_unbound_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pytest

from maplib import Model

# SPARQL 1.1 18.6: a variable that does not occur in the pattern is unbound in every
# solution. Expressions that use it must evaluate as with any other unbound variable
# instead of failing the query.


@pytest.fixture
def model():
m = Model()
m.reads(
'<http://example.net/a> <http://example.net/p> "b" .\n'
'<http://example.net/b> <http://example.net/p> "a" .\n',
format="ntriples",
)
return m


def test_order_by_variable_not_in_pattern(model):
df = model.query(
"SELECT ?s WHERE { ?s <http://example.net/p> ?o } ORDER BY ?nope ?o"
)
assert df["s"].to_list() == ["<http://example.net/b>", "<http://example.net/a>"]


def test_distinct_order_by_variable_not_in_pattern(model):
df = model.query(
"SELECT DISTINCT ?s WHERE { ?s <http://example.net/p> ?o } ORDER BY ?nope"
)
assert sorted(df["s"].to_list()) == [
"<http://example.net/a>",
"<http://example.net/b>",
]


def test_bound_variable_not_in_pattern(model):
df = model.query(
"SELECT ?s WHERE { ?s <http://example.net/p> ?o FILTER(!BOUND(?nope)) }"
)
assert df.height == 2


def test_coalesce_variable_not_in_pattern(model):
df = model.query(
"SELECT ?s ?x WHERE { ?s <http://example.net/p> ?o "
"BIND(COALESCE(?nope, ?o) AS ?x) } ORDER BY ?x"
)
assert df["x"].to_list() == ["a", "b"]


def test_filter_on_variable_not_in_pattern(model):
df = model.query(
'SELECT ?s WHERE { ?s <http://example.net/p> ?o FILTER(?nope = "a") }'
)
assert df.height == 0