How pgrust Manages Database Transactions: A Layered Approach to PostgreSQL Integration

pgrust implements a layered transaction model that mirrors PostgreSQL's native transaction and sub-transaction semantics while exposing them to Rust through dependency-injection seams, enabling safe control over atomic operations from within PL/pgSQL functions.

pgrust is a Rust implementation of PostgreSQL's PL/pgSQL runtime that requires sophisticated transaction management to maintain database consistency. The project exposes PostgreSQL's transaction engine to Rust through carefully designed seams—dependency-injection points that bridge Rust's type system with PostgreSQL's C-based transaction management in xact.c. This architecture enables safe handling of nested subtransactions, exception blocks, and explicit commit/rollback operations while preserving PostgreSQL's atomicity guarantees.

The Layered Architecture of pgrust Transaction Management

pgrust organizes transaction control into four distinct layers, each abstracting PostgreSQL's native functionality into Rust-friendly interfaces.

Executor Layer (pl_exec)

The PL/pgSQL executor drives SQL work via SPI and manages transaction contexts through the seams defined in crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs. This layer provides the primary interface for starting, committing, and aborting transaction contexts within stored procedures.

The executor exposes three critical subtransaction control functions:

  • begin_internal_subtransaction() – starts a nested (savepoint) transaction
  • release_current_subtransaction() – commits the nested transaction on the no-error path
  • rollback_and_release_current_subtransaction() – aborts the nested transaction when an exception is raised

For explicit transaction control, the executor also provides spi_commit(chain) and spi_rollback(chain), which commit or rollback the outer transaction from non-atomic SPI blocks (corresponding to PL/pgSQL COMMIT and ROLLBACK statements).

Transaction Engine

The transaction engine contains thin wrappers around PostgreSQL's native xact.c functions, installed by the handler to eliminate direct C dependencies. Located in crates/backend/access/transam/transam_xact/src/lib.rs (lines 203-210), this layer maps Rust calls to PostgreSQL's internal transaction primitives:

  • BeginInternalSubTransaction(name) – low-level start of a subtransaction
  • ReleaseCurrentSubTransaction() – low-level commit
  • RollbackAndReleaseCurrentSubTransaction() – low-level abort

SPI Interface

The SPI interface provides commit and rollback primitives that operate when PL/pgSQL runs inside non-atomic SPI contexts. The spi_commit(chain) function maps to SPI_commit() or SPI_commit_and_chain(), while spi_rollback(chain) maps to SPI_rollback() or SPI_rollback_and_chain(), implementing the optional AND CHAIN clause syntax.

Client-Side Status

External callers inspect transaction state through the libpq façade in crates/interfaces/libpq/fe/src/client.rs (lines 91-100). The Client::transaction_status() method returns a PgTransactionStatusType enum with variants Idle, Intrans, Inerror, and Unknown, correlating directly with PostgreSQL's PQTRANS_* constants.

How Transaction Control Works in pgrust

The transaction management system handles three distinct operational modes: outer transactions for standard execution, subtransactions for exception handling, and snapshot management for consistency.

Managing Outer Transactions

When a PL/pgSQL function executes under a normal BEGIN … END block, the executor operates within the current PostgreSQL transaction. If the function issues a COMMIT or ROLLBACK statement, the executor calls the spi_commit or spi_rollback seams, which invoke PostgreSQL's SPI_commit or SPI_rollback functions. The chain boolean parameter implements the optional AND CHAIN clause, allowing the next statement to start a new transaction automatically.

Handling Sub-transactions and Exception Blocks

PL/pgSQL BEGIN … EXCEPTION … END blocks compile to internal subtransactions managed through the executor seams. The implementation follows this sequence:

  1. Before executing the block body, the executor calls begin_internal_subtransaction() to establish a savepoint
  2. If the block completes without error, the executor calls release_current_subtransaction() to commit the nested transaction
  3. If an exception occurs, the executor calls rollback_and_release_current_subtransaction() to abort the subtransaction and restore state

Throughout the block, the executor saves the current ResourceOwner via current_resource_owner and set_current_resource_owner seams, ensuring proper resource cleanup after the subtransaction ends.

Maintaining Transaction Snapshots

Before any SPI call, the executor snapshots the current transaction state using GetTransactionSnapshot via the snapshot manager seams (such as PushActiveSnapshot and PopActiveSnapshot in backend/utils/time/snapmgr_seams). This guarantees that read-only queries see a consistent view even if the outer transaction later rolls back, preventing phantom reads and ensuring ACID compliance.

Code Examples: Implementing Transaction Control

The following examples demonstrate practical transaction management in pgrust applications.

Explicit COMMIT from PL/pgSQL

To commit the current transaction from within a stored procedure:

use plpgsql_exec_seams::spi_commit;

// Commit without chaining
spi_commit(false)?;   // Maps to SPI_commit()

// Commit with AND CHAIN
spi_commit(true)?;    // Maps to SPI_commit_and_chain()

Exception Handling with Subtransactions

Implementing safe exception blocks requires explicit subtransaction management:

use plpgsql_exec_seams::{
    begin_internal_subtransaction,
    release_current_subtransaction,
    rollback_and_release_current_subtransaction,
};

fn run_exception_block() -> PgResult<()> {
    // Start a nested subtransaction
    begin_internal_subtransaction()?;

    // Execute statements that might error
    let result = some_sql_execution();

    match result {
        Ok(_) => {
            // No error – commit the subtransaction
            release_current_subtransaction()?;
        }
        Err(e) => {
            // Error occurred – roll back and propagate
            rollback_and_release_current_subtransaction()?;
            return Err(e);
        }
    }
    Ok(())
}

Checking Transaction Status from Rust Clients

Applications using the libpq façade can monitor connection state:

use libpq_fe::client::Client;
use libpq_fe::result::PgTransactionStatusType;

let conn = Client::connect("host=localhost dbname=mydb")?;
match conn.transaction_status() {
    PgTransactionStatusType::Idle => println!("Not in a transaction"),
    PgTransactionStatusType::Intrans => println!("Inside a transaction"),
    PgTransactionStatusType::Inerror => println!("Transaction is aborted"),
    _ => println!("Unknown state"),
}

Summary

pgrust delivers a transaction-aware execution engine that bridges Rust's memory safety with PostgreSQL's battle-tested transaction semantics:

  • Layered architecture separates concerns between the executor, transaction engine, SPI interface, and client visibility
  • Seam-based design in crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs eliminates direct C dependencies while preserving PostgreSQL compatibility
  • Subtransaction support enables proper exception handling through begin_internal_subtransaction() and rollback_and_release_current_subtransaction()
  • Explicit transaction control via spi_commit() and spi_rollback() allows stored procedures to manage transaction boundaries
  • Status visibility through Client::transaction_status() exposes real-time transaction state to calling applications

Frequently Asked Questions

How does pgrust handle nested subtransactions?

pgrust handles nested subtransactions through the executor seams in crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs. When entering an exception block, the executor calls begin_internal_subtransaction() to create a savepoint. If an error occurs, rollback_and_release_current_subtransaction() restores the state to the pre-block condition. This implementation mirrors PostgreSQL's native savepoint mechanism while providing Rust-safe error handling.

Can Rust code in pgrust execute explicit COMMIT statements?

Yes, Rust code can execute explicit commits through the SPI interface. The spi_commit(chain) function in the executor seams maps directly to PostgreSQL's SPI_commit() or SPI_commit_and_chain() functions. This allows PL/pgSQL functions implemented in Rust to execute COMMIT and COMMIT AND CHAIN operations even when running inside non-atomic SPI contexts.

What transaction status information is available to clients?

Clients can query transaction status through the Client::transaction_status() method exposed in crates/interfaces/libpq/fe/src/client.rs (lines 91-100). This returns a PgTransactionStatusType enum with four states: Idle (not in a transaction), Intrans (inside a transaction), Inerror (transaction aborted), and Unknown (connection failed). These values map directly to PostgreSQL's PQTRANS_* constants.

How does pgrust ensure consistency during exception handling?

pgrust ensures consistency by combining subtransaction rollback with resource owner management. When an exception occurs, the executor calls rollback_and_release_current_subtransaction() to abort the subtransaction, then restores the saved ResourceOwner via the set_current_resource_owner seam. Additionally, the system captures transaction snapshots using GetTransactionSnapshot before SPI calls, ensuring read-only queries maintain a consistent view regardless of subsequent rollback operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →