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 from RawStmt through CachedPlanSource to permanent storage in the pg_prepared_statement catalog.
  • Portals provide the runtime execution context via portalmem_seams, binding cached plans to specific parameters and managing cursor state through CreateNewPortal, PortalDefineQuery, and PortalRun.
  • Transaction integration follows PostgreSQL semantics with PreCommit_Portals handling commit validation and finish_prepared_transaction supporting two-phase commit with persistent portals.
  • Plan cache mode (plan_cache_mode) controls optimization strategy, consulted when retrieving plans via get_cached_plan.
  • Resource safety is maintained through Rust's ownership model wrapping PostgreSQL's memory contexts, ensuring proper cleanup via PortalDrop and 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:

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 →