Talking to Postgres With Just std
The Postgres wire protocol has a reputation. It's allegedly complicated. In practice, the subset you need for SELECT, INSERT, UPDATE, DELETE with bind parameters is small enough to fit in one file.
See examples/data/postgres_driver.ltl. The whole thing depends only on std::net::tcp, std::bin, and std::collections.
◉ The API
let mut conn = postgres::connect("db.internal", 5432, "app", "prod", pw)?
conn
|> postgres::query(
"SELECT id, name FROM users WHERE active = $1 AND created > $2",
[Param::Bool(true), Param::Int(since_ts)])?
|> iter()
|> map(|row| row["name"] as String)
|> collect()
Parameters bind by position, never by string interpolation. The driver speaks the Extended Query protocol — Parse, Bind, Execute, Sync — so your query plan is cached server-side even on the first run.
◉ Why is this short?
Three reasons:
Pattern matching kills the boilerplate
Reading message types from the wire is a match on a tag byte. No state machine, no parser combinators:
match read_message(stream)? {
Msg::DataRow(fields) => ...,
Msg::CommandComplete(_) => ...,
Msg::ReadyForQuery(_) => break,
Msg::ErrorResponse(info) => return Err(info.format()),
other => continue,
}
Parameters are a sum type, not a trait
Param is a closed enum: Null | Bool | Int | Float | Text | Bytea. The OID table is a 6-line match. You don't need generics or trait objects.
Streaming is just an iterator
Rows implements Iterator<Row>. next() reads one message at a time. No buffering an entire result set in memory. No async adapter layer — if you want concurrency, spawn the call.
◉ What's not in the 200 lines
A production driver would add:
- TLS (via std::tls::wrap_stream) - SCRAM-SHA-256 auth (~80 more lines) - Connection pooling (trivial — std::sync::Pool<Conn>) - COPY protocol for bulk loads - Prepared-statement caching
None of those change the shape of the core. They layer on top.
◉ Why write it instead of binding libpq?
Because the FFI boundary is where pipeline-native languages lose their identity. Every result goes through C structs, every parameter through char *, every async op through a callback. You end up writing imperative glue inside a functional language.
A pure-Lateralus driver stays a pipeline all the way down. connect returns Result. query returns Result<Rows>. Rows is an iterator. You |> through it the way you |> through everything else.
◉ Try it
Clone lateralus-examples, spin up a local Postgres, point the example at it, run lateralus run data/postgres_driver.ltl. 200 lines, zero deps, first query runs.
Try it in the playground
Every snippet on this page runs in your browser. No install, no signup.
▶ Open Playground Star on GitHub