Introduce a deterministic translation layer that maps source-specific failures (database, driver, adapter, compile profile, lane) into the canonical RuntimeError envelope and stable codes defined in ADR 027. This mapping is target-aware but lane-neutral and must be reproducible across environments.
- Give users and agents stable, portable error semantics regardless of database or driver
- Enable policy engines, budgets, and CI to act on errors by code rather than by string matching
- Preserve useful vendor context without leaking sensitive details per ADR 024
- Make error handling testable via fixtures and golden results
- DB vendor errors: SQLSTATE for Postgres, error numbers for MySQL, SQLite result codes, etc
- Driver/transport: connection refused, TLS handshake failures, timeouts, cancellation
- Adapter/runtime: pool exhaustion, transaction misuse, capability negotiation failures
- Compile profile: unsupported feature under active capabilities, lowering constraints
- Lane/build: malformed Plan, missing annotations, invalid params detected before compile
- A fully-populated
RuntimeErrorper ADR 027:code,message,severity,retryable,planId,sqlFingerprint,coreHash,profileHash,details
- Capture the raw failure plus context:
{ source, vendor, plan, sql, params, adapterMeta, timings } - Classify with ordered matchers:
db → driver → adapter → compileProfile → lane - Map to a canonical code and populate envelope fields
- Scrub sensitive artifacts per ADR 024 and attach vendor-safe details
- Emit to hooks and sinks with stable structure
- First matching classifier wins
- If no classifier matches, use
E.RUNTIME.UNKNOWN - Attach
details.vendorwith sanitized vendor fields - Provide
suggestedActionwhen possible
severityandretryableare derived from mapping tables with defaults- Examples:
- Unique violation →
severity: error,retryable: false - Connection reset →
severity: error,retryable: true - Statement timeout →
severity: error,retryable: maybe(policy-controlled)
- Unique violation →
Use codes from ADR 027, examples included here for clarity:
E.RUNTIME.CONNECTIONE.RUNTIME.TIMEOUTE.RUNTIME.CANCELLEDE.RUNTIME.PERMISSIONE.RUNTIME.CONSTRAINT_UNIQUEE.RUNTIME.CONSTRAINT_FKE.RUNTIME.CONSTRAINT_CHECKE.RUNTIME.SYNTAXE.RUNTIME.RESOURCE_EXHAUSTEDE.PLAN.UNSUPPORTED_FEATUREE.PLAN.VALIDATIONE.MIGRATION.CONFLICTE.RUNTIME.UNKNOWN
| SQLSTATE | Meaning | Code | Retryable | Notes |
|---|---|---|---|---|
| 23505 | unique_violation | E.RUNTIME.CONSTRAINT_UNIQUE |
false | include constraint name if available |
| 23503 | foreign_key_violation | E.RUNTIME.CONSTRAINT_FK |
false | include constraint, table |
| 23514 | check_violation | E.RUNTIME.CONSTRAINT_CHECK |
false | include constraint |
| 22P02 | invalid_text_representation | E.RUNTIME.VALIDATION |
false | bad cast/codec mismatch |
| 42601 | syntax_error | E.RUNTIME.SYNTAX |
false | usually from Raw SQL lane |
| 42501 | insufficient_privilege | E.RUNTIME.PERMISSION |
false | adapter should include role |
| 57014 | query_canceled | E.RUNTIME.CANCELLED |
maybe | user cancel vs statement timeout |
| 57000 | operator_intervention | E.RUNTIME.CANCELLED |
maybe | admin kill |
| 53300 | too_many_connections | E.RUNTIME.RESOURCE_EXHAUSTED |
true | pool/backoff hints |
| 55P03 | lock_not_available | E.RUNTIME.RESOURCE_EXHAUSTED |
true | include lock info when safe |
| 40001 | serialization_failure | E.RUNTIME.RETRY |
true | app may retry transaction |
| Errno | Meaning | Code | Retryable |
|---|---|---|---|
| 1062 | ER_DUP_ENTRY | E.RUNTIME.CONSTRAINT_UNIQUE |
false |
| 1216 | ER_NO_REFERENCED_ROW | E.RUNTIME.CONSTRAINT_FK |
false |
| 1217 | ER_ROW_IS_REFERENCED | E.RUNTIME.CONSTRAINT_FK |
false |
| 1142 | ER_TABLEACCESS_DENIED | E.RUNTIME.PERMISSION |
false |
| 2006 | CR_SERVER_GONE_ERROR | E.RUNTIME.CONNECTION |
true |
| 2013 | CR_SERVER_LOST | E.RUNTIME.CONNECTION |
true |
| 1205 | ER_LOCK_WAIT_TIMEOUT | E.RUNTIME.TIMEOUT |
maybe |
ECONNREFUSED,ETIMEDOUT→E.RUNTIME.CONNECTION,retryable: true- TLS failure →
E.RUNTIME.CONNECTION,retryable: false,details.vendor.tlsReason
- Lowering requires
sql.jsonAggbut capability absent →E.PLAN.UNSUPPORTED_FEATURE,suggestedAction: enable jsonAgg or rewrite projection - Placeholder style mismatch detected at compile time →
E.PLAN.VALIDATION
- Pool exhausted beyond budget →
E.RUNTIME.RESOURCE_EXHAUSTED,retryable: true,details.policy - Cross-tenant contract pin violation →
E.PLAN.VALIDATION,severity: error
message: human-friendly summary without vendor internals or PIIcode: from mapping tableseverity: error unless explicitly downgraded by policyretryable: boolean or "maybe" when policy can swayplanId,sqlFingerprint: include when availablecoreHash,profileHash: include if the failure occurred after verificationdetails:vendor: sanitized fields such as sqlstate, errno, constraint, tablecapabilities: only if relevant to the failurepolicy: lint or budget that triggeredorigin:db | driver | adapter | compileProfile | lane
Compile profiles and adapters expose registries:
interface ErrorMapper {
matches(input: UnknownError): boolean
map(input: UnknownError, ctx: ErrorContext): RuntimeError
priority: number // lower first
}
registerDbMapper('pg', pgSqlStateMapper)
registerDriverMapper('pg', pgDriverMapper)
registerProfileMapper('sql/pg', pgLoweringMapper)- Mappers must be pure and deterministic
- Priority ensures DB vendor classification happens before driver fallbacks
- Mappers must never access raw params content beyond redaction rules
- Fixture-driven tests for each adapter/profile with vendor-native error samples and expected
RuntimeError - Golden JSON for envelope stability across upgrades
- Diff tests to ensure message text changes do not affect code, retryable, or structured details
- Do not include full SQL or parameter values by default
- Include
sqlFingerprintand structured hints only - Redact identifiers in message unless they are public DDL names and policy allows
- Store vendor raw messages only in debug logs gated behind local dev flags
- New mappings may be added without breaking changes
- Remapping an existing vendor code to a different canonical code is a breaking change and must be documented in release notes
- Envelope fields are governed by ADR 027 upgrade policy
- Do we want a policy overlay to force
retryable: falsefor certain orgs even when vendor semantics suggest retry? - Should we standardize a machine-consumable
suggestedActioncatalog for agents?
- Stable, analyzable errors across targets
- Cleaner CI with consistent outcomes
- Safer telemetry with structured, redacted details
- Ongoing maintenance of mapping tables per target and version
- Some vendor nuance is flattened into common codes
- Keep vendor specifics available under
details.vendorin a controlled way - Document any lossy mappings and offer
suggestedActionwith target-aware tips
- Start with Postgres and MySQL coverage for the most common SQLSTATE/errno families
- Ship mapping tables with compile profiles and adapters, not in the lane
- Provide a lightweight utility
normalizeError(e, ctx)for use in runtime and hooks - Ensure hooks get the normalized error, never raw vendor exceptions