Skip to content

finding(app-shell): the Studio dataset-filter bridge drops an Is null row on commit, so picking it ERASES the stored filter #9363

Description

@os-tesla

Found by the os-dev seat implementing objectui#9359. ⛔ Deliberately not repaired in that card's PR: different package, different file, and a different mechanism — this one does not read the shared value-less set at all. Grading and domain:* are the triage seat's.

Measured

groupToCondition (packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.ts) is the WRITE half of the Studio inspector's bridge between the visual FilterBuilder and the Mongo-style FilterCondition stored on dataset.filter / measure.filter. Driven directly, one date column, one condition row:

builder row committed FilterCondition
equals with value "acme" (control) { closed_at: { $eq: "acme" } }
isEmpty { closed_at: { $exists: false } }
isNotEmpty { closed_at: { $exists: true } }
isNull undefined — the row is dropped
isNotNull undefined — the row is dropped
is_null (canonical) undefined
is_empty (canonical) undefined

The control fires in the same run, so the empty answers are a reading about those operators and not a dead function.

Why it is reachable, and why it is worse than "a row is ignored"

DatasetDefaultInspector.tsx mounts the real builder and commits through this function on every change:

FilterBuilder ... onChange={(g) => onCommit(groupToCondition(g))}

It passes no extraOperators, so the dropdown offers the DEFAULT operator list — and isNull / isNotNull are in it. They are not OPT_IN_OPERATORS (only containsCaseInsensitive, exists and notExists are), so Is null is an ordinary menu entry in this inspector.

⚠️ groupToCondition returns undefined when no row survives. So an author who has a working dataset filter, opens the inspector and switches the single condition's operator to Is null, commits undefined — the stored filter is erased, not merely left unchanged. Nothing errors. The panel still shows the condition.

The cause

OP_TO_MONGO has no row for isNull / isNotNull, and the two value-less operators the function DOES understand are matched as raw literals:

if (c.operator === 'isEmpty')    { parts.push({ [c.field]: { $exists: false } }); continue; }
if (c.operator === 'isNotEmpty') { parts.push({ [c.field]: { $exists: true  } }); continue; }
const mop = OP_TO_MONGO[c.operator];
if (!mop) continue; // unmapped (e.g. notContains/between) — drop rather than emit a bad filter

That continue is a deliberate decision for notContains / between — operators this dialect genuinely cannot express, where dropping beats emitting a wrong filter. isNull / isNotNull are a different case: the Mongo dialect CAN express them (the spec's $null, and the file's own header names it), the builder draws them as COMPLETE rows with no value input, and the inspector offers them in its menu. So the drop is not the documented fallback, it is an unhandled operator falling into the fallback's path.

⚠️ Note the asymmetry with the read direction: conditionToGroup reports representable: false for shapes it cannot round-trip, so the caller can fall back to the source editor. The write direction has no equivalent signal — it drops silently.

Relationship to objectui#9359 — same CLASS, different mechanism

objectui#9359 is a reader of the shared VALUELESS_FILTER_BUILDER_OPERATORS that forgot to fold the operator. This file does not read that set at all; it keeps its own raw literals and its own operator table. So it is not fixed by that card's repair, and not fixed by widening anything — a point worth keeping, because the two look alike from a distance.

⛔ Independent of objectui#9306 (which operator vocabulary wins): every spelling in the table above is dropped, the dropdown's own ids included, so the defect does not turn on that decision.

Reproduce

groupToCondition({ id: 'g', logic: 'and', conditions: [
  { id: 'c1', field: 'closed_at', operator: 'isNull', value: '' },
]})

Returns undefined. The same group with operator: 'isEmpty' returns { closed_at: { $exists: false } }.

Not measured, and worth a look by whoever takes this

  • whether onCommit(undefined) clears the persisted key or is filtered upstream — measured only at the function boundary here, not in a browser;
  • whether the same table gap affects the other operators it lacks (startsWith, endsWith, notContains, between) in a way the "drop rather than emit a bad filter" comment no longer covers.

Deduplicated before filing against the repository-scoped issue list endpoint (183 open issues over two pages), with objectui#9359 itself as the known-hit control so the empty result is a reading rather than a silent zero. Filed unassigned and unlabelled by an automated development seat working objectui#9359; session reference session_01UzHd6hDYatoDn17BuwKxnZ.

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

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpriority:p1

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions