How pgrust Handles Query Execution and Expression Evaluation in PL/pgSQL
pgrust implements PL/pgSQL query execution through a layered "seam" architecture that decouples the executor from PostgreSQL's SPI via runtime-installed bridge functions.
The malisper/pgrust project reimplements PostgreSQL's PL/pgSQL interpreter in Rust, introducing a clean abstraction layer between statement execution and the Server Programming Interface (SPI). This design allows the PL/pgSQL executor to remain agnostic of PostgreSQL internals while still leveraging the native query engine for actual data operations.
The Seam-Based Architecture
pgrust organizes its execution pipeline into three distinct layers that separate parsing, planning, and physical execution:
- PL/pgSQL Executor Layer: Parses statements and builds
PlannedStmtnodes, but cannot call SPI directly. Instead, it invokes seam functions installed at runtime by the handler. - SPI Seam Functions: Bridge functions declared in
crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rsthat translate PL/pgSQL execution requests into SPI calls. - SPI Implementation: The actual PostgreSQL interface logic located in
crates/backend/executor/spi/src/*that handles plan preparation, parameter binding, and result retrieval.
This architecture ensures that variable scoping, datum conversion, and memory management happen in Rust while the heavy lifting of query execution passes through to PostgreSQL's native engine.
Query Execution Flow
When executing a static SQL statement from PL/pgSQL, pgrust follows a six-step pipeline that transforms high-level statements into SPI operations.
Step 1: Statement Compilation
The parser constructs execution nodes such as PLpgSQL_stmt_execsql from the PL/pgSQL grammar. These nodes capture the query string, target variables, and execution modifiers.
Step 2: Seam Invocation
The executor calls exec_execsql_via_spi located in crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs, passing the query and a snapshot of current variable values:
pub fn exec_execsql_via_spi(
query: String,
parse_mode: RawParseMode,
parse_state: PlpgsqlExprParseState,
datum_snapshot: Vec<Option<EvalParamValue>>,
read_only: bool,
into: bool,
tcount: i64,
) -> PgResult<ExecsqlResult>;
Step 3: Plan Preparation
The seam forwards to SPI_prepare_plan in crates/backend/executor/spi/src/prepare.rs, which parses the SQL text and generates an execution plan compatible with PostgreSQL's executor.
Step 4: Parameter Binding
The datum_snapshot vector contains the current values of PL/pgSQL scalar variables mapped to $dno+1 parameters. The seam constructs a ParamListInfo structure to bind these values to the prepared plan.
Step 5: Query Execution
The SPI layer invokes SPI_execute_plan_with_paramlist from crates/backend/executor/spi/src/execsql.rs, running the query under the current transaction snapshot.
Step 6: Result Materialization
Execution returns a Rust struct (ExecsqlResult or RunSelectResult) containing the SPI return code, processed row count, and optional result rows, allowing the PL/pgSQL executor to continue without direct C struct manipulation.
Expression Evaluation Flow
PL/pgSQL expression evaluation uses a specialized path that wraps expressions in single-row SELECT statements.
Evaluation Seam Invocation
The executor calls exec_eval_expr_via_spi (line 389 in plpgsql_exec_seams/src/lib.rs) when computing scalar expressions:
pub fn exec_eval_expr_via_spi(
query: String,
parse_mode: RawParseMode,
parse_state: PlpgsqlExprParseState,
datum_snapshot: Vec<Option<EvalParamValue>>,
maxtuples: i64,
read_only: bool,
) -> PgResult<EvalExprResult>;
Single-Row Execution
Unlike general queries, expressions set maxtuples = 1 and wrap the expression text in a SELECT statement. The seam delegates to the same SPI execution path (SPI_execute_plan_with_paramlist) but configures the portal for singleton retrieval.
Datum Conversion
The raw datum returned by SPI converts into EvalExprResult, exposing the computed value, isnull flag, and type information. This allows PL/pgSQL to handle NULL propagation and type coercion according to variable declarations.
Dynamic SQL Execution
For EXECUTE ... USING statements, pgrust provides exec_dynexecute_via_spi (line 298 in plpgsql_exec_seams/src/lib.rs). This seam accepts a SQL string with parameter placeholders and a vector of DynUsingParam structs containing runtime values:
pub fn exec_dynexecute_via_spi(
query: String,
parse_mode: RawParseMode,
parse_state: PlpgsqlExprParseState,
using_params: Vec<DynUsingParam>,
read_only: bool,
into: bool,
tcount: i64,
) -> PgResult<DynExecuteResult>;
The implementation routes through SPI_execute_extended in the backend SPI crate, handling plan caching and parameter binding for dynamically constructed queries.
Summary
- Seam abstraction: pgrust decouples the PL/pgSQL executor from PostgreSQL internals via bridge functions in
plpgsql_exec_seams. - Variable binding: Scalar datums pass as
datum_snapshotvectors that convert to PostgreSQL parameter lists at the SPI boundary. - Unified SPI path: Both static queries and expressions ultimately execute through
SPI_execute_plan_with_paramlistincrates/backend/executor/spi/src/execsql.rs. - Return type safety: Results return as Rust structs (
ExecsqlResult,EvalExprResult) that mirror SPI return codes without exposing raw C pointers to the PL/pgSQL layer. - Dynamic support: Runtime SQL strings execute via
exec_dynexecute_via_spiwith explicit parameter binding throughDynUsingParamstructs.
Frequently Asked Questions
How does pgrust handle parameter binding for PL/pgSQL variables?
pgrust captures PL/pgSQL variable values into a datum_snapshot vector during executor entry. The seam functions translate this snapshot into PostgreSQL's ParamListInfo structure before calling SPI, binding variables to $dno+1 parameter positions. This occurs in crates/pl/plpgsql/src/plpgsql_exec_seams/src/lib.rs before the SPI layer receives the request.
What is the difference between exec_execsql_via_spi and exec_eval_expr_via_spi?
exec_execsql_via_spi handles general SQL statements and returns full result sets via ExecsqlResult, while exec_eval_expr_via_spi specifically evaluates scalar expressions by wrapping them in single-row SELECT statements and returning EvalExprResult. The expression evaluator sets maxtuples = 1 and focuses on datum extraction rather than row sets.
Where does the actual query execution happen in pgrust?
The physical execution occurs in crates/backend/executor/spi/src/execsql.rs, which implements SPI_execute_plan_with_paramlist and SPI_execute_extended. These functions interface with PostgreSQL's native executor engine, running plans under the current transaction snapshot and returning results back through the seam layer.
How does pgrust support dynamic SQL with the EXECUTE statement?
pgrust implements dynamic SQL through exec_dynexecute_via_spi in the seams crate, which accepts a query string and DynUsingParam bindings. This function routes to SPI_execute_extended in the backend SPI crate, preparing parametrized plans and binding runtime values supplied via the USING clause without exposing raw PostgreSQL plan structures to the PL/pgSQL executor.
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 →