pgdumpx 0.2.0

Read-only, bounded inspection, extraction, and row scanning for PostgreSQL custom-format dumps
Documentation
use pgdumpx::{Archive, FieldRef, OwnedField};
use std::{
    cell::Cell,
    io::{self, Cursor, Read, Seek, SeekFrom},
    path::PathBuf,
};

#[test]
fn official_none_and_gzip_rows_are_invariant_under_one_byte_source_reads() {
    let expected_ids = [
        b"1".as_slice(),
        b"2".as_slice(),
        b"3".as_slice(),
        b"4".as_slice(),
        b"5".as_slice(),
        b"6".as_slice(),
        b"7".as_slice(),
    ];

    for fixture_name in ["pg18-none-copy-basic.dump", "pg18-gzip-copy-basic.dump"] {
        let reader = OneByteReader::new(fixture(fixture_name));
        let mut archive = Archive::open(reader).unwrap();
        let mut rows = archive.table_rows(b"public", b"orders").unwrap();
        let mut actual_ids = Vec::new();

        while let Some(row) = rows.next_row().unwrap() {
            let Some(FieldRef::Bytes(id)) = row.field(0) else {
                panic!("official fixture row must have a non-NULL order_id");
            };
            actual_ids.push(id.to_vec());
        }

        assert_eq!(
            actual_ids,
            expected_ids.map(<[u8]>::to_vec),
            "fixture {fixture_name}"
        );
    }
}

#[test]
fn find_first_evaluates_each_row_once_and_never_after_the_first_match() {
    for fixture_name in ["pg18-none-copy-basic.dump", "pg18-gzip-copy-basic.dump"] {
        let mut archive = Archive::open(Cursor::new(fixture(fixture_name))).unwrap();
        let mut rows = archive.table_rows(b"public", b"orders").unwrap();
        let order_number = rows.column_index(b"order_number").unwrap().unwrap();
        let predicate_calls = Cell::new(0_u64);

        let found = rows
            .find_first(|row| {
                predicate_calls.set(predicate_calls.get() + 1);
                row.field(order_number) == Some(FieldRef::Bytes(b"MIDDLE-400"))
            })
            .unwrap()
            .unwrap();

        assert_eq!(found.field(0), Some(&OwnedField::Bytes(b"4".to_vec())));
        assert_eq!(
            predicate_calls.get(),
            4,
            "predicate must run once for rows 1-4 and never for later rows in {fixture_name}"
        );
    }
}

fn fixture(name: &str) -> Vec<u8> {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/archives")
        .join(name);
    std::fs::read(path).expect("committed official fixture must be readable")
}

#[derive(Debug)]
struct OneByteReader {
    inner: Cursor<Vec<u8>>,
}

impl OneByteReader {
    fn new(bytes: Vec<u8>) -> Self {
        Self {
            inner: Cursor::new(bytes),
        }
    }
}

impl Read for OneByteReader {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        if output.is_empty() {
            return Ok(0);
        }
        self.inner.read(&mut output[..1])
    }
}

impl Seek for OneByteReader {
    fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
        self.inner.seek(position)
    }
}