Root `orderBy` in GraphQL API unconditionally capitalizes cube name, breaking lowercase-first cube names (surfaced under Tesseract)

Open Beginner friendly
#11,700 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
graphql, rust, typescript
Domain
api

Research direction

Start in packages/cubejs-api-gateway/src/graphql.ts, focusing on getJsonQuery and its root orderBy handler; compare it with the guarded normalization used by getMemberType, root where, and per-cube orderBy. Run the minimal lowercaseOrders and UppercaseOrders GraphQL queries, and consider the issue complete when root orderBy works for both casing styles without changing the working alternatives.

Written by the indexing model from the issue text.

Description

Describe the bug

A GraphQL query using the root orderBy argument on the cube(...) field fails under the Tesseract SQL planner with TesseractUserError: Cannot resolve: <CapitalizedCubeName> whenever the cube's name: starts with a lowercase letter — reproduces for snake_case and camelCase names alike (e.g. orders, lowercaseOrders). The identical cube works fine in orderBy if its name: happens to start with an uppercase letter (e.g. UppercaseOrders), even though GraphQL's own schema generation still exposes it as a lowercase-first field (uppercaseOrders) in the selection set. where filters, and the alternate per-cube orderBy syntax (cube { someCube(orderBy: {...}) }, as opposed to the root cube(orderBy: {...}) form), both work correctly regardless of casing — only the root-level orderBy is affected.

Root cause

This traces to getJsonQuery in packages/cubejs-api-gateway/src/graphql.ts, where the root orderBy handler unconditionally capitalizes the cube name with no check for whether the cube already exists under its real (as-declared) name:

if (orderBy) {
  Object.entries<any>(orderBy).forEach(([cubeName, members]) => {
    Object.entries<any>(members).forEach(([member, value]) => {
      order.push([`${capitalize(cubeName)}.${member}`, value]);
    });
  });
}

For a cube named lowercaseOrders, this produces the order path "LowercaseOrders.count" — a string that doesn't match any real cube name, since the actual cube is lowercaseOrders.

This same file already has the correct, guarded pattern in three other places just a few lines away, which the root orderBy handler should be using instead:

  • getMemberTypemetaConfig.find(cube => cube.config.name === cubeName || cube.config.name === capitalize(cubeName))
  • whereArgToQueryFilters (root where) — const normalizedKey = cubeExists ? key : capitalize(key);
  • Per-cube orderBy (the cube { someCube(orderBy: {...}) } form, in the same getJsonQuery function) — const cubeName = cubeExists ? cubeNode.name.value : capitalize(cubeNode.name.value);

Only the root orderBy block skips the existence check and always capitalizes.

This also explains why this reads as a "Tesseract bug" without being one in origin: getJsonQuery feeds the same (incorrect) order path to whichever planner is active. The legacy planner's cube-name resolution appears to tolerate the mismatch (likely a case-insensitive or otherwise more forgiving lookup); Tesseract's resolver (resolve_cube_name in rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/symbol_path.rs) does an exact, case-sensitive cube_evaluator.cube_exists(name) check with no fallback, so it's the first place this pre-existing bug produces a hard failure instead of silently resolving anyway.

Suggested fix

Mirror the guarded pattern already used elsewhere in the same file:

if (orderBy) {
  Object.entries<any>(orderBy).forEach(([cubeName, members]) => {
    const cubeExists = metaConfig.find((cube) => cube.config.name === cubeName);
    const normalizedCubeName = cubeExists ? cubeName : capitalize(cubeName);
    Object.entries<any>(members).forEach(([member, value]) => {
      order.push([`${normalizedCubeName}.${member}`, value]);
    });
  });
}

To Reproduce

  1. Run Cube with the minimal schema below (two cubes, identical except for the first letter of name:) with CUBEJS_TESSERACT_SQL_PLANNER unset (default/Tesseract).
  2. Query the lowercase-named cube:
    { cube(orderBy: { lowercaseOrders: { count: desc } }, limit: 5) { lowercaseOrders { count } } }
    
    Result:
    {"errors":[{"message":"TesseractUserError: Cannot resolve: LowercaseOrders","locations":[{"line":1,"column":3}],"path":["cube"]}],"data":null,"extensions":{}}
    
  3. Query the uppercase-named cube, using the lowercase field name GraphQL requires for the selection set:
    { cube(orderBy: { uppercaseOrders: { count: desc } }, limit: 5) { uppercaseOrders { count } } }
    
    Result: succeeds, returns data — even though the cube is declared as UppercaseOrders and GraphQL only exposes it as the lowercase-first field uppercaseOrders.
  4. Remove orderBy from either query (keep where/limit only) and note both succeed regardless of casing — only orderBy is affected.

Expected behavior

The orderBy: { <cube_name>: { <measure_or_dimension>: asc | desc } } argument should order results by the given field regardless of what case the cube's name: starts with, the same way it does under the legacy planner (CUBEJS_TESSERACT_SQL_PLANNER=false), which handles both of the schemas below correctly.

Minimally reproducible Cube Schema

cubes:
  - name: lowercaseOrders
    sql: >
      SELECT 1 AS id, 'completed' AS status
      UNION ALL
      SELECT 2 AS id, 'completed' AS status
      UNION ALL
      SELECT 3 AS id, 'processing' AS status

    dimensions:
      - name: id
        sql: id
        type: number
        primary_key: true

      - name: status
        sql: status
        type: string

    measures:
      - name: count
        type: count

  - name: UppercaseOrders
    sql: >
      SELECT 1 AS id, 'completed' AS status
      UNION ALL
      SELECT 2 AS id, 'completed' AS status
      UNION ALL
      SELECT 3 AS id, 'processing' AS status

    dimensions:
      - name: id
        sql: id
        type: number
        primary_key: true

      - name: status
        sql: status
        type: string

    measures:
      - name: count
        type: count

Version

  • Cube image: cubejs/cube:latest, resolved to v1.7.30
  • SQL planner: Tesseract (default; bug does not reproduce with CUBEJS_TESSERACT_SQL_PLANNER=false, consistent with the legacy planner's cube-name resolution tolerating the mismatched capitalization)
  • Data source: reproduced against MSSQL (CUBEJS_DB_TYPE=mssql); noted since #9567 shows other Tesseract/MSSQL-specific issues, but this bug is not MSSQL-specific — the root cause is in the shared GraphQL query-building layer (getJsonQuery), not the planner or SQL generation, and it also reproduces with the synthetic inline-SQL cube above.

Additional context

  • Possibly related: #6497 ("Cube 'Orders' not found for path 'Orders.count'" using GraphQL orderBy, older engine, different root cause per PR #5680) and #9567 (Tesseract generating the wrong SQL dialect for MSSQL) — flagging both since they touch the same orderBy/Tesseract/MSSQL surface area, though neither matches this exact bug.
  • The per-cube orderBy syntax (cube { someCube(orderBy: {...}) { ... } }) already has the correct guarded capitalization check in the same file (not independently verified against a live server here, but should be a viable workaround based on the code) in place of the root cube(orderBy: {...}) form.
  • Since Tesseract became the default SQL planner as of Cube Core v1.7 GA, this latent bug likely affects any GraphQL API consumer using root-level orderBy who hasn't explicitly opted into the (deprecated) legacy planner.
Dominant language
Rust
Stars
20.9k
Forks
2.1k
Avg merge
1d 31m
Merged PRs (30d)
203

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cube-js/cube

All issues in cube-js/cube

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.