Skip to content

fix(query): fall back to full scan for computed properties in predicates - #144

Open
mrdevrobot wants to merge 1 commit into
mainfrom
fix/query-pushdown-computed-property
Open

fix(query): fall back to full scan for computed properties in predicates#144
mrdevrobot wants to merge 1 commit into
mainfrom
fix/query-pushdown-computed-property

Conversation

@mrdevrobot

Copy link
Copy Markdown
Contributor

Summary

  • Fixes Query predicate on a computed (get-only) property silently returns false for every document #143: predicates touching a get-only computed property (e.g. IsOpen => State != Closed) were pushed down into a BSON field lookup by property name, silently returning false for every document since no such field is ever stored.
  • Added IsPersistedMember (a member is only pushed down if it's a field or a property with a setter) and gated every member-name extraction point in BsonExpressionEvaluator on it: bare bool member, Nullable.HasValue, logical NOT, .Equals(), string instance methods, static string helpers, the IN operator (both list.Contains(x.Prop) and Enumerable.Contains(list, x.Prop)), the general binary comparison path, and CompareTo.
  • When a member fails the check, TryCompileBody returns null, so DocumentCollection.FetchAsync falls through to its existing full-scan + in-memory-filter strategy, which compiles the real expression tree and evaluates the actual getter correctly - matching plain LINQ-to-Objects semantics instead of silently returning wrong results.

Test plan

  • Unit test: entity with a get-only bool property derived from a stored enum field; insert with the property true, assert FindAsync(x => x.ComputedProp) returns it (currently returns empty without the fix)
  • Regression: existing tests for the still-covered patterns (bare bool on a real stored bool field, !x.Prop, string methods, IN, binary comparisons, CompareTo) continue to push down correctly
  • dotnet build/dotnet test on BLite.Core

🤖 Generated with Claude Code

BsonExpressionEvaluator translated any bare bool member access (and
other member-based patterns: NOT, Equals, string methods, IN, binary
comparisons, CompareTo) into a BSON-level field lookup by property
name, with no check that the property is actually persisted. A
get-only computed property (e.g. `public bool IsOpen => State !=
Closed`) has no backing BSON field, so the generated predicate scanned
every field in the document, never found one named "isopen", and
silently returned false for every document - regardless of the real
value. `.Where(x => x.IsOpen)` / `.FindAsync(x => x.IsOpen)` therefore
always returned empty, while a plain `FindByIdAsync` (no predicate)
returned the correct document intact.

Added IsPersistedMember (a property is only pushed down if it has a
setter) and gated every member-name extraction point in
BsonExpressionEvaluator on it. When the check fails, TryCompileBody
returns null and the caller falls through to the existing full-scan +
in-memory-filter strategy, which evaluates the real getter correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 6, 2026 12:52
@mrdevrobot mrdevrobot self-assigned this Sep 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Compound predicates can still partially compile (dropping the computed-property side) and be executed as Strategy 2 without an in-memory re-filter, risking incorrect query results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes incorrect query results when predicates reference get-only computed properties by preventing those members from being pushed down as BSON field lookups, so queries can fall back to the existing full-scan + in-memory evaluation path.

Changes:

  • Introduces IsPersistedMember and gates all member-name-based predicate compilation paths on it.
  • Causes TryCompileBody to stop compiling member-based predicates for non-persisted members, intended to trigger Strategy 3 fallback in DocumentCollection.FetchAsync.
File summaries
File Description
src/BLite.Core/Query/BsonExpressionEvaluator.cs Adds persisted-member gating across predicate compilation paths to avoid incorrect BSON pushdown for computed properties.
Review details

Suppressed comments (1)

src/BLite.Core/Query/BsonExpressionEvaluator.cs:711

  • There are existing unit tests for BsonExpressionEvaluator, but none covering the regression scenario described in #143 (computed/get-only property used in a predicate should force a Strategy 3 fallback and still return correct results). Adding a focused test would help prevent this silently returning wrong results again (including for compound predicates like x => x.ComputedProp && x.Age > 10).
    private static bool IsPersistedMember(MemberInfo member)
        => member is not PropertyInfo { CanWrite: false };
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 123 to +127
// ── Bare bool member: e => e.IsActive → IsActive == true ──────────────
if (body is MemberExpression bareM &&
bareM.Expression == parameter &&
bareM.Type == typeof(bool))
bareM.Type == typeof(bool) &&
IsPersistedMember(bareM.Member))
Comment on lines +703 to +712
/// <summary>
/// True for a field, or a property with a setter - the shapes BLite's document mapper actually
/// persists as a BSON field. A get-only property (<c>public bool IsOpen => State != Closed</c>) has
/// no backing BSON field at all, so pushing it down into <see cref="CreatePredicate"/> would scan
/// every document for a field name that can never exist and silently return <c>false</c> for
/// everyone - wrong, instead of falling back to a real in-memory evaluation of the getter.
/// </summary>
private static bool IsPersistedMember(MemberInfo member)
=> member is not PropertyInfo { CanWrite: false };

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.

Query predicate on a computed (get-only) property silently returns false for every document

2 participants