Skip to content

feat(analytics): honour widget dateGranularity, sortBy/sortOrder, and limit in dataset queries (#3588) - #3652

Merged
os-zhuang merged 3 commits into
mainfrom
claude/analytics-query-builder-params-vi64vw
Jul 27, 2026
Merged

feat(analytics): honour widget dateGranularity, sortBy/sortOrder, and limit in dataset queries (#3588)#3652
os-zhuang merged 3 commits into
mainfrom
claude/analytics-query-builder-params-vi64vw

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #3588 (framework side). The companion renderer change is objectstack-ai/objectui#claude/analytics-query-builder-params-vi64vw.

The problem

Three widget options were accepted by the metadata layer and then dropped by the analytics query builder. They reached no SQL and raised no error, so the only way to notice was to read the sql a dataset response echoes — a dashboard could declare dateGranularity: 'month' and quietly render one bar per record.

The issue's framing was right on two of three counts; the third turned out to sit in the renderer, not here:

# Issue's ask Where it actually lands
1 push dateGranularity into GROUP BY here (+ the renderer had to send it at all)
2 emit ORDER BY from sortBy/sortOrder, make limit depend on it here
3 give funnels a defined stage order objectui only — AdvancedChartImpl re-sorted funnel data by value, so any ordering this layer produced was overridden client-side

Changes

dateGranularity now buckets. DatasetSelection gained an optional dateGranularity, applied to every selected date dimension. Precedence per dimension: an explicit timeDimensions granularity → the selection's → the dataset dimension's own default. A widget can bucket a trend by month without the dataset committing every other consumer to that granularity.

order / limit / offset now apply on every path. They are applied to the assembled grid — after measure-scoped sub-queries merge, after compareTo columns attach, and after derived measures are computed. Two consequences that motivated doing it here rather than only in SQL:

  • a derived measure (a win-rate ratio, say) becomes a valid sort key even though no single statement computes it;
  • the ObjectQL aggregate path orders identically to native SQL. That path has no ordering grammar at all (EngineAggregateOptions has nowhere to put one), and native SQL declines granularity — so every date-bucketed query is handed to it. Ordering a bucketed trend was unreachable in SQL by construction.

A single-query selection still pushes the window down into the statement, so the database does the work and the echoed SQL shows it; the post-pass is then a no-op re-sort.

limit is deterministic. Without an order, a limit orders by the selected dimensions first, so it truncates a reproducible window rather than an arbitrary subset. An order key naming nothing the selection projects is rejected as a 400 instead of silently ignored.

Widget options is a contract again. The four query-affecting keys plus stageOrder are declared on a new DashboardWidgetOptionsSchema, so sortDirection or granularity is an author-time error rather than an option that reads as if it works. The bag stays open (passthrough) — icon, columns, striped, density and friends pass through untouched, and every existing widget still validates.

Two latent bugs found on the way

Neither was reachable before, which is why they were quiet:

  • order/limit were forwarded to every sub-query. A measure-scoped supplementary query selects one measure, so an inherited ORDER BY named a column it never selected, and an inherited LIMIT truncated it before the merge — dropping rows from the assembled grid. Nothing hit this only because nothing passed order.
  • compareTo skipped granularity resolution. It hand-built its query instead of going through buildQuery, so a month-bucketed primary grid was merged against raw-timestamp comparison rows. No dimension key matched and every <measure>__compare column came back empty. Found by a test written for the granularity work.

SQL echo

ObjectQLStrategy now attaches a representative sql (with date_trunc, WHERE, ORDER BY, LIMIT). Without this the fix would have removed the sql field from exactly the responses the issue used to diagnose the bug — that path returned none, and bucketing routes queries to it. Filter values are rendered as $n placeholders and returned in params, never inlined, since the echoed statement travels to the browser.

Testing

  • New dataset-selection-window.test.ts — 20 tests over granularity precedence (including the compareTo merge), ordering on both strategy paths, deterministic limit, derived-measure sort keys, the sub-query regression guard, and the SQL echo.
  • Full @objectstack/service-analytics suite: 199 passed (179 pre-existing, unchanged).
  • @objectstack/spec dashboard suite green; spec, service-analytics and rest all build (dts typecheck) clean.

Not verified in a running browser — the rendered result depends on the objectui companion PR, so end-to-end confirmation belongs there.


Generated by Claude Code

… limit in dataset queries (#3588)

Three presentation options were accepted by the metadata layer and then dropped
by the analytics query builder — no SQL, no error. The only symptom was the
`sql` a dataset response echoes, so a dashboard could declare
`dateGranularity: 'month'` and quietly render one bar per record.

- `DatasetSelection.dateGranularity` buckets every selected date dimension.
  Precedence per dimension: an explicit `timeDimensions` granularity, then the
  selection's, then the dataset dimension's own default — so a widget can bucket
  a trend by month without the dataset committing every consumer to it.
- `order`/`limit`/`offset` are applied to the ASSEMBLED grid: after
  measure-scoped sub-queries merge, after `compareTo` columns attach, and after
  derived measures are computed. A derived measure is therefore a valid sort
  key, and the ObjectQL aggregate path — which has no ordering grammar, and
  which native SQL hands every date-bucketed query to — orders identically to
  native SQL. A single-query selection still pushes the window into the
  statement. An order key naming nothing the selection projects is rejected
  (400) rather than ignored.
- A `limit` without an `order` orders by the selected dimensions first, so it
  truncates a reproducible window instead of an arbitrary subset.
- The four query-affecting widget `options` keys plus `stageOrder` are declared
  on `DashboardWidgetOptionsSchema`, making a typo an author-time error. The bag
  stays open, so renderer extras pass through untouched.

Two latent bugs surfaced while fixing the above:

- `order`/`limit` were forwarded to EVERY sub-query. A measure-scoped
  supplementary query selects one measure, so an inherited ORDER BY named a
  column it never selected and an inherited LIMIT truncated it before the merge,
  dropping rows from the grid. Nothing hit this only because nothing passed
  `order`.
- The `compareTo` pass hand-built its query and skipped granularity resolution,
  merging a month-bucketed primary grid against raw-timestamp comparison rows.
  No dimension key matched, so every `<measure>__compare` column came back
  empty.

`ObjectQLStrategy` now also echoes a representative `sql` (date_trunc, WHERE,
ORDER BY, LIMIT; filter values parameterized, never inlined). The `sql` field
previously vanished whenever a query was date-bucketed, leaving an author unable
to tell "not implemented" from "this strategy doesn't report".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEZLBGyNoJMLvtGPqsMPRC
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 1:04pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/l labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/rest, packages/services, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 27, 2026 12:56
…uery options (#3588)

`content/docs/references/ui/dashboard.mdx` is generated from the spec and went
stale when `DashboardWidgetOptionsSchema` was added — CI's generated-artifact
drift check caught it.

Also rewrites the hand-written `options` row in the dashboards guide, which
described the bag as "Additional display options". That description is what let
this bug happen: four of its keys change the compiled query, and an author had
no way to know which. The guide now lists the five declared keys with their
effects and the behavioural notes that are easy to get wrong (an unprojectable
`sortBy` is rejected, a bare `limit` orders by dimensions first, a derived
measure is a valid sort key, a funnel without a declared stage order falls back
to value-descending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEZLBGyNoJMLvtGPqsMPRC
…getOptions (#3588)

The api-surface gate reports 0 breaking, 2 added — the new
`DashboardWidgetOptionsSchema` const and its inferred `DashboardWidgetOptions`
type, both purely additive. Regenerated and committed as the gate asks.

Verified the whole spec generated-artifact set locally this time rather than one
gate per CI round-trip: check:docs, check:api-surface, check:skill-refs,
check:skill-docs, check:spec-changes, check:upgrade-guide, check:react-blocks —
all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEZLBGyNoJMLvtGPqsMPRC
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:24
@os-zhuang
os-zhuang merged commit 1986594 into main Jul 27, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/analytics-query-builder-params-vi64vw branch July 27, 2026 13:24
os-zhuang pushed a commit that referenced this pull request Jul 27, 2026
#3652 rewrote `ObjectQLStrategy.generateSql` (date_trunc for bucketed
dimensions, COUNT DISTINCT, ORDER BY/LIMIT/OFFSET) and made `execute()`
echo the rendered SQL on the result. Kept all of that.

Resolved the WHERE clause in favour of this branch's version, which
renders the same caller filters plus the two things #3652's inline loop
does not:

- the READ SCOPE (`compileScopedFilterToSql`), the point of #3602
  residual 2 — without it the echoed string still reads as an unscoped
  table scan while the aggregate is scoped;
- `assertJoinedScopesEnforceable`, so it never renders SQL for a query
  `execute()` would reject.

Its filter rendering is also closer to what runs: `contains` binds
`%v%` (a bare comparand under LIKE would only match exactly), and
values go through `coerceFilterValueForObjectQL`, so the comparand
shown is the one this path hands the engine.

Scope VALUES stay in `params`, which the `execute()` echo discards —
only the predicate shape reaches the browser, same as NativeSQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…#3654) (#3691)

The ObjectQL fallback path (date-granularity bucketing, in-memory driver,
federated) could not join, so cross-object grouping like
`revenue by account.region` was rejected outright (#3664 stopgap). It now
serves the common case by FK-expand:

1. group the base aggregate on the lookup FK column (`account`) — which
   the engine CAN do — scoped to the base object (and, per #3651's second
   belt, threading the ExecutionContext to the engine too);
2. resolve each FK id to the related attribute (`region`) with a read of
   the referenced object scoped to THAT object's own RLS;
3. re-bucket by the resolved attribute in memory, recombining measures
   (sum/count add, min/max take the extremum).

A base row whose referenced record the caller cannot read buckets under
an explicit `(restricted)` group: the measure still counts (grand totals
preserved) but the hidden attribute never appears — no leak (ADR-0021
D-C / #3602). `/analytics/sql` renders the equivalent LEFT JOIN, so
preview and execution accept/reject the same set.

Bounded — still rejected LOUD (never silently wrong): cross-object in a
MEASURE or FILTER, multi-hop dims, and non-recombinable measures
(avg/count_distinct) with a cross-object dim. NativeSQLStrategy is
unchanged.

The pure re-bucketing step is isolated in cross-object-rebucket.ts and
exhaustively unit-tested (recombination, restricted-total conservation,
null-vs-restricted, multi-dim). Integration tests drive the real
AnalyticsService with a two-call aggregate stub, asserting both the base
and FK-resolution are scoped and the restricted bucket forms; reverting
the strategy turns all three red.

Re-applied cleanly on top of #3651/#3652 (executeAggregate carries
context; generateSql renders scope) after those landed on main.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analytics query builder ignores widget dateGranularity, sortBy/sortOrder, and funnel stage order

2 participants