Skip to content

[finding] convertFiltersToAST hands the TRUE identity groups back UNLOWERED — { $and: [] } / { $or: [{}] } / { $and: [{}] } fail isFilterAST, so a filter meaning "every row" is refused on the wire #8770

Description

@claude

Measured while landing objectui#8513. That card fixed the CONSUMER half (ValueDataSource now answers these three correctly); this is the producer half, on a different consumer, and it needs a decision rather than a mechanical repair — so it is filed, not folded in. ⛔ Not claimed.

Measured

convertFiltersToAST({ $and: []   })  =>  { $and: []   }   isFilterAST: false
convertFiltersToAST({ $or:  [{}] })  =>  { $or:  [{}] }   isFilterAST: false
convertFiltersToAST({ $and: [{}] })  =>  { $and: [{}] }   isFilterAST: false
convertFiltersToAST({ $or:  []   })  =>  ['$or', '=', []]  isFilterAST: true    <- correct

(Run against @objectstack/spec 17.3.0 and this repo's built @object-ui/core.)

Why

lowerLogicalGroup returns undefined for a group that reduces to the TRUE identity — deliberately, and its docblock explains it: TRUE is "the absence of a constraint", and emitting a childless ['and'] would be isFilterAST FALSE and parseFilterAST undefined, i.e. no filter at all, which widens.

But when that undefined is the ONLY thing the filter produced, convertFiltersToAST falls to its general tail:

// If no conditions, return original filter
if (conditions.length === 0) {
  return filter;
}

— and hands back the caller's original OBJECT. So the group does not disappear at that level after all; it reappears one level up, in the $ dialect, in a slot the AST is expected to occupy. toFilterNode and mergeFilterNodes pass it straight through, and every consumer on that chain (ObjectGrid's schemaFilter, plugin-list's buildEffectiveFilter, plugin-view's ObjectView) drops it onto $filter.

Consequence, per consumer

  • provider: 'object' (the wire). @object-ui/data-objectstack routes an object $filter through this same function and then gates on isFilterAST. These three shapes do not pass it, so a filter whose ruled answer is EVERY row (objectstack#5322, merged objectstack#5365) is refused instead of being dropped as no-constraint.
  • provider: 'value'. Answered ZERO rows until objectui#8513; correct as of that card, which is why this one is about the producer and not about the matcher.

The decision this needs

Returning undefined from toFilterNode for a TRUE-identity filter is the obvious shape and is NOT obviously safe: convertFiltersToAST is exported and its declared return type includes Record<string, any>, the conditions.length === 0 tail also serves inputs that are not combinators at all (an all-null filter, {}), and undefined vs. the original object is exactly how some callers tell "no constraint" from "unlowerable". Whoever takes this should decide whether the identity fold belongs in lowerLogicalGroup's caller, in toFilterNode, or in a distinguishable return value — not pick one in passing.

Relationship to neighbours

Same function as objectui#8567 / #8568 / #8530 / #8555, and a residual of objectui#6948 (which added the $and / $or branch). Four different defect shapes with four different fixes — ⛔ not foldable; they serialise.

Dedup

Run, instrument validated with a control in the same session. search_issues (repo-scoped, semantic) over the convertFiltersToAST neighbourhood returns objectui#8567, #8568, #8530, #6948, #8555 and #7752 — 9 total, none of them this shape. Closed cards were in range.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions