lenso-postgres-kit 0.1.0

Module-owned PostgreSQL schema lifecycle kit for Lenso applications
Documentation

lenso-postgres-kit

lenso-postgres-kit gives a stateful Lenso Module an explicit lifecycle for its own PostgreSQL schema. It is intentionally not a shared State Module, a generic SQL Capability, or an ORM.

The owning Module still defines:

  • its data model and immutable ordered migrations;
  • its SQL queries and transaction boundaries;
  • its backup, restore, retention, and data-governance policy;
  • the dedicated PostgreSQL role used to enforce schema access in production.

The kit supplies:

  • atomic, advisory-locked schema setup and upgrade;
  • a private migration ledger with checksum drift detection;
  • fail-closed runtime preparation that never migrates automatically;
  • a verified SQLx pool with the Module schema selected as its search_path.

Author one owned schema

Keep SQL in the owning Module's migrations/ directory and include it in the binary at compile time. The Rust declaration remains the explicit ordered schema plan; the SQL body stays reviewable as SQL:

orders-module/
├── migrations/
│   ├── 001_create_orders.sql
│   └── 002_add_order_status.sql
└── src/
    └── lib.rs
use lenso_postgres_kit::{
    Migration, OwnedPostgres, SchemaOperator, SchemaPlan, sql_migrations,
};

const MIGRATIONS: &[Migration] = sql_migrations![
    (
        1,
        "create-orders",
        "migrations/001_create_orders.sql",
    ),
    (
        2,
        "add-order-status",
        "migrations/002_add_order_status.sql",
    ),
];

# async fn example(database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
let plan = SchemaPlan::new("orders_module", MIGRATIONS)?;

// An operator runs this explicitly during installation or deployment.
SchemaOperator::connect(database_url, plan.clone()).await?.setup().await?;

// Module preparation only verifies the exact schema and then exposes its pool.
let postgres = OwnedPostgres::prepare(database_url, plan).await?;
let count: i64 = sqlx::query_scalar("SELECT count(*) FROM orders")
    .fetch_one(postgres.pool())
    .await?;
# let _ = count;
# Ok(())
# }

Paths are relative to the owning crate's Cargo.toml. sql_migrations! expands to include_str! for each file. There is no runtime directory scan, so missing files fail compilation, file changes trigger a rebuild, and the existing migration checksum still binds version, stable name, and exact SQL. Never edit an applied SQL file; append the next numbered file.

When a new migration is linked, preparation returns UpgradeRequired. Stop the owning Module, run SchemaOperator::upgrade, and then prepare the new generation. setup never adopts an existing unmanaged schema, and checksum drift or a newer database fails closed.

Isolation contract

search_path provides convenient unqualified queries; it is not a security boundary. Give each Module a dedicated non-superuser PostgreSQL role, make that role the owner of only its schema, and restrict grants at the database level. The kit verifies that the current role owns the selected schema.

Sharing one physical PostgreSQL cluster does not grant one Module access to another Module's tables. Cross-Module workflows belong in explicit Capability calls and application-level coordination, not in shared SQL transactions.

Validation

cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --locked --all-features
LENSO_POSTGRES_TEST_URL=postgres://... \
  cargo test --locked --test postgres_acceptance -- --ignored