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()— oneSqlTypeTagper?marker, in 1-based order. Returns an empty array for statements with no parameters. Backed bySQLDescribeParam.column_types()— oneColumnMetaper result column. Returns an empty array for non-result statements (INSERT, UPDATE, DELETE, DDL). Backed bySQLDescribeCol.
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 not — parameter_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.