errs

package
v1.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package errs defines SQLFlow's error codes and the error type that carries them.

Codes are public API. A provider shows them to its own customers and writes automation against them, so a code never changes meaning and never disappears. See registry.go for the rule and the test that enforces it.

A code has three dot-separated parts, class.domain.reason:

user.sql.bind_failed
system.sink.unreachable

The class says whose fault it is. A consumer that has never seen a code still matches its class prefix and knows whether to show the customer a message or page an operator. That guarantee is what lets new codes ship without breaking anyone, so match on the prefix rather than on an exhaustive list.

Index

Constants

View Source
const (
	// ExitOK is a clean shutdown, including a drain on SIGTERM.
	ExitOK = 0

	// ExitInternal is a failure we could not classify. Retryable, because a
	// bug we have not characterized may well be transient.
	ExitInternal = 1

	// ExitUserError marks a failure only the user can fix: config, SQL,
	// credentials. Terminal. A supervisor that retries it loops forever.
	ExitUserError = 10

	// ExitSourceUnreachable and ExitSinkUnreachable mark a dependency that
	// was not there. Retryable: the dependency may come back.
	ExitSourceUnreachable = 11
	ExitSinkUnreachable   = 12

	// ExitResourceLimit marks a declared limit the pipeline exceeded.
	// Retryable, though it repeats until someone raises the limit or the
	// workload shrinks.
	ExitResourceLimit = 13

	// ExitStateCorrupt marks a state file that cannot be read. Terminal: a
	// restart reads the same bytes. An operator has to look at it.
	ExitStateCorrupt = 14
)

Exit codes tell a supervisor what to do next. They are a coarse projection of the error codes, and they live here so the two classifications cannot drift apart: #161 consumes this table rather than restating it.

The distinction that matters is Retryable. Restarting a pipeline whose config is wrong burns a restart and fails the same way, forever. That is the crash loop a supervisor has to be told to avoid.

Variables

This section is empty.

Functions

func ExitCode

func ExitCode(err error) int

ExitCode maps an error to the code the process should exit with.

An unknown code still resolves, because it falls back to the class prefix. A future system.limit.disk_exhausted exits ExitInternal rather than crashing the mapping, and gains its own entry when someone adds it.

func HasCode

func HasCode(err error, code Code) bool

HasCode reports whether the chain carries this exact code. Prefer ClassOf when the caller only needs to know whose fault it is: it keeps working as new codes appear.

func Retryable

func Retryable(exit int) bool

Retryable reports whether restarting the process could succeed. A supervisor should stop restarting a pipeline that exits with a code this reports false for.

Types

type Class

type Class string

Class is the first part of a code. Two exist, and no third will be added: every failure is either something the user can fix or something we can.

const (
	// ClassUser marks a failure the user can fix: bad SQL, bad config, a
	// schema that does not match the data.
	ClassUser Class = "user"

	// ClassSystem marks everything else: a source or sink that cannot be
	// reached, a resource limit, a bug of ours.
	ClassSystem Class = "system"
)

func ClassOf

func ClassOf(err error) Class

ClassOf returns the class of the outermost coded error in the chain.

type Code

type Code string

Code identifies one failure. Use the constants in registry.go rather than building a Code from a string.

const (
	// Config: the pipeline file itself.
	CodeConfigNotFound    Code = "user.config.not_found"
	CodeConfigParseFailed Code = "user.config.parse_failed"
	CodeConfigInvalid     Code = "user.config.invalid"

	// Data: the messages themselves, as opposed to the pipeline definition.
	// A malformed record is the producer's problem, never ours.
	CodeDataMalformed Code = "user.data.malformed"
	CodeDataInvalid   Code = "user.data.invalid"

	// SQL: the handler's query and the schema it binds against.
	CodeSQLBindFailed      Code = "user.sql.bind_failed"
	CodeSQLTypeUnsupported Code = "user.sql.type_unsupported"
	CodeSQLInvalid         Code = "user.sql.invalid"

	// Source and sink configuration the user got wrong.
	CodeSourceSecurityInvalid Code = "user.source.security_invalid"
	CodeSourceInvalid         Code = "user.source.invalid"
	CodeSinkInvalid           Code = "user.sink.invalid"

	// Source and sink failures that are not the user's doing.
	CodeSourceUnreachable Code = "system.source.unreachable"
	CodeSourceInternal    Code = "system.source.internal"
	CodeSinkUnreachable   Code = "system.sink.unreachable"
	CodeSinkWriteFailed   Code = "system.sink.write_failed"
	CodeSinkInternal      Code = "system.sink.internal"

	// Durable state and the offsets committed with it.
	CodeStateCorrupt      Code = "system.state.corrupt"
	CodeStateCommitFailed Code = "system.state.commit_failed"
	CodeStateInternal     Code = "system.state.internal"

	// Batch orchestration.
	CodeBatchInternal Code = "system.batch.internal"

	// The last resort. CodeOf returns it for an error carrying no code, so an
	// unclassified failure still reports as ours rather than the user's.
	CodeInternalUnexpected Code = "system.internal.unexpected"
)

The registry is append-only. A published code keeps its meaning forever, because providers show these to their customers and write automation against them.

Adding a code is safe: consumers match on the class prefix, so an unknown code still routes correctly. Removing one or changing what it means is not. TestRegistryIsAppendOnly compares this file against testdata/codes.golden and fails on a removal.

Each domain carries a catch-all, so a new failure always has a home before anyone gives it a specific code. User domains catch all with `invalid`, system domains with `internal`. Reach for a specific code first; use the catch-all rather than inventing a code in a hurry.

func CodeOf

func CodeOf(err error) Code

CodeOf returns the code of the outermost coded error in the chain.

An error carrying no code reports as CodeInternalUnexpected rather than as a user error. Guessing the other way would blame a customer for our bug.

func (Code) Class

func (c Code) Class() Class

Class returns the code's class, or an empty Class when the code is malformed.

func (Code) Domain

func (c Code) Domain() string

Domain returns the subsystem the failure came from: config, sql, source, sink, state, batch, limit or internal.

func (Code) IsSystem

func (c Code) IsSystem() bool

IsSystem reports whether the failure is ours rather than the user's.

func (Code) IsUser

func (c Code) IsUser() bool

IsUser reports whether the user can fix this failure. Prefer it to comparing against a list of codes: it keeps working when new codes appear.

func (Code) Reason

func (c Code) Reason() string

Reason returns the specific failure within the domain.

type Definition

type Definition struct {
	Code    Code
	Summary string
	Action  string
}

Definition documents one code. Action is what the operator should do, and it is the reason this registry exists rather than a bare list of constants: a code with no recommended action is not usable by the people who see it.

func All

func All() []Definition

All returns every registered definition, ordered by code. Documentation and the append-only test both read it.

func Lookup

func Lookup(c Code) (Definition, bool)

Lookup returns the definition for a code. A caller that gets ok == false is holding a code this build does not know, which is normal when a newer component produced it: fall back to the class prefix.

type Error

type Error struct {
	Code    Code
	Msg     string
	Pos     *Position
	Wrapped error
}

Error carries a code alongside the message.

Interior call sites keep using fmt.Errorf with %w. Only the places where an error becomes observable need a code: the CLI boundary, the log, and the `validate` response. The code survives any amount of wrapping in between, which is why converting the codebase does not mean touching every raise site.

func New

func New(code Code, format string, args ...any) *Error

New builds a coded error with no cause.

func Wrap

func Wrap(code Code, err error, format string, args ...any) *Error

Wrap attaches a code to an existing error. Wrapping an error that already carries a code keeps the outer one: the boundary nearest the user decides how to describe the failure.

func (*Error) At

func (e *Error) At(source string, line, column int) *Error

At records where in the user's text the failure happened.

func (*Error) Error

func (e *Error) Error() string

Error prefixes the code rather than appending it. A wrapped YAML or SQL error runs to many lines, and a trailing code lands below all of them where nobody reads it. In front, the code is always the first thing on the line and stays greppable.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Position

type Position struct {
	// Source names what Line and Column index into: "sql" or "config".
	Source string
	Line   int
	Column int
}

Position points at the place in the user's own text that caused a user error. `validate` and `preview` return it so a UI can highlight the line rather than printing a paragraph.

func PositionOf

func PositionOf(err error) (Position, bool)

PositionOf returns the position of the outermost coded error, if it has one.

func (Position) String

func (p Position) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL