Skip to content

[Decision] How does a filter say 「is empty」 on a multi-value field? A declared $empty operator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399

Description

@objectstack-fleet

Ruled: 5865693155 · letter A · 2026-09-28T07:49Z

This card carries the one question ruling B on #20311 left open: how the lowered filter spells the multi-value arm (null or []), and so where the per-type table is applied. #20311 keeps the implementation, and is pm:blocked on this card.

Parent: #20311 (ruled B, 5861435168). Derived from the in-flight dispatch on #20311 (claim 5864663193), whose dev stopped before code with premise_still_valid: false (report 5864899591). Filed by domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017). Grade inherited from the parent: domain:spec · priority:p2 · area:records.

维护者速读

一句话问题:您裁定「为空」对多值字段(多选、标签、多值查找)= 空值或空列表。但今天平台的查询语言里没有任何写法能表达「这个列表是空的」:空列表放进等值条件会被一条更早的裁决(#19757 乙,5 天前)明令拒绝,也没有别的操作符能测它。所以裁决 B 的第三条臂无法按原计划写进协议。

为什么要您拍板:两条出路都改公开契约。一条是给查询语言新增一个操作符,另一条是推翻 #19757 的一部分。执行席不能自己选。

选项:

选项 做什么 客户可感知的后果
A(荐) 新增 $empty 操作符 查询语言多一个「是否为空」操作符。它的说明就是您裁的那张按类型展开的表。is_empty 不再需要知道字段类型,由真正执行查询的地方(数据库驱动等 8 处)按字段声明的类型展开。 已存视图里的「为空」不用改就对了。多值字段的「为空」第一次能用(今天报错)。代价:8 处执行面各实现一次,要分阶段落地(先让执行面认识新操作符,最后再切换翻译)。
B 翻译时带上字段类型 is_empty 在翻译成查询时就按类型展开;多值臂写成「等于空列表」。 必须重开 #19757 乙,为空列表开口子;SQL 系、内存驱动、公式求值器都要学会在 JSON 列上比较空列表(今天都拒绝或查不到);3 处服务端翻译口要改成传类型;已存的共享规则条件会把「当时的字段类型」冻结进去。
C 先做文本和空值两臂,多值臂另裁 只交付裁决 B 的三分之二。 多值字段的「为空」继续报错;若以后多值臂按 A 落地,C 这一半要返工。

业务含义直译:A 等于「在查询语言里正式有一个『为空』按钮,谁执行谁按字段类型解释」。Notion 数据库查询、ServiceNow 的 ISEMPTY 都是这么建模的。B 等于「每次存查询时把字段类型写死进条件」。C 等于「先修一半」。

席位意见:荐 A,回退 B。实测的生产路径是:objectui 把已存视图规则里的「为空」原样发给服务端,服务端翻译时不看类型。A 不需要改任何调用方;B 要先改 3 个服务端入口,还要推翻一条 5 天前的裁决。

您要做的:回一个字母 —— A / B / C。

Background

  • Ruling B (5861435168), verbatim: 「@objectstack/spec declares what 「is empty」 means once, per field type: text-like = null or ''; multi-value (multi-select, tags, multi-value lookup) = null or []; every other type = null only. The three objectui builders read that one lowering; none keeps a meaning of its own.」 Execution item 1 places it as 「a per-type table in filter.zod.ts」 applied at the lowering.
  • The dispatch measured, and this seat re-read on origin/main, that the lowered FilterCondition can carry the text arm and the null arm but not the multi-value arm.

Governing text

  • The empty-list refusal, ruling 乙 on [finding] the comparand-SHAPE face declares it closes the door "for every driver at once", but an array in the IMPLICIT-EQUALITY slot passes it — and driver-mongodb alone answers it, as an exact-array match #19757 (record 5793368540), packages/spec/src/data/filter-comparand-shape.ts, verbatim: 「An EMPTY array is still an array: { tags: [] } is refused too.」 Re-check: git show origin/main:packages/spec/src/data/filter-comparand-shape.ts | grep -c "An EMPTY array is still an array" → 1; control grep -c "Refused BY RULING" on the same file → 15.
  • The operator vocabulary has no emptiness or size test: FILTER_OPERATORS in packages/spec/src/data/filter.zod.ts ends its "Special" row with '$null', '$exists'. Re-check: git show origin/main:packages/spec/src/data/filter.zod.ts | grep -c "'\$null', '\$exists'," → 1; control grep -c "export const FILTER_OPERATORS" → 1.
  • An emptied multi-value set is stored as [], never null (the [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as [] or null? #9447 maintainer ruling), packages/objectql/src/validation/record-validator.ts. Re-check: grep -c "empty set is representable" on that file at origin/main → 1; control grep -c "function isEmptyForRequired" → 1.
  • The production path reaches the server typeless: objectui packages/core/src/utils/filter-converter.ts viewFilterRuleToNode passes a stored view rule's is_empty through as a valueless operator. packages/metadata-protocol/src/protocol.ts then lowers it with parsedFilter = parseFilterAST(parsedFilter), although the same file reads field declarations through resolveQueryFields. Re-check (objectui origin/main 9f0c84a): grep -c "'is_empty'," → 1, control grep -c "function viewFilterRuleToNode" → 1. objectstack origin/main 2f122b6e: grep -c "parsedFilter = parseFilterAST(parsedFilter)" → 1, control grep -c "resolveQueryFields" → 10.

Protocol statement. Option A adds one member to FILTER_OPERATORS and FieldOperatorsSchema (a widening, Clause-②: yes, minor). Option B changes ruling 乙's accept set for one shape (a widening at the shared face). C changes neither now. Every option changes the protocol; none is a code-only fix.

Options, with the costs the dev measured (report 5864899591, re-read by this seat where marked)

Recommendation

A, fallback B. C is not recommended (a half-delivery).

os-decision-facets

  • ① 项目长远合理性:A 让「为空」成为查询语言里的一个一等谓词,按字段声明的类型在执行处解释,和平台对文本操作符的既有做法(按声明类型判定)同一个模式,不产生特例。B 把类型相关的片段烘进每一条存储的条件,还要为一个 5 天前的裁决开口子,特例增生。两年后的主流形状:Notion 数据库查询的 is_empty 按属性类型定义,ServiceNow 有 ISEMPTY 操作符,都是「一个操作符、按类型解释」(⚠️ 本席未重新实测这两家)。
  • ② 实际业务拉动:今天撞上的是已存视图的「为空」筛选。objectui 把它原样发给服务端,服务端不看类型;多值字段上 objectui 的构建器写出 $in: [''],SQL 驱动直接拒绝。A 不改任何调用方就修好它;B 要先改 3 个服务端入口。
  • ③ 防 AI 犯错:A 给「为空」一个唯一的规范写法,AI 直接写 FilterCondition 时写 { f: { $empty: true } } 即可。B 下 AI 得手搓按类型的 $or,这正是 '' 被比到数字列上的来路(driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field — where { amount: { $gt: "abc" } } is DATABASE_ERROR / 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336)。两者都是响亮拒绝:A 下未实现的执行面拒绝未知操作符,不会静默。
  • ④ 创业阶段不扩散:A 新增一个操作符,但它是已声明的视图操作符 is_empty 的规范形态,不是新能力。B 不加操作符,却要推翻一条裁决并改更多入口。C 最省,但把裁决 B 留成半成品。

Prior rulings read: is_empty,empty,multi-value,equality → 17 hits; ADR-0037 Decision §1, ADR-0045 Decision §4, ADR-0045 Decision §7, ADR-0053 Decision §3, ADR-0055 Decision §2, ADR-0057 D12, ADR-0058 D3, ADR-0074 Decision §1, ADR-0076 D10 (each hit is the bare word in an unrelated decision; none rules on an emptiness predicate); thread: #20311 ruling 5861435168, #19757 ruling 乙 5793368540.

推荐:A(回退 B)。只看①选 A;②③④ 是否翻转:否。
置信缺口:Notion / ServiceNow 的建模未重新实测;生产环境里用了「为空」的已存视图与共享规则读不到;A 在 8 个执行面上的实现成本是 dev 的估计,未逐面实测。

After the ruling

Dedupe

Dedupe words: is_empty multi-value empty list · $empty filter operator · empty array equality refused is_empty

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

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions