cube-js / cube-js/cube

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

Open Beginner friendly
#11,700 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
20.8k
Forks
2.1k
Avg merge
1d 2h
Merged PRs (30d)
181

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: ` 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`](https://github.com/cube-js/cube/blob/master/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:

```typescript
if (orderBy) {
Object.entries(orderBy).forEach(([cubeName, members]) => {
Object.entries(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:

- `getMemberType` — `metaConfig.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:

```typescript
if (orderBy) {
Object.entries(orderBy).forEach(([cubeName, members]) => {
const cubeExists = metaConfig.find((cube) => cube.config.name === cubeName);
const normalizedCubeName = cubeExists ? cubeName : capitalize(cubeName);
Object.entries(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:
```graphql
{ cube(orderBy: { lowercaseOrders: { count: desc } }, limit: 5) { lowercaseOrders { count } } }
```
Result:
```json
{"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:
```graphql
{ 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: { : { : 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**

```yaml
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.

Contributor guide

Open the contributing guide

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.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.