How pgrust Handles Prepared Statements and the Plan Cache: A Deep Dive
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:
- Prepared-Statement Table: A per-backend hash table (
prepared_queries) that maps statement names toPreparedStatementstructs. This table preserves insertion order to ensurepg_prepared_statementsreturns rows in the same order PostgreSQL expects. - Plan-Cache Seams: Thin Rust wrappers around PostgreSQL’s
utils/cache/plancache.cfunctions that exposeCachedPlanSourceandCachedPlanmanagement to safe Rust code. - Command Implementation: High-level
PREPAREandEXECUTElogic 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. 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:
struct PreparedQueryTable {
entries: Vec<PreparedStatement>,
index: HashMap<String, usize>,
}
According to the source in 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:
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 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:
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. These functions provide the glue between high-level commands and the underlying CachedPlanSource structures:
create_cached_plan: Allocates aCachedPlanSourceand copies the raw parse tree (mirrorsCreateCachedPlan).complete_cached_plan: Stores the rewritten query list and parameter OID array (mirrorsCompleteCachedPlan).get_cached_plan: Re-plans if schema changes occurred, registers the plan with aResourceOwner, and returns aCachedPlanhandle (mirrorsGetCachedPlan).release_cached_plan: Decrements reference counts when portals are dropped (mirrorsReleaseCachedPlan).- Accessor seams (
plansource_fixed_result,plansource_num_params, etc.): Provide safe read-only access toCachedPlanSourcefields.
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 (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
PreparedQueryTablethat maintains insertion order using a Vec/HashMap hybrid structure, ensuring compatibility withpg_prepared_statementsoutput. - 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.rsprovide safe Rust wrappers forCreateCachedPlan,CompleteCachedPlan,GetCachedPlan, andReleaseCachedPlan, 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 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.
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 →