Skip to main content

Crate turnframe_store_postgres

Crate turnframe_store_postgres 

Source
Expand description

turnframe-store-postgres: the PostgreSQL implementation of the Turnframe persistence contract.

§This crate is optional

The contract is turnframe-store: seven object-safe traits and an executable conformance suite that proves an implementation right. This crate is one implementation of them. An adopter with an existing schema, another database, or a different operational story can implement the traits themselves and never depend on this crate — the runtime cannot tell the difference, and the conformance suite will say so either way.

What you get by using it is a schema that has already been argued about: the constraints below are where the rules of the contract live, so a race that would break an invariant loses on an index rather than on a lucky interleaving.

§Getting started

use turnframe_core::ids::{AccountId, ConversationId};
use turnframe_store::prelude::*;
use turnframe_store_postgres::PgStores;

let store = PgStores::connect("postgres://turnframe@localhost/turnframe").await?;
store.migrate().await?;

let stores: Stores = store.stores()?;
let account = AccountId::from("aurora");
let conversation = ConversationId::new();

stores
    .conversations()
    .create_conversation(ConversationRecord::new(
        conversation,
        account.clone(),
        chrono::Utc::now(),
    ))
    .await?;

// Another tenant cannot tell it apart from a conversation that never existed.
assert_eq!(
    stores
        .conversations()
        .load_conversation(&AccountId::from("other"), &conversation)
        .await,
    Err(StoreError::NotFound)
);

§Where each rule lives

RuleHow it is enforced
at most one open blocking card per case (I5)the partial unique index tf_one_open_blocking_interaction_per_case
one admission per idempotency key (I14)UNIQUE (account_id, idempotency_key) plus INSERT … ON CONFLICT DO NOTHING, then a re-read that replays the winner
one external action per destinationUNIQUE (destination, idempotency_key)
a claimed outbox row is one dispatcher’sSELECT … FOR UPDATE SKIP LOCKED inside the claiming statement
compare-and-swap, never blind overwritethe expected state is the WHERE clause of the write; zero affected rows means the precondition failed
a bundle is all or nothingone transaction, committed only after the last item succeeded
tenant isolationaccount_id leads every primary key and every index, so a query that forgets it cannot use one

§Runtime-checked queries, on purpose

Every statement in this crate goes through sqlx::query and reads its columns by name. None of them uses the sqlx::query! family.

Those macros check SQL against a live database at compile time, which is a real benefit and the wrong trade for a published library: it makes the crate unbuildable in a clean checkout unless a database is reachable or a .sqlx cache is committed and kept in step with every edit. A contributor with no PostgreSQL, a cargo install, a docs.rs build and a downstream cargo vendor would all fail on something that has nothing to do with their change.

The check that macros would have given is bought back by the conformance suite instead: it runs the whole persistence contract against a real PostgreSQL 16, so a column renamed on one side and not the other fails a test rather than a build — later, but against behaviour rather than shape. sqlx::migrate! is still a macro and still used: it reads migrations/ while compiling and needs no database.

§Assumptions this adapter makes

  • READ COMMITTED. PostgreSQL’s default. The idempotency admission and every compare-and-swap rely on a statement re-reading a row another transaction has just committed. At REPEATABLE READ those become serialization failures, reported as Conflict — correct, but it turns routine contention into caller-visible refusals.
  • Microsecond timestamps. timestamptz keeps microseconds; instants this adapter stamps are truncated to match, so a value written and read back compares equal.
  • One schema. Set PgStoreConfig::schema to keep the tables out of public; PgStores::migrate creates it.

See the README for the schema table by table and for running the conformance suite.

Modules§

error
Translating PostgreSQL failures into the closed error surface of the persistence contract.

Structs§

PgStoreConfig
How a PgStores opens and shapes its connection pool.
PgStores
Every Turnframe store over one PostgreSQL pool.

Enums§

ConfigError
Why a PgStoreConfig was refused.

Statics§

MIGRATOR
The migrations of this crate, embedded in the binary at compile time.

Functions§

migrate
Applies every migration this crate carries to a pool the caller owns.