1# Chapter 7: scan/, one plan node to judge them all 2 3When Postgres runs a query, it first makes a *plan*: a tree of nodes, each of 4which hands rows to the one above it. A sequential scan reads a table, a 5Sort sorts what it is handed, a Limit stops after so many. Normally a 6function in your `WHERE` is evaluated by the node that filters, one row at a 7time, and that is the one thing that must not happen here: one row at a 8time is one request at a time, waited on in turn. 9 10So this folder adds a node of its own, a *CustomScan* called `JevScan`, and 11puts it between a table's scan and whatever was above it. Then it takes 12every call to the extension's functions out of the nodes they were in and 13answers them itself, many rows at once: 14 15```mermaid 16flowchart BT 17 subgraph before["What Postgres would plan"] 18 S1["Seq Scan on people<br/>Filter: country = 'PT' AND jev(people, q)"] --> L1["Sort, Limit"] 19 end 20 subgraph after["What it plans with postjevsql"] 21 S2["Seq Scan on people<br/>Filter: country = 'PT'"] --> J2["Custom Scan (JevScan)<br/>judges up to jev.concurrency rows at once<br/>Filter: jev's answer"] --> L2["Sort, Limit"] 22 end 23``` 24 25The child keeps every ordinary condition, so a row the SQL filters out 26never reaches the node and is never judged. The node pulls rows ahead while 27fewer than its window are unanswered, spawns one judgement per row, and 28hands rows up in the order the child gave them, waiting only when the 29oldest is still out. A `LIMIT` above simply stops pulling, so nothing more 30than a window past the last row returned is ever sent. 31 32> **Aside: being lazy on purpose.** Postgres evaluates `a OR b` by skipping 33> `b` when `a` is true. But this node answers every call *before* the 34> condition runs, so it could pay for a `b` that Postgres was never going to 35> read. `reach.rs` works out, for each call, the condition under which 36> Postgres would reach it (earlier `OR` arms not true, earlier `AND` arms not 37> false, the `CASE` tests before it), and the node evaluates that first. A 38> call that would not be reached is left NULL and costs nothing. 39 40## Where the node goes 41 42The calls can be anywhere in a query, and each place needs its own hook: 43 44| Where the call is | Who puts the node there | 45| --- | --- | 46| A table's `WHERE`, or its select list when it is the whole query | `set_rel_pathlist_hook` wraps every path of the relation, index and parameterized ones included (`plan.rs`). | 47| The select list above an `ORDER BY` | `create_upper_paths_hook` makes the projection above the Sort a jev scan too. | 48| An aggregate's argument, `HAVING`, `DISTINCT`, a window function | `planner_hook`'s post-pass, after the plan is built, puts a jev scan between that node and its child (`lift.rs`). | 49| Each partition of a partitioned table | Each member is wrapped, sharing the statement's dedupe and budget. | 50| CTEs and subqueries | Nothing extra: each is planned on its own, so the hooks above reach it. | 51 52Whatever is left over (a call over two relations, one in `RETURNING`) would 53run row by row, so `guard.rs` checks the finished plan in `ExecutorStart` and 54refuses it before a single row is pulled. The same check prices the plan 55against `jev.max_rows` and `jev.max_cost`. 56 57> **Try it.** `nix develop -c buck2 test //tests:batching` runs the scan 58> through all of the shapes in that table, each against a per-row reference, 59> on Postgres 18; `//tests:batching-pg17` runs it on 17. 60 61## For the people who maintain it 62 63| File | What | 64| --- | --- | 65| [mod.rs](mod.rs) | The safe surface: the `Judge` trait the extension implements, and the types it trades in (`Function`, `Arg`, `Call`, `Value`, `Out`, `Field`, `Property`, `Estimate`, `Judgement`). | 66| [plan.rs](plan.rs) | `register` (the hooks, the custom scan methods, the syscache callback), `support` (the planner support function and its per-call cost), finding the calls, `wrap_paths`, `wrap_projections`, `plan_custom_path`, `rows_before` for EXPLAIN's candidate rows, and `refuse_shipping_servers` for sidecar mode. | 67| [lift.rs](lift.rs) | The `planner_hook` post-pass: a jev scan under an Agg, WindowAgg, Group or Result whose own expressions call the functions. | 68| [reach.rs](reach.rs) | Each call's reach: the condition under which Postgres evaluates it. | 69| [guard.rs](guard.rs) | The `ExecutorStart` check: refuse a call left outside a jev scan, then price the plan through `Judge::afford`. | 70| [exec.rs](exec.rs) | Execution: pull ahead while the window has room, judge each row in a scoped task, emit in the child's order, rescan, EXPLAIN. | 71| [statement.rs](statement.rs) | `Statement`: what one statement's scans share (the budget, the registry of judgments in flight), keyed by the `EState`, gone when its query context resets; an EvalPlanQual recheck finds its parent's. | 72| [backend.rs](backend.rs) | `on_backend`: a closure a judgement queues for the backend, and the future of its result. | 73| [tuple.rs](tuple.rs) | `ScanTuple`, and `ScanTuple::eval`, the only way the node evaluates an expression. | 74| [ffi.rs](ffi.rs) | Postgres helpers that are `static inline` or macros in the headers, so bindgen does not carry them, and iterators over `List`. | 75 76← Previous: [Chapter 6, postjevsql-pg/src/](../) · Up: [src](../) · Next: [Chapter 8, postjevsql-core/](../../../postjevsql-core/) →