Skip to content

Dataset order sorts a select/lookup dimension by its stored value, not the label the user reads #3680

Description

@os-zhuang

Follow-up from #3588 / #3652. Known, deliberately-accepted behaviour today — filing it so the next person who hits it doesn't have to re-derive the cause.

Behaviour

DatasetSelection.order (what a widget's options.sortBy lowers to) sorts on the stored dimension value. For a select or lookup dimension the user never sees that value — the response carries a resolved display label, and the chart axis / table cell shows the label. So a sort that is internally correct can read as arbitrary.

The gap is one of ordering-in-the-pipeline, not of comparison logic:

  • DatasetExecutor.executeSelection applies the ordering as the last step of its own work — dataset-executor.ts:457 (applyOrdering), then applyWindow.
  • AnalyticsService.queryDataset resolves dimension values → display labels afterwardsanalytics-service.ts:634 (resolveDimensionLabels).

So by construction the sort key is the pre-resolution value.

Impact, by dimension type

Dimension type Sorted by Visible effect
lookup / master_detail the FK id (e.g. 8eqtuKI4G9IhUsPS) Worst case. Ids are opaque and effectively random, so "sort by Account" produces an arbitrary order that still presents as sorted.
select the stored option value (needs_analysis) Diverges from label order whenever the two don't share a prefix ordering — most visibly with localized labels, where value is ASCII and the label is not (active合作中).
date the bucket key (2026-04, 2026-Q2) Fine. Bucket keys are chosen to be sort-stable, so lexicographic order is chronological.
string / number / boolean the value itself Fine — value and label are the same thing.

Repro

Any dataset with a lookup dimension, e.g. the showcase showcase_project_metrics.account:

POST /api/v1/analytics/dataset/query
{
  "datasetName": "showcase_project_metrics",
  "selection": {
    "dimensions": ["account"],
    "measures": ["project_count"],
    "order": { "account": "asc" }
  }
}

Rows come back ordered by the account FK id; the account column in the response has already been rewritten to the account name, so the output reads as an unsorted list of names.

Why it is this way

Sorting in the executor is what makes ordering work at all on the ObjectQL aggregate path (no ordering grammar) and over derived measures (no SQL column) — see the rationale block at the top of dataset-executor.ts. Moving the sort after label resolution would fix the key but re-open the question of where limit applies, since the two must stay adjacent: ordering and windowing have to be one step, or a "top 10 by account name" truncates the wrong ten.

Currently noted only in a code comment; there is no author-facing documentation of it.

Options

  1. Sort after label resolution for label-bearing dimensions. Move applyOrdering + applyWindow up into queryDataset, after resolveDimensionLabels. Keeps one sort site, but the executor's totals/compareTo paths would need to keep their own ordering-free contract, and the executor stops being self-contained for non-dataset callers.
  2. Resolve labels before sorting when the order key is a label-bearing dimension. Narrower: only pays the resolution cost when someone actually sorts by such a dimension. Needs the label resolver reachable from the executor, which today it isn't (deliberately — the executor is "pure query shaping + arithmetic").
  3. Document it and leave the behaviour. Cheapest; the sort is at least deterministic and stable. Would want a line in the dashboards guide's sortBy notes stating that select/lookup dimensions sort by stored value.

Option 2 looks like the best cost/benefit if we want it fixed; option 3 is defensible for select dimensions but weak for lookups, where the order is genuinely meaningless to the reader.

Not affected

Sorting by a measure (the common case — "top 10 industries by revenue", the original #3588 scenario) is unaffected: measures have no label mapping, so the value sorted on is the value displayed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions