fix(lens): apply search filters on @optional properties (#170) - #176
Conversation
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>
|
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. |
|
Thanks @karelklima . I'll go by your guidance here. FYI, we have this fixed deployed in production and so far so good. |
|
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. |
|
Yep, saw it. No issues.
…On Wed, Jul 29, 2026 at 5:10 PM Karel Klíma ***@***.***> wrote:
*karelklima* left a comment (karelklima/ldkit#176)
<#176 (comment)>
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.
—
Reply to this email directly, view it on GitHub
<#176?email_source=notifications&email_token=AAHL42ZHQHZAJXX67GFAX2L5HIHXVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJRHE3TGNJWHE4KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5119735698>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAHL423CK26SPNOITCBCQYL5HIHXVAVCNFSNUABFKJSXA33TNF2G64TZHM2DAMBYGAZDCNRSHNEXG43VMU5TIOBUG44TIOJVGQ2KC5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Summary
Fixes #170.
A
find({ where })filter on a property declared@optionalis silently ignored. The query builder wraps the property in anOPTIONALblock and emits the searchFILTERinside 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@optionalproperty is wrapped inOPTIONAL { ... }andSearchHelper'sFILTERis pushed between the opening and closing braces:Fix
Emit the search
FILTERafter theOPTIONALblock closes, so it constrains the result set:Operator polarity is handled explicitly:
$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.$equals,$gt,$contains,$in, …) are left bare: an unbound value makes the comparison a runtime error, which SPARQL treats asfalseand correctly excludes the row (a resource without the value cannot match "equals X").Required (non-optional) properties are unchanged — their
FILTERstill 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 searchFILTERoutside itsOPTIONALblock; thread anoptionalflag toSearchHelper.library/lens/search_helper.ts—addFilterguards negative operators with!BOUND(?v) || (...)when the property is optional.tests/e2e/search.test.ts— new coverage for$equals/$not/$notInon an@optionalproperty, including the absent-value (unbound) cases.Testing
deno task fmt:check— cleandeno task lint— clean