Root `orderBy` in GraphQL API unconditionally capitalizes cube name, breaking lowercase-first cube names (surfaced under Tesseract)
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:
getMemberType—metaConfig.find(cube => cube.config.name === cubeName || cube.config.name === capitalize(cubeName))whereArgToQueryFilters(rootwhere) —const normalizedKey = cubeExists ? key : capitalize(key);- Per-cube
orderBy(thecube { someCube(orderBy: {...}) }form, in the samegetJsonQueryfunction) —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
- Run Cube with the minimal schema below (two cubes, identical except for the first letter of
name:) withCUBEJS_TESSERACT_SQL_PLANNERunset (default/Tesseract). - Query the lowercase-named cube:
Result:{ cube(orderBy: { lowercaseOrders: { count: desc } }, limit: 5) { lowercaseOrders { count } } }{"errors":[{"message":"TesseractUserError: Cannot resolve: LowercaseOrders","locations":[{"line":1,"column":3}],"path":["cube"]}],"data":null,"extensions":{}} - Query the uppercase-named cube, using the lowercase field name GraphQL requires for the selection set:
Result: succeeds, returns data — even though the cube is declared as{ cube(orderBy: { uppercaseOrders: { count: desc } }, limit: 5) { uppercaseOrders { count } } }UppercaseOrdersand GraphQL only exposes it as the lowercase-first fielduppercaseOrders. - Remove
orderByfrom either query (keepwhere/limitonly) and note both succeed regardless of casing — onlyorderByis 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 tov1.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 sameorderBy/Tesseract/MSSQL surface area, though neither matches this exact bug. - The per-cube
orderBysyntax (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 rootcube(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
orderBywho 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from cube-js/cube
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·