minco-sqlx-postgres 1.4.0

Bounded SQLx PostgreSQL pools, migrations, and safe seed execution for Minco
Documentation
use minco_db::{SEED_SET_MANIFEST, SeedClass, SeedEnvironment, build_seed_plan, load_seed_catalog};
use minco_sqlx_postgres::{PostgresPoolConfig, apply_seed_plan, connect, verify_seed_plan};
use std::{fs, path::PathBuf};
use tempfile::TempDir;
use uuid::Uuid;

#[tokio::test]
async fn required_transaction_seed_plan_is_idempotent_and_verifiable() {
    let Ok(url) = std::env::var("MINCO_TEST_POSTGRES_URL") else {
        eprintln!("MINCO_TEST_POSTGRES_URL not set; PostgreSQL seed proof skipped");
        return;
    };
    let suffix = Uuid::new_v4().simple().to_string();
    let table = format!("minco_seed_{suffix}");
    let project = TempDir::new().expect("temporary project");
    let root = project.path().join("seeds");
    fs::create_dir(&root).expect("create seed root");
    fs::write(
        root.join("demo.sql"),
        format!(
            concat!(
                "INSERT INTO {} (id, value) VALUES (1, 'demo') ",
                "ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value;\n",
            ),
            table,
        ),
    )
    .expect("write seed SQL");
    fs::write(
        root.join("demo.verify.sql"),
        format!("SELECT EXISTS (SELECT 1 FROM {table} WHERE id = 1 AND value = 'demo');\n"),
    )
    .expect("write seed verification");
    fs::write(
        root.join(SEED_SET_MANIFEST),
        concat!(
            "schema = 1\n",
            "id = \"test-postgres-seeds\"\n",
            "owner = \"application:test\"\n",
            "backend = \"postgres\"\n",
            "\n",
            "[[seed]]\n",
            "id = \"test-postgres-demo\"\n",
            "version = 1\n",
            "class = \"demo\"\n",
            "source = \"demo.sql\"\n",
            "verify = \"demo.verify.sql\"\n",
            "depends_on = []\n",
            "environments = [\"local\"]\n",
            "idempotency = \"upsert\"\n",
            "mutable_state = \"owned_rows\"\n",
            "risk = \"replaces_owned_rows\"\n",
            "transaction = \"required\"\n",
            "preservation = \"preserve_unowned_rows\"\n",
        ),
    )
    .expect("write seed manifest");
    let catalog =
        load_seed_catalog(project.path(), &[PathBuf::from("seeds")]).expect("load seed catalog");
    let plan = build_seed_plan(
        &catalog,
        SeedClass::Demo,
        SeedEnvironment::Local,
        Some("test-postgres-seeds"),
    )
    .expect("build seed plan");
    let pool = connect(&PostgresPoolConfig::serverless(url))
        .await
        .expect("connect PostgreSQL");
    sqlx::query(sqlx::AssertSqlSafe(format!(
        "CREATE TABLE {table} (id BIGINT PRIMARY KEY, value TEXT NOT NULL)"
    )))
    .execute(&pool)
    .await
    .expect("create target table");

    apply_seed_plan(&pool, project.path(), &plan)
        .await
        .expect("apply first seed plan");
    apply_seed_plan(&pool, project.path(), &plan)
        .await
        .expect("reapply idempotent seed plan");

    let verification = verify_seed_plan(&pool, project.path(), &plan)
        .await
        .expect("verify seed plan");
    assert!(verification.iter().all(|entry| entry.verified));
    let count =
        sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!("SELECT COUNT(*) FROM {table}")))
            .fetch_one(&pool)
            .await
            .expect("count seeded rows");
    assert_eq!(count, 1);

    sqlx::query(sqlx::AssertSqlSafe(format!("DROP TABLE {table}")))
        .execute(&pool)
        .await
        .expect("clean up target table");
}

#[tokio::test]
async fn seed_verification_is_enforced_as_read_only_by_postgres() {
    let Ok(url) = std::env::var("MINCO_TEST_POSTGRES_URL") else {
        eprintln!("MINCO_TEST_POSTGRES_URL not set; PostgreSQL seed proof skipped");
        return;
    };
    let suffix = Uuid::new_v4().simple().to_string();
    let table = format!("minco_seed_read_only_{suffix}");
    let project = TempDir::new().expect("temporary project");
    let root = project.path().join("seeds");
    fs::create_dir(&root).expect("create seed root");
    fs::write(
        root.join("demo.sql"),
        format!("INSERT INTO {table} (id, value) VALUES (1, 'demo');\n"),
    )
    .expect("write seed SQL");
    fs::write(
        root.join("demo.verify.sql"),
        format!("DELETE FROM {table} WHERE id = 1 RETURNING true;\n"),
    )
    .expect("write mutating verification SQL");
    fs::write(
        root.join(SEED_SET_MANIFEST),
        concat!(
            "schema = 1\n",
            "id = \"test-postgres-read-only-seeds\"\n",
            "owner = \"application:test\"\n",
            "backend = \"postgres\"\n",
            "\n",
            "[[seed]]\n",
            "id = \"test-postgres-read-only-demo\"\n",
            "version = 1\n",
            "class = \"demo\"\n",
            "source = \"demo.sql\"\n",
            "verify = \"demo.verify.sql\"\n",
            "depends_on = []\n",
            "environments = [\"local\"]\n",
            "idempotency = \"insert_once\"\n",
            "mutable_state = \"none\"\n",
            "risk = \"non_destructive\"\n",
            "transaction = \"required\"\n",
            "preservation = \"preserve_all_existing\"\n",
        ),
    )
    .expect("write seed manifest");
    let catalog =
        load_seed_catalog(project.path(), &[PathBuf::from("seeds")]).expect("load seed catalog");
    let plan = build_seed_plan(
        &catalog,
        SeedClass::Demo,
        SeedEnvironment::Local,
        Some("test-postgres-read-only-seeds"),
    )
    .expect("build seed plan");
    let pool = connect(&PostgresPoolConfig::serverless(url))
        .await
        .expect("connect PostgreSQL");
    sqlx::query(sqlx::AssertSqlSafe(format!(
        "CREATE TABLE {table} (id BIGINT PRIMARY KEY, value TEXT NOT NULL)"
    )))
    .execute(&pool)
    .await
    .expect("create target table");
    apply_seed_plan(&pool, project.path(), &plan)
        .await
        .expect("apply seed plan");

    let error = verify_seed_plan(&pool, project.path(), &plan)
        .await
        .expect_err("mutating verification must be rejected");
    assert!(error.to_string().contains("read-only transaction"));
    let count =
        sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!("SELECT COUNT(*) FROM {table}")))
            .fetch_one(&pool)
            .await
            .expect("count seeded rows");
    assert_eq!(count, 1);

    sqlx::query(sqlx::AssertSqlSafe(format!("DROP TABLE {table}")))
        .execute(&pool)
        .await
        .expect("clean up target table");
}