Skip to content

[Detail Bug] Firestore Query.or loses AND grouping for multi-constraint sub-queries, widening results #95

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_a5ffdadd-e247-41b5-868a-d0307fa3d4c6/bugs/bug_66565029-b4bc-4cd8-8552-f57debb5423e

Introduced in 8e11e17 by @fwal on Dec 8, 2025

Summary

  • Context: Query.or / Query.and in packages/effect-firebase/src/lib/firestore/query/query.ts build the Or / And composite constraints consumed by the admin, client and mock query builders.
  • Bug: Query.or(...queries) flattens each sub-query with queries.flat(), discarding the grouping of any sub-query that contains more than one constraint, so an And sub-query (which Query.and deliberately returns flat when it has no nested composite) collapses into separate OR operands.
  • Actual vs. expected: Query.or(Query.and(where('a','==',1), where('b','==',2)), where('c','==',3)) yields Or([Where a, Where b, Where c]) and is evaluated as a==1 OR b==2 OR c==3, instead of the intended Or([And([Where a, Where b]), Where c]) evaluating to (a==1 AND b==2) OR c==3.
  • Impact: Firestore queries silently widen their filter — rows that the caller did not intend to match are returned. The mock (applyConstraints), the Admin SDK (buildCompositeFilter) and the Client SDK (toFilterConstraint) all recurse over the children of And/Or faithfully, so the wrong tree is faithfully produced at every backend for the pure-Where case shown here. (Backends additionally throw on any non-filter child such as OrderBy/Limit inside a composite — see "Reach" below — so the silent corruption is limited to composites of Wheres.)

Code with Bug

// packages/effect-firebase/src/lib/firestore/query/query.ts

export const and = <S>(...queries: ReadonlyArray<Query<S>>): Query<S> => {
  const constraints = queries.flat();
  const hasComposite = constraints.some(
    (c) => c._tag === 'And' || c._tag === 'Or',
  );
  if (hasComposite) {
    return [new And({ constraints })] as Query<S>;
  }
  return constraints as Query<S>; // <-- BUG 🔴 `and` of all-Where args returns them flat (no `And` node), and grouping is forgotten when nested in `or`
};

export const or = <S>(...queries: ReadonlyArray<Query<S>>): Query<S> => {
  const constraints = queries.flat(); // <-- BUG 🔴 flattening destroys per-argument grouping of any multi-constraint sub-query
  return [new Or({ constraints })] as Query<S>;
};

Explanation

  • Query.and intentionally returns a flat list when its arguments contain no nested And/Or (common case for multiple Where filters).
  • Query.or flattens its arguments into a single constraints list. If one argument is a multi-constraint query (e.g., from Query.and(where a, where b) returning [Where a, Where b]), that argument is split into separate operands of the outer Or.
  • This changes semantics from (a AND b) OR c to a OR b OR c, widening results silently for pure-Where composites.
  • This is reproducible end-to-end in the mock: intended tree Or([And([a,b]), c]) returns ['1','2','3'], while Query.or(Query.and(a,b), c) returns ['1','2','3','4'] (includes a doc matching only a==1).

Recommended Fix

Preserve grouping of each or argument by wrapping any argument that contains more than one constraint in an And before flattening:

// packages/effect-firebase/src/lib/firestore/query/query.ts
export const or = <S>(...queries: ReadonlyArray<Query<S>>): Query<S> => {
  const constraints = queries
    .map((q) => (q.length > 1 ? [new And({ constraints: q })] : q))
    .flat(); // <-- FIX 🟢 wrap multi-constraint sub-queries so their AND grouping survives
  return [new Or({ constraints })] as Query<S>;
};

History

This bug was introduced in commit 8e11e17. The commit added "provisional support for queries" and authored both Query.and and Query.or from scratch with their current queries.flat() flattening logic — and deliberately returns a flat Where[] when no nested composite is present, and or flattens its arguments into a single Or node with no per-argument grouping preservation.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions