1//! Planning: find the calls, wrap every path, build the plan node. 2 3use std::ffi::{CString, c_char}; 4use std::sync::OnceLock; 5 6use pgrx::datum::Internal; 7use pgrx::pg_sys::panic::ErrorReport; 8use pgrx::{PgLogLevel, PgSqlErrorCode, pg_guard, pg_sys}; 9 10use super::ffi::{append, is_a, len, ptrs, walk}; 11use super::{Arg, Judge, exec, reach}; 12 13/// The three method tables, built once per process by [`register`]. 14pub(super) struct Tables { 15 pub path: pg_sys::CustomPathMethods, 16 pub scan: pg_sys::CustomScanMethods, 17 pub exec: pg_sys::CustomExecMethods, 18} 19 20// SAFETY: the tables hold a `&'static CStr`'s pointer and function 21// pointers, and are never written after `register` publishes them. 22unsafe impl Send for Tables {} 23unsafe impl Sync for Tables {} 24 25static TABLES: OnceLock<Tables> = OnceLock::new(); 26static PREVIOUS: OnceLock<pg_sys::set_rel_pathlist_hook_type> = OnceLock::new(); 27static PREVIOUS_UPPER: OnceLock<pg_sys::create_upper_paths_hook_type> = OnceLock::new(); 28 29pub(super) fn tables() -> &'static Tables { 30 TABLES.get().expect("scan::register ran in _PG_init") 31} 32 33/// Installs the planner hooks and the executor check for `J`. Call once, from `_PG_init`; it 34/// creates nothing, so it is safe in the postmaster. 35pub fn register<J: Judge>() { 36 let built = Tables { 37 path: pg_sys::CustomPathMethods { 38 CustomName: J::NAME.as_ptr(), 39 PlanCustomPath: Some(plan_custom_path::<J>), 40 ReparameterizeCustomPathByChild: None, 41 }, 42 scan: pg_sys::CustomScanMethods { 43 CustomName: J::NAME.as_ptr(), 44 CreateCustomScanState: Some(exec::create_state::<J>), 45 }, 46 exec: exec::methods::<J>(), 47 }; 48 if TABLES.set(built).is_err() { 49 return; 50 } 51 // SAFETY: _PG_init runs single-threaded before any planning in this 52 // process; the previous hook is kept and called first. 53 unsafe { 54 pg_sys::RegisterCustomScanMethods(&tables().scan); 55 let _ = PREVIOUS.set(pg_sys::set_rel_pathlist_hook); 56 pg_sys::set_rel_pathlist_hook = Some(set_rel_pathlist::<J>); 57 let _ = PREVIOUS_UPPER.set(pg_sys::create_upper_paths_hook); 58 pg_sys::create_upper_paths_hook = Some(create_upper_paths::<J>); 59 pg_sys::CacheRegisterSyscacheCallback( 60 pg_sys::SysCacheIdentifier::PROCOID as i32, 61 Some(forget_oids), 62 pg_sys::Datum::from(0usize), 63 ); 64 super::guard::install::<J>(); 65 super::lift::install::<J>(); 66 } 67} 68 69/// Planner weight of one call, in the planner's cost units: a network 70/// round trip dwarfs any local qual, so these sort last and plans that 71/// judge fewer rows win. A weight, not an estimate; EXPLAIN's price is 72/// computed separately. 73const PER_CALL_COST: f64 = 1000.0; 74 75/// The functions' planner support function (`SUPPORT` in their SQL). 76/// Being called at all is half its job: `eval_const_expressions` asks it 77/// to simplify each call, which loads this library, and so installs the 78/// hook, before paths are built for the first query in a backend. 79pub fn support(request: Internal) -> Internal { 80 let not_handled = Internal::from(Some(pg_sys::Datum::from(0usize))); 81 let Some(datum) = request.unwrap() else { return not_handled }; 82 let node = datum.cast_mut_ptr::<pg_sys::Node>(); 83 // SAFETY: the SQL signature is `(internal) RETURNS internal`, which 84 // only the planner can call, and it passes a SupportRequest* node. 85 unsafe { 86 if is_a(node.cast(), pg_sys::NodeTag::T_SupportRequestCost) { 87 let cost = node.cast::<pg_sys::SupportRequestCost>(); 88 (*cost).startup = 0.0; 89 (*cost).per_tuple = PER_CALL_COST; 90 return Internal::from(Some(pg_sys::Datum::from(cost as usize))); 91 } 92 } 93 not_handled 94} 95 96thread_local! { 97 /// `function_oids`' answer, `Some(None)` meaning "not installed". 98 /// Dropped whenever any pg_proc row changes (CREATE, DROP or ALTER 99 /// EXTENSION all change them), so it is never stale. 100 static OIDS: std::cell::RefCell<Option<Option<Vec<pg_sys::Oid>>>> = const { std::cell::RefCell::new(None) }; 101} 102 103#[pg_guard] 104unsafe extern "C-unwind" fn forget_oids(_arg: pg_sys::Datum, _cache: i32, _hash: u32) { 105 OIDS.with(|o| *o.borrow_mut() = None); 106} 107 108/// The OIDs of `J::FUNCTIONS`, in order, or `None` if the extension is 109/// not installed in this database. The planner hooks and the executor 110/// check ask on every query, so the answer is cached per backend. 111/// 112/// # Safety 113/// Called inside a transaction. 114pub(super) unsafe fn function_oids<J: Judge>() -> Option<Vec<pg_sys::Oid>> { 115 if let Some(known) = OIDS.with(|o| o.borrow().clone()) { 116 return known; 117 } 118 let found = unsafe { lookup_oids::<J>() }; 119 OIDS.with(|o| *o.borrow_mut() = Some(found.clone())); 120 found 121} 122 123unsafe fn lookup_oids<J: Judge>() -> Option<Vec<pg_sys::Oid>> { 124 unsafe { 125 let extension = pg_sys::get_extension_oid(J::EXTENSION.as_ptr(), true); 126 if extension == pg_sys::InvalidOid { 127 return None; 128 } 129 let schema = pg_sys::get_namespace_name(pg_sys::get_extension_schema(extension)); 130 let oids = J::FUNCTIONS 131 .iter() 132 .map(|f| { 133 let name = CString::new(f.name).expect("function names have no NUL"); 134 let names = append( 135 std::ptr::null_mut(), 136 [pg_sys::makeString(schema), pg_sys::makeString(pg_sys::pstrdup(name.as_ptr()))], 137 ); 138 let types: Vec<pg_sys::Oid> = f 139 .args 140 .iter() 141 .map(|a| match a { 142 Arg::Row => pg_sys::ANYELEMENTOID, 143 Arg::Text => pg_sys::TEXTOID, 144 Arg::Float8 => pg_sys::FLOAT8OID, 145 Arg::TextArray => pg_sys::TEXTARRAYOID, 146 }) 147 .collect(); 148 pg_sys::LookupFuncName(names, types.len() as i32, types.as_ptr(), true) 149 }) 150 .collect(); 151 Some(oids) 152 } 153} 154 155/// The outermost calls to `oids` in `node`; calls nested in a claimed 156/// call's arguments are evaluated as its arguments. 157unsafe fn calls_in(node: *mut pg_sys::Node, oids: &[pg_sys::Oid], found: &mut Vec<*mut pg_sys::FuncExpr>) { 158 unsafe { 159 walk(node, &mut |n| { 160 if is_a(n.cast(), pg_sys::NodeTag::T_FuncExpr) { 161 let call = n.cast::<pg_sys::FuncExpr>(); 162 if oids.contains(&(*call).funcid) { 163 found.push(call); 164 return false; 165 } 166 } 167 true 168 }) 169 }; 170} 171 172pub(super) unsafe fn contains_call(node: *mut pg_sys::Node, oids: &[pg_sys::Oid]) -> bool { 173 let mut found = Vec::new(); 174 unsafe { calls_in(node, oids, &mut found) }; 175 !found.is_empty() 176} 177 178#[pg_guard] 179unsafe extern "C-unwind" fn set_rel_pathlist<J: Judge>( 180 root: *mut pg_sys::PlannerInfo, 181 rel: *mut pg_sys::RelOptInfo, 182 rti: pg_sys::Index, 183 rte: *mut pg_sys::RangeTblEntry, 184) { 185 // SAFETY: the planner's arguments, valid for the call. 186 unsafe { 187 if let Some(Some(previous)) = PREVIOUS.get() { 188 previous(root, rel, rti, rte); 189 } 190 wrap_paths::<J>(root, rel, rte); 191 } 192} 193 194/// Replaces every path of `rel` with the scan over it, if `rel` has calls 195/// the scan can evaluate. 196unsafe fn wrap_paths<J: Judge>(root: *mut pg_sys::PlannerInfo, rel: *mut pg_sys::RelOptInfo, rte: *mut pg_sys::RangeTblEntry) { 197 unsafe { 198 // An appendrel parent (a partitioned or inherited table) is not 199 // wrapped: each member is, with the quals and select list 200 // translated to it, and the members' scans share the statement's 201 // dedupe and budget. Under DML and row locks the scan is also 202 // an EvalPlanQual recheck plan, answered without sending 203 // (`exec.rs`, `recheck`). 204 let member = (*rel).reloptkind == pg_sys::RelOptKind::RELOPT_OTHER_MEMBER_REL; 205 if !(member || (*rel).reloptkind == pg_sys::RelOptKind::RELOPT_BASEREL) || (*rte).inh || pg_sys::is_dummy_rel(rel) 206 { 207 return; 208 } 209 let Some(oids) = function_oids::<J>() else { return }; 210 211 let mut found = Vec::new(); 212 for clause in ptrs::<pg_sys::RestrictInfo>((*rel).baserestrictinfo) { 213 calls_in((*clause).clause.cast(), &oids, &mut found); 214 } 215 // The select list is this relation's to compute only when the 216 // relation is the whole query. A call that ends up evaluated 217 // above the scan anyway (under an aggregate, past a LIMIT) is 218 // simply never referenced, and the scan skips it. 219 // A member's is its topmost parent's, translated to the member's 220 // columns: the same expressions the planner pushes down to it 221 // (`apply_scanjoin_target_to_paths`), so the scan computes them. 222 let whole = if member { 223 !(*rel).top_parent.is_null() && pg_sys::bms_equal((*(*rel).top_parent).relids, (*root).all_query_rels) 224 } else { 225 pg_sys::bms_equal((*rel).relids, (*root).all_query_rels) 226 }; 227 if whole { 228 // Not under an aggregate or window function: the grouping or 229 // window node evaluates those, and `lift.rs` gives it a scan. 230 for entry in ptrs::<pg_sys::TargetEntry>((*root).processed_tlist) { 231 walk((*entry).expr.cast(), &mut |n| { 232 if is_a(n.cast(), pg_sys::NodeTag::T_Aggref) || is_a(n.cast(), pg_sys::NodeTag::T_WindowFunc) { 233 return false; 234 } 235 if is_a(n.cast(), pg_sys::NodeTag::T_FuncExpr) && oids.contains(&(*n.cast::<pg_sys::FuncExpr>()).funcid) { 236 let call = if member { 237 pg_sys::adjust_appendrel_attrs_multilevel(root, n, rel, (*rel).top_parent) 238 } else { 239 n 240 }; 241 found.push(call.cast()); 242 return false; 243 } 244 true 245 }); 246 } 247 } 248 let mut calls: Vec<*mut pg_sys::FuncExpr> = Vec::new(); 249 for call in found { 250 let node = call.cast::<pg_sys::Node>(); 251 let claimable = !pg_sys::contain_agg_clause(node) 252 && !pg_sys::contain_window_function(node) 253 && !pg_sys::expression_returns_set(node) 254 && pg_sys::bms_is_subset(pg_sys::pull_varnos(root, node), (*rel).relids); 255 if claimable && !calls.iter().any(|&c| pg_sys::equal(c.cast(), call.cast())) { 256 calls.push(call); 257 } 258 } 259 if calls.is_empty() { 260 return; 261 } 262 263 let private = append(std::ptr::null_mut(), calls.iter().copied()); 264 // The clauses holding calls run in this node, so it reads their 265 // other columns too (`id` in `id <= 5 OR jev(t, …)`). 266 let mut reads = private; 267 for clause in ptrs::<pg_sys::RestrictInfo>((*rel).baserestrictinfo) { 268 if contains_call((*clause).clause.cast(), &oids) { 269 reads = append(pg_sys::list_copy(reads), [(*clause).clause]); 270 } 271 } 272 let needed = pg_sys::pull_var_clause(reads.cast(), pg_sys::PVC_RECURSE_PLACEHOLDERS as i32); 273 let mut wrapped = std::ptr::null_mut(); 274 for child in ptrs::<pg_sys::Path>((*rel).pathlist) { 275 provide(child, needed); 276 wrapped = append(wrapped, [custom_path(rel, child, (*rel).reltarget, private)]); 277 } 278 (*rel).pathlist = wrapped; 279 // Partial paths would be gathered above the scan, with the calls 280 // evaluated per row in the workers. 281 (*rel).partial_pathlist = std::ptr::null_mut(); 282 } 283} 284 285#[pg_guard] 286unsafe extern "C-unwind" fn create_upper_paths<J: Judge>( 287 root: *mut pg_sys::PlannerInfo, 288 stage: pg_sys::UpperRelationKind::Type, 289 input_rel: *mut pg_sys::RelOptInfo, 290 output_rel: *mut pg_sys::RelOptInfo, 291 extra: *mut std::ffi::c_void, 292) { 293 // SAFETY: the planner's arguments, valid for the call. 294 unsafe { 295 if let Some(Some(previous)) = PREVIOUS_UPPER.get() { 296 previous(root, stage, input_rel, output_rel, extra); 297 } 298 if stage == pg_sys::UpperRelationKind::UPPERREL_ORDERED { 299 wrap_projections::<J>(root, output_rel); 300 } 301 } 302} 303 304/// With ORDER BY, the planner moves volatile select-list expressions past 305/// the Sort (`make_sort_input_target`), into a projection over the sorted 306/// rows. Each such projection that computes calls becomes the scan over 307/// the Sort: rows are judged in sorted order, and a LIMIT above still 308/// stops the judging. 309unsafe fn wrap_projections<J: Judge>(_root: *mut pg_sys::PlannerInfo, rel: *mut pg_sys::RelOptInfo) { 310 unsafe { 311 let Some(oids) = function_oids::<J>() else { return }; 312 for i in 0..len((*rel).pathlist) { 313 let cell = (*(*rel).pathlist).elements.add(i); 314 let path = (*cell).ptr_value.cast::<pg_sys::Path>(); 315 if !is_a(path.cast(), pg_sys::NodeTag::T_ProjectionPath) { 316 continue; 317 } 318 let projection = path.cast::<pg_sys::ProjectionPath>(); 319 let child = (*projection).subpath; 320 let mut calls: Vec<*mut pg_sys::FuncExpr> = Vec::new(); 321 for expr in ptrs::<pg_sys::Node>((*(*path).pathtarget).exprs) { 322 let mut found = Vec::new(); 323 calls_in(expr, &oids, &mut found); 324 for call in found { 325 if !pg_sys::expression_returns_set(call.cast()) 326 && !calls.iter().any(|&c| pg_sys::equal(c.cast(), call.cast())) 327 { 328 calls.push(call); 329 } 330 } 331 } 332 if calls.is_empty() { 333 continue; 334 } 335 // The child cannot be made to compute more (a Sort does not 336 // project), so it must already carry what the calls read. 337 let private = append(std::ptr::null_mut(), calls.iter().copied()); 338 let needed = pg_sys::pull_var_clause( 339 private.cast(), 340 (pg_sys::PVC_INCLUDE_AGGREGATES | pg_sys::PVC_INCLUDE_WINDOWFUNCS | pg_sys::PVC_INCLUDE_PLACEHOLDERS) as i32, 341 ); 342 if ptrs::<pg_sys::Node>(needed).any(|n| !pg_sys::list_member((*(*child).pathtarget).exprs, n.cast())) { 343 continue; 344 } 345 (*cell).ptr_value = custom_path(rel, child, (*path).pathtarget, private).cast(); 346 } 347 } 348} 349 350/// Makes `child` output every Var in `needed`: a call's arguments may use 351/// columns the query needs nowhere else (`WHERE jev(t, …)` needs `t.*`). 352unsafe fn provide(child: *mut pg_sys::Path, needed: *mut pg_sys::List) { 353 unsafe { 354 let target = (*child).pathtarget; 355 let missing: Vec<*mut pg_sys::Expr> = 356 ptrs::<pg_sys::Expr>(needed).filter(|&v| !pg_sys::list_member((*target).exprs, v.cast())).collect(); 357 if missing.is_empty() { 358 return; 359 } 360 // Copied: paths share the relation's target. 361 let target = pg_sys::copy_pathtarget(target); 362 for var in missing { 363 pg_sys::add_new_column_to_pathtarget(target, var); 364 } 365 (*child).pathtarget = target; 366 } 367} 368 369/// The scan over `child`, computing `target`. 370unsafe fn custom_path( 371 rel: *mut pg_sys::RelOptInfo, 372 child: *mut pg_sys::Path, 373 target: *mut pg_sys::PathTarget, 374 calls: *mut pg_sys::List, 375) -> *mut pg_sys::CustomPath { 376 unsafe { 377 let path = pg_sys::palloc0(size_of::<pg_sys::CustomPath>()).cast::<pg_sys::CustomPath>(); 378 let p = &mut (*path).path; 379 p.type_ = pg_sys::NodeTag::T_CustomPath; 380 p.pathtype = pg_sys::NodeTag::T_CustomScan; 381 p.parent = rel; 382 p.pathtarget = target; 383 p.param_info = (*child).param_info; 384 p.parallel_aware = false; 385 p.parallel_safe = false; 386 p.parallel_workers = 0; 387 // Over a relation, the child's estimate already counts the calls' 388 // selectivity and cost (they are in its restriction clauses). 389 // Rows come out in the child's order. 390 p.rows = (*child).rows; 391 // PG18 counts disabled nodes separately from the cost. 392 #[cfg(not(feature = "pg17"))] 393 { 394 p.disabled_nodes = (*child).disabled_nodes; 395 } 396 p.startup_cost = (*child).startup_cost; 397 p.total_cost = (*child).total_cost; 398 p.pathkeys = (*child).pathkeys; 399 (*path).flags = pg_sys::CUSTOMPATH_SUPPORT_PROJECTION; 400 (*path).custom_paths = append(std::ptr::null_mut(), [child]); 401 (*path).custom_private = calls; 402 (*path).methods = &tables().path; 403 path 404 } 405} 406 407/// Builds the node. `custom_scan_tlist` is the child's columns, then one 408/// column per call; `custom_exprs` holds each call's arguments (a row 409/// argument is written as JSON by `crate::row::RowJson`), then each 410/// call's reach (`reach.rs`), and `custom_private` the number of 411/// child columns and each call's index in `J::FUNCTIONS`. Only Nodes go 412/// in the plan: it is copied and cached. 413#[pg_guard] 414unsafe extern "C-unwind" fn plan_custom_path<J: Judge>( 415 root: *mut pg_sys::PlannerInfo, 416 rel: *mut pg_sys::RelOptInfo, 417 best_path: *mut pg_sys::CustomPath, 418 tlist: *mut pg_sys::List, 419 clauses: *mut pg_sys::List, 420 custom_plans: *mut pg_sys::List, 421) -> *mut pg_sys::Plan { 422 // SAFETY: the planner's arguments, valid for the call; the path is 423 // one `wrap_paths` built. 424 unsafe { 425 let Some(oids) = function_oids::<J>() else { 426 pgrx::error!("{} is no longer installed", J::EXTENSION.to_string_lossy()); 427 }; 428 let calls: Vec<*mut pg_sys::FuncExpr> = ptrs((*best_path).custom_private).collect(); 429 let child = ptrs::<pg_sys::Plan>(custom_plans).next().expect("one child plan"); 430 refuse_shipping_servers::<J>(child); 431 432 // The child ran every restriction clause, the calls' included; 433 // those are now this node's, evaluated on the answers. 434 take_calls(child, &oids); 435 let mut quals = std::ptr::null_mut(); 436 let (mut judged, mut kept) = (std::ptr::null_mut(), std::ptr::null_mut()); 437 for clause in ptrs::<pg_sys::RestrictInfo>(clauses) { 438 if (*clause).pseudoconstant { 439 continue; 440 } 441 if contains_call((*clause).clause.cast(), &oids) { 442 quals = append(quals, [(*clause).clause]); 443 judged = append(judged, [clause]); 444 } else { 445 kept = append(kept, [clause]); 446 } 447 } 448 if !judged.is_null() { 449 (*child).plan_rows = rows_before(root, rel, (*child).plan_rows, judged, kept); 450 } 451 452 let mut scan_tlist = std::ptr::null_mut(); 453 let mut resno: i16 = 0; 454 let mut entry = |expr: *mut pg_sys::Expr| { 455 resno += 1; 456 pg_sys::makeTargetEntry(expr, resno, std::ptr::null_mut::<c_char>(), false) 457 }; 458 for column in ptrs::<pg_sys::TargetEntry>((*child).targetlist) { 459 scan_tlist = append(scan_tlist, [entry(pg_sys::copyObjectImpl((*column).expr.cast()).cast())]); 460 } 461 let prefix = len(scan_tlist) as i32; 462 let mut private = pg_sys::lappend_int(std::ptr::null_mut(), prefix); 463 let mut exprs = std::ptr::null_mut(); 464 for &call in &calls { 465 scan_tlist = append(scan_tlist, [entry(call.cast())]); 466 let index = oids.iter().position(|&o| o == (*call).funcid).expect("a claimed call"); 467 private = pg_sys::lappend_int(private, index as i32); 468 for arg in ptrs::<pg_sys::Expr>((*call).args) { 469 exprs = append(exprs, [pg_sys::copyObjectImpl(arg.cast()).cast::<pg_sys::Expr>()]); 470 } 471 } 472 // Each call's reach, set by the post-pass once the final quals and 473 // projection are known (`reach.rs`). 474 exprs = append(exprs, calls.iter().map(|_| reach::always())); 475 476 let scan = pg_sys::palloc0(size_of::<pg_sys::CustomScan>()).cast::<pg_sys::CustomScan>(); 477 (*scan).scan.plan.type_ = pg_sys::NodeTag::T_CustomScan; 478 (*scan).scan.plan.targetlist = tlist; 479 (*scan).scan.plan.qual = quals; 480 // No relation of its own: setrefs maps the tlist and quals onto 481 // custom_scan_tlist (INDEX_VAR), matching each call by equal(). 482 (*scan).scan.scanrelid = 0; 483 (*scan).flags = (*best_path).flags; 484 (*scan).custom_plans = custom_plans; 485 (*scan).custom_exprs = exprs; 486 (*scan).custom_private = private; 487 (*scan).custom_scan_tlist = scan_tlist; 488 (*scan).methods = &tables().scan; 489 scan.cast() 490 } 491} 492 493/// The rows the child emits once the calls' quals (`judged`) are taken 494/// off it: every row the scan judges. Its estimate counted their 495/// selectivity, and `jev.max_rows`, `jev.max_cost` and EXPLAIN read it. 496/// A table's is recomputed as the planner computes it 497/// (`set_baserel_size_estimates`, `get_parameterized_baserel_size`): 498/// its tuples times the selectivity of the clauses it still runs. Any 499/// other child's (a foreign table's remote estimate, a subquery's) is 500/// divided by the calls' selectivity, which a rounded row count makes 501/// approximate. 502unsafe fn rows_before( 503 root: *mut pg_sys::PlannerInfo, 504 rel: *mut pg_sys::RelOptInfo, 505 rows: f64, 506 judged: *mut pg_sys::List, 507 kept: *mut pg_sys::List, 508) -> f64 { 509 unsafe { 510 let table = matches!( 511 (*rel).reloptkind, 512 pg_sys::RelOptKind::RELOPT_BASEREL | pg_sys::RelOptKind::RELOPT_OTHER_MEMBER_REL 513 ) && (*rel).rtekind == pg_sys::RTEKind::RTE_RELATION 514 && (*rel).fdwroutine.is_null() 515 && (*rel).tuples > 0.0; 516 let before = if table { 517 (*rel).tuples * pg_sys::clauselist_selectivity(root, kept, 0, pg_sys::JoinType::JOIN_INNER, std::ptr::null_mut()) 518 } else { 519 let selectivity = 520 pg_sys::clauselist_selectivity(root, judged, 0, pg_sys::JoinType::JOIN_INNER, std::ptr::null_mut()); 521 if selectivity > 0.0 { rows / selectivity } else { rows } 522 }; 523 pg_sys::clamp_row_est(before).max(rows) 524 } 525} 526 527/// Removes the calls' quals from the child: the scan node under any 528/// gating Result. Volatile, they are never index or remote conditions, 529/// so `qual` (and a Result's `resconstantqual`) is where they are. 530/// Refuses if a call survives anywhere it would run per row. 531unsafe fn take_calls(child: *mut pg_sys::Plan, oids: &[pg_sys::Oid]) { 532 unsafe { 533 let without = |list: *mut pg_sys::List| { 534 let mut kept = std::ptr::null_mut(); 535 for expr in ptrs::<pg_sys::Node>(list) { 536 if !contains_call(expr, oids) { 537 kept = append(kept, [expr]); 538 } 539 } 540 kept 541 }; 542 let mut node = child; 543 loop { 544 (*node).qual = without((*node).qual); 545 if is_a(node.cast(), pg_sys::NodeTag::T_Result) { 546 let result = node.cast::<pg_sys::Result>(); 547 (*result).resconstantqual = without((*result).resconstantqual.cast()).cast(); 548 } 549 if contains_call((*node).targetlist.cast(), oids) { 550 pgrx::error!("a jev call is in the output of the scan's child plan"); 551 } 552 match (*node).lefttree { 553 next if !next.is_null() && is_a(node.cast(), pg_sys::NodeTag::T_Result) => node = next, 554 _ => break, 555 } 556 } 557 } 558} 559 560/// Refuses to wrap a `ForeignScan` whose server lists this extension in 561/// its `extensions` option (contract, *Sidecar invariants*). postgres_fdw 562/// ships a listed extension's IMMUTABLE functions to the remote; the 563/// calls are VOLATILE today, so this guards the day a helper is not, and 564/// a remote that judged rows would do so per row, outside any request. 565unsafe fn refuse_shipping_servers<J: Judge>(plan: *mut pg_sys::Plan) { 566 unsafe { 567 if plan.is_null() { 568 return; 569 } 570 if is_a(plan.cast(), pg_sys::NodeTag::T_ForeignScan) { 571 let server = (*plan.cast::<pg_sys::ForeignScan>()).fs_server; 572 if server != pg_sys::InvalidOid && lists_extension::<J>(server) { 573 let name = std::ffi::CStr::from_ptr((*pg_sys::GetForeignServer(server)).servername).to_string_lossy(); 574 let extension = J::EXTENSION.to_string_lossy(); 575 ErrorReport::new( 576 PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, 577 format!("foreign server \"{name}\" lists {extension} in its extensions option"), 578 "jev scan", 579 ) 580 .set_detail("Its calls could be shipped to the remote and judged there, one row at a time. Nothing was sent.") 581 .set_hint(format!("Remove {extension} from the server's extensions option.")) 582 .report(PgLogLevel::ERROR); 583 } 584 } 585 refuse_shipping_servers::<J>((*plan).lefttree); 586 refuse_shipping_servers::<J>((*plan).righttree); 587 let subplans = if is_a(plan.cast(), pg_sys::NodeTag::T_Append) { 588 (*plan.cast::<pg_sys::Append>()).appendplans 589 } else if is_a(plan.cast(), pg_sys::NodeTag::T_MergeAppend) { 590 (*plan.cast::<pg_sys::MergeAppend>()).mergeplans 591 } else if is_a(plan.cast(), pg_sys::NodeTag::T_CustomScan) { 592 (*plan.cast::<pg_sys::CustomScan>()).custom_plans 593 } else if is_a(plan.cast(), pg_sys::NodeTag::T_SubqueryScan) { 594 append(std::ptr::null_mut(), [(*plan.cast::<pg_sys::SubqueryScan>()).subplan]) 595 } else { 596 std::ptr::null_mut() 597 }; 598 for sub in ptrs::<pg_sys::Plan>(subplans) { 599 refuse_shipping_servers::<J>(sub); 600 } 601 } 602} 603 604/// Whether `server`'s `extensions` option names `J::EXTENSION`, split as 605/// postgres_fdw splits it (`ExtractExtensionList`: a comma-separated 606/// identifier list, unquoted names folded to lower case). 607unsafe fn lists_extension<J: Judge>(server: pg_sys::Oid) -> bool { 608 unsafe { 609 let options = (*pg_sys::GetForeignServer(server)).options; 610 for option in ptrs::<pg_sys::DefElem>(options) { 611 if std::ffi::CStr::from_ptr((*option).defname) != c"extensions" { 612 continue; 613 } 614 let raw = pg_sys::pstrdup(pg_sys::defGetString(option)); 615 let mut names: *mut pg_sys::List = std::ptr::null_mut(); 616 if !pg_sys::SplitIdentifierString(raw, b',' as c_char, &mut names) { 617 // postgres_fdw's validator refuses such a value, so it 618 // cannot name anything; neither does it here. 619 continue; 620 } 621 if ptrs::<c_char>(names).any(|n| std::ffi::CStr::from_ptr(n) == J::EXTENSION) { 622 return true; 623 } 624 } 625 false 626 } 627}