Skip to content

Prepare-Time Metadata

prepare() returns a Statement before any parameters are bound and before anything is executed. At that point the driver already knows the shape of the statement: how many parameters it takes, which types they are, which columns the result set will have, and whether those columns are nullable. Two methods on Statement expose that shape.

Useful for build-time tools that validate SQL against a live database, for generating bindings, or for logging a schema snapshot next to a migration. No execution, no binding — just prepare and describe.

Two describe methods

fun ref parameter_types(): (Array[SqlTypeTag] val | MetadataError)
fun ref column_types():    (Array[ColumnMeta]  val | MetadataError)
  • parameter_types() — one SqlTypeTag per ? marker, in 1-based order. Returns an empty array for statements with no parameters. Backed by SQLDescribeParam.
  • column_types() — one ColumnMeta per result column. Returns an empty array for non-result statements (INSERT, UPDATE, DELETE, DDL). Backed by SQLDescribeCol.

Both are available the moment prepare() succeeds; neither requires any parameter to be bound.

SqlTypeTag is parallel to SqlValue

Where SqlValue carries a value with its storage, SqlTypeTag names the type without a value:

type SqlTypeTag is
  ( SqlTagBool
  | SqlTagTinyInt | SqlTagSmallInt | SqlTagInteger | SqlTagBigInt
  | SqlTagFloat
  | SqlTagText
  | SqlTagDate | SqlTagTime | SqlTagTimestamp | SqlTagDecimal
  | SqlTagUnknown )

Eleven of the variants are primitives — one per SqlValue family. SqlTagUnknown is a class carrying the raw ODBC type code (raw_type: I16) for any SQL type this library does not map to a SqlValue. Every variant has a .string() that renders short human-readable names: "Integer", "Text", "Timestamp", "Unknown(-7)", and so on.

ColumnMeta bundles three facts

class val ColumnMeta
  let name:     String val
  let type_tag: SqlTypeTag
  let nullable: Nullability

Nullability is a tri-state, not a boolean:

type Nullability is (NoNulls | Nullable | NullableUnknown)

NullableUnknown is distinct from Nullable on purpose — the driver didn’t answer the question. Some drivers genuinely don’t know, particularly for computed or joined columns. Treating “unknown” as “nullable” is a reasonable default, but the library surfaces the distinction so you don’t have to guess what the driver meant.

ColumnMeta.string() renders all three as "name: Type (NOT NULL)" or similar.

A describe round-trip

The sample prepares an INSERT and a SELECT, then prints the parameter and column metadata for each:

use "lib:odbc"
use "odbc"

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

    match Odbc.connect(Dsn("DSN=" + dsn_name))
    | let conn: Connection =>
      conn.exec("DROP TABLE IF EXISTS tut_meta")
      match \exhaustive\ conn.exec(
        "CREATE TABLE tut_meta"
          + " (id INTEGER NOT NULL,"
          + " name VARCHAR(32),"
          + " created TIMESTAMP)")
      | let _: (USize | NoRowCount) => None
      | let e: ExecError =>
        env.err.print("create: " + e.string()); conn.close(); return
      end

      // Parameter metadata on an INSERT. No bind, no execute — just describe.
      match \exhaustive\ conn.prepare(
        "INSERT INTO tut_meta VALUES (?, ?, ?)")
      | let stmt: Statement =>
        env.out.print("parameter_types() on INSERT:")
        match \exhaustive\ stmt.parameter_types()
        | let tags: Array[SqlTypeTag] val =>
          var i: USize = 1
          for t in tags.values() do
            env.out.print("  $" + i.string() + ": " + t.string())
            i = i + 1
          end
        | let e: MetadataError => env.err.print("  " + e.string())
        end
        stmt.close()
      | let e: PrepareError =>
        env.err.print("prepare insert: " + e.string())
      end

      // Parameter and column metadata on a SELECT.
      match \exhaustive\ conn.prepare(
        "SELECT id, name, created FROM tut_meta WHERE id > ?")
      | let stmt: Statement =>
        env.out.print("\nparameter_types() on SELECT:")
        match \exhaustive\ stmt.parameter_types()
        | let tags: Array[SqlTypeTag] val =>
          var i: USize = 1
          for t in tags.values() do
            env.out.print("  $" + i.string() + ": " + t.string())
            i = i + 1
          end
        | let e: MetadataError => env.err.print("  " + e.string())
        end

        env.out.print("\ncolumn_types() on SELECT:")
        match \exhaustive\ stmt.column_types()
        | let cols: Array[ColumnMeta] val =>
          for col in cols.values() do
            env.out.print("  " + col.string())
          end
        | let e: MetadataError => env.err.print("  " + e.string())
        end

        stmt.close()
      | let e: PrepareError =>
        env.err.print("prepare select: " + e.string())
      end

      conn.exec("DROP TABLE IF EXISTS tut_meta")
      conn.close()

    | let e: ConnectError =>
      env.err.print("connect: " + e.string())
    end
./build/13-metadata
parameter_types() on INSERT:
  $1: Integer
  $2: Text
  $3: Timestamp

parameter_types() on SELECT:
  $1: Integer

column_types() on SELECT:
  id: Integer (NOT NULL)
  name: Text (NULLABLE)
  created: Timestamp (NULLABLE)

The two parameter_types() calls hit the same underlying ODBC call (SQLDescribeParam); the column_types() call on the SELECT hits SQLDescribeCol once per result column.

Partial variants

As elsewhere, _p variants raise on error:

fun ref parameter_types_p(): Array[SqlTypeTag] val ?
fun ref column_types_p():    Array[ColumnMeta]  val ?

Convenient for tooling where any failure aborts the pass.

MetadataError kinds

Kind Meaning
MetadataStatementClosed Statement has been close()d
MetadataConnectionClosed Owning connection has been close()d
DriverDoesNotSupportDescribeParam Driver does not implement SQLDescribeParam (SQLSTATE IM001 or HYC00)
DriverMetadataError Any other driver-reported metadata failure

Same shape as the other error classes: e.kind() for the primitive, e.string() for a redacted one-liner, e.unsafe_diag() for the raw DiagChain.

!!! note “Driver support for SQLDescribeParam” Not all drivers implement SQLDescribeParam. psqlODBC does. SQLite’s ODBC driver does notparameter_types() against it returns MetadataError(DriverDoesNotSupportDescribeParam), classified from SQLSTATE IM001 / HYC00. column_types() uses SQLDescribeCol, which is far more universally supported.

What’s next

That closes the Prepared Statements section. Transactions is next.