Skip to content

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

Description

@scottflaska

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 Tesseract engine generating Postgresql dialect queries for MSSQL DB #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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions