mockgres 0.0.29

An in-memory database that replicates a reasonable subset of Postgres functionality to make unit tests that rely on a database to run.
Documentation
mod common;

use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};

use geo_types::Point;
use tokio_postgres::error::SqlState;
use tokio_postgres::types::Type;

static NEXT_FILE_ID: AtomicU64 = AtomicU64::new(1);

struct CopyFile(PathBuf);

impl CopyFile {
    fn new(contents: &[u8]) -> Self {
        let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "mockgres-inheritance-{}-{id}.data",
            std::process::id()
        ));
        std::fs::write(&path, contents).expect("write inheritance COPY fixture");
        Self(path)
    }

    fn sql_path(&self) -> String {
        self.0
            .to_str()
            .expect("temporary path is UTF-8")
            .replace('\'', "''")
    }
}

impl Drop for CopyFile {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.0);
    }
}

fn assert_sqlstate(error: &tokio_postgres::Error, expected: &SqlState) {
    let db_error = error.as_db_error().expect("expected database error");
    assert_eq!(db_error.code(), expected, "unexpected error: {db_error:?}");
}

#[tokio::test(flavor = "multi_thread")]
async fn inherited_columns_precede_local_columns_and_accept_copy_rows() {
    let ctx = common::start().await;
    ctx.client
        .execute(
            "create table person (\
                name text not null, \
                age int4 default 40, \
                location point\
            )",
            &[],
        )
        .await
        .expect("create inheritance parent");
    ctx.client
        .execute(
            "create table employee (salary int4, manager name) inherits (person)",
            &[],
        )
        .await
        .expect("create inheritance child");

    let file = CopyFile::new(b"alice\t30\t(1.5,-2)\t1000\tboss\ncarol\t25\t(3,4)\t900\t\\N\n");
    let copied = ctx
        .client
        .execute(&format!("copy employee from '{}'", file.sql_path()), &[])
        .await
        .expect("COPY inherited row layout");
    assert_eq!(copied, 2);

    ctx.client
        .execute(
            "insert into employee (name, location, salary, manager) \
             values ('bob', '(0,0)', 800, 'alice')",
            &[],
        )
        .await
        .expect("insert child row with inherited default");
    let rows = ctx
        .client
        .query("select * from employee order by name", &[])
        .await
        .expect("select inheritance child");
    let columns: Vec<_> = rows[0]
        .columns()
        .iter()
        .map(|column| (column.name(), column.type_().clone()))
        .collect();
    assert_eq!(
        columns,
        [
            ("name", Type::TEXT),
            ("age", Type::INT4),
            ("location", Type::POINT),
            ("salary", Type::INT4),
            ("manager", Type::NAME),
        ]
    );
    assert_eq!(rows[0].get::<_, String>(0), "alice");
    assert_eq!(rows[0].get::<_, i32>(1), 30);
    assert_eq!(rows[0].get::<_, Point<f64>>(2), Point::new(1.5, -2.0));
    assert_eq!(rows[1].get::<_, String>(0), "bob");
    assert_eq!(rows[1].get::<_, i32>(1), 40);

    let error = ctx
        .client
        .execute(
            "insert into employee (name, salary) values (null, 100)",
            &[],
        )
        .await
        .expect_err("inherited NOT NULL should be enforced");
    assert_sqlstate(&error, &SqlState::NOT_NULL_VIOLATION);

    let _ = ctx.shutdown.send(());
}

#[tokio::test(flavor = "multi_thread")]
async fn multiple_inheritance_merges_columns_defaults_and_reports_conflicts() {
    let ctx = common::start().await;
    ctx.client
        .execute(
            "create table root_parent (common int4 default 7, required text not null)",
            &[],
        )
        .await
        .expect("create root parent");
    ctx.client
        .execute(
            "create table left_parent (left_value text) inherits (root_parent)",
            &[],
        )
        .await
        .expect("create left parent");
    ctx.client
        .execute(
            "create table right_parent (right_value point) inherits (root_parent)",
            &[],
        )
        .await
        .expect("create right parent");
    ctx.client
        .execute(
            "create table leaf (local_value name) inherits (left_parent, right_parent)",
            &[],
        )
        .await
        .expect("create multiply inherited child");
    ctx.client
        .execute(
            "insert into leaf (required, left_value, right_value, local_value) \
             values ('yes', 'left', '(8,9)', 'local')",
            &[],
        )
        .await
        .expect("insert multiply inherited row");

    let row = ctx
        .client
        .query_one(
            "select common, required, left_value, right_value, local_value from leaf",
            &[],
        )
        .await
        .expect("select multiply inherited row");
    assert_eq!(row.get::<_, i32>(0), 7);
    assert_eq!(row.get::<_, String>(1), "yes");
    assert_eq!(row.get::<_, String>(2), "left");
    assert_eq!(row.get::<_, Point<f64>>(3), Point::new(8.0, 9.0));
    assert_eq!(row.get::<_, String>(4), "local");

    ctx.client
        .execute(
            "create table conflicting_default (common int4 default 8)",
            &[],
        )
        .await
        .expect("create conflicting default parent");
    let error = ctx
        .client
        .execute(
            "create table bad_default () \
             inherits (root_parent, conflicting_default)",
            &[],
        )
        .await
        .expect_err("conflicting inherited defaults should fail");
    assert_sqlstate(&error, &SqlState::INVALID_COLUMN_DEFINITION);

    ctx.client
        .execute(
            "create table resolved_default (common int4 default 9) \
             inherits (root_parent, conflicting_default)",
            &[],
        )
        .await
        .expect("local default should resolve inherited conflict");

    ctx.client
        .execute("create table text_parent (common text)", &[])
        .await
        .expect("create conflicting type parent");
    let error = ctx
        .client
        .execute(
            "create table bad_type () inherits (root_parent, text_parent)",
            &[],
        )
        .await
        .expect_err("conflicting inherited types should fail");
    assert_sqlstate(&error, &SqlState::DATATYPE_MISMATCH);

    let error = ctx
        .client
        .execute(
            "create table duplicate_parent () inherits (root_parent, root_parent)",
            &[],
        )
        .await
        .expect_err("duplicate direct parent should fail");
    assert_sqlstate(&error, &SqlState::DUPLICATE_OBJECT);

    let _ = ctx.shutdown.send(());
}

#[tokio::test(flavor = "multi_thread")]
async fn inherited_identity_columns_are_regular_not_null_columns() {
    let ctx = common::start().await;
    ctx.client
        .execute(
            "create table identity_parent (id int4 generated always as identity)",
            &[],
        )
        .await
        .expect("create identity parent");
    ctx.client
        .execute(
            "create table identity_child () inherits (identity_parent)",
            &[],
        )
        .await
        .expect("create identity child");
    ctx.client
        .execute("insert into identity_child values (42)", &[])
        .await
        .expect("identity property should not be inherited");
    let id: i32 = ctx
        .client
        .query_one("select id from identity_child", &[])
        .await
        .expect("select inherited identity column")
        .get(0);
    assert_eq!(id, 42);

    let error = ctx
        .client
        .execute("insert into identity_child values (null)", &[])
        .await
        .expect_err("identity NOT NULL should be inherited");
    assert_sqlstate(&error, &SqlState::NOT_NULL_VIOLATION);

    let _ = ctx.shutdown.send(());
}