Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2. Selecting

In this chapter we’ll go from a connected Session to typed Rust values: a SELECT, a decoder tuple, and a Vec<B> you can iterate.

Setup

use babar::codec::{bool, int4, nullable, text};
use babar::query::Query;
use babar::{Config, Session};

#[tokio::main(flavor = "current_thread")]
async fn main() -> babar::Result<()> {
    let session: Session = Session::connect(                          // type: Session
        Config::new("localhost", 5432, "postgres", "postgres")
            .password("postgres")
            .application_name("ch02-selecting"),
    )
    .await?;

    // No parameters; one row of three columns.
    let q: Query<(), (i32, String, bool)> = Query::raw(               // type: Query<(), (i32, String, bool)>
        "SELECT 1::int4 AS id, 'alice'::text AS name, true AS active",
        (),
        (int4, text, bool),
    );

    let rows: Vec<(i32, String, bool)> = session.query(&q, ()).await?; // type: Vec<(i32, String, bool)>
    for (id, name, active) in &rows {
        println!("{id}\t{name}\t{active}");
    }

    session.close().await?;
    Ok(())
}

The shape of a query

Every Query<A, B> carries two type parameters:

  • A — the parameter tuple you bind at call time. () if there are no $N placeholders.
  • B — the row tuple you’ll get back, one per row.

The codec tuple at the end of Query::raw decides B. (int4, text, bool) decodes columns into (i32, String, bool). There is no intermediate Row type and no .get::<T, _>() accessor: by the time session.query(...).await? returns, the bytes are already typed values.

Nullable columns

Postgres columns are nullable by default. babar refuses to guess: if the column might be NULL, wrap its codec in nullable(...) and let the row tuple use Option<T>.

#![allow(unused)]
fn main() {
use babar::codec::{int4, nullable, text};

let q: Query<(), (i32, Option<String>)> = Query::raw(
    "SELECT id, note FROM users ORDER BY id",
    (),
    (int4, nullable(text)),
);
}

If you forget the nullable(...) wrapper and Postgres sends a NULL, the codec returns a clear decode error rather than a panic or a silent String::default(). For example, decoding the note column as plain text against a row where note IS NULL:

#![allow(unused)]
fn main() {
use babar::codec::{int4, text};

// Wrong: `text` (not `nullable(text)`) and `String` (not `Option<String>`).
let q: Query<(), (i32, String)> = Query::raw(
    "SELECT id, note FROM users WHERE id = 1",
    (),
    (int4, text),
);

match session.query(&q, ()).await {
    Ok(rows) => println!("{rows:?}"),
    Err(e) => eprintln!("decode failed: {e}"),
}
}

…prints something like:

decode failed: decode error at column 1 ("note"): unexpected NULL for non-nullable codec `text`;
  wrap it in `nullable(text)` and decode into `Option<String>`

The fix is the one-line change shown above: swap text for nullable(text) and String for Option<String> in the row tuple. babar would rather make you spell it out than quietly hand you an empty string.

Multiple rows

session.query(&q, args) always returns Vec<B> — one tuple per row, in server order. For one-row reads it’s perfectly idiomatic to write:

#![allow(unused)]
fn main() {
let row = session.query(&q, (id,)).await?.into_iter().next();
}

…and treat None as “no such row”. For large result sets, prefer streaming — see Chapter 4.

When a row doesn’t fit your tuple

If your decoder asks for (i32, String) but the SQL returns three columns, decoding fails with a clear Error::ColumnAlignment { expected, actual, .. } before any rows are decoded. Make the column list explicit (SELECT id, name FROM ...) so the row shape and the codec tuple stay in lockstep — SELECT * is allowed but a liability for typed code.

Next

Chapter 3: Parameterized commands introduces Command<A>, the sql! macro, and the Encoder<A> / Decoder<A> traits at a user level.