fix(data): implicit field filters AND-merge with an explicit filter instead of being silently dropped (#4164) - #4190
Merged
Conversation
…nstead of being silently dropped (#4164) `?filter={"status":"open"}&owner_id=usr_1` applied only the explicit filter: the implicit-filter pass ran under `if (!options.where)`, so once an explicit filter claimed `where`, leftover REAL field params were neither merged nor reported — they rode to the engine as stray top-level AST keys no driver reads, and the response over-returned. The mirror of #4134's silent zero. They now compose the way the request reads: `{ $and: [explicit, implicit] }` — the same combinator the engine already uses to fold the `$search` predicate and an expand's declared filter into an existing `where` (engine.ts), and one the #3774 filter-logic conformance suite pins across every FilterCondition backend. Decisions: - #4134's field gate runs BEFORE the merge, so every merged key is a verified field name — the merge can never introduce a zero-matching predicate. That ordering is why this fix is safe now and was not safe before #4134. - Contradictory sides (`?filter={"status":"open"}&status=closed`) apply both predicates and intersect to an HONEST zero — no same-field special case. - A vacuous explicit filter (`?filter={}`, `?filter=`) leaves the implicit predicates standing alone instead of wrapping them in a one-armed `$and`. - A non-object truthy `where` (the unparseable-filter-JSON tolerance keeps the raw string, #4181) is deliberately NOT merged into — that path keeps its pre-#4164 shape until the tolerance is fixed at the source. Pinned by test. - `engine.count` reads the same `options.where`, so pagination totals are computed over the merged predicate and cannot disagree with `records`. Verified against a real engine (mini-driver taught to honor `$and` — a matcher that skips it would green-light a merge that does nothing) and live on the showcase app: explicit 4 rows, implicit 2 rows, merged intersection 1 row, paged `total` computed over the intersection; `?filter=…&pageSize=5` still 400s (#4134 regression guard). Filed while reproducing the excluded edge: #4181 — an unparseable `filter` string is silently ignored and returns the UNFILTERED page (worst member of the #3948 family), plus the `filter`-beats-`where` alias precedence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKsxtrsCwSL3uAcxAdVj83
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4164。#4134(已合并,#4169)的镜像案:那边是未知参数清零,这边是已知字段参数被显式
filter挤掉——写下去的收窄谓词既不生效也不报错,作为野键漂在 AST 顶层,响应多返回。语义(维护者已拍板:AND 合并)
?filter={"priority":"medium"}&status=todo现在按字面意思执行:两个谓词都生效,{ $and: [显式, 隐式] }。选这个组合子不是发明——是对齐三处既有先例:engine 层折叠
$search谓词、expand 的 FK 匹配 + 声明式 filter、嵌套 expand 的 AST where,全部用同一个{ $and: [...] }模式;且$and是 #3774 conformance 套件在四个 FilterCondition 后端上钉死的组合子,不踩驱动方言。实现决定
?pageSize=5返回 200 + 空列表 #4134 的字段网关先跑,合并后跑——所以每个被合并的键都是已验证的真实字段,合并永远不可能引入一个匹配不到的谓词。这个顺序正是本修复现在才安全的原因:REST 列表:未知查询参数被静默当作字段过滤器 ——?pageSize=5返回 200 + 空列表 #4134 之前做 AND 合并,?filter=…&pageSize=5会把垃圾谓词合进去→静默清零,比不修更糟。?filter={"status":"open"}&status=closed→ 两个谓词都应用 → 诚实的空集(过滤器都跑了),与 REST 列表:未知查询参数被静默当作字段过滤器 ——?pageSize=5返回 200 + 空列表 #4134 修掉的"过滤器没跑的假 0"本质不同。?filter={}、?filter=)视为不存在:隐式谓词独立成 where,不包单臂$and。顺带修掉filter={}也会吞隐式参数的边角。filterJSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181,见下),往垃圾里合并谓词既不能让它生效也不能暴露真正的 bug。该路径保持 REST 列表:显式filter在场时,同时传的字段级参数被静默丢弃(#4134 的邻居) #4164 前的形状,测试 pin 住,等 REST 列表:无法解析的filterJSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181 在源头修。engine.count读同一个options.where——分页total/hasMore按合并后的谓词计算,不会和records打架(有专测钉住)。验证
协议层(
protocol-data.test.ts):翻转了 #4134 PR 里特意留下的 pin 测试(它当时就注明"等一次 deliberate change"——就是这次),新增:多隐式键合成单臂、矛盾对、空 filter/空串、count 看到合并 where、$filter字符串别名同形、非 object where 原样保留(pin 到 #4181)。真实引擎(
protocol-unknown-query-param.test.ts):先给迷你驱动的matchesWhere教会$and(忽略$and的 matcher 会给一个啥都没干的合并放行——这是本测试文件自身的坑),然后钉:显式 8 行 ∩title→ 1 行;矛盾 → 0;top=3时total=8(合并谓词的匹配数,不是 10 也不是 3)。跑起来的服务器(showcase + 真实 SQL 驱动,已自行关闭):
turbo run test132/132 全绿(首轮有 4 个包在高并发下抖动,--concurrency=4重跑全过,单独重跑也全过——资源竞争,非本改动);eslint 干净。影响面
in-repo 调用方没有"filter + 裸字段"混用(#4134 时已清点:objectui 适配器/console/marketplace/import/export/公开表单全部只发保留参数);flows 与 MCP agent 是唯二可能混用的入口,恰是意图从此被忠实执行的受益者。会变化的只有依赖丢弃行为的外部调用方——它们拿到的从"更宽的错误集合"变为"按其所写的交集"。
复现边角时坐实并归档的新 issue
#4181:无法解析的
filterJSON 字符串被静默忽略,返回未过滤整页(真实 InMemoryDriver 实测filter='{status:done'→200 total=3,与 baseline 相同)——#3948 家族目前最恶性的成员,方向是全量多返回。同 issue 里附带坐实了filter静默压过where的别名优先级问题。本 PR 刻意不动它们(源头修法不同),只用测试把边界 pin 住。Generated by Claude Code