Skip to content

fix(lens): apply search filters on @optional properties (#170) - #176

Merged
karelklima merged 1 commit into
karelklima:mainfrom
pau-minoves:fix/optional-property-search-filter
Aug 10, 2026
Merged

fix(lens): apply search filters on @optional properties (#170)#176
karelklima merged 1 commit into
karelklima:mainfrom
pau-minoves:fix/optional-property-search-filter

Conversation

@pau-minoves

Copy link
Copy Markdown
Contributor

Summary

Fixes #170.

A find({ where }) filter on a property declared @optional is silently ignored. The query builder wraps the property in an OPTIONAL block and emits the search FILTER inside that block, where it only leaves the value unbound instead of removing the row — so every resource of the class comes back regardless of the filter.

Root cause

In QueryBuilder.getShape, an @optional property is wrapped in OPTIONAL { ... } and SearchHelper's FILTER is pushed between the opening and closing braces:

OPTIONAL {
  ?iri <…/prop> ?iri_1 .
  FILTER (?iri_1 = "x") .   # only unbinds ?iri_1 — never removes the row
}

Fix

Emit the search FILTER after the OPTIONAL block closes, so it constrains the result set:

OPTIONAL { ?iri <…/prop> ?iri_1 . }
FILTER (!BOUND(?iri_1) || (?iri_1 != "x"))   # $not — keeps resources without the value

Operator polarity is handled explicitly:

  • Negative operators ($not, $notIn) are guarded with !BOUND(?v) || (...), so resources that lack the value are kept — "not equal to X" shouldn't drop resources that have no value. This matches the expected output in Search filter on @optional property does not exclude results #170.
  • Positive operators ($equals, $gt, $contains, $in, …) are left bare: an unbound value makes the comparison a runtime error, which SPARQL treats as false and correctly excludes the row (a resource without the value cannot match "equals X").

Required (non-optional) properties are unchanged — their FILTER still sits at the top level exactly as before, so existing behavior and tests are unaffected.

Changes

  • library/lens/query_builder.ts — move an optional property's search FILTER outside its OPTIONAL block; thread an optional flag to SearchHelper.
  • library/lens/search_helper.tsaddFilter guards negative operators with !BOUND(?v) || (...) when the property is optional.
  • tests/e2e/search.test.ts — new coverage for $equals / $not / $notIn on an @optional property, including the absent-value (unbound) cases.

Testing

  • deno task fmt:check — clean
  • deno task lint — clean
  • Full test suite: 288 passed, 0 failed (4 new tests added; no existing tests changed).

A `find({ where })` filter targeting a property declared `@optional` was
silently ignored: the query builder wrapped the property in an `OPTIONAL`
block and emitted the search `FILTER` *inside* that block, where it only
leaves the value unbound instead of removing the row. Every resource of the
class was returned regardless of the filter.

Emit the search FILTER *after* the OPTIONAL block closes so it constrains the
result set. Negative operators ($not, $notIn) are guarded with
`!BOUND(?v) || (...)` so resources that lack the value are kept ("not equal
to X" should not drop resources that have no value); positive operators are
left bare, so an unbound value makes the comparison an error and correctly
excludes the row.

Adds e2e coverage for $equals / $not / $notIn on an optional property,
including the unbound (absent-value) cases.

Fixes karelklima#170

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@karelklima

Copy link
Copy Markdown
Owner

Hi @pau-minoves, thanks for the PR!

I will do some internal testing of the functionality before commenting on the code. There is one important thing that I need to consider from the library interface perspective - whether the positive filter on an optional property (e.g. $equals) only match the entity if the property actually exists. Because the current solution, as far as I understand it from briefly reading through the solution, accepts entities that lack such property.

@pau-minoves

Copy link
Copy Markdown
Contributor Author

Thanks @karelklima . I'll go by your guidance here. FYI, we have this fixed deployed in production and so far so good.

@karelklima

Copy link
Copy Markdown
Owner

I am having trouble merging / verifying this work and publishing a new LDkit version because of upstream issues with esm.sh and Deno. Will try to resolve it soon, but no ETA.

@pau-minoves

pau-minoves commented Jul 30, 2026 via email

Copy link
Copy Markdown
Contributor Author

@karelklima
karelklima merged commit 5a6eba1 into karelklima:main Aug 10, 2026
2 of 3 checks passed
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.

Search filter on @optional property does not exclude results

2 participants