Skip to content

Multi-stage measure order_by template crashes when queried through a view that doesn't expose the referenced member by its bare name #10856

@tlangton3

Description

@tlangton3

Summary

A multi_stage: true measure (e.g. type: rank) whose order_by.sql template references another measure by name crashes Cube at schema-compile time when queried through a view that doesn't expose the referenced member under its bare name (either not at all, or only under an alias).

The view shouldn't need to expose the referenced member at all — it's a cube-internal implementation detail of the rank, not something the consumer asks for. But Cube's resolver evaluates the rank's template in the view's context rather than the cube's, fails to find the bare name, and trips an unguarded property access.

Cube version 1.6.45 (also reproduces on master). Tesseract enabled. Backend-agnostic — the crash is in JS schema compilation, before Tesseract takes over for SQL generation.

Repro

Minimal — the view exposes only the rank and the time dim. num_parcels is not included at all:

cubes:
  - name: orders
    sql: "SELECT * FROM orders"
    dimensions:
      - name: id
        sql: id
        type: number
        primary_key: true
      - name: created_at
        sql: created_at
        type: time
    measures:
      - name: num_parcels
        type: sum
        sql: parcels
      - name: volume_by_day_rank
        multi_stage: true
        type: rank
        reduce_by:
          - created_at
        order_by:
          - sql: "{num_parcels}"
            dir: desc

views:
  - name: orders_view
    cubes:
      - join_path: orders
        includes:
          - created_at
          - volume_by_day_rank

Query:

{
  "measures": ["orders_view.volume_by_day_rank"],
  "timeDimensions": [{ "dimension": "orders_view.created_at", "granularity": "day" }]
}

Adding { name: num_parcels, alias: shipment_count } to the view's includes (intentional aliased exposure for consumers) crashes identically — the resolver looks up the bare num_parcels, finds only shipment_count on the view, returns undefined just the same.

Stack trace

TypeError: Cannot read properties of undefined (reading '_objectWithResolvedProperties')
    at packages/cubejs-schema-compiler/dist/src/adapter/BaseQuery.js:2838
    at packages/cubejs-schema-compiler/dist/src/compiler/CubeSymbols.js:1147

Equivalent positions in source:

  • packages/cubejs-schema-compiler/src/adapter/BaseQuery.js:3593 — unguarded resolvedSymbol._objectWithResolvedProperties access
  • packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts:1308 — lookup returns undefined

Root cause

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js:3349:

const orderBySql = (symbol.orderBy || [])
  .map(o => ({ sql: this.evaluateSql(cubeName, o.sql), dir: o.dir }));

cubeName is the consumer's queried cube — i.e., the view orders_view, not the underlying cube orders where the rank was defined. The rank's order_by template {num_parcels} is evaluated against the view's symbol table, which doesn't carry num_parcels unless the view explicitly includes it bare.

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js:3593 then accesses ._objectWithResolvedProperties on undefined → TypeError.

Semantically the rank's order_by should resolve in the cube's context (where the rank was authored), not the consumer's view context. {num_parcels} is the rank's implementation detail, not a member the view consumer needs to know about.

The Rust planner already does this correctly: rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_symbol.rs:544-550 compiles the rank's order_by at schema-compile time in path.cube_name() — i.e., the owning cube. The crash fires earlier in JS schema validation before Tesseract takes over.

Current workaround

Include the referenced member bare in every view that exposes any measure whose templates reference it:

- num_parcels                                # workaround — forces resolver to find it
- { name: num_parcels, alias: shipment_count }  # what the consumer actually wants

Brittle, easy to forget, and a leaky abstraction — view authors shouldn't need to know which internal references each cube measure's templates touch.

Suggested fix direction

Two-part patch in packages/cubejs-schema-compiler/:

  1. Substantive: Change BaseQuery.js:3349 (and the equivalent for any other measure-internal template — filters, case, etc., if the same gap applies) to evaluate the template in the measure's owning cube context instead of the consumer's queried cubeName. Mirrors the Rust planner's behaviour.

  2. Defensive guard: Replace the unguarded resolvedSymbol._objectWithResolvedProperties at BaseQuery.js:3593 with a clear UserError when resolvedSymbol is undefined, so any future case hitting the same gap surfaces a diagnostic instead of a generic TypeError.

A regression test in packages/cubejs-schema-compiler/test/integration/ exercising the repro above (view exposes only the rank, not the referenced measure) should be sufficient coverage. Happy to put up a PR.

Related

#10854multi_stage reduce_by against type: time granularity-suffixed projections (fix in #10855). Both are Tesseract-era resolution gaps but live in different code paths.

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