guard.rsannotatedguard.rssource170 lines · 7.2 KB · raw

Refuses a plan before it runs if any call to the extension's functions was left outside a jev scan. Such a call would run once per row, and its body refuses; but by then the plan's scans may already have judged a window of rows, and that is money spent on a failed statement. The check runs in ExecutorStart, after the plan is initialized and before any row is pulled, so nothing has been sent.

The same check then prices the plan: the rows every jev scan's child is estimated to produce go to [Judge::afford], which refuses a statement over its spend guards before its first request.

12use std::ffi::c_void;
13use std::sync::OnceLock;
15use pgrx::pg_sys::panic::ErrorReport;
16use pgrx::{PgLogLevel, PgSqlErrorCode, pg_guard, pg_sys};
17
18use super::ffi::ptrs;
19use super::{Estimate, Judge};
20use super::plan::{contains_call, function_oids, tables};
21
22static PREVIOUS: OnceLock<pg_sys::ExecutorStart_hook_type> = OnceLock::new();

Safety

Called once, from _PG_init.

26pub(super) unsafe fn install<J: Judge>() {
27    unsafe {
28        let _ = PREVIOUS.set(pg_sys::ExecutorStart_hook);
29        pg_sys::ExecutorStart_hook = Some(executor_start::<J>);
30    }
31}
33#[pg_guard]
34unsafe extern "C-unwind" fn executor_start<J: Judge>(query: *mut pg_sys::QueryDesc, eflags: i32) {
35    // SAFETY: the executor's arguments; the plan state exists once the
36    // standard start returns.
37    unsafe {
38        match PREVIOUS.get() {
39            Some(Some(previous)) => previous(query, eflags),
40            _ => pg_sys::standard_ExecutorStart(query, eflags),
41        }
42        // EXPLAIN without ANALYZE spends nothing, and showing the plan is
43        // how a user finds the call that would be refused.
44        if eflags as u32 & pg_sys::EXEC_FLAG_EXPLAIN_ONLY != 0 {
45            return;
46        }
47        let Some(oids) = function_oids::<J>() else { return };
48        if let Some(tag) = unbatched((*query).planstate, &oids) {
49            let tag = format!("{tag:?}");
50            ErrorReport::new(
51                PgSqlErrorCode::ERRCODE_FEATURE_NOT_SUPPORTED,
52                format!(
53                    "a jev call in this query is outside the jev scan, in a {} node",
54                    tag.trim_start_matches("T_")
55                ),
56                "jev scan",
57            )
58            .set_detail("It would send one request per row, one at a time. Nothing was sent.")
59            .set_hint("Give each call the columns of one relation, outside a join's select list and conditions.")
60            .report(PgLogLevel::ERROR);
61        }
62        if let Err(refusal) = J::afford(&estimate((*query).planstate)) {
63            refusal.report(PgLogLevel::ERROR);
64        }
65    }
66}

What the plan's jev scans are estimated to judge: their children's rows, which is after every SQL filter. A scan rescanned under a nested loop counts once; the scan's own count while running backs that up.

71unsafe fn estimate(root: *mut pg_sys::PlanState) -> Estimate {
72    #[pg_guard]
73    unsafe extern "C-unwind" fn visit(state: *mut pg_sys::PlanState, context: *mut c_void) -> bool {
74        // SAFETY: `context` is the Estimate below, alive for the walk; a
75        // jev scan's plan has its one child in `custom_plans`.
76        unsafe {
77            let estimate = &mut *context.cast::<Estimate>();
78            let plan = (*state).plan;
79            if (*plan).type_ == pg_sys::NodeTag::T_CustomScan {
80                let scan = plan.cast::<pg_sys::CustomScan>();
81                if std::ptr::eq((*scan).methods, &tables().scan) {
82                    let one = scan_estimate(scan);
83                    estimate.rows += one.rows;
84                    estimate.bytes += one.bytes;
85                }
86            }
87            pg_sys::planstate_tree_walker_impl(state, Some(visit), context)
88        }
89    }
90
91    let mut estimate = Estimate::default();
92    // SAFETY: an initialized plan state tree; `visit` never raises.
93    unsafe { visit(root, (&raw mut estimate).cast()) };
94    estimate
95}

What one jev scan is estimated to judge: its child's rows and width.

Safety

scan is a jev scan's plan, with its one child in custom_plans.

101pub(super) unsafe fn scan_estimate(scan: *mut pg_sys::CustomScan) -> Estimate {
102    unsafe {
103        let child = ptrs::<pg_sys::Plan>((*scan).custom_plans).next().expect("one child plan");
104        Estimate { rows: (*child).plan_rows, bytes: (*child).plan_rows * f64::from((*child).plan_width) }
105    }
106}

The tag of the first plan node that evaluates a call itself.

109unsafe fn unbatched(root: *mut pg_sys::PlanState, oids: &[pg_sys::Oid]) -> Option<pg_sys::NodeTag> {
110    struct Search<'a> {
111        oids: &'a [pg_sys::Oid],
112        found: Option<pg_sys::NodeTag>,
113    }
114
115    #[pg_guard]
116    unsafe extern "C-unwind" fn visit(state: *mut pg_sys::PlanState, context: *mut c_void) -> bool {
117        // SAFETY: `context` is the Search below, alive for the walk; the
118        // plan states and their plans belong to the running executor.
119        unsafe {
120            let search = &mut *context.cast::<Search>();
121            let plan = (*state).plan;
122            if expressions(plan).into_iter().any(|e| contains_call(e, search.oids)) {
123                search.found = Some((*plan).type_);
124                return true;
125            }
126            pg_sys::planstate_tree_walker_impl(state, Some(visit), context)
127        }
128    }
129
130    let mut search = Search { oids, found: None };
131    // SAFETY: an initialized plan state tree; `visit` never raises.
132    unsafe { visit(root, (&raw mut search).cast()) };
133    search.found
134}

Where a plan node keeps expressions it evaluates per row. A jev scan's own call columns are descriptors, never evaluated, so only its call arguments count.

139unsafe fn expressions(plan: *mut pg_sys::Plan) -> Vec<*mut pg_sys::Node> {
140    use pg_sys::NodeTag::*;
141    unsafe {
142        let mut found: Vec<*mut pg_sys::Node> = vec![(*plan).targetlist.cast(), (*plan).qual.cast()];
143        match (*plan).type_ {
144            T_NestLoop | T_MergeJoin | T_HashJoin => found.push((*plan.cast::<pg_sys::Join>()).joinqual.cast()),
145            T_Result => found.push((*plan.cast::<pg_sys::Result>()).resconstantqual),
146            T_FunctionScan => found.push((*plan.cast::<pg_sys::FunctionScan>()).functions.cast()),
147            T_ValuesScan => found.push((*plan.cast::<pg_sys::ValuesScan>()).values_lists.cast()),
148            // RETURNING and ON CONFLICT are evaluated per row, above the
149            // scan; MERGE's actions likewise.
150            T_ModifyTable => {
151                let modify = plan.cast::<pg_sys::ModifyTable>();
152                found.push((*modify).returningLists.cast());
153                found.push((*modify).onConflictSet.cast());
154                found.push((*modify).onConflictWhere);
155                found.push((*modify).mergeActionLists.cast());
156            }
157            T_CustomScan => {
158                let scan = plan.cast::<pg_sys::CustomScan>();
159                if std::ptr::eq((*scan).methods, &tables().scan) {
160                    found.push((*scan).custom_exprs.cast());
161                }
162            }
163            _ => {}
164        }
165        // NULL is an empty list; a FunctionScan's RangeTblFunctions are
166        // nodes the expression walker descends through to their funcexpr.
167        found.retain(|e| !e.is_null());
168        found
169    }
170}