Skip to content

Binding Parameters

A prepared statement has one or more ? markers, e.g. INSERT INTO t VALUES (?, ?). Binding associates a value with a marker by 1-based index.

The bind method

fun ref bind(i: ParamIndex, v: SqlValue): (Bound | BindError)

ParamIndex wraps U16 — like ColIndex, it keeps 1-based index arithmetic from silently mixing with other numeric values.

The value is any SqlValue variant (see SQL Types):

SqlBool(true)
SqlTinyInt(12)
SqlSmallInt(1000)
SqlInteger(1_000_000)
SqlBigInt(9_000_000_000)
SqlFloat(3.14159)
SqlText("hello")
SqlDate(2026, 4, 16)
SqlTime(9, 30, 0)
SqlTimestamp(2026, 4, 16, 9, 30, 0)
SqlDecimal("1234.5678")
SqlNull

On success, Bound. On failure, a BindError with a kind.

BindError kinds

Kind Meaning
ParamIndexOutOfRange Zero, or greater than the parameter count
ParamTooLarge Value exceeded the maximum bind size (rare)
DriverRejected Driver rejected the bind call
BindStatementClosed Statement has been close()d
BindConnectionClosed Connection has been close()d

Every BindError carries the failing ParamIndexe.param_index() — so you can point at the culprit in a batch.

A complete example

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_prep")
      match \exhaustive\ conn.exec(
        "CREATE TABLE tut_prep (id INTEGER, label VARCHAR(32))")
      | let _: (USize | NoRowCount) => None
      | let e: ExecError =>
        env.err.print("create: " + e.string()); conn.close(); return
      end

      // Prepare an INSERT with two parameter markers.
      match \exhaustive\ conn.prepare(
        "INSERT INTO tut_prep (id, label) VALUES (?, ?)")
      | let stmt: Statement =>
        // Bind each parameter by 1-based index.
        match \exhaustive\ stmt.bind(ParamIndex(1), SqlInteger(42))
        | Bound => None
        | let e: BindError =>
          env.err.print("bind id: " + e.string())
          stmt.close(); conn.close(); return
        end
        match \exhaustive\ stmt.bind(ParamIndex(2), SqlText("hello"))
        | Bound => None
        | let e: BindError =>
          env.err.print("bind label: " + e.string())
          stmt.close(); conn.close(); return
        end

        // execute_update for DML: returns affected row count.
        match \exhaustive\ stmt.execute_update()
        | let n: USize => env.out.print("inserted " + n.string() + " row")
        | NoRowCount => env.out.print("inserted (no row count)")
        | let e: ExecError => env.err.print("execute: " + e.string())
        end

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

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

    | let e: ConnectError =>
      env.err.print("connect: " + e.string())
    end
./build/05-prepared-bind
inserted 1 row

bind_null as a convenience

stmt.bind(ParamIndex(3), SqlNull) works. If you do that a lot:

stmt.bind_null(ParamIndex(3))

Same effect, slightly clearer intent.

Partial variants

The _p variants raise instead of returning a union:

try
  stmt.bind_p(ParamIndex(1), SqlInteger(42))?
  stmt.bind_p(ParamIndex(2), SqlText("hello"))?
  stmt.execute_update_p()?
else
  env.err.print("insert failed")
end

Great for loops where any failure should abort the iteration — see Reusing Statements.

What’s next

Now the two executes — execute() for SELECTs and execute_update() for DML.