From 59a1b98afba80fc830f4d666dac253ce97413d44 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sun, 4 Oct 2026 09:03:49 +0200 Subject: [PATCH] Return empty string from STRAFTER/STRBEFORE when there is no match SPARQL 1.1 17.4.3.9/17.4.3.10: if the second argument does not occur in the first, an empty simple literal is returned. Previously the whole first argument was returned. Refs DataTreehouse/maplib#79. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/expressions/functions/str_after.rs | 3 +- .../src/expressions/functions/str_before.rs | 3 +- py_maplib/tests/test_str_before_after.py | 51 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 py_maplib/tests/test_str_before_after.py diff --git a/lib/query_processing/src/expressions/functions/str_after.rs b/lib/query_processing/src/expressions/functions/str_after.rs index e48996f8..9c96221f 100644 --- a/lib/query_processing/src/expressions/functions/str_after.rs +++ b/lib/query_processing/src/expressions/functions/str_after.rs @@ -8,7 +8,8 @@ pub fn str_after(c: Column, s: String) -> Result { if let Some(range_to) = range_to { Some(&x[range_to + s.len()..]) } else { - Some(x) + // SPARQL 1.1 17.4.3.9/17.4.3.10: no occurrence gives the empty string + Some("") } } else { None diff --git a/lib/query_processing/src/expressions/functions/str_before.rs b/lib/query_processing/src/expressions/functions/str_before.rs index b1279082..271049cb 100644 --- a/lib/query_processing/src/expressions/functions/str_before.rs +++ b/lib/query_processing/src/expressions/functions/str_before.rs @@ -8,7 +8,8 @@ pub fn str_before(c: Column, s: String) -> Result { if let Some(range_to) = range_to { Some(&x[0..range_to]) } else { - Some(x) + // SPARQL 1.1 17.4.3.9/17.4.3.10: no occurrence gives the empty string + Some("") } } else { None diff --git a/py_maplib/tests/test_str_before_after.py b/py_maplib/tests/test_str_before_after.py new file mode 100644 index 00000000..aa00bb08 --- /dev/null +++ b/py_maplib/tests/test_str_before_after.py @@ -0,0 +1,51 @@ +import pytest + +from maplib import Model + +# SPARQL 1.1 17.4.3.9 (STRBEFORE) and 17.4.3.10 (STRAFTER): +# "If there is no such occurrence, an empty simple literal is returned." + + +@pytest.mark.parametrize( + "expression,expected", + [ + ('STRAFTER("abc", "b")', "c"), + ('STRAFTER("abc", "xyz")', ""), + ('STRAFTER("abc", "")', "abc"), + ('STRBEFORE("abc", "b")', "a"), + ('STRBEFORE("abc", "xyz")', ""), + ('STRBEFORE("abc", "")', ""), + ], +) +def test_str_before_after_simple_literal(expression, expected): + m = Model() + df = m.query(f"SELECT (STR({expression}) AS ?s) WHERE {{}}") + assert df["s"].to_list() == [expected] + + +@pytest.mark.parametrize( + "expression,expected", + [ + ('STRAFTER("abc"@en, "b")', "c"), + ('STRAFTER("abc"@en, "z")', ""), + ('STRBEFORE("abc"@en, "b")', "a"), + ('STRBEFORE("abc"@en, "z")', ""), + ], +) +def test_str_before_after_lang_string(expression, expected): + m = Model() + df = m.query(f"SELECT (STR({expression}) AS ?s) WHERE {{}}") + assert df["s"].to_list() == [expected] + + +def test_str_after_no_match_on_data(): + m = Model() + m.insert( + """PREFIX ex: + CONSTRUCT { ex:a ex:v "prefix_a" . ex:b ex:v "other_b" . } WHERE {}""" + ) + df = m.query( + """PREFIX ex: + SELECT ?x (STRAFTER(?v, "prefix_") AS ?s) WHERE { ?x ex:v ?v } ORDER BY ?x""" + ) + assert df["s"].to_list() == ["a", ""]