Error Reference
flyql ships a single registry of every diagnostic code and parser errno. The registry is the source of truth that keeps Python, JavaScript, and Go in lockstep, and every Diagnostic or parser error a consumer sees carries an optional reference back into it.
The error registry
Section titled “The error registry”The file errors/registry.json enumerates every error flyql can raise or report. Each entry has:
- a
name(symbolic, e.g.ERR_KEY_PARSE_FAILED,CODE_UNKNOWN_COLUMN) - a canonical
messagetemplate - an optional
description(human-readable context) - an optional
dynamic_messageflag (true when the runtime message interpolates context the template doesn’t show)
A codegen step (make generate-errors) produces per-language modules from the registry — constants, message maps, and a REGISTRY map of ErrorEntry records — so consumers never read the JSON at runtime.
The registry has four categories:
| Category | Code type | Used by |
|---|---|---|
core_parser | int | Parser.parse() errors |
columns_parser | int | columns.Parser.parse() errors |
validator | string | diagnose() (core + columns validators) |
matcher | string | Evaluator runtime errors (Python only — JS uses native RegExp, Go uses stdlib regexp) |
The matcher category does not ship a REGISTRY map — matcher raises plain FlyqlError, not Diagnostic or ParserError. Other categories all expose *_REGISTRY (Py/JS) or *Registry (Go).
Reading a Diagnostic
Section titled “Reading a Diagnostic”Validators return Diagnostic instances. The shape is identical across languages, with the registry entry on the error field (Py/JS) or Entry field (Go — the rename avoids a clash with the Error() method on Go error types).
| Field | Type | Meaning |
|---|---|---|
range | Range | source span the user must edit to fix the issue |
message | string | rich, interpolated message (e.g. "column 'foo' is not defined") |
severity | "error" | "warning" | severity level decided at the emit site |
code | string | short identifier (e.g. "unknown_column") |
error / Entry | ErrorEntry? | registry entry for this code, or null/zero-value if the code isn’t in the registry |
The nested ErrorEntry shape:
| Field | Type | Meaning |
|---|---|---|
code | int | string | same as the parent diagnostic’s code (parsers use ints) |
name | string | symbolic name for static lookup (e.g. "CODE_UNKNOWN_COLUMN") |
message | string | canonical registry template (un-interpolated) |
description | string | longer human-readable context (often empty until backfilled) |
dynamic_message / dynamicMessage / DynamicMessage | bool | true when the runtime message carries context the template doesn’t |
Reading a ParserError
Section titled “Reading a ParserError”ParserError (Py/JS) and *ParseError / *ParserError (Go) carry the same registry pointer:
| Field | Type | Meaning |
|---|---|---|
errno (Py/JS) / Code (Go) | int | numeric error code |
message (Py/JS) / Message (Go) | string | rich runtime message |
range (Py/JS) / Range (Go) | Range? | source span (core parser only — columns parser does not report a range) |
error (Py/JS) / Entry (Go) | ErrorEntry (? in Py/JS) | registry entry, populated at the terminal raise/return site |
Consumer examples
Section titled “Consumer examples”from flyql import parse, ParserError
try: parse("a=")except ParserError as e: print(e.errno, e.message) if e.error is not None: print(e.error.name, e.error.description)from flyql import diagnosefrom flyql.core import ColumnSchema, Columnfrom flyql.flyql_type import Type
schema = ColumnSchema.from_columns([Column(name="host", column_type=Type.String)])for d in diagnose(parse("foo='X'").root, schema): if d.error is not None: print(f"{d.error.name}: {d.message}")import { parse, ParserError } from 'flyql'
try { parse('a=')} catch (e) { if (e instanceof ParserError) { console.log(e.errno, e.message) if (e.error) { console.log(e.error.name, e.error.description) } }}import { diagnose, Column, ColumnSchema, Type } from 'flyql'
const schema = ColumnSchema.fromColumns([new Column('host', Type.String)])for (const d of diagnose(parse("foo='X'").root, schema)) { if (d.error) { console.log(`${d.error.name}: ${d.message}`) }}import ( flyql "github.com/iamtelescope/flyql/golang")
result, err := flyql.Parse("a=")if err != nil { if pe, ok := err.(*flyql.ParseError); ok { fmt.Println(pe.Code, pe.Message) if pe.Entry.Name != "" { fmt.Println(pe.Entry.Name, pe.Entry.Description) } }}_ = resultschema := flyql.FromColumns([]flyql.Column{flyql.NewColumn("host", flyql.TypeString)})ast, _ := flyql.Parse("foo='X'")for _, d := range flyql.Diagnose(ast.Root, schema, nil) { if d.Entry.Name != "" { fmt.Printf("%s: %s\n", d.Entry.Name, d.Message) }}Note the field-name asymmetry: Go uses Entry; Python and JavaScript use error. Go convention reserves the field/method name Error for the Error() string method on error types, so the registry pointer lives under Entry.
Dynamic-message codes
Section titled “Dynamic-message codes”Some entries carry dynamic_message: true. For these, the runtime message interpolates context the registry template doesn’t capture. Examples:
ERR_UNEXPECTED_EOFtemplate:"unexpected EOF". Runtime:"unexpected EOF after 'not'".ERR_KEY_PARSE_FAILEDtemplate:"key parsing failed". Runtime: the wrapped key parser’s specific message.
Consumer contract:
- Tooltips and human-facing display: prefer the runtime
message. Useerror.description(when non-empty) as supplementary context. - Aggregation, grouping, structured logging: use
error.codeas the key. Useerror.messageonly when you need the canonical template for log fingerprints. - Static analysis or doc deep-linking: use
error.name(symbolic) — the most stable identifier across releases.
Optional error field
Section titled “Optional error field”The error / Entry field is optional — it can be missing (null in Py/JS, zero-value ErrorEntry{} in Go) when:
- User code constructs a
Diagnosticdirectly with a code that isn’t in the registry (e.g. flyql-vue editor wiring with codes like"syntax"or"deprecated"). - Renderer extension hooks return user-supplied diagnostics with custom codes.
- External callers construct a
ParserErrorwithout passing the new arg.
Internal validators and parsers always populate the field via centralized helpers (make_diag Py / makeDiag JS / MakeDiag Go). On registry miss the helpers return the diagnostic with error=None / error=null / zero-value Entry rather than panicking. Drift between code constants and the registry is caught at build time by the parity tests.
Defensive consumer pattern:
if diagnostic.error is not None: label = diagnostic.error.nameelse: label = diagnostic.codeconst label = d.error != null ? d.error.name : d.code// Use Entry.Name as the miss signal. Entry.Code is `any` and is never// untyped-nil for typed-string codes even when the entry is zero-value.label := d.Codeif d.Entry.Name != "" { label = d.Entry.Name}Categories at a glance
Section titled “Categories at a glance”| Category | Code type | Holds | REGISTRY emitted |
|---|---|---|---|
core_parser | int | parser errnos surfaced by ParserError / *ParseError | yes |
columns_parser | int | columns parser errnos | yes |
validator | string | validator diagnostic codes (12 entries) | yes |
matcher | string | matcher runtime errors (Python only) | no — matcher raises FlyqlError |
Full code list
Section titled “Full code list”The complete list of names, messages, and descriptions lives in errors/registry.json in the repository. Auto-rendering this list from the registry at docs build time is planned future work.