Planning: find the calls, wrap every path, build the plan node.
The three method tables, built once per process by [register].
SAFETY: the tables hold a &'static CStr's pointer and function
pointers, and are never written after register publishes them.
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}
Installs the planner hooks and the executor check for J. Call once, from _PG_init; it
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}
Planner weight of one call, in the planner's cost units: a network round trip dwarfs any local qual, so these sort last and plans that judge fewer rows win. A weight, not an estimate; EXPLAIN's price is computed separately.
73const PER_CALL_COST: f64 = 1000.0;
The functions' planner support function (SUPPORT in their SQL).
Being called at all is half its job: eval_const_expressions asks it
to simplify each call, which loads this library, and so installs the
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}
96thread_local! {
function_oids' answer, Some(None) meaning "not installed".
Dropped whenever any pg_proc row changes (CREATE, DROP or ALTER
EXTENSION all change them), so it is never stale.
The OIDs of J::FUNCTIONS, in order, or None if the extension is
not installed in this database. The planner hooks and the executor
check ask on every query, so the answer is cached per backend.
Safety
Called inside a transaction.
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}
The outermost calls to oids in node; calls nested in a claimed
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}
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}
Replaces every path of rel with the scan over it, if rel has calls
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}
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}
With ORDER BY, the planner moves volatile select-list expressions past
the Sort (make_sort_input_target), into a projection over the sorted
rows. Each such projection that computes calls becomes the scan over
the Sort: rows are judged in sorted order, and a LIMIT above still
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}
Makes child output every Var in needed: a call's arguments may use
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}
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}
Builds the node. custom_scan_tlist is the child's columns, then one
column per call; custom_exprs holds each call's arguments (a row
argument is written as JSON by crate::row::RowJson), then each
call's reach (reach.rs), and custom_private the number of
child columns and each call's index in J::FUNCTIONS. Only Nodes go
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}
The rows the child emits once the calls' quals (judged) are taken
off it: every row the scan judges. Its estimate counted their
selectivity, and jev.max_rows, jev.max_cost and EXPLAIN read it.
A table's is recomputed as the planner computes it
(set_baserel_size_estimates, get_parameterized_baserel_size):
its tuples times the selectivity of the clauses it still runs. Any
other child's (a foreign table's remote estimate, a subquery's) is
divided by the calls' selectivity, which a rounded row count makes
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}
Removes the calls' quals from the child: the scan node under any
gating Result. Volatile, they are never index or remote conditions,
so qual (and a Result's resconstantqual) is where they are.
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}
Refuses to wrap a ForeignScan whose server lists this extension in
its extensions option (contract, Sidecar invariants). postgres_fdw
ships a listed extension's IMMUTABLE functions to the remote; the
calls are VOLATILE today, so this guards the day a helper is not, and
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}
Whether server's extensions option names J::EXTENSION, split as
postgres_fdw splits it (ExtractExtensionList: a comma-separated
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}