Working with Postgres

Create Tables in a Postgres Database

postgres cat-database

Use the postgres⮳ crate to create tables in a Postgres database.

postgres::Client::connect⮳ helps in connecting to an existing database. The recipe uses a URL string format with Client::connect. It assumes an existing database named library, the username is postgres and the password is postgres.

use postgres::Client;
use postgres::NoTls;

pub fn main() -> anyhow::Result<()> {
    // The connection URL is formatted as
    // postgresql://<user>:<password>@<host>/<db>, for example
    // postgresql://postgres:postgres@localhost/library
    let mut client = Client::connect(
        "postgresql://postgres:mysecretpassword@rust_howto_dev-postgres-1/library",
        NoTls,
    )?;

    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS author (
            id              SERIAL PRIMARY KEY,
            name            VARCHAR NOT NULL,
            country         VARCHAR NOT NULL
            )
    ",
    )?;

    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS book  (
            id              SERIAL PRIMARY KEY,
            title           VARCHAR NOT NULL,
            author_id       INTEGER NOT NULL REFERENCES author
            )
    ",
    )?;
    println!("Tables created!");
    Ok(())
}

Insert and Query Data

postgres cat-database

The recipe inserts data into the author table using postgres::Client::execute⮳ method of postgres::Client⮳. Then, displays the data from the author table using postgres::Client::query⮳ method of postgres::Client⮳.

use std::collections::HashMap;

use postgres::Client;
use postgres::Error;
use postgres::NoTls;

struct Author {
    _id: i32,
    name: String,
    country: String,
}

pub fn main() -> Result<(), Error> {
    // The connection URL is formatted as
    // postgresql://<user>:<password>@<host>/<db>, for example postgresql://
    // postgres:postgres@localhost/library
    let mut client = Client::connect(
        "postgresql://postgres:mysecretpassword@rust_howto_dev-postgres-1/library",
        NoTls,
    )?;

    let mut authors = HashMap::new();
    authors.insert(String::from("Chinua Achebe"), "Nigeria");
    authors.insert(String::from("Rabindranath Tagore"), "India");
    authors.insert(String::from("Anita Nair"), "India");

    for (key, value) in &authors {
        let author = Author {
            _id: 0,
            name: key.to_string(),
            country: value.to_string(),
        };

        client.execute(
            "INSERT INTO author (name, country) VALUES ($1, $2)",
            &[&author.name, &author.country],
        )?;
    }

    for row in client.query("SELECT id, name, country FROM author", &[])? {
        let author = Author {
            _id: row.get(0),
            name: row.get(1),
            country: row.get(2),
        };
        println!("Author {} is from {}", author.name, author.country);
    }

    Ok(())
}

Aggregate Data

postgres cat-database csv-sample

This recipe lists the nationalities of the first 7999 artists in the database of the Museum of Modern Art⮳ in descending order.

use postgres::Client;
use postgres::Error;
use postgres::NoTls;

struct Nation {
    nationality: String,
    count: i64,
}

// https://github.com/MuseumofModernArt/collection/tree/main
pub fn main() -> Result<(), Error> {
    // The connection URL is formatted as
    // postgresql://<user>:<password>@<host>/<db>, for example
    // postgresql://postgres:postgres@127.0.0.1/moma
    let mut client = Client::connect(
        "postgresql://postgres:mysecretpassword@rust_howto_dev-postgres-1/moma",
        NoTls,
    )?;

    for row in client.query(
        "SELECT nationality, COUNT(nationality) AS count
 FROM artists GROUP BY nationality ORDER BY count DESC",
        &[],
    )? {
        let (nationality, count): (Option<String>, Option<i64>) =
            (row.get(0), row.get(1));

        if nationality.is_some() && count.is_some() {
            let nation = Nation {
                nationality: nationality.unwrap(),
                count: count.unwrap(),
            };
            println!("{} {}", nation.nationality, nation.count);
        }
    }

    Ok(())
}

Connect to and Query Postgres Asynchronously with tokio-postgres

tokio-postgres tokio-postgres-crates.io tokio-postgres-github tokio-postgres-lib.rs

tokio-postgres provides an asynchronous PostgreSQL client. It is built on top of the tokio runtime and thus supports non-blocking interactions with PostgreSQL databases. This crate offers connection pooling, prepared statements, transactions, and support for various PostgreSQL data types. It performs better than SQLx.


// In this example, we
// - connect to a PostgreSQL database using `tokio_postgres::connect`,
// - spawn the connection on a separate task using `tokio::spawn`,
// - create a table if it does not already exist,
// - insert a new row into the table,
// - query the table for rows and print the results,
// - update a row in the table,
// - delete a row from the table,

#[tokio::main]
async fn main() -> Result<(), tokio_postgres::Error> {
    // Connect to the database.
    //  The libpq-style connection strings consist of space-separated
    // key-value pairs: https://docs.rs/tokio-postgres/latest/tokio_postgres/config/struct.Config.html
    let (client, connection) = tokio_postgres::connect(
        "host=rust_howto_dev-postgres-1 user=postgres password=mysecretpassword dbname=library",
        tokio_postgres::NoTls,
    )
    .await?;

    // Spawn the connection on a separate task.
    // The connection object performs the actual communication with the
    // database.
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("Connection error: {}", e);
        }
    });

    // Create a table
    client
        .execute(
            "CREATE TABLE IF NOT EXISTS person (
                id SERIAL PRIMARY KEY,
                name VARCHAR NOT NULL,
                data BYTEA
            )",
            &[],
        )
        .await?;

    // Insert a new row
    client
        .execute(
            "INSERT INTO person (name, data) VALUES ($1, $2)",
            &[&"John Doe", &Some(b"example data".to_vec())],
        )
        .await?;

    // Query the rows
    let mut id: i32 = 0;
    for row in client
        .query("SELECT id, name, data FROM person", &[])
        .await?
    {
        id = row.get(0);
        let name: &str = row.get(1);
        let data: Option<Vec<u8>> = row.get(2);

        println!("Found person: {} with id: {}", name, id);
        if let Some(data) = data {
            println!("Data: {:?}", String::from_utf8(data));
        }
    }
    // Note: calling a method like Client::query on its own does nothing.
    // The associated request is not sent to the database until the future
    // returned by the method is first polled (awaited). Requests are
    // executed in the order that they are first polled, not in the order that
    // their futures are created.

    // Update a row
    client
        .execute(
            "UPDATE person SET name = $1 WHERE id = $2",
            &[&"Jane Doe", &id],
        )
        .await?;

    // Delete a row
    client
        .execute("DELETE FROM person WHERE id = $1", &[&id])
        .await?;

    Ok(())
}

cornucopia for Postgres

cornucopia-website cornucopia cornucopia-crates.io cornucopia-github cornucopia-lib.rs cat-database

cornucopia generates type-checked Rust from your PostgreSQL: cornucopia-rs⮳.

cornucopia is a tool powered by rust-postgres designed to generate type-checked Rust interfaces from your PostgreSQL queries. It works by preparing your queries against an actual database and then running an extensive validation suite on them. Once the queries are prepared and validated, Rust code is generated into a module, which can be imported and used in your project.

The basic premise is thus to:

  • Write PostgreSQL queries.
  • Use Cornucopia to generate Rust code.
  • Use the generated code in your project.
// // COMING SOON