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

classstatuswhen
Unauthorized401no key, or a key that is not valid
Forbidden403the key may not do this on this project or branch
NotFound404no deployment for the project and branch
Invalid400neither spec nor expr in the body
Shorthand400the expr did not parse
ContextRequired400the branch has policies and the request has no context
DeployError400deploy or validate failed (not a query route)
Query::Spec::InvalidSpecError422the spec’s shape is wrong
ActiveRecord::RecordInvalid422the spec is well-formed but a value fails validation; message prefixed Validation failed:
Semantic::NotFound422a field reference resolved to nothing
Semantic::Ambiguous422a name is both a dimension and a measure
Planner::ResolutionError422the planner cannot build the query from the model
Planner::SegmentDatasourceError422a segment keyed in another datasource; an internal retry signal, normally not surfaced
Planner::SecurityPolicyError422a policy’s context dimension is unreachable from the query
Unimplemented422a feature the planner does not plan
Dialect500the base dialect failed to load
Accounts500the 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

classmessage
Unauthorizedan API key is required: Authorization: Bearer <key>
Unauthorizedthis API key is not valid
Forbiddenquery key <name> is not granted <uid>/<branch>
Forbidden<email> has no access to <uid>
Forbidden<email> has <level> access to <uid>; this needs <level>
Forbiddenquery keys are read only; deploy with your personal key
NotFoundno deployment for project <uid> branch <branch>
Invalidgive a spec or an expr
ContextRequiredthis branch has security policies; a context is required to plan

Field references

classmessage
Semantic::NotFoundNo field named 'x' in this model. optionally followed by Did you mean: A, B, C?
Semantic::AmbiguousField '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)

messagecause
malformed spec: ...unknown key, wrong JSON type, or unparseable JSON anywhere in the spec
projection N needs a fielda non-calculation projection without field
'x' is not an order_by (asc, desc)bad order_by
Unknown decorator type: Xbad 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 Xbad 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: .

messagecause
Alias can't be blankcalculation 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 onlyaxis: tip on a dimension
X can only be applied to dimensionstruncate, extract or customize on a measure
only date/datetime types can be date truncatedtruncate or extract on a non-date dimension
Grain must be set, Extract must be set, Mode must be moving or running, Transform invalid transform typedecorator 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 fieldtop_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 numericnon-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

classmessage
Planner::ResolutionErrorAt least one projection required
Planner::ResolutionErrorNo universe can resolve the query within datasource D. The projected fields cannot be joined into one query from the model; see Universe formation.
Planner::ResolutionErrorCould not find path for X
Planner::ResolutionErrorcannot parse date ...
Planner::ResolutionErrorCalculation X references itself through Y
Planner::ResolutionErrorCould not resolve segment S: ...
Planner::SecurityPolicyErrorSecurity policy context dimension '<Name>' is not reachable in the universe for this query. Cannot safely enforce security policy.
Unimplementednot implemented: custom predicate
Unimplementedcalculations over rule-bearing measures, inclusion sub-plan outside the node's universe

Shorthand (Shorthand, 400)

messagecause
nothing to planempty expr
top needs a count, as in `category top 10 by revenue` top without an integer
'...': unknown function f(); see .helpunknown 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:

constantvalueeffect
MIN_LEN4references shorter than four characters are never corrected
FLOOR0.5the best candidate must score at least this
MARGIN0.2and 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}]}
keymeaning
termwhat the request said
field_uid, field_namewhat was used
scorethe 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 on message. Messages name fields and may change wording.
  • Surface Semantic::NotFound with its Did you mean candidates and Semantic::Ambiguous with its @d/@m hint directly to whoever is building the query; they are written to be shown.
  • Log corrections with the request. A rename in the model can turn an exact match into a fuzzy one without any error.

Next steps