Reusing Statements¶
The point of preparing is to reuse. A single Statement can execute many times: rebind, run, repeat. For a batch insert of a thousand rows, that’s one prepare and a thousand executes instead of a thousand parse-and-plans.
The reuse pattern¶
Two operations drive the lifecycle:
- Rebind and re-execute — after
execute_update(), the statement is back in “ready to bind”. Bind new values and execute again. close_cursor()— afterexecute()(SELECT) the statement has an open cursor.close_cursor()closes it and returns to “ready to bind”.
A batch of inserts, followed by a SELECT¶
The sample prepares an INSERT, executes it three times, then prepares a SELECT and iterates:
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_reuse")
match \exhaustive\ conn.exec(
"CREATE TABLE tut_reuse (id INTEGER, label VARCHAR(32))")
| let _: (USize | NoRowCount) => None
| let e: ExecError =>
env.err.print("create: " + e.string()); conn.close(); return
end
// Batch insert: prepare once, re-bind each row.
let items: Array[(I32, String val)] val =
[(1, "alpha"); (2, "bravo"); (3, "charlie")]
match \exhaustive\ conn.prepare(
"INSERT INTO tut_reuse (id, label) VALUES (?, ?)")
| let stmt: Statement =>
for (id, label) in items.values() do
try
stmt.bind_p(ParamIndex(1), SqlInteger(id))?
stmt.bind_p(ParamIndex(2), SqlText(label))?
stmt.execute_update_p()?
else
env.err.print("insert " + id.string() + " failed")
end
end
stmt.close()
| let e: PrepareError =>
env.err.print("prepare: " + e.string())
end
// Now prepare a SELECT and fetch rows back using the same pattern:
// execute() opens a cursor, values() iterates, close_cursor() resets.
match \exhaustive\ conn.prepare(
"SELECT id, label FROM tut_reuse ORDER BY id")
| let stmt: Statement =>
match \exhaustive\ stmt.execute()
| Executed =>
for result in stmt.values() do
match \exhaustive\ result
| let row: Row =>
try
let id =
match \exhaustive\ row.int(ColIndex(1))?
| let v: I64 => v.string()
| SqlNull => "NULL"
end
let label =
match \exhaustive\ row.text(ColIndex(2))?
| let v: String val => v
| SqlNull => "NULL"
end
env.out.print(id + " " + label)
end
| let e: FetchError =>
env.err.print("fetch: " + e.string())
end
end
stmt.close_cursor()
| 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_reuse")
conn.close()
| let e: ConnectError =>
env.err.print("connect: " + e.string())
end
./build/06-prepared-reuse
1 alpha
2 bravo
3 charlie
What to notice¶
Binding marks a parameter dirty¶
The library tracks which parameters have been rebound since the last execute and only pushes changed ones to the driver — matters when parameters carry large text or binary. Invisible to callers: just bind what changes and execute.
Partial variants make batches terse¶
for (id, label) in items.values() do
try
stmt.bind_p(ParamIndex(1), SqlInteger(id))?
stmt.bind_p(ParamIndex(2), SqlText(label))?
stmt.execute_update_p()?
else
env.err.print("insert " + id.string() + " failed")
end
end
One try/else per iteration. Any step erroring drops into else; the loop moves on. Wrap the whole for in a single try to abort the batch on first failure.
Trade-off: the else branch doesn’t know which error fired. For identical inserts the item value tells you enough. When you need the kind, use the non-partial bind() / execute_update().
close_cursor() vs close()¶
close_cursor()— closes any open cursor, unbinds columns, leaves the statement ready to bind and re-execute.close()— frees theSQLHSTMT. Idempotent. After this the statement is done.
The sample calls close_cursor() after the SELECT loop, then close() when done.
Prepare once, use many — but only on the same connection¶
Statement is tied to the Connection it was prepared on. Close the connection and the statement is useless. Both are ref (non-sendable), so you can’t send a prepared statement across actors.
For cross-actor reuse: either each actor owns its own DbSession and prepares its own statements, or you funnel all database work through a single actor owning the connection. DbSession shows the second pattern.
What’s next¶
So far every Statement has been used for bind / execute / fetch. Prepare-Time Metadata shows what else a Statement will tell you — parameter types, column types, nullability — before any value is bound.