发现于 #5685(PR #6570)的实现过程。按 PD#10 单独记录,未在那个 PR 里顺手改 —— $between 是另一个算子,不在 #5685 的测量范围内。
事实
#5685 修的是 $gt/$gte/$lt/$lte 四个槽位缺 string。同一文件里 $between 的两个端点带完全相同的矛盾,且 #5685 的 PR 没有碰它:
packages/spec/src/data/filter.zod.ts —— RangeOperatorSchema(约 :125)与 FieldOperatorsSchema.$between(约 :275)都是:
$between: z.tuple([
z.union([z.number(), z.date(), FieldReferenceSchema]),
z.union([z.number(), z.date(), FieldReferenceSchema])
]).optional(),
即两个端点都没有 string。
为什么这不是「顺带一提」
日期宏解析器会走进数组。packages/core/src/utils/filter-tokens.ts 的 walk:
if (Array.isArray(node)) return node.map(walk);
所以下面这个写法是解析器完整支持的,而且是区间过滤最自然的表达:
{ close_date: { $between: ['{current_year_start}', '{current_year_end}'] } }
→ { close_date: { $between: ['2026-01-01', '2026-12-31'] } }
解析结果的两个端点都是 string,恰好是声明拒收的类型。$between 的语义(闭区间)天然就是日期窗口的主场,所以这里的 declared ≠ practiced 比 $gt 那四个更贴近作者的真实写法。
危害面 / 为什么归为 observation-class
与 #5685 同源:RangeOperatorSchema 没有运行期消费者;FieldOperatorsSchema.$between 经 NormalizedFilterSchema 可达,但 objectql 的读写路径与各 driver 都不拿它校验 where。所以今天不产生运行期失败 —— 危害仍是发布出去的契约在误导作者(尤其 AI 作者):读到 $between: [number | date, ...] 会得出「区间端点要传 Date 对象」的结论,而平台自己的日期宏路径只会给字符串。
严重度留给分诊轮,与 #5685 当初一样。
建议方向(未拍板)
与 #5685 的落点保持一致即可 —— 把 z.string() 并进两个端点的联合,理由与 PR #6570 的 docblock 同源(本 schema 是 field-agnostic 的、ISO refine 会拒掉 Field.time 声明的 HH:MM 形态、driver 侧 coerceFilterValue 已按列类型规范化端点)。若采纳,Filter< T > 里 $between?: T[K] extends number | Date ? [T[K], T[K]] : never 的 guard 也应同步。
一并留意:RangeOperatorSchema 与 FieldOperatorsSchema 是同一契约的两份拼写,#5685 的 PR 已经踩过一次「只改文档面会留下可达面仍拒绝」的坑,$between 若要改也应两处同改。
相关
#5685(同一处矛盾的 $gt/$gte/$lt/$lte 半边)、PR #6570(其实现,docblock 里记录了裸 string vs ISO refine 的实测取舍)、#3810。
发现于 #5685(PR #6570)的实现过程。按 PD#10 单独记录,未在那个 PR 里顺手改 ——
$between是另一个算子,不在 #5685 的测量范围内。事实
#5685 修的是
$gt/$gte/$lt/$lte四个槽位缺string。同一文件里$between的两个端点带完全相同的矛盾,且 #5685 的 PR 没有碰它:packages/spec/src/data/filter.zod.ts——RangeOperatorSchema(约 :125)与FieldOperatorsSchema.$between(约 :275)都是:即两个端点都没有
string。为什么这不是「顺带一提」
日期宏解析器会走进数组。
packages/core/src/utils/filter-tokens.ts的walk:所以下面这个写法是解析器完整支持的,而且是区间过滤最自然的表达:
解析结果的两个端点都是 string,恰好是声明拒收的类型。
$between的语义(闭区间)天然就是日期窗口的主场,所以这里的 declared ≠ practiced 比$gt那四个更贴近作者的真实写法。危害面 / 为什么归为 observation-class
与 #5685 同源:
RangeOperatorSchema没有运行期消费者;FieldOperatorsSchema.$between经NormalizedFilterSchema可达,但 objectql 的读写路径与各 driver 都不拿它校验where。所以今天不产生运行期失败 —— 危害仍是发布出去的契约在误导作者(尤其 AI 作者):读到$between: [number | date, ...]会得出「区间端点要传 Date 对象」的结论,而平台自己的日期宏路径只会给字符串。严重度留给分诊轮,与 #5685 当初一样。
建议方向(未拍板)
与 #5685 的落点保持一致即可 —— 把
z.string()并进两个端点的联合,理由与 PR #6570 的 docblock 同源(本 schema 是 field-agnostic 的、ISO refine 会拒掉Field.time声明的HH:MM形态、driver 侧coerceFilterValue已按列类型规范化端点)。若采纳,Filter< T >里$between?: T[K] extends number | Date ? [T[K], T[K]] : never的 guard 也应同步。一并留意:
RangeOperatorSchema与FieldOperatorsSchema是同一契约的两份拼写,#5685 的 PR 已经踩过一次「只改文档面会留下可达面仍拒绝」的坑,$between若要改也应两处同改。相关
#5685(同一处矛盾的
$gt/$gte/$lt/$lte半边)、PR #6570(其实现,docblock 里记录了裸stringvs ISO refine 的实测取舍)、#3810。