# How pgrust Handles Prepared Statements and the Plan Cache: A Deep Dive

> Discover how pgrust handles prepared statements and plan caching with its unique three-layer architecture for efficient Rust-based PostgreSQL integration. Learn more.

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

---

**pgrust reproduces PostgreSQL’s PREPARE/EXECUTE workflow using a three-layer architecture that combines a thread-local prepared-statement table, low-level plan-cache seams, and high-level command handlers to mirror PostgreSQL’s plan caching behavior in safe Rust.**

pgrust is a Rust reimplementation of PostgreSQL backend internals that requires faithful reproduction of the plan cache to maintain compatibility. Handling prepared statements and the plan cache correctly is critical for both performance and correctness. This article explores how pgrust manages the complete lifecycle of prepared statements—from initial preparation through execution and cache invalidation—using specific implementations found in the `backend/commands/prepare` and `backend/utils/cache` crates.

## The Three-Layer Architecture

pgrust implements prepared statements by stitching together three logical layers that mirror PostgreSQL’s C implementation:

1.  **Prepared-Statement Table**: A per-backend hash table (`prepared_queries`) that maps statement names to `PreparedStatement` structs. This table preserves insertion order to ensure `pg_prepared_statements` returns rows in the same order PostgreSQL expects.
2.  **Plan-Cache Seams**: Thin Rust wrappers around PostgreSQL’s [`utils/cache/plancache.c`](https://github.com/malisper/pgrust/blob/main/utils/cache/plancache.c) functions that expose `CachedPlanSource` and `CachedPlan` management to safe Rust code.
3.  **Command Implementation**: High-level `PREPARE` and `EXECUTE` logic that orchestrates parsing, analysis, storage, and execution.

## How pgrust Stores Prepared Statements (The PREPARE Workflow)

When processing a `PREPARE` command, pgrust follows a precise sequence defined in [`crates/backend/commands/prepare/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/crates/backend/commands/prepare/src/lib.rs). The `PrepareQuery` function implements the following steps:

### Name Validation and Raw Statement Wrapping

First, pgrust validates that the supplied statement name is non-empty, returning `ERRCODE_INVALID_PSTATEMENT_DEFINITION` if the check fails. It then wraps the original query node in a `RawStmt` using `make_raw_stmt`.

### Creating and Completing the CachedPlanSource

The system creates a `CachedPlanSource` by calling `plancache_seam::create_cached_plan`, which mirrors PostgreSQL’s `CreateCachedPlan` function. For each parameter, pgrust resolves `TYPE_NAME` arguments to OIDs via `parsetype_seam::typename_type_id_raw_pstate`.

After resolution, pgrust invokes `analyze_seam::analyze_and_rewrite_varparams` (the port of `pg_analyze_and_rewrite_varparams`) to transform the raw parse tree. It then completes the plan source with `plancache_seam::complete_cached_plan`, storing the rewritten query list and parameter OID array.

### Storage in the Per-Backend Table

The resulting `PreparedStatement` is inserted into the thread-local `PreparedQueryTable` via `PreparedQueryTable::insert`. This structure uses a hybrid approach to maintain insertion order while providing fast lookup:

```rust
struct PreparedQueryTable {
    entries: Vec<PreparedStatement>,
    index: HashMap<String, usize>,
}

```

According to the source in [`backend/commands/prepare/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/backend/commands/prepare/src/lib.rs) (lines 26-41), the `index` maps statement names to positions in the `entries` vector, ensuring `pg_prepared_statements` returns rows in creation order.

Names are hashed using a function that truncates identifiers to `NAMEDATALEN-1` characters, matching PostgreSQL’s identifier length limits:

```rust
fn hash_key(stmt_name: &str) -> String {
    // Truncates to NAMEDATALEN-1 to match PostgreSQL behavior
    // Source: backend/commands/prepare/src/lib.rs lines 199-209
}

```

## How pgrust Executes Prepared Statements (The EXECUTE Workflow)

The `ExecuteQuery` function in [`backend/commands/prepare/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/backend/commands/prepare/src/lib.rs) handles statement execution through the following sequence:

### Lookup and Fixed-Result Validation

The function calls `FetchPreparedStatement(name, true)` to retrieve the `PreparedStatement` from the table, erroring if the name does not exist. It then verifies the plan returns a fixed result shape using `plancache_seam::plansource_fixed_result`. If the plan could return a variable number of columns, `EXECUTE` aborts.

### Parameter Evaluation and Portal Creation

When parameters are present, pgrust creates an `ExecutorState` via `execexpr_seam::create_executor_state` and evaluates parameters through `EvaluateParams`. It then allocates a new invisible portal using `portal_seam::create_new_portal` and populates the portal’s query string using `plancache_seam::plansource_query_string`.

### Fetching and Executing the Cached Plan

The critical step invokes `plancache_seam::get_cached_plan` (lines 72-80), which returns a `CachedPlanHandle`. This seam performs re-planning automatically if the underlying query has changed since preparation:

```rust
let cplan = plancache_seam::get_cached_plan(
    entry.plansource, 
    param_li.clone(),
    ResourceOwnerHandle::NULL, 
    None
)?;
let plan_list = plancache_seam::cached_plan_stmt_list(mcx, cplan)?;

```

The `plan_list` is handed off to the executor via the portal system, executing the prepared statements while reusing the cached plan when possible.

## Plan Cache Seams: The Interface to CachedPlanSource

The plan-cache layer exposes PostgreSQL’s C implementation through safe Rust seams defined 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). These functions provide the glue between high-level commands and the underlying `CachedPlanSource` structures:

- **`create_cached_plan`**: Allocates a `CachedPlanSource` and copies the raw parse tree (mirrors `CreateCachedPlan`).
- **`complete_cached_plan`**: Stores the rewritten query list and parameter OID array (mirrors `CompleteCachedPlan`).
- **`get_cached_plan`**: Re-plans if schema changes occurred, registers the plan with a `ResourceOwner`, and returns a `CachedPlan` handle (mirrors `GetCachedPlan`).
- **`release_cached_plan`**: Decrements reference counts when portals are dropped (mirrors `ReleaseCachedPlan`).
- **Accessor seams** (`plansource_fixed_result`, `plansource_num_params`, etc.): Provide safe read-only access to `CachedPlanSource` fields.

These seams are initialized when the `utils/cache/plancache` unit starts (`init_plan_cache`), registering the Rust wrappers with the underlying C structures.

## PL/pgSQL Integration and Plan Reuse

The PL/pgSQL executor in pgrust reuses the same plan-cache infrastructure for dynamic SQL execution. In [`crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs) (lines 224-234), the `exec_prepare_plan` function folds into the slow path of `exec_eval_expr`. When PL/pgSQL encounters an `EXECUTE` statement, it obtains (or re-obtains) a cached plan via the same `get_cached_plan` seam used by the SQL-level `EXECUTE` command, ensuring consistent plan caching behavior across procedural and declarative SQL.

## Summary

- **pgrust stores prepared statements** in a thread-local `PreparedQueryTable` that maintains insertion order using a Vec/HashMap hybrid structure, ensuring compatibility with `pg_prepared_statements` output.
- **The PREPARE workflow** involves creating a `CachedPlanSource`, resolving parameter OIDs, analyzing and rewriting the query, and completing the plan before storage.
- **The EXECUTE workflow** validates fixed-result shapes, evaluates parameters, creates an invisible portal, and fetches the cached plan via `get_cached_plan`, which handles automatic re-planning on invalidation.
- **Plan-cache seams** in [`plancache_seams/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/plancache_seams/src/lib.rs) provide safe Rust wrappers for `CreateCachedPlan`, `CompleteCachedPlan`, `GetCachedPlan`, and `ReleaseCachedPlan`, bridging the gap between high-level commands and the C-backed plan cache.
- **PL/pgSQL integration** reuses these same seams for dynamic execution, ensuring plan cache consistency across the entire backend.

## Frequently Asked Questions

### What data structure does pgrust use to store prepared statements?

pgrust uses a `PreparedQueryTable` structure that combines a `Vec<PreparedStatement>` with a `HashMap<String, usize>`. This design preserves insertion order (so `pg_prepared_statements` returns rows deterministically) while providing O(1) name lookups. The table is stored as a thread-local resource to match PostgreSQL’s per-backend isolation model.

### How does pgrust handle plan cache invalidation during EXECUTE?

When `ExecuteQuery` calls `plancache_seam::get_cached_plan`, the seam internally checks whether the plan’s dependencies have changed. If the underlying relation structure or statistics have changed since preparation, the seam automatically triggers re-planning before returning the `CachedPlanHandle`, mirroring PostgreSQL’s `GetCachedPlan` invalidation logic.

### Can PL/pgSQL leverage pgrust's prepared statement infrastructure?

Yes. The PL/pgSQL executor in [`pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs) calls the same plan-cache seams—specifically utilizing `exec_prepare_plan` within the expression evaluation path. This allows PL/pgSQL `EXECUTE` statements to use cached plans and benefit from identical invalidation and reuse semantics as SQL-level prepared statements.

### What is the difference between CachedPlanSource and CachedPlan in pgrust?

A `CachedPlanSource` (created by `create_cached_plan`) acts as the long-lived metadata container that holds the raw parse tree, rewritten statements, and parameter information. A `CachedPlan` is the transient, executable plan snapshot obtained by calling `get_cached_plan`, which locks the plan for execution and handles resource ownership. The source persists across multiple executions, while the plan handle is acquired and released per execution.