The pgrust Query Executor Architecture: Safe Rust Implementation of PostgreSQL's Engine
The pgrust query executor reimplements PostgreSQL's execution engine in safe Rust using an owned QueryDesc handle that bundles all executor state, eliminating C-style lifetime issues while preserving the node-based plan execution model.
The pgrust project (malisper/pgrust) provides a complete Rust-based reimplementation of PostgreSQL's backend, with its query executor architecture representing a fundamental shift from the original C implementation. By centralizing execution state within a single owned handle and leveraging Rust's memory safety guarantees, pgrust eliminates the scattered context objects and complex lifetime management that characterize the traditional PostgreSQL executor. This architecture maintains full compatibility with PostgreSQL's plan node system while providing modern memory safety and extension mechanisms.
Core Architecture Components
The QueryDesc Owner Handle
At the center of the pgrust executor architecture lies the QueryDesc struct, defined in crates/_support/types/nodes/src/querydesc.rs. This structure serves as the single ownership handle for all executor state, replacing PostgreSQL's C-style approach of passing multiple pointers and context objects.
The QueryDesc contains:
operation– the command type (SELECT, INSERT, UPDATE, DELETE)snapshotandcrosscheck_snapshot– active read snapshots for MVCCdest– the destination receiver for result tuplesparams– external parameter bindingsinstrument_options– flags for query instrumentationalready_executed– a guard preventing multipleExecutorRuncallswork– aMcxOwned<QueryWorkState>bundle that owns the entire executor working set
The work field contains the critical execution state: EStateData (per-query executor state including memory contexts and plan counters), the immutable PlannedStmt produced by the planner, the original source_text for debugging, and the initialized planstate tree.
Memory Management via McxOwned
Unlike PostgreSQL's C implementation that manually manages multiple memory contexts with complex lifetimes, pgrust wraps all per-query state in a McxOwned<QueryWorkState> bundle. This approach leverages Rust's ownership system to ensure that when the QueryDesc drops, the entire per-query memory context and all associated executor structures free in a single, deterministic operation.
Query Execution Lifecycle
Initialization with ExecutorStart
The execution lifecycle begins with CreateQueryDesc, implemented via QueryDesc::create. This method:
- Allocates a dedicated per-query "ExecutorState" memory context
- Instantiates an empty
EStateinside that context - Copies read-only inputs (
PlannedStmt, source text, snapshots) into the owned bundle
The ExecutorStart phase, driven by the execMain seam implementation in crates/backend/executor/execMain/src/lib.rs, calls ExecInitNode on the top plan node. This recursively builds the full plan-state tree inside the same memory context, populating QueryDesc::work.planstate and establishing the result TupleDesc.
Execution with ExecutorRun
The ExecutorRun phase implements the core driver loop. The executor repeatedly:
- Pulls the next tuple from the plan tree via node-specific
ExecProcNodeimplementations - Sends each tuple to the
DestReceiverheld inQueryDesc::dest
Individual plan nodes reside in dedicated crates such as crates/backend/executor/nodeSeqscan, crates/backend/executor/nodeIndexscan, and crates/backend/executor/nodeModifyTable. These modules implement the standard PostgreSQL ExecProcNode contract, but return Result<PgResult<...>, PgError> instead of using C's longjmp for error handling.
Cleanup and Resource Management
Execution concludes with ExecutorEnd and FreeQueryDesc. When the QueryDesc handle drops, the McxOwned bundle automatically frees the per-query memory context and all associated structures, eliminating the risk of memory leaks or use-after-free errors common in the C implementation.
Seam-Based Extensibility
The execMain_seams Namespace
The pgrust executor implements a dense set of seams (hook points) collected under the execMain_seams namespace in crates/backend/executor/execMain/src/lib.rs. Each seam is a thin wrapper that external crates can override without modifying core executor code.
Key seams include:
exec_check_permissions_select– permission validation before SELECT executionexec_foreign_update– foreign-data-wrapper update handlingexec_partition_check– partition-specific constraint validationexec_init_result_rel– result-relation initialization for modifications
Integration Examples
Extensions like pg_stat_statements and plpgsql hook into these seams to intercept execution events. For example, crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs consumes executor seams to integrate the PL/pgSQL interpreter, while crates/contrib/pg_stat_statements/src/store.rs hooks into execution points to collect query statistics.
Plan Node Implementation
Each plan node type lives in its own crate under crates/backend/executor/, maintaining the classic PostgreSQL node-based architecture while adding Rust's type safety. Nodes access the current executor state through the execMain_seams bundle, ensuring a single source of truth for the EState and plan-state tree.
The node implementations follow the standard ExecProcNode contract:
- Initialization:
ExecInitNodebuilds the node-specific state - Execution:
ExecProcNodereturns the next tuple or indicates completion - Cleanup:
ExecEndNodereleases node-specific resources
Practical Example
use pgrust::exec::execmain::Executor;
use pgrust::_support::types::nodes::QueryDesc;
use pgrust::_support::types::destreceiver::DestReceiverHandle;
use pgrust::_support::types::params::ParamListInfo;
// Create the owned query descriptor with all execution state
let mut qdesc = QueryDesc::create(
&MemoryContext::Current,
&planned_stmt,
"SELECT id FROM my_table",
None,
None,
DestReceiverHandle::null(),
ParamListInfo::empty(),
0,
).expect("failed to create QueryDesc");
// Execute the standard lifecycle
Executor::executor_start(&mut qdesc).expect("ExecutorStart failed");
Executor::executor_run(&mut qdesc, 0).expect("ExecutorRun failed");
Executor::executor_end(&mut qdesc).expect("ExecutorEnd failed");
// Cleanup occurs automatically when qdesc drops
This example demonstrates the high-level flow: create a QueryDesc that owns all state, run the executor lifecycle, and rely on Rust's drop mechanics for cleanup.
Summary
- Single ownership model: The
QueryDeschandle incrates/_support/types/nodes/src/querydesc.rsowns all executor state viaMcxOwned<QueryWorkState>, eliminating lifetime issues. - Safe error handling: All executor functions return
Resulttypes instead of using C-stylelongjmp, preventing undefined behavior. - Seam-based extensions: The
execMain_seamsnamespace provides clean hook points for extensions without core code modification. - Node-based execution: Individual crates like
nodeSeqscan,nodeIndexscan, andnodeModifyTableimplement the standardExecProcNodecontract in safe Rust. - Deterministic cleanup: The
McxOwnedbundle ensures all per-query resources free automatically when execution completes.
Frequently Asked Questions
How does pgrust handle executor memory management differently from PostgreSQL?
PostgreSQL's C implementation uses multiple memory contexts with manual management and complex pointers-as-handles, requiring careful tracking of which context owns which object. Pgrust consolidates all per-query state into a single McxOwned<QueryWorkState> bundle owned by QueryDesc, leveraging Rust's ownership system to ensure automatic, deterministic cleanup when the handle drops.
What are executor seams in pgrust?
Executor seams are hook points defined in the execMain_seams namespace that allow external crates to intercept and modify executor behavior. These seams wrap core operations like permission checks, foreign table updates, and result relation initialization, enabling extensions such as pg_stat_statements and plpgsql to integrate without modifying the executor's core logic.
Which file defines the core execution handle in pgrust?
The core execution handle QueryDesc is defined in crates/_support/types/nodes/src/querydesc.rs. This file contains the struct definition that owns the entire execution state, including the EStateData, PlannedStmt, and plan state tree, all wrapped in the McxOwned<QueryWorkState> bundle.
How does pgrust handle errors in the executor without using longjmp?
Instead of PostgreSQL's longjmp-based error handling, pgrust node implementations return Result<PgResult<...>, PgError> types. This allows the executor to use Rust's standard error propagation mechanisms, ensuring that resources clean up properly through the Drop trait implementation on QueryDesc and its contained McxOwned bundle, even when errors occur during 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 →