Repository navigation
feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record - #25903
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
…docs, changeset, ledger
- lint: `measure-duration-field-type-refused` at `duration.from` / `.to`, with an
`os explain` entry; `dataset-field-unknown` checks both columns.
- spec: `DATASET_DURATION_FIELD_TYPE`; the unit's type alias shares its schema's
name; liveness drills `duration.{from,to,unit}`.
- docs: the duration measure in analytics.mdx and validating-metadata.mdx.
- changeset: minor on spec, service-analytics and lint.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
…s for the duration unit type Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
- The field read gate resolves a duration measure first, as every other seam does: a dimension sharing its name no longer hides its two columns. - MySQL divides by a DOUBLE literal, so the arm is double precision like the other two; the docs and changeset claim live parity for SQLite and PostgreSQL only. - getMeta lists a registered dataset's duration measures with their aggregate, label and format, and every seam accepts the `<cube>.<name>` spelling it lists them under, its scoped filter included. - Docs: path and expression refusals happen at parse, not in the lint rule. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
…0122) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
…-4m2bvo # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
…the base object changes A duration measure has no `field`, so `measureNamesNoField` read it as a plain row count and kept it across a base-object change, though its `from` / `to` name two of the old object's fields. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
📓 Docs Drift CheckThis PR changes 4 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 27fe43b990d50f9142f618c7ae20148b72aa6e82 && git checkout 27fe43b990d50f9142f618c7ae20148b72aa6e82
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c3722882633d2941810df0ab542280407958f3f 8821a8d40e7c4e93f7353433648dbb386b3123fa && git checkout -B drift-repro 0c3722882633d2941810df0ab542280407958f3f && git merge --no-ff 8821a8d40e7c4e93f7353433648dbb386b3123fa
node scripts/docs-audit/affected-docs.mjs --json 0c3722882633d2941810df0ab542280407958f3f
|
|
Independent full review (Fable), four rounds, verbatim. Reviewer: an isolated Fable agent that did not write the change, fed the card, the rulings and the code. Round 1 reviewed the whole diff at The commit ids quoted below were taken before the sign-off trailers were reworded for the push. Trees are identical, and the pushed ids are: Round 1 — whole diffVerdict: CHANGES REQUESTED Findings
Checked and fine
Round 2 — delta
|
|
Test UI (11/16) is red, and the failure is not this PR's.
Generated by Claude Code |
Contract reviewServed-tier: Reviewed read-only against card #25778 (body, triage 6397322215, Claim 6398317568, os-dev-report 6407556287), the PR body, its two comments (docs-drift 6407514107, the dev's four-round Fable record 6407520493), the net diff of all 38 files, and the head's check-runs. The face is read off the committed tree with ① Derived judgmentsThe new key. Generated surfaces match the source. SQL generation, correctness and injection. Per-row seconds are spelled once per dialect in Preview agrees with SQL. Lint agrees with the runtime. ui/ inspector. After #25825 the console is built from Docs. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ataset form's Duration row The measures repeater's `duration` row adds one leaf to the dataset form and one authored row label per translated locale: the positive control goes 679 -> 680 and the dataset panel's leaf count 38 -> 39. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Contract reviewServed-tier: This is a delta over 6407737885, the PASS of record at ① Derived judgmentsEach new count follows from the PR's own
Each test is still a real control; no assertion was loosened. Both pins remain exact equalities ( No other pin over the same data needs to move with these two. Swept every hard-coded Why Test Core (5/8) was red, read from the record rather than the dev. The shard's only failing command was ② Semver levelUnchanged from 6407737885: ③ Boundary flags
Implemented-by: VERDICT: PASS |
…-4m2bvo # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
The merge of main at 493a6c4 (#25813) deferred content/docs/references/index.mdx. Regenerated from the merged tree: main's column-apply-on page and SeedColumn beside this branch's dataset duration schemas — 1544 schemas, 199 pages. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Contract reviewServed-tier: This is a delta over 6407978732, the PASS of record at ① Derived judgments789 is the count the head actually holds, and all three statements of it agree. The test computes No pin from either side was lost, and none was duplicated. Set-compared the The three conflict hunks were resolved as a union, re-derived rather than picked. Reproduced the merge with
The regenerated index closes. At the head the fourteen module rows sum to 199 pages and 1544 schemas, which is what the Total row and the front-matter description state. Each module's own Everything else the PR carries is unchanged. ② Semver levelUnchanged from 6407978732: ③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
…-4m2bvo # Conflicts: # packages/services/service-analytics/src/strategies/native-sql-strategy.ts
…n the statement, and the all-NULL group sorts and pages as it Main now answers a measure's empty-group identity in the native statement (COALESCE) instead of folding it after the rows come back, and orders with NULLS LAST. The duration measure's statement takes the same wrap, so: - the exact-SQL cases expect COALESCE(SUM(...), 0) on the filtered sum; - the presentation case's client hands back the statement's 0; - the live ordering case no longer leaves out group c (no record carrying both columns): ascending it pages as b (-3599.25), c (0), a (91800) on SQLite and PostgreSQL alike. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Contract reviewServed-tier: This is a delta over 6409105319, the PASS of record at ① Derived judgmentsThe conflict was exactly the one #25953 predicted, and only that one. Reproduced the merge with Nothing of #25953 was lost or reverted. Every added line of The resolution took main's side of The duration measure's empty-group rule is now #25953's rule, read from the same policy. The test changes are controls, not loosened pins; each new expectation follows from the code. Everything else the PR carries is unchanged. The net diff of the head against origin/main lists 40 paths; the net diff of ② Semver levelUnchanged from 6409105319: ③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
Requested by 田騏 · project thread
Fixes #25778
Clause-②: yes (widening)
Metadata-protocol: added
Summary
A dataset measure can now aggregate the time between two
datetimefields of each record. This is item 1 of the card. Item 2 (comparing against "now minus N days" where N lives on a related record) needs its own key, so it is split to #25808 as the order directed.packages/spec/src/ui/dataset.zod.ts):DatasetMeasureSchemagains an optionalduration: { from, to, unit }in place offield.fromandtoare two bare column names of the dataset's own object.unitisseconds | minutes | hours | days.aggregateisavg | min | max | sum.*in either column, the same column twice, any other unit, a counting or missing aggregate, andfield/derived/currencybeside it.datetimewith400 DATASET_INVALID.NativeSQLStrategycomputesto - fromper row in double precision, per dialect: SQLitejulianday, PostgreSQLEXTRACT(EPOCH …), MySQLTIMESTAMPDIFF(MICROSECOND …) / 1.0E6. It divides by the unit and wraps the result in the aggregate, or in the conditional aggregate when the measure has a scoped filter.tobefore itsfromcounts as negative. Over a group with no record carrying both,sumanswers0and the othersnull. The0is answered in the statement (withEmptyGroupIdentity, as for every other measure since fix(service-analytics): an all-NULL sum group sorts and pages as the 0 it answers, the same on SQLite and PostgreSQL #25953), so it sorts and pages as0.cube.measures. Every other path refuses it with400 INVALID_FIELD, naming the measure: agranularitybucket, a field-to-field filter, an object served through middleware or an external datasource, an unknown dialect, or a non-SQL driver. The field-level read gate judges both columns.getMetalists the measure, and theCUBE.MEASUREspelling it lists is served too.os validate: new gating rulemeasure-duration-field-type-refused, atdatasets[N].measures[M].duration.from/.to, with anos explainentry.dataset-field-unknownnow covers both columns.ui/packages/app-shell, in this repo since the objectui merge): the dataset inspector's base-object change now clears a duration measure with the other measures that name the old object's fields. Before this it was kept as if it were a plain row count, because it has nofield.data-modeling/analytics.mdxgets a "Duration measures" section, anddeployment/validating-metadata.mdxexplains the rule. The liveness ledger drillsduration.{from,to,unit}.Tests
packages/services/service-analytics/src/__tests__/dataset-duration-measure.test.ts: live on SQLite and PostgreSQL, with 7 cases on each driver (14/14 in total).0on both drivers), and the cube door.code+status: granularity, objectql-only, and date / number / text columns.native-sql-duration-measure.test.ts(23):getMeta, theCUBE.MEASUREspelling, and the$fieldrefusal.packages/spec/src/ui/dataset-measure-duration.test.ts(18),packages/lint/src/validate-dataset-measure-duration.test.ts(12), and the ADR-0122 isomorphism pins.ui/packages/app-shell/.../DatasetDefaultInspector.firstPickKeepsCount-12848.test.tsx: the move fixture gains a duration measure, and the move clears it. The test fails with the predicate reverted.8821a8d4, with 1 pre-existing failure ("the server is not on UTC", because the local PG runs on UTC). Lint 7310 pass.tsc --noEmitis clean for service-analytics, and the typecheck of lint, platform-objects and spec exits 0.@object-ui/app-shelltype-check.@object-ui/componentsfails its own declaration build in this container at the main tip, in files this diff does not touch, so CI'sType Check UIis the reading for it. It is green on this head.Review
An independent full review ran on Fable. Round 1 was CHANGES REQUESTED, with 1 must-fix, 2 should-fix and 4 nits:
getMetanot listing the measure;CUBE.MEASUREspelling;All of them are applied except F6 (refusing a dimension/measure name collision at parse), which is a narrowing and so is out of scope. Three delta reviews followed, the last one after the ui/ merge, and each answered ACCEPT. The verdict record is in a comment on this PR.
Acceptance notes
$fieldoffset read across a relationship, and the [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 rulings keep SQL$fieldto the same table.granularitybucket (a monthly trend) is refused loudly rather than answered. Follow-up candidate: an engine aggregation operand for durations.duration. A duration measure keeps its key through edits (patchMeasurespreads the row), and picking a field beside it is refused by the spec at save. Follow-up candidate: a from / to / unit editor inDatasetDefaultInspector.uniton result fields. The unit lives on the dataset definition, and adding it to the result is a further protocol addition. Deliberately not done here.CUBE.MEASUREdrops its filter.query({ cube: 'sales', measures: ['sales.won_amount'] })runsSUM(amount)while['won_amount']runsSUM(CASE WHEN stage = $1 THEN amount END). That spelling is the onegetMetalists. The duration measure resolves its filter by the bare key, but ordinary measures do not. Filed as analytics: a filtered dataset measure asked on the cube door asCUBE.MEASURE, the nameGET /analytics/metalists, answers the unfiltered aggregate #25906.0(analytics native SQL: an all-NULLsumgroup is folded to 0 after ORDER BY / LIMIT ran on the NULL, so SQLite and PostgreSQL return different sorted pages #25907, fixed on main by fix(service-analytics): an all-NULL sum group sorts and pages as the 0 it answers, the same on SQLite and PostgreSQL #25953). It used to fold to0only after the statement had ordered and paged it as NULL, at opposite ends on SQLite and PostgreSQL. Since the merge of main at38050ced, the duration measure takes the same in-statement identity, and the live ordering test includes that group: ascending, it pages b (-3599.25), c (0), a (91800) on both drivers.DatasetSchemachecks uniqueness per list). Refusing that is a narrowing (review F6).🤖 Generated with Claude Code
https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW