Errors and corrections
Every failure is JSON with one shape, and the class tells you whose fault it is: the envelope, the key, the spec, or the model. Successful responses may also carry a corrections array when a misspelled field was silently matched. This page lists both so your application can act on them.
The envelope
{"error": {"class": "Semantic::NotFound", "message": "No field named 'revenu' in this model. Did you mean: Revenue, Net Paid?"}}
class is stable and meant for code; message is for people and may name fields from the model.
Classes and statuses
| class | status | when |
|---|---|---|
Unauthorized | 401 | no key, or a key that is not valid |
Forbidden | 403 | the key may not do this on this project or branch |
NotFound | 404 | no deployment for the project and branch |
Invalid | 400 | neither spec nor expr in the body |
Shorthand | 400 | the expr did not parse |
ContextRequired | 400 | the branch has policies and the request has no context |
DeployError | 400 | deploy or validate failed (not a query route) |
Query::Spec::InvalidSpecError | 422 | the spec’s shape is wrong |
ActiveRecord::RecordInvalid | 422 | the spec is well-formed but a value fails validation; message prefixed Validation failed: |
Semantic::NotFound | 422 | a field reference resolved to nothing |
Semantic::Ambiguous | 422 | a name is both a dimension and a measure |
Planner::ResolutionError | 422 | the planner cannot build the query from the model |
Planner::SegmentDatasourceError | 422 | a segment keyed in another datasource; an internal retry signal, normally not surfaced |
Planner::SecurityPolicyError | 422 | a policy’s context dimension is unreachable from the query |
Unimplemented | 422 | a feature the planner does not plan |
Dialect | 500 | the base dialect failed to load |
Accounts | 500 | the account store failed |
400 means fix the request envelope; 401/403 means fix the key or its grants; 422 means fix the spec or the model; 5xx is ours.
Messages you will meet
Envelope and auth
| class | message |
|---|---|
Unauthorized | an API key is required: Authorization: Bearer <key> |
Unauthorized | this API key is not valid |
Forbidden | query key <name> is not granted <uid>/<branch> |
Forbidden | <email> has no access to <uid> |
Forbidden | <email> has <level> access to <uid>; this needs <level> |
Forbidden | query keys are read only; deploy with your personal key |
NotFound | no deployment for project <uid> branch <branch> |
Invalid | give a spec or an expr |
ContextRequired | this branch has security policies; a context is required to plan |
Field references
| class | message |
|---|---|
Semantic::NotFound | No field named 'x' in this model. optionally followed by Did you mean: A, B, C? |
Semantic::Ambiguous | Field 'x' is ambiguous — more than one field answers to it. Use x@d or x@m to pick the dimension or the measure. |
Spec shape (Query::Spec::InvalidSpecError)
| message | cause |
|---|---|
malformed spec: ... | unknown key, wrong JSON type, or unparseable JSON anywhere in the spec |
projection N needs a field | a non-calculation projection without field |
'x' is not an order_by (asc, desc) | bad order_by |
Unknown decorator type: X | bad decorator type |
unknown attribute 'x' on the <type> decorator of <owner> | attribute not in the decorator vocabulary |
'X' is not a predicate (filter on F) | bad predicate name |
filters must be an array or a logic tree, got ... | bad filters type |
a filter node needs a field or an and/or key, got X | bad tree node |
Filters here are a flat AND list — each entry must name a field; and:/or: groups are not supported in this position. ... | an and/or group inside an array |
Segment 'X' has no definition — ..., Segment 'X' needs at least one key dimension — ..., Unknown segment mode 'x' — use include or exclude, apply_to: no measure projection matches 'x' ..., apply_to: 'x' matches multiple projections — ..., join applies only to expanding segments — ... | segment rules, listed in full on Segments |
Validation (ActiveRecord::RecordInvalid)
All prefixed Validation failed: .
| message | cause |
|---|---|
Alias can't be blank | calculation without an alias |
Sql can't be blank (calculation 'X') | calculation without a formula |
Sql columns like x,y are not permitted in calculations. | bare identifiers in a formula; use [Name]@m / [Name]@d |
Sql could not find a projection with alias X. If you meant to references a measure or dimension used @m or @d to clarify: [My Measure]@m | [Alias] that matches no projection |
Axis the tooltip holds measures only | axis: tip on a dimension |
X can only be applied to dimensions | truncate, extract or customize on a measure |
only date/datetime types can be date truncated | truncate or extract on a non-date dimension |
Grain must be set, Extract must be set, Mode must be moving or running, Transform invalid transform type | decorator missing its required attribute |
Predicate <Name> predicate is not supported for <Field>. | predicate not allowed on that field type |
Predicate top n can only be applied to a dimension field | top_n on a measure |
Filter value can't be blank (<predicate> on <field>) | missing filter value |
Filter value is not a number, Filter value values must be numeric | non-numeric value on a numeric field |
A bad calculation data_type (outside string, integer, decimal, date, date_time, boolean, bigint, binary) and a window size, offset or buckets below 1 are rejected in the same class.
Planner
| class | message |
|---|---|
Planner::ResolutionError | At least one projection required |
Planner::ResolutionError | No universe can resolve the query within datasource D. The projected fields cannot be joined into one query from the model; see Universe formation. |
Planner::ResolutionError | Could not find path for X |
Planner::ResolutionError | cannot parse date ... |
Planner::ResolutionError | Calculation X references itself through Y |
Planner::ResolutionError | Could not resolve segment S: ... |
Planner::SecurityPolicyError | Security policy context dimension '<Name>' is not reachable in the universe for this query. Cannot safely enforce security policy. |
Unimplemented | not implemented: custom predicate |
Unimplemented | calculations over rule-bearing measures, inclusion sub-plan outside the node's universe |
Shorthand (Shorthand, 400)
| message | cause |
|---|---|
nothing to plan | empty expr |
top needs a count, as in `category top 10 by revenue` | top without an integer |
'...': unknown function f(); see .help | unknown decorator function |
The corrections array
A field reference that matches nothing exactly is scored against every field of the wanted kind by trigram overlap of the normalized text (lower-cased, runs of non-alphanumerics collapsed to one space) against the field’s name, uid and synonyms. Three constants decide what happens:
| constant | value | effect |
|---|---|---|
MIN_LEN | 4 | references shorter than four characters are never corrected |
FLOOR | 0.5 | the best candidate must score at least this |
MARGIN | 0.2 | and lead the runner-up by at least this |
When both hold the candidate is used silently and the response reports it:
{"sql": "...", "datasource": "Warehouse", "datasource_uid": "warehouse", "adapter": "postgres",
"corrections": [{"term": "departmnt", "field_uid": "department", "field_name": "Department", "score": 0.8}]}
| key | meaning |
|---|---|
term | what the request said |
field_uid, field_name | what was used |
score | the overlap, rounded to two decimals |
corrections is omitted when empty. When the floor or the margin is not met, the request fails with Semantic::NotFound and up to three candidates scoring at least 0.25 in Did you mean: A, B, C?.
zsql prints corrections to stderr before the SQL, one per line:
departmnt → Department (~0.8)
Treat a correction as a warning in an interactive tool (show the user what was substituted) and as an error in a pipeline, where a silently substituted field is a bug waiting to be noticed.
Handling errors in your app
async function plan(body) {
const res = await fetch(`https://app.0sql.io/projects/tpcds/branches/main/sql`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
const { class: cls, message } = json.error;
switch (cls) {
case "Semantic::NotFound":
case "Semantic::Ambiguous":
case "Query::Spec::InvalidSpecError":
case "ActiveRecord::RecordInvalid":
case "Shorthand":
throw new UserFixable(message); // show it; the request can be edited
case "Planner::ResolutionError":
case "Planner::SecurityPolicyError":
case "Unimplemented":
throw new ModelProblem(message); // the model or the policy needs a change
case "ContextRequired":
case "Invalid":
case "Unauthorized":
case "Forbidden":
case "NotFound":
throw new IntegrationBug(cls, message); // your code built the request or key wrong
default:
throw new Error(`${cls}: ${message}`); // 5xx: retry later, then report it
}
}
if (json.corrections?.length) {
log.warn("0sql corrected fields", json.corrections);
}
return json; // { sql, datasource, datasource_uid, adapter, ... }
}
Three habits that pay off:
- Branch on
class, never onmessage. Messages name fields and may change wording. - Surface
Semantic::NotFoundwith itsDid you meancandidates andSemantic::Ambiguouswith its@d/@mhint directly to whoever is building the query; they are written to be shown. - Log
correctionswith the request. A rename in the model can turn an exact match into a fuzzy one without any error.