Skip to content

fix(sparql): bind boolean expression results as xsd:boolean - #1113

Open
Anai-Guo wants to merge 1 commit into
trustgraph-ai:masterfrom
Anai-Guo:fix/bind-boolean-xsd-datatype
Open

fix(sparql): bind boolean expression results as xsd:boolean#1113
Anai-Guo wants to merge 1 commit into
trustgraph-ai:masterfrom
Anai-Guo:fix/bind-boolean-xsd-datatype

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Sep 5, 2026

Copy link
Copy Markdown

Problem

bool is a subclass of int in Python, so in _eval_extend (trustgraph-flow/trustgraph/query/sparql/algebra.py) the (int, float) branch catches Python bools first and the bool branch written just below it is dead code:

elif isinstance(val, (int, float)):
    new_sol[var_name] = Term(type=LITERAL, value=str(val))
elif isinstance(val, str):
    new_sol[var_name] = Term(type=LITERAL, value=val)
elif isinstance(val, bool):                     # unreachable
    new_sol[var_name] = Term(
        type=LITERAL, value=str(val).lower(),
        datatype="http://www.w3.org/2001/XMLSchema#boolean"
    )

evaluate_expression() returns a Python bool for a large part of the expression language — its own docstring says so ("The result value (Term, bool, number, string, or None)") — including every relational operator, IN, EXISTS / NOT EXISTS, REGEX, isIRI / isLITERAL / isBLANK, BOUND and LANGMATCHES.

So BIND(?a > ?b AS ?ok) binds Term(LITERAL, value="True") with no datatype, instead of Term(LITERAL, value="true", datatype=xsd:boolean).

Why it produces wrong results, not just a wrong lexical form

_effective_boolean() in expressions.py only reads a literal as a boolean when its datatype is xsd:boolean. An untyped literal falls through to the string rule:

if val.datatype == "http://www.w3.org/2001/XMLSchema#boolean":
    return v.lower() in ("true", "1")
...
return len(v) > 0

"False" is five characters long, so its EBV is True. A query like

BIND(?a > ?b AS ?ok) . FILTER(?ok)

keeps every solution, including the ones where the comparison was false.

Fix

Move the bool branch ahead of (int, float). The branch bodies are untouched.

Verification

_eval_extend was extracted from algebra.py and _effective_boolean from expressions.py with ast.get_source_segment (no hand-copying), the graph-walking neighbours stubbed, and the BIND(...) . FILTER(?ok) chain run before and after the patch:

BIND value unpatched bound term FILTER patched bound term FILTER
True value='True' datatype=None True value='true' datatype=xsd:boolean True
False value='False' datatype=None True value='false' datatype=xsd:boolean False
1 value='1' datatype=None True value='1' datatype=None True
0 value='0' datatype=None True value='0' datatype=None True
42 value='42' datatype=None True value='42' datatype=None True
2.5 value='2.5' datatype=None True value='2.5' datatype=None True
'text' value='text' datatype=None True value='text' datatype=None True

Only bool results change; int, float, str and Term results bind exactly as before.

🤖 Generated with Claude Code

bool is a subclass of int, so in _eval_extend the
`isinstance(val, (int, float))` branch catches Python bools first and the
`isinstance(val, bool)` branch below it is unreachable. BIND on any
expression that yields a bool - every relational operator, IN, EXISTS,
NOT EXISTS, REGEX, isIRI, BOUND, LANGMATCHES - therefore binds a plain
untyped literal "True"/"False" instead of an xsd:boolean "true"/"false".

That is not just a lexical-form problem. _effective_boolean() in
expressions.py only reads a literal as a boolean when its datatype is
xsd:boolean; an untyped literal falls through to `len(v) > 0`, and
"False" is five characters long. So

    BIND(?a > ?b AS ?ok) . FILTER(?ok)

keeps the solution even when the comparison was false.

Move the bool branch ahead of (int, float). The branch bodies are
unchanged, and int/float/str/Term results bind exactly as before.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Contributor License Agreement

Thank you for your contribution! Before we can accept it, the following contributor(s) must sign our CLA:

@Anai-Guo

Please read the appropriate agreement:

  • Contributing as an individual? Read the Individual CLA
  • Contributing on behalf of a company or organisation? Read the Entity CLA

Once you have read the appropriate agreement, post the following as a comment on this PR (copy and paste exactly):

I have read the CLA Document and I hereby sign the CLA

The bot will record your signature and update this PR automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant