How pgrust Handles Prepared Statements and Portals: PostgreSQL's Two-Stage Model in Rust
pgrust exposes PostgreSQL's prepared statement and portal mechanics through safe Rust seams, where plancache_seams caches parsed query plans and portalmem_seams manages cursor-style execution contexts that bind parameters and run queries.
The pgrust project (malisper/pgrust) provides Rust bindings that mirror PostgreSQL's backend internals, implementing the database's classic two-stage model for prepared statements. This architecture separates query compilation from execution, using distinct memory contexts and system catalogs to manage cached plans and runtime portals. Understanding how pgrust handles prepared statements and portals reveals how the crate maintains semantic parity with PostgreSQL while providing idiomatic Rust APIs for memory and resource management.
The Two-Stage Execution Model
pgrust follows PostgreSQL's strict separation between plan caching and execution. Prepared statements are not merely SQL strings but cached execution plans stored in permanent memory, while portals serve as the runtime abstraction that links a plan to specific parameter values and transaction states.
Stage 1: Parsing and Caching
The preparation phase begins with parsing a SQL string into a RawStmt parse tree. In crates/backend/utils/cache/plancache_seams/src/lib.rs, the create_cached_plan seam allocates a CachedPlanSource that owns the raw tree, the original query string, and the command tag. The plan is then completed via complete_cached_plan, which supplies the rewritten query list and infers parameter OIDs. Finally, save_cached_plan moves the CachedPlanSource into PostgreSQL's permanent-memory arena and registers it in the pg_prepared_statement system catalog.
Stage 2: Execution via Portals
Once cached, a prepared statement executes through a portal—PostgreSQL's cursor abstraction. The portal seams in crates/backend/utils/mmgr/portalmem_seams/src/lib.rs expose the C primitives as safe Rust functions. CreatePortal or CreateNewPortal allocates a new Portal handle, optionally named for cursor visibility. PortalDefineQuery attaches the prepared plan (CachedPlanHandle) to the portal, while PortalSetParams binds runtime values to the plan's parameters. Execution occurs through PortalRun or PortalRunUtility, which fetches rows or completes commands according to the plan type.
Portal Lifecycle Management
Portals maintain complex state including parameter bindings, snapshot visibility, and resource ownership. The portalmem seam layer manages this lifecycle while keeping the portal hash table (PortalHashTable) synchronized with PostgreSQL's dynahash implementation.
Portal Creation and Query Binding
Creating a portal requires allocating from the portal memory context and defining its query source. The PortalDefineQuery seam in portalmem_seams attaches the cached plan to the portal structure, establishing the link between the immutable prepared statement and the mutable execution context. For named portals, this insertion updates the global PortalHashTable, which maintains insertion order to support cursor-related system functions like pg_cursors.
Parameter Binding and Execution
Before execution, PortalSetParams binds the runtime ParamListInfo to the portal, converting Rust types into PostgreSQL Datum values. The PortalRun seam then executes the plan, supporting both multi-row fetch operations and utility commands. For SELECT statements, the portal manages the TupleTable results and maintains the cursor position for subsequent fetches.
Resource Cleanup
When execution completes or the transaction ends, PortalDrop tears down the portal, releasing resource owner references and removing the entry from the portal hash table. The isTopCommit parameter controls whether the portal survives past the current transaction boundary for holdable cursors.
Transaction Lifecycle and Prepared Transactions
Portals integrate deeply with PostgreSQL's transaction management. Before a COMMIT or PREPARE TRANSACTION, the system calls PreCommit_Portals (exposed through portalmem_seams) to validate portal states and handle holdable cursors.
When using two-phase commit, portals are not automatically closed during transaction preparation, preserving the same semantics as PostgreSQL. The finish_prepared_transaction seam in crates/backend/tcop/utility_out_seams/src/lib.rs finalizes the prepared transaction (commit or abort) after portal work completes, ensuring that persistent portals remain valid across the prepared transaction boundary.
Plan Cache Mode and Optimization
The GUC plan_cache_mode (defined in crates/backend/utils/guc_tables/src/tables.rs) controls whether prepared statements use generic plans, custom plans, or automatic selection. This setting is consulted by get_cached_plan in plancache_seams when retrieving cached plans, allowing the planner to re-optimize queries based on specific parameter values or reuse generic plans for stability.
Complete Code Example
The following Rust example demonstrates the full lifecycle from parsing to portal execution using pgrust's seam APIs:
use ::mcx::Mcx;
use ::types_error::PgResult;
use ::nodes::parsestmt::RawStmt;
use ::nodes::nodes::CommandTag;
use ::nodes::params::ParamListInfo;
use ::types_core::Oid;
fn exec_prepared_statement() -> PgResult<()> {
// 1️⃣ Parse the statement and cache it.
let mcx = Mcx::new();
let raw = parse_sql(&mcx, "SELECT $1 + $2")?; // <-- parses to RawStmt
let src = create_cached_plan(
&mcx,
&raw,
"SELECT $1 + $2",
CommandTag::SELECT,
)?;
let plan = get_cached_plan(
src,
ParamListInfo::new(&[Oid::INT4, Oid::INT4]), // param OIDs
ResourceOwner::default(),
None,
)?;
// 2️⃣ Create an (unnamed) portal and bind the prepared plan.
let portal = create_new_portal()?; // ← portalmem seam
PortalDefineQuery(
&portal,
None, // no named portal
"SELECT $1 + $2",
CommandTag::SELECT,
)?;
PortalSetParams(&portal, &[Datum::Int4(10), Datum::Int4(20)])?;
// 3️⃣ Run the portal (FETCH ALL semantics for a SELECT).
PortalRun(&portal, 0)?; // 0 → fetch all rows
// 4️⃣ Retrieve the single result row.
let row = PortalGetResult(&portal)?; // wraps PGResult<TupleTable>
println!("Result: {}", row[0].as_int4());
// 5️⃣ Clean up.
PortalDrop(&portal, false)?;
Ok(())
}
All functions in this example are seams that directly map to PostgreSQL's C API:
| Seam | C Function | Source File |
|---|---|---|
create_cached_plan |
CreateCachedPlan |
crates/backend/utils/cache/plancache_seams/src/lib.rs |
complete_cached_plan |
CompleteCachedPlan |
same |
save_cached_plan |
SaveCachedPlan |
same |
get_cached_plan |
GetCachedPlan |
same |
create_new_portal |
CreateNewPortal |
crates/backend/utils/mmgr/portalmem_seams/src/lib.rs |
PortalDefineQuery |
PortalDefineQuery |
same |
PortalSetParams |
PortalSetParams |
same |
PortalRun |
PortalRunUtility / PortalRunMulti |
same |
PortalDrop |
PortalDrop |
same |
finish_prepared_transaction |
FinishPreparedTransaction |
crates/backend/tcop/utility_out_seams/src/lib.rs |
Summary
- Prepared statements in pgrust are cached execution plans managed by
plancache_seams, progressing fromRawStmtthroughCachedPlanSourceto permanent storage in thepg_prepared_statementcatalog. - Portals provide the runtime execution context via
portalmem_seams, binding cached plans to specific parameters and managing cursor state throughCreateNewPortal,PortalDefineQuery, andPortalRun. - Transaction integration follows PostgreSQL semantics with
PreCommit_Portalshandling commit validation andfinish_prepared_transactionsupporting two-phase commit with persistent portals. - Plan cache mode (
plan_cache_mode) controls optimization strategy, consulted when retrieving plans viaget_cached_plan. - Resource safety is maintained through Rust's ownership model wrapping PostgreSQL's memory contexts, ensuring proper cleanup via
PortalDropand resource owner management.
Frequently Asked Questions
What is the difference between a prepared statement and a portal in pgrust?
A prepared statement is a cached execution plan stored in permanent memory via plancache_seams, containing the parsed query tree and command tag. A portal is a runtime execution context created through portalmem_seams that binds a cached plan to specific parameter values and transaction snapshots, effectively acting as a cursor that can fetch results incrementally.
How does pgrust handle transaction boundaries with open portals?
Before any COMMIT or PREPARE TRANSACTION, pgrust calls PreCommit_Portals to validate portal states. For regular commits, holdable cursors may persist, while for prepared transactions (two-phase commit), portals remain open across the prepare phase until finish_prepared_transaction finalizes the transaction outcome, matching PostgreSQL's standard behavior.
Where are the prepared statement cache and portal hash table implemented?
The prepared statement cache is implemented in crates/backend/utils/cache/plancache_seams/src/lib.rs, managing CachedPlanSource structures and integration with the pg_prepared_statement system catalog. The portal hash table lives in crates/backend/utils/mmgr/portalmem_seams/src/lib.rs, maintaining an insertion-ordered mirror of PostgreSQL's dynahash to support cursor-related system functions.
How does plan_cache_mode affect prepared statement execution in pgrust?
The plan_cache_mode setting (defined in crates/backend/utils/guc_tables/src/tables.rs) determines whether get_cached_plan returns a generic plan, a custom plan re-optimized for specific parameters, or automatically chooses between them. This GUC is consulted during plan retrieval, allowing developers to balance between planning overhead and execution performance based on workload characteristics.
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 →