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/) →