Skip to content

fix(data-objectstack): queryDataset 透传服务端 drillRanges,恢复日期分桶钻取 (#3813) - #3824

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3813-drillranges-passthrough
Aug 8, 2026
Merged

fix(data-objectstack): queryDataset 透传服务端 drillRanges,恢复日期分桶钻取 (#3813)#3824
yinlianghui merged 1 commit into
mainfrom
claude/issue-3813-drillranges-passthrough

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3813

问题

queryDataset 的返回是逐项挑键重建的,drillRanges 从来不在那份名单里 —— 于是服务端 framework#1752 的日期区间钻取旁路,被本仓唯一的真实 adapter 静默丢掉,而消费端五处已经在读它。

影响不是"钻取变弱",而是"钻取整块消失":dateGranularity 维度把一个时间跨度归成一个桶,等值过滤表达不了,所以 service-analytics 故意把日期维度排除在 dimensionFields / drillRawRows 之外,改为每行发一条半开区间 [gte, lt)。因此对只按时间分组的图表/报表,drillRanges 是唯一能让

canDrill = !!drillObject && (drillDims.length > 0 || !!drillRanges?.length)

为真的东西 —— 键被挑掉,整个钻取入口就没了。日期 + 非日期混合分组时钻取还在,但过滤条件缺时间边界,点 6 月的柱子打开的是该地区全时段(超集)。

两侧测试都看不见:dashboard/report 的用例 mock 自己的 data source 直接喂 drillRanges,adapter 自己的用例从没断言过这个键。

跨仓证据链

objectui(基线 origin/main @ d83f6b3de,即 PR #3817 落地后的现状):

  • packages/data-objectstack/src/index.ts:3355(改前)return { rows, fields, object, dimensionFields, drillRawRows, totals }; —— 挑键处,无 drillRanges
  • packages/data-objectstack/src/index.ts:3257 是本仓唯一的 queryDataset 实装(apps/console/src/dataSource.ts:14 就是原样 re-export,中间没有二次挑键的包装层)。
  • 消费端五处:packages/plugin-dashboard/src/DatasetWidget.tsx:471(读取)、:593(canDrill)、packages/plugin-report/src/DatasetReportRenderer.tsx:316(读取)、:431(行钻 hasRange)、:855(透视单元格钻 hasRange)。
  • 共享消费函数:packages/core/src/utils/dataset-format.ts:165DatasetDrillRangebuildDatasetDrillFilter(把区间落成 ObjectQL { $gte, $lt })。

objectstack(只读实证,@ b4872a868):

  • packages/services/service-analytics/src/analytics-service.ts:741 起:等值钻取块的 drillDims 只收 !!d.field && d.type !== 'date' 的维度 —— 日期维度被排除,所以只按日期分组时 dimensionFields/drillRawRows 根本不产出
  • 同文件 :807 起:rangeDims 非空时 result.drillRanges = result.rows.map(...),并重新设上 object(注释写明:只按时间分组的报表也需要 base object 才能打开列表)。
  • packages/rest/src/rest-server.ts:6284:res.json(result) —— 裸下发,不裁剪、也没有 { success, data } 包装。

改动

packages/data-objectstack/src/index.ts,两处:

  1. 运行时:const drillRanges = Array.isArray((data as any)?.drillRanges) ? (data as any).drillRanges : undefined;,并加入返回字面量。与 drillRawRows 同一档校验强度 —— 只校验数组性,不发明空数组(缺失保持 undefined)。
  2. 返回类型:新增 drillRanges?: Array< Record< string, DatasetDrillRange > >,条目类型是 @object-ui/coreDatasetDrillRange,引用而非重述

为什么引用而不是在 adapter 里写 { field, gte, lt }(#3613/#3752 的同一条纪律):DatasetDrillRange 是本仓这个形状的唯一声明 —— 它既是 buildDatasetDrillFilter 接受的类型,也是 DatasetWidget / DatasetReportRenderer 给 state 标的类型;@objectstack/spec 目前不拥有这个形状(产出侧的 AnalyticsResultWithDrillservice-analytics局部类型,未导出),所以共享的仓内接口就是当下唯一可用的契约,在 adapter 里重述一遍等于造它的第三种方言。边界值声明为 unknown 也是诚实的:这里和 drillRawRows 一样只校验了数组性,没校验条目内部。

没有改成整体透传信封(spread):#3752 已经把"这个信封不是 AnalyticsResult、且刻意不返回 sql"钉成了 pin,整体透传会把 sql 一起带出并让那条 pin 变红 —— 契约边界是故意的,不是遗漏。

测试(packages/data-objectstack/src/queryDataset.test.ts,+4)

三条运行时用例都 mock 服务端真实回包形状,而不是理想形状(这正是两侧测试此前失明的原因):裸信封(无 success/data 包装)、带 sql、且只按日期分组时只有 object + drillRanges,没有 dimensionFields/drillRawRows

  1. 只按日期维度分组:drillRanges 原样、按行对齐地到达调用方(通过声明的类型读,不用 as any);顺带断言 dimensionFields/drillRawRows 确为 undefined(fixture 一旦被"补全"成理想形状,这条用例就不再覆盖"drillRanges 是唯一钻取依据"的场景了);再用消费端自己的判据算出 drillDims === []canDrill === true(issue 的验收锚);最后用共享的buildDatasetDrillFilter 断出 { close_date: { $gte: '2026-06-01', $lt: '2026-07-01' } } —— 落到点中的那个桶。
  2. 日期 + 非日期混合分组:两条旁路都到达,合成的过滤器同时含等值维(用 RAW 值 NA,不是行里的显示标签 "North America")与时间边界 —— 这正是"超集"那一半影响的反面。
  3. 服务端没发(纯非日期分组):drillRanges 保持 undefined,不发明空数组。
  4. 编译期 pin:Equal 钉住条目类型与 DatasetDrillRange 结构同一(本地 { field: string; gte: string; lt: string } 复制品会让这行红)、钉住"按行对齐的数组"与"可选";并在 data-objectstack 的 queryDataset 返回类型漏掉 fields[].percentScale,而 #3136 明令渲染端必须按它缩放 #3752 那条"信封不是 AnalyticsResult"的 pin 里补了 HasKey ... 'drillRanges'

命令与输出(仓根 flock + --max-old-space-size=4096 + --maxWorkers=2):

pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/src/queryDataset.test.ts
Test Files 1 passed (1)
Tests 19 passed (19)
pnpm --workspace-concurrency=2 --filter @object-ui/data-objectstack type-check
> tsc --noEmit (无输出 = 通过)
pnpm --workspace-concurrency=2 --filter @object-ui/data-objectstack lint
✖ 343 problems (0 errors, 343 warnings) # 全是既有的 no-explicit-any warning
node scripts/check-control-bytes.mjs
✅ check-control-bytes: OK (scanned 3740 tracked text file(s); skipped 85 binary).

消费半径定向跑(规则/数据形状的消费者在别的包里,预期零改动零变化 —— 它们 mock 自己的 data source,本改动只是给 adapter 返回多一个可选键):

pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ \
packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx \
packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx
Test Files 26 passed (26)
Tests 451 passed (451)

反向验证(方向先判后跑)

:①删掉运行时透传 → 只按日期那条与混合那条,而"服务端没发"那条保持绿(它断言的是 undefined,是"不许发明空数组"的守卫,不是修复检测器 —— 按 fixture 三分法,这条通过是因为"本来就什么都没产出",必须说清而不是记作反向证据);②只删类型声明 → type-check 红、vitest 仍绿(vitest 不做类型检查)。

,① 删 return {...} 里的 drillRanges:

 × keeps drillRanges verbatim for a DATE-ONLY grouping, so canDrill is true
AssertionError: expected undefined to deeply equal [ { closed_month: { …(3) } }, …(1) ]
× keeps BOTH sidecars for a mixed date + non-date grouping (no superset drill)
AssertionError: expected { Object (stage, account.region) } to deeply equal { stage: 'won', …(2) }
- "close_date": {
Test Files 1 failed (1)
Tests 2 failed | 17 passed (19)

"服务端没发"那条如判定所述保持绿。

,② 只删返回类型里那一段声明(运行时透传留着):

src/index.ts(3364,67): error TS2353: Object literal may only specify known properties,
and 'drillRanges' does not exist in type '{ rows: ... }'
src/queryDataset.test.ts(324,35): error TS2344: Type 'false' does not satisfy the constraint 'true'
# ↑ 就是 HasKey 那条 pin
src/queryDataset.test.ts(401,68) / (437,19) / (441,19) / (457,121) / (490,128) / (516,19)
/ (521,52) / (539,53): error TS2339: Property 'drillRanges' does not exist on type ...

两个方向都与判定一致;两次破坏后均已还原,提交内容只含上述三个文件。

drillRawTotals:按「声明即使用」不补,另立观察项 #3822

服务端同样产出 drillRawTotals(analytics-service.ts:756 起,framework#3214 小计行钻取),adapter 也同样把它挑掉了。本 PR 故意不补:

  • 本仓消费者数为 0(grep -rn drillRawTotals packages apps objectui 侧零命中;buildDatasetDrillFilter 只被喂 drillRawRows)。
  • 所以透传它修复不了任何用户可见行为,只是给返回类型加一个没有任何面在读的键 —— 一个「声明了但没人使用」的面(ADR-0049 enforce-or-remove 要避免的那类债);能为它写的测试也只能自证透传本身。
  • drillRanges关键不对称:后者缺失让日期分组图表 canDrill 恒为 false(功能整块消失、五处在读、有等价判据可断言);前者缺失时,小计行钻取在本仓根本还没有实现面。
  • 正确时机:落小计行钻取消费端时同一改动里透传 + 声明 + 一条"点小计行按该分组维度过滤、点总计行不过滤"的测试。已按观察项(finding,不带 pm:queue)记在 queryDataset 未透传 drillRawTotals(小计行钻取,framework#3214)—— 本仓零消费者,观察项 #3822,不会随本 issue 关闭而丢。

changeset

.changeset/dataset-drill-ranges-passthrough-3813.md —— @object-ui/data-objectstack: patch,同 #3817 档位先例(运行时修复且用户可见:恢复日期分桶钻取入口、修掉混合分组的超集过滤)。未标 major(AGENTS.md 版本策略)。未触 content/docs/releases/**


Generated by Claude Code

…eryDataset (#3813)
The runtime half of the hand-picked-envelope family #3613/#3752 fixed on the type
face. queryDataset rebuilds its result by picking keys off the REST payload one by
one, and drillRanges was never in the list — so the analytics service's #1752
date-range drill sidecar was dropped by the only real adapter in this repo, while
five consumer sites were already reading it (DatasetWidget.tsx:471/:593,
DatasetReportRenderer.tsx:316/:431/:855).
The effect was a MISSING drill, not a degraded one. A dateGranularity dimension
groups a span of records into one bucket, which equality filters cannot express,
so service-analytics excludes date dims from dimensionFields/drillRawRows and
sends a half-open [gte, lt) range per row instead (analytics-service.ts:807+).
For a chart or report grouped only by time, drillRanges is therefore the only
thing that can make canDrill = !!object && (drillDims.length > 0 ||
!!drillRanges?.length) true — dropped, the whole drill entry point disappeared.
A mixed date + non-date grouping kept its drill but lost the time bound, so
clicking June's bar opened every month (a superset).
Neither side's tests could see it: the dashboard/report suites mock their own data
source and feed drillRanges in directly, and the adapter's suite never asserted the
key. The new tests mock the envelope the SERVER actually sends — bare (res.json,
no success/data wrapper), carrying sql, and for a date-only grouping carrying
object + drillRanges and NO dimensionFields/drillRawRows — then assert the key
arrives verbatim and row-aligned, that the consumers' own canDrill predicate is
true, and that the shared buildDatasetDrillFilter scopes the list to the clicked
bucket (both halves for a mixed grouping).
The entry type is @object-ui/core's DatasetDrillRange BY REFERENCE, not a local
{ field, gte, lt } copy: it is the single in-repo declaration of the shape (what
buildDatasetDrillFilter accepts and what both renderers type their state with) and
@objectstack/spec does not own it yet, so a restatement would be a third dialect.
drillRawTotals (#3214) is deliberately NOT added: zero consumers in this repo, so
passing it through would add a declared-but-unexercised key with no user-facing
effect. Filed separately as a finding.
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 8, 2026 5:53pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-GLmEZYpN.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)481.72KB106.05KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.85KB3.18KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)230.82KB56.70KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)117.21KB30.27KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)233.78KB57.92KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.74KB49.69KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。

核验(净 diff 3 文件):透传按原样(仅数组性校验,缺失保持 undefined 不发明空数组);条目类型引用 core 共享 DatasetDrillRange 而非本地重述(spec 不拥有该形状,产出侧类型未导出 —— 取共享消费类型是正确的次优);刻意不整体 spread 的理由成立(#3752_NoSql pin 会红)—— 对在飞先例的互动意识到位。四条新测试全部 mock 真实信封形状(裸包/带 sql/日期-only)堵住双侧失明;反向验证两方向命中,且第三条「服务端没发保持 undefined」被诚实归类为守卫而非检测器。drillRawTotals 故意不补的论证采纳:零消费者时透传即 ADR-0049 要避的债,正确时机是消费端落地同批 —— #3822 finding 立单边界正确。19/19 CI 零失败;changeset patch;trailer 自查零命中。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 18:01
@yinlianghui
yinlianghui added this pull request to the merge queueAug 8, 2026
Merged via the queue into main with commit 3765678Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3813-drillranges-passthrough branch August 8, 2026 18:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ObjectStackAdapter.queryDataset 丢掉服务端的 drillRanges,日期分桶钻取(#1752)经真实 adapter 永不可用

2 participants

@yinlianghui@claude