# How pgrust Handles Prepared Statements and Portals: PostgreSQL's Two-Stage Model in Rust

> Learn how pgrust implements PostgreSQL's prepared statements and portals in Rust. Discover safe Rust seams for query planning and execution contexts.

- Repository: [Michael Malis/pgrust](https://github.com/malisper/pgrust)
- Tags: internals
- Published: 2026-07-13

---

**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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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:

```rust
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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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`](https://github.com/malisper/pgrust/blob/main/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.