You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
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 的第三条臂无法按原计划写进协议。
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.
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.
The production path reaches the server typeless: objectui packages/core/src/utils/filter-converter.tsviewFilterRuleToNode 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/main9f0c84a): grep -c "'is_empty'," → 1, control grep -c "function viewFilterRuleToNode" → 1. objectstack origin/main2f122b6e: 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)
A — a declared emptiness operator. Add $empty: boolean. Its describe IS the ruled table: text-like = STRING_VALUE_TYPES (null or ''); multi-value = isMultiValueField(def) (null or []); every other type = null. is_empty / is_not_empty lower typelessly to { f: { $empty: true|false } }, and each compile surface expands it by the field's declared type through one spec function.
The three runtime lowering doors (protocol.ts, engine.ts, service-analytics filter-normalizer.ts) must pass resolvers. The transport-schema door in data-engine.zod.ts has no object schema at all.
Lowered sharing-rule criteria freeze the field's type at authoring time.
C — B's text and null arms now, the multi-value arm to its own ruling. Ruling B ships two of three arms; if the third later lands as A, the B half is rework.
Recommendation
A, fallback B. C is not recommended (a half-delivery).
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 ispm:blockedon this card.Parent: #20311 (ruled B,
5861435168). Derived from the in-flight dispatch on #20311 (claim5864663193), whose dev stopped before code withpremise_still_valid: false(report5864899591). Filed bydomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017). Grade inherited from the parent:domain:spec·priority:p2·area:records.维护者速读
一句话问题:您裁定「为空」对多值字段(多选、标签、多值查找)= 空值或空列表。但今天平台的查询语言里没有任何写法能表达「这个列表是空的」:空列表放进等值条件会被一条更早的裁决(#19757 乙,5 天前)明令拒绝,也没有别的操作符能测它。所以裁决 B 的第三条臂无法按原计划写进协议。
为什么要您拍板:两条出路都改公开契约。一条是给查询语言新增一个操作符,另一条是推翻 #19757 的一部分。执行席不能自己选。
选项:
$empty操作符is_empty不再需要知道字段类型,由真正执行查询的地方(数据库驱动等 8 处)按字段声明的类型展开。is_empty在翻译成查询时就按类型展开;多值臂写成「等于空列表」。业务含义直译:A 等于「在查询语言里正式有一个『为空』按钮,谁执行谁按字段类型解释」。Notion 数据库查询、ServiceNow 的
ISEMPTY都是这么建模的。B 等于「每次存查询时把字段类型写死进条件」。C 等于「先修一半」。席位意见:荐 A,回退 B。实测的生产路径是:objectui 把已存视图规则里的「为空」原样发给服务端,服务端翻译时不看类型。A 不需要改任何调用方;B 要先改 3 个服务端入口,还要推翻一条 5 天前的裁决。
您要做的:回一个字母 —— A / B / C。
Background
5861435168), verbatim: 「@objectstack/specdeclares 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 infilter.zod.ts」 applied at the lowering.origin/main, that the loweredFilterConditioncan carry the text arm and the null arm but not the multi-value arm.Governing text
driver-mongodbalone answers it, as an exact-array match #19757 (record5793368540),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; controlgrep -c "Refused BY RULING"on the same file → 15.FILTER_OPERATORSinpackages/spec/src/data/filter.zod.tsends its "Special" row with'$null', '$exists'. Re-check:git show origin/main:packages/spec/src/data/filter.zod.ts | grep -c "'\$null', '\$exists',"→ 1; controlgrep -c "export const FILTER_OPERATORS"→ 1.[], never null (the [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447 maintainer ruling),packages/objectql/src/validation/record-validator.ts. Re-check:grep -c "empty set is representable"on that file atorigin/main→ 1; controlgrep -c "function isEmptyForRequired"→ 1.packages/core/src/utils/filter-converter.tsviewFilterRuleToNodepasses a stored view rule'sis_emptythrough as a valueless operator.packages/metadata-protocol/src/protocol.tsthen lowers it withparsedFilter = parseFilterAST(parsedFilter), although the same file reads field declarations throughresolveQueryFields. Re-check (objectuiorigin/main9f0c84a):grep -c "'is_empty',"→ 1, controlgrep -c "function viewFilterRuleToNode"→ 1. objectstackorigin/main2f122b6e:grep -c "parsedFilter = parseFilterAST(parsedFilter)"→ 1, controlgrep -c "resolveQueryFields"→ 10.Protocol statement. Option A adds one member to
FILTER_OPERATORSandFieldOperatorsSchema(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)$empty: boolean. Its describe IS the ruled table: text-like =STRING_VALUE_TYPES(null or''); multi-value =isMultiValueField(def)(null or[]); every other type = null.is_empty/is_not_emptylower typelessly to{ f: { $empty: true|false } }, and each compile surface expands it by the field's declared type through one spec function.$emptyfirst, and the lowering flips last. The two faces with no declarations (the formula matcher, objectql having) judge by value; they diverge only on a non-text column holding'', the write-door class record write door:''skips every type check, so a number, boolean, date, datetime or time column stores an empty string — normalise it to null at the door (seam from objectui#10813) #20308 closed.driver-mongodbalone answers it, as an exact-array match #19757 stands untouched, and a stored filter keeps its meaning if its field's type changes.parseFilterASTtakes a field-definition resolver and emits per-type fragments.[]as an equality comparand, so 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 — anddriver-mongodbalone answers it, as an exact-array match #19757 (and its save-door twin) must be reopened for the empty list, and the SQL family, driver-memory and the formula matcher must learn to execute it on a JSON column.protocol.ts,engine.ts, service-analyticsfilter-normalizer.ts) must pass resolvers. The transport-schema door indata-engine.zod.tshas no object schema at all.Recommendation
A, fallback B. C is not recommended (a half-delivery).
os-decision-facets
is_empty按属性类型定义,ServiceNow 有ISEMPTY操作符,都是「一个操作符、按类型解释」($in: [''],SQL 驱动直接拒绝。A 不改任何调用方就修好它;B 要先改 3 个服务端入口。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" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336)。两者都是响亮拒绝: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
''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 (this seat) declares$emptyin the spec: the operator, its describe carrying the ruled table, and one expansion function keyed on the field definition (type plusmultiple). One sibling card per compile-surface lane (domain:engine: driver-sql and its heirs, turso RemoteTransport, driver-memory, driver-mongodb, formula, objectql having;domain:services: service-analytics' two faces) isBlocked-by:[Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's spec PR. A last card flips theis_emptylowering to$emptyonce every surface answers. objectui#10813 converges its builders on$empty.Clause-②: yes,minor.driver-mongodbalone answers it, as an exact-array match #19757 乙 for the empty list; then [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 adds the resolver toparseFilterAST, and sibling cards convert the three lowering doors and teach the SQL family, driver-memory and the formula matcher empty-list equality on JSON columns.''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 ships the text and null arms withPart of; the multi-value arm returns to the box.Dedupe
filter operator for empty multi-value field empty array is_empty $empty operator: 0 hits.$empty,$size, an empty list/array/set near filter/is_empty/operator, or is_empty near multi/[]: 3 hits, objectql: a per-aggregationfilterrefuses an unknown operator only when rows exist —aggregations: [{ filter: { amount: { $median: 1 } } }]answers 400 on a populated table and 200 on an empty one #20122, fix(objectql)!:havingtakes the rest ofwhere's filter doors — the comparand-type door, row-independent refusals, a resolved{ $field }, and a refused non-condition #20117 and fix(service-analytics)!: the analyticswheredoor refuses a list in the equality slot instead of reading it asIN(#19888) #20008. They are an unknown-operator refusal on per-aggregation filters,havingdoors, and the analytics list-in-equality refusal. None is a carrier.Dedupe words:
is_empty multi-value empty list·$empty filter operator·empty array equality refused is_empty