Skip to content

Reading Diagnostics

A DiagChain is an array of DiagRecord values — one per entry in ODBC’s diagnostic-record stack. Drivers often return several records per failure; the first is usually the primary error, the rest are context.

type DiagChain is Array[DiagRecord] val

class val DiagRecord
  let sqlstate:    String val  // "42601", "23505", "08001", ...
  let native_code: I32          // driver-specific error code
  fun message():   String val   // human-readable text
  fun string():    String iso^  // "[SQLSTATE] message"
  • sqlstate — five-character SQLSTATE. The library’s kind classifier uses the first two characters.
  • native_code — driver-specific integer. Postgres returns its internal code; MariaDB returns a MySQL error number. Useful when ODBC’s standard collapses errors the driver distinguishes.
  • message() — the driver’s message. May contain credentials or query text. Audit accordingly.

A demonstration

The sample forces two errors (bad DSN, SQL syntax error), then peeks at both:

use "lib:odbc"
use "odbc"

actor Main
  new create(env: Env) =>
    let dsn_name =
      try env.args(1)?
      else "psqlred"
      end

    // A bad DSN: the redacted .string() tells us the kind + SQLSTATE
    // but no driver-supplied message (which could contain credentials).
    match Odbc.connect(Dsn("DSN=definitely_not_a_real_dsn"))
    | let conn: Connection => conn.close()
    | let e: ConnectError =>
      env.out.print("redacted: " + e.string())
      env.out.print("  kind:   " + e.kind().string())
      // unsafe_diag() gives the raw driver-supplied chain. In real code
      // route this to a developer log, not a user-facing surface.
      let diag = e.unsafe_diag()
      env.out.print("  diag records: " + diag.size().string())
      try
        let rec = diag(0)?
        env.out.print(
          "  first: [" + rec.sqlstate + "] " + rec.message())
      end
    end

    // Connect for real, then trigger a syntax error to show ExecError.
    match Odbc.connect(Dsn("DSN=" + dsn_name))
    | let conn: Connection =>
      match \exhaustive\ conn.exec("SELCT 1")
      | let _: (USize | NoRowCount) => None
      | let e: ExecError =>
        env.out.print("\nredacted: " + e.string())
        env.out.print("  kind:   " + e.kind().string())
        match e.unsafe_sql()
        | let sql: String val =>
          env.out.print("  sql:    " + sql)
        | None => None
        end
        let diag = e.unsafe_diag()
        try
          let rec = diag(0)?
          env.out.print(
            "  driver: [" + rec.sqlstate + "] " + rec.message())
        end
      end

      // A DROP IF EXISTS on some drivers yields SQL_SUCCESS_WITH_INFO.
      // last_warnings() surfaces that chain.
      conn.exec("DROP TABLE IF EXISTS tut_never_existed")
      match conn.last_warnings()
      | let w: Warnings =>
        env.out.print("\nwarnings present: " + w.string())
      | None =>
        env.out.print("\nno warnings")
      end

      conn.close()
    | let e: ConnectError =>
      env.err.print("connect: " + e.string())
    end
./build/09-errors
redacted: ConnectError: driver connect failed [IM002]
  kind:   driver connect failed
  diag records: 1
  first: [IM002] [unixODBC][Driver Manager]Data source name not found and no default driver specified

redacted: ExecError: syntax error [42601]
  kind:   syntax error
  sql:    SELCT 1
  driver: [42601] ERROR: syntax error at or near "SELCT";
Error while executing the query

warnings present: Warnings: 1 diagnostic record(s)

SQLSTATE classes the library classifies

Every ExecError gets a kind from the SQLSTATE’s first two characters:

Prefix Library kind Meaning
08 ConnectionLost Connection exception
23 ConstraintViolation Integrity constraint
42 SyntaxError Syntax error or access rule violation
anything else QueryError Generic driver-reported failure

To dispatch on another class (40 for transaction rollback, 57014 for query canceled), walk the chain:

match \exhaustive\ conn.exec(sql)
| let _: (USize | NoRowCount) => // ok
| let e: ExecError =>
  let diag = e.unsafe_diag()
  try
    let rec = diag(0)?
    if rec.sqlstate == "57014" then
      env.err.print("canceled")
    elseif rec.sqlstate.compare_sub("40", 2) is Equal then
      env.err.print("transient; retry")
    else
      env.err.print("other: " + e.string())
    end
  end
end

Size caps

Two safety limits on the reader:

  • Messages capped at 4096 bytes; longer ones get a ...[truncated] suffix.
  • Chains capped at 16 records; overflow is replaced with a synthetic record (SQLSTATE 00000, “diagnostic chain truncated”).

Defence-in-depth against malicious or misbehaving drivers. You won’t hit these in normal use.

What’s next

Warnings covers diagnostics on successful operations — SQL_SUCCESS_WITH_INFO.