Skip to content

fix: close LINQ Where predicate-translation gaps#124

Merged
Tr00d merged 4 commits into
masterfrom
fix-linq-visitors
Jul 9, 2026
Merged

fix: close LINQ Where predicate-translation gaps#124
Tr00d merged 4 commits into
masterfrom
fix-linq-visitors

Conversation

@Tr00d

@Tr00d Tr00d commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #122. WhereExpressionVisitor mishandled several common LINQ Where predicate shapes: one produced incorrect results, two threw non-descriptive errors, and one was unsupported. Each is addressed in its own commit, developed test-first against GenerateUrl()/exception assertions, with the Where XML documentation updated to describe supported and unsupported shapes.

  • Negation was discarded. Where(x => !(x.Name == "foo")) generated name=eq.foo — the inverse of the intended filter — which also affected Update/Delete calls using the same predicate. Negation is now translated to a not. filter. PrepareFilter is also corrected so a negated logical group serializes as not.and=(...), which additionally fixes the public .Not() API for group filters.
  • Captured-collection Contains is translated to in. Where(x => ids.Contains(x.Id)) previously threw rather than producing an in filter. Instance List.Contains, static Enumerable.Contains, and the span-based MemoryExtensions.Contains used for arrays on modern .NET are all supported.
  • A boolean column may be used directly as a predicate. Where(x => x.IsActive) and Where(x => !x.IsActive) previously threw "Unable to parse"; they now translate to eq and not.eq.
  • Column-to-column comparison returns a descriptive error. Where(x => x.Start < x.End) previously failed with variable 'x' not defined. PostgREST filters compare a column to a literal value and cannot express a column-to-column comparison, so this now throws an ArgumentException recommending a computed/generated column or an RPC.

All new tests assert on GenerateUrl() and require no network access; the full LINQ unit suite and the TestLinqWhere integration test pass.

Tr00d added 4 commits July 9, 2026 14:39
Negation (`!`) in a Where predicate was silently dropped, producing the
opposite filter. Override VisitUnary to wrap the operand in a `not.`
filter, and fix PrepareFilter to serialize negated groups as
`not.and=(...)` / `not.or=(...)`.
`Where(x => ids.Contains(x.Col))` previously threw instead of producing a
Postgrest `in` filter. VisitMethodCall now classifies Contains by which
side references the model: a column containing a constant keeps the
cs/like translation, while a captured collection containing a column
becomes `in`. Handles instance, static Enumerable, and span-based
MemoryExtensions.Contains; ambiguous shapes throw a descriptive error.
`Where(x => x.IsActive)` and `Where(x => !x.IsActive)` threw "Unable to
parse the supplied predicate" because the visitor only built filters from
binary and method-call expressions. VisitMember now translates a boolean
column that references the model into `column.eq.true`, working as a bare
body, inside &&/|| groups, and negated (via the existing not. wrapping).
`Where(x => x.StartDate < x.EndDate)` crashed with a cryptic
"variable 'x' ... not defined" because the visitor tried to evaluate the
right-hand column as a local value. VisitBinary now detects a right side
that references the model and throws an ArgumentException explaining that
Postgrest has no column-to-column filter, suggesting a computed column or
RPC instead.
@Tr00d Tr00d marked this pull request as ready for review July 9, 2026 14:03
@Tr00d Tr00d merged commit 7b2465a into master Jul 9, 2026
1 check passed
@Tr00d Tr00d deleted the fix-linq-visitors branch July 9, 2026 14:25
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.

2 participants