Repository navigation
fix(service-analytics): a dataset selection asking one column twice is refused with a repair the selection can make, not the engine's "own 'alias'" - #28851
Conversation
…s refused with a repair the selection can make, not the engine's "own 'alias'" - dataset-group-column-door.ts (was preview-group-column-door.ts): assertDatasetGroupColumnsDistinct(object, groupBy, query, memberColumn) is the one refusal for every queryDataset face. It judges the keys it is handed, with the engine's code, status, param, path, object and fields, and a prescription naming the selection's members over the column: drop the raw dimension or the bucket's granularity, or ask one granularity. - objectql-strategy.ts: execute and executeCrossObject call it on the keys they built, right before ctx.executeAggregate, so native (which declines every bucketed selection to this strategy) and ObjectQL answer it. resolveCubeFieldName becomes a module-level export (not root). - analytics-service.ts: the draft preview calls it on aggregateGroupKeys. - The engine door in packages/objectql is untouched; a direct engine.aggregate caller keeps its alias prescription (pinned). - Tests on real SQLite: the sentence pinned verbatim on all three faces, the faces agree field for field, the cross-object path, the raw-member listing, and the precedence pin for a bucket over a text column. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 10 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 1bed4a453bf4499f971e665d45ee3f6d2fc931ef && git checkout 1bed4a453bf4499f971e665d45ee3f6d2fc931ef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3e62a22c4f31c6b8ada6e6e3b3c96996396edec6 ac21b1b68431d4179834bbc021f601fb61f071f7 && git checkout -B drift-repro 3e62a22c4f31c6b8ada6e6e3b3c96996396edec6 && git merge --no-ff ac21b1b68431d4179834bbc021f601fb61f071f7
node scripts/docs-audit/affected-docs.mjs --json 3e62a22c4f31c6b8ada6e6e3b3c96996396edec6
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28851 · 2026-11-02T01:05ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kbUJTTWAxjV9nqr1P
Requested by Leehom · project thread
Fixes #28798
Clause-②: no — the accept set of
queryDatasetis unchanged on every face: each selection served onmainis served, each refused is refused with status 400. Only the refusal's prescription moves, and the ObjectQL strategy now refuses these keys itself, right beforeengine.aggregate. Nothing is widened: no package-root export, public member, answer member, error code or status is added.Metadata-protocol: none
Skills-docs: none — no file under
skills/**orcontent/docsquotes the duplicate-column refusal or tells a dataset author to alias a group entry.Card thread:
session_01FEkQ5kbUJTTWAxjV9nqr1P.维护者速读
code、状态码、param、path、object、fields都不变,只换处方:不再叫作者「给分组项起一个 alias」(数据集选择里没有 alias),而是点名选择里涉及这一列的成员,并给出能做的修法:去掉原始维度或去掉那个时间桶的粒度;两个粒度时只留一个。engine.aggregate之前调用它;引擎的文字不动。INVALID_FIELD),现在答本拒绝(INVALID_QUERY),与草稿预览原本的回答一致。见下文「一处优先级变化」。回滚即 revert。packages/services/service-analytics/src/dataset-group-column-door.ts(由preview-group-column-door.ts改名重写),strategies/objectql-strategy.ts中execute和executeCrossObject在ctx.executeAggregate前各一行调用。pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/preview-time-filter-fk-parity.test.ts,预期 37 passed。What changes
One refusal for every dataset face.
preview-group-column-door.tsbecomesdataset-group-column-door.ts, andassertPreviewGroupColumnsDistinct(query, cube)becomesassertDatasetGroupColumnsDistinct(object, groupBy, query, memberColumn). It judges thegroupBykeys it is handed, so each caller hands it the keys it built:ObjectQLStrategy.executecalls it onaggregateGroupKeys(query, cube), right beforectx.executeAggregate;executeCrossObjectcalls it on its own keys (an FK column for a cross-object dimension), right before its basectx.executeAggregate;aggregateGroupKeys(q, cube)overaggregateObjectOf(cube).Each call sits right before the engine call, so every refusal the strategy gave before keeps its precedence.
The sentence. The head and tail are the engine's, so a reader of both sees the same shape. Only the prescription between them differs, and it names the selection's members over the column, read with the strategy's
resolveCubeFieldName:dimensions'closed_at';timeDimensions'OBJECT.closed_at' by month). Drop the raw dimension fromdimensions, or drop the granularity from thetimeDimensionsentry."timeDimensions'closed_at' by month, 'closed_at' by year). Ask one granularity of it intimeDimensions."A dimension bucketed by its own
timeDimensionsentry is not listed as raw. "Drop the granularity" rather than "drop the entry", because atimeDimensionsentry without a granularity still windows the query.Surface.
resolveCubeFieldNamebecomes a module-level export ofobjectql-strategy.ts, asaggregateGroupKeysandaggregateObjectOfalready are. The package root re-exportsObjectQLStrategyby name only, so the root surface is unchanged.ObjectQLStrategygains no member. The door imports only a type from the strategy module, so there is no runtime import cycle.Untouched.
packages/objectqlis untouched, and a directengine.aggregatecaller still gets the engine's "own 'alias'" prescription (pinned).generateSql(/analytics/sql, analytics: the /analytics/sql dry run renders one bucket for a selection the query refuses (two granularities of one column, a column raw beside its bucket) #28841) is untouched.Tests:
src/__tests__/preview-time-filter-fk-parity.test.ts(34 → 37 cases). Changeset:.changeset/28798-dataset-column-twice-refusal-prescription.md,@objectstack/service-analyticspatch.Why
Measured on
origin/mainfa59f787with a probe over real SQLite (all three faces):… The query was NOT run. Give one of the group entries its own 'alias'. …, and a dataset selection has no alias.engine.aggregatewith['closed_at', { field: 'closed_at', dateGranularity: 'month' }]answers the same refusal. That is the backstop this change leaves in place.One precedence change (for the seat's ruling)
The engine runs its other
groupBydoors before the duplicate-column door. One of them refuses a date bucket over a non-date column (INVALID_FIELD). Measured onmain, the selectiondimensions: ['region']withtimeDimensions: [{ dimension: 'zone', granularity: 'month' }](zoneis declared over the text columnregion) answered:INVALID_FIELD,groupBy.1.dateGranularity;With the door before
engine.aggregate, as ruled, all three faces now answer the column refusal (INVALID_QUERY,groupBy.1.field). It is still a 400 refusal, and fixing it meets the other refusal. The new pin[#28798] answers it on every face before the engine judges a bucket over a text columnholds this. No other measured selection changed. The os-dev-report carries this as an open question with options.Tests
Red first, green, ablations
Red on base.
analytics-service.ts,strategies/objectql-strategy.tsandpreview-group-column-door.tswere restored tofa59f787, and the new door file was moved aside. The test stayed at HEAD. Result:Tests 11 failed | 26 passed (37).Received: "… Give one of the group entries its own 'alias'. …".INVALID_FIELD.Restored with
git checkout HEAD --, thengit statuswas clean.Green.
37 passed (37). The full service-analytics suite gaveTest Files 203 passed (203),Tests 4876 passed | 282 skipped.tsc --noEmitand scoped eslint both exited 0.Ablations used
node scripts/ablation-replace.mjswith the fix committed first. Each anchor went 1 → 0, and each restore came back with blob == HEAD and an emptygit diff HEAD:executecall, so the engine answers again. Result:9 failed | 27 passed, every live-face refusal pin on the direct path.executeCrossObjectcall. Result:1 failed | 35 passed, the cross-object pair.9 failed | 27 passed, because the preview serves both columns again.3 failed | 33 passed, the raw-beside-bucket cases.1 failed | 36 passed.timeDimensionsmember's spelling instead of its column. Result:5 failed | 32 passed.Out of scope (reported to the seat, not fixed)
main, the draft preview serves a bucket over a text column (timeDimensions: [{ dimension: 'zone', granularity: 'month' }]) as onenullbucket holding the total. Both live faces refuse it withINVALID_FIELD. That is a separate preview parity gap.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5kbUJTTWAxjV9nqr1P
Generated by Claude Code