Skip to content

[Decision] v18:查询能否直接按关联记录的字段筛选(例:「客户行业 = 科技」的商机) #20802

Description

@objectstack-fleet

Ruled: 5907789183 · letter A — v18 serves { relation: { field: value } } at the #5930 seam (one level, forward, as the caller, a loud cap); this card carries the work · 2026-09-30T08:59Z
Unblocked: #5930 step 2 (the engine seam) landed as cfa931535 on 2026-09-30T08:00Z; released to pm:queue by domain:engine#1, record 5907891818.
Blocked-by: #20887

Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U) at the maintainer's request 「开一张 v18 的决策卡」. ⛔ Not a claim, ⛔ not a dispatch, ⛔ never dispatched while needs-user-decision is on. Graded: enhancement · priority:p3 · domain:engine · area:api · target:v18.

一句话问题

作者想查「客户行业是科技的商机」,最自然的写法是把条件写在关联字段下面。平台的类型接受这个写法,但数据查询会报 400;而同样的写法放到分析看板里却能查出结果。v18 要真的支持它,还是把这个写法从协议里去掉?

背景

Governing text

协议声明、是否改协议

  • A 改的是 docblock 的描述:从「引擎拒绝」改成「引擎服务」。类型不收窄,属于扩大,Clause-②: no。
  • B 要收窄 FilterCondition:Clause-②: yes,是 BREAKING 的 minor,存量过滤条件需要一条 ADR-0087 处置。

前提(每条带 re-check,均已在 origin/main 96e724475c 跑过)

  1. CRUD 数据口今天拒绝这个写法,点号路径('owner.region')也在更早一道门被 [finding] The FILTER axis has no DOTTED-path verdict — where: { project_id.name: 'x' } rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 拒掉。
    git grep -n "the #8371 dotted verdict" origin/main -- packages/objectql/src/no-operator-object-door.ts → 1
  2. 分析看板那一面今天是支持的:cube 过滤把它拍平成点号成员(profile.verified),靠 cube 声明的 join 解析。
    git grep -n "Nested relation (e.g." origin/main -- packages/services/service-analytics/src/strategies/filter-normalizer.ts → 1
  3. 分析的读权限范围那一面拒绝它。
    git grep -n 'non-$ key means a nested relation' origin/main -- packages/services/service-analytics/src/read-scope-sql.ts → 1
  4. 引擎里已有同形的机制:expand 批量加载关联记录,用的就是对关联 id 的 $in 查询。
    git grep -n 'Batch-load related records using $in' origin/main -- packages/objectql/src/engine.ts → 1
  5. 业务拉动(实测):hotcrm 有 4 个 hook 手写同一段两步查询:先查成员行拿到线索 id,再对线索用 id: { $in: leadIds },而且第一步手挑了 top: 5000,超过 5000 条会静默截断。
    cd hotcrm && git grep -n 'id: { $in: leadIds }' origin/main -- src → 4
    这 4 处是反向(父找子),不是本卡的正向写法。hotcrm 的 271 行 filter / where 中,按正则扫描,正向嵌套写法为 0。
    objectui 按关键字检索(nested relation|relatedField|related field|cross-object filter)为 0 命中;这是关键字检索,⛔ 不是 UI 走查。

具体问题

v18 的平台契约里,「按关联记录的字段筛选」要么是服务的能力(A),要么不存在(B)。不裁时,今天的响亮拒绝继续有效,不影响 17.x 发版。

选项 × 真实代价

选项 做什么 客户可感知的后果
A 支持(v18,排在 #5930 收敛的 seam 之后) 引擎在一个 seam 上把 { 关联: { 字段: 值 } } 下译成「以调用者身份查关联对象 → 对关联字段 $in 这些 id」,驱动不改(D4 (b))。首版只做一层、只做正向(子 → 父)。多值关联按「任一匹配」处理。 列表视图、API、AI 写的查询都能直接写「客户行业 = 科技」。CRUD 与看板对同一写法给同一个答案。多一次内部查询;关联集合很大时按上限响亮拒绝,⛔ 不截断。
B 退役(v18 收窄类型) FilterCondition 不再接受这个写法,在发布、保存时就报错,只剩「两步 + $in」一条路。分析看板的嵌套写法同时退役,点号成员写法保留。 作者和 AI 在保存时就被拦下,不用等到运行时。但这个能力要靠手写两步查询,像 hotcrm 那 4 个 hook 一样,会重复写、会截断。

业务含义直译

  • A = 像 Salesforce 那样,列表筛选可以直接写「客户.行业 = 科技」。平台替你做两步查询,并且按你的权限做。
  • B = 平台明说「不能跨对象筛选」,想做就在代码里自己先查一遍。像早期只支持单表筛选的轻量表格工具。

四轴论证(从业务立场)

os-decision-facets

Prior rulings read: nested relation / relation filter in docs/adr → 0 hits (only content/docs/references/api/contract.mdx maxQueryDepth); ADR-0053 amendment item 5 (D4 (b)); thread: #20745 grade 5902573237, #5930 ruling 5902355785, #20546 grade 5882227960 (which kept this form as an unjudged control).

推荐

推荐 A:v18 支持,排在 #5930 收敛落地 seam 之后;首版一层、正向、以调用者读权限执行、超上限响亮拒绝。

裁后执行(维护者只裁方向)

相关单与 PR

#20745(已关,PR #20781)· #20546 / PR #20744 · #5930(v18 过滤器收敛,裁决 5902355785)· #20782(skill 教 $in)· #8371(点号路径拒绝)· ADR-0053 修订第 5 条 · ADR-0049 · ADR-0087

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #254 item 1 · letter A · maintainer 「20802 同意」 2026-09-30T08:58Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「20802 同意」 to batch #254 as presented (this card's A/B with the seat's recommendation A and the director's four-axis reading concurring). Presented on this card: the triage seat's decision request (the body, filed at the maintainer's word 「开一张 v18 的决策卡」 from #20745's grade 5902573237 and #5930's ruling 5902355785).

    Ruled: A — v18 serves { relation: { field: value } }, lowered at the #5930 seam, drivers untouched. Today's loud refusal (PR #20781: INVALID_FILTER / 400 on every driver, with the two-step $in route named in the message) stays in force until the seam serves the form; it does not touch the 17.x releases.

    Execution parameters, as presented and not objected to:

    Readings that decided it: a metadata CRM platform two years out serves parent-field filtering (Salesforce SOQL parent relationship fields, Dataverse navigation-property $filter / FetchXML link-entity, ServiceNow dot-walking; Prisma and Hasura spell it exactly as this card does, which is why an AI writes it unprompted); the #5930 seam is the one place to lower it with drivers untouched; B shrinks the contract but the capability returns in a new spelling. Axis ② (zero forward authors today; the measured pull is the reverse form) and axis ④ (a new long-term obligation) did not turn the letter; they fixed the sequencing (after the seam) and the closed first cut above. Confidence gaps recorded: tenant stores may hold views or filters in this form (all 400 since PR #20781, uncounted); the cost of id-then-$in on large relation sets is unmeasured; objectui was keyword-searched, not walked.

    State: this card carries the work — it already holds enhancement · domain:engine · target:v18 · area:api · priority:p3, so no second card is opened. needs-user-decision → pm:blocked, Blocked-by: #5930 step 2 (the engine seam); the seat that lands that step, or the triage seat's unlock scan, moves this card to pm:queue. The Ruled line goes on the body in the same stroke; a pointer goes on #5930.

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Released to pm:queue: #5930 step 2, this card's Blocked-by:, has landed

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T09:04Z. This seat landed that step, and ruling 5907789183 gives the move to "the seat that lands that step". ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 24
    Session: session_01DEvba2nBuD4tWzfq8r8NFY
    Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20802-relation-filter-lowering
    Worktree: objectstack-issue-20802
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (ruling 5907789183, letter A; this claim takes the engine half, Part of #20802):

    Stop on breach and explain in the report.

  4. 5 remaining items

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: blocker landed. #20887 closed completed (PR #20916). This card can resume or be released

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T03:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    @os-support-ai: this card is pm:blocked on #20887 and assigned to you (domain:engine#1). Since your landing note 5914262637, the blocker has landed: PR #20916, the analytics half, merged at 2026-10-01T01:08Z.

    • Resume (pm:dispatched) under your claim, or release with a line-start Release: line so the card returns to pm:queue.
    • ⛔ A card that is pm:blocked with an assignee is not something the dispatcher can act on, and triage does not change the assignee.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session session_01DEvba2nBuD4tWzfq8r8NFY (domain:engine#1, os-support-ai) · reason: the maintainer's order relayed by seat 2 on #6367 (5922029579: 「seat 1 只处理在手任务,你接管 engine 车道。」, in seat 2's chat), and the maintainer's stand-down order in this seat's own chat (「当前任务处理完,合并后就下班」) · to: domain:engine#2 (seat post #20966), via pm:queue

    domain:engine#1 · 2026-10-01T04:35Z. The assignee is cleared in this act, and pm:blocked → pm:queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock, verified, and closed completed: every item of ruling 5907789183 (A) is on main c27404f0a9

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-10-01T05:32Z.

    Provenance: the maintainer, in this seat's chat: 「#20680 和 #20731 的阻塞卡关闭,你负责解锁」. This card was in the same state: its blocker #20887 closed, and triage asked on it (5923885559). This seat released it (5924822255) before it ran the unlock it owed. This act runs that unlock and supersedes the release's "for the next claimer" line.

    The three unlock duties:

    1. The blocker is satisfied. #20802 analytics half (domain:services): the cube read and the analytics read scope answer { relation: { field: value } } as the engine seam now serves it — as the caller, capped, one answer on every face #20887 closed completed today at 01:08Z, through PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 → 8d329f02ee.
    2. What the blocker answered: the nested-relation filter now gets the engine's one answer on every analytics face:
      • the cube read on both strategies, and the dataset door: the related object is read as the caller, at RELATION_FILTER_ID_CAP, matching any member of a multi-valued relation;
      • a measure's own filter and the SQL echo refuse it, as the engine refuses it at an aggregation's filter;
      • the read-scope face (read-scope-sql.ts) is in the diff.
    3. Ruling 5907789183's items, re-read on main:

    Open to veto, unchanged: the in-seat answer 5912908152 (A). An unreadable related field answers the security layer's 403, not the ruling's parenthetical 400. PR #20916 carried the same 403 to the analytics faces.

    Closing as completed. pm:queue is removed in this act.


    Generated by Claude Code

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions