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 ParamIndex — e.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.