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) transactionrelease_current_subtransaction()– commits the nested transaction on the no-error pathrollback_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 subtransactionReleaseCurrentSubTransaction()– low-level commitRollbackAndReleaseCurrentSubTransaction()– 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:
- Before executing the block body, the executor calls
begin_internal_subtransaction()to establish a savepoint - If the block completes without error, the executor calls
release_current_subtransaction()to commit the nested transaction - 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.rseliminates direct C dependencies while preserving PostgreSQL compatibility - Subtransaction support enables proper exception handling through
begin_internal_subtransaction()androllback_and_release_current_subtransaction() - Explicit transaction control via
spi_commit()andspi_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →