Skip to main content

inillucent_sql/
plan.rs

1//! The logical and physical plans.
2//!
3//! Invariant: a physical plan is *legal* before it is fast. Every access path
4//! this planner produces returns exactly the rows a full scan of the same term
5//! would return, and every predicate a path consumes is either fully enforced
6//! by the path or left in the residual filter. A predicate that is neither is a
7//! wrong answer, so the two lists are built together and the compiler emits
8//! whatever is left over.
9//!
10//! The phase-6 planner is deliberately minimal: FROM terms stay in written
11//! order, joins are nested loops, and the only paths are a full scan, a rowid
12//! lookup or range, and an index seek over an equality prefix with an optional
13//! range on the column after it. Cost is not modelled yet; a path is chosen
14//! because it is more selective by construction, not because a number said so.
15
16use inillucent_value::Collation;
17
18use crate::ast::{BinaryOp, CompoundOp, JoinKind, NullOrder, SortOrder};
19use crate::bind::{BoundExpr, BoundSelect, BoundSource, ColumnUse, SourceRows};
20use crate::catalog_view::{IndexInfo, TableInfo};
21use crate::cost;
22
23mod covering;
24mod describe;
25mod hint;
26mod outer_paths;
27mod partial;
28mod pattern;
29mod pushdown;
30mod range;
31mod terms;
32use covering::covering_slots;
33use describe::{index_seek_detail, range_detail};
34pub use hint::unanswerable_index_hint;
35use hint::{forced_path, index_usable, outer_terms};
36use outer_paths::{choose_term_path, derived_affinities, hold_terms_before_right_joins, TermPath};
37use partial::implies;
38pub use pushdown::{calls_a_volatile_function, is_repeatable_condition};
39use terms::{
40    collation_of, compares_unconverted, comparison_against_rowid, comparison_collation,
41    constant_in_list, indexable_comparison, virtual_constraint,
42};
43mod queue;
44mod seek_union;
45pub use queue::RecursiveQueue;
46
47/// A comparison an access path can enforce.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum BoundKind {
50    /// `>=`
51    GreaterEqual,
52    /// `>`
53    Greater,
54    /// `<=`
55    LessEqual,
56    /// `<`
57    Less,
58}
59
60/// One end of a scan range.
61#[derive(Clone, Debug, PartialEq)]
62pub struct RangeBound {
63    /// Which comparison the bound enforces.
64    pub kind: BoundKind,
65    /// The value to compare against.
66    pub value: BoundExpr,
67    /// Whether the seek compares `value` without converting it to the
68    /// column's affinity. See [`AccessPath::IndexSeek`]'s `unconverted`.
69    pub unconverted: bool,
70}
71
72/// One seek over an index, as a branch of an [`AccessPath::IndexSeekUnion`].
73#[derive(Clone, Debug, PartialEq)]
74pub struct IndexSeekBranch {
75    /// The equality prefix this branch pins, one value per leading index
76    /// column.
77    pub equalities: Vec<BoundExpr>,
78    /// The positions in `equalities` whose value is compared unconverted.
79    /// See [`AccessPath::IndexSeek`]'s `unconverted`.
80    pub unconverted: Vec<usize>,
81    /// The lower bound on the column after the prefix, when there is one.
82    pub low: Option<RangeBound>,
83    /// The upper bound on that same column.
84    pub high: Option<RangeBound>,
85}
86
87/// How one FROM term's rows are produced.
88#[derive(Clone, Debug, PartialEq)]
89pub enum AccessPath {
90    /// Every row of the table, in rowid order.
91    TableScan {
92        /// The table B-tree's root page.
93        root: u32,
94    },
95    /// One row, found by rowid.
96    RowidSeek {
97        /// The table B-tree's root page.
98        root: u32,
99        /// The rowid to look up.
100        key: BoundExpr,
101    },
102    /// A contiguous run of rows, by rowid.
103    RowidRange {
104        /// The table B-tree's root page.
105        root: u32,
106        /// The lower bound, when there is one.
107        low: Option<RangeBound>,
108        /// The upper bound, when there is one.
109        high: Option<RangeBound>,
110    },
111    /// Rows found through an index, then fetched from the table.
112    IndexSeek {
113        /// The table B-tree's root page.
114        table_root: u32,
115        /// The index B-tree's root page.
116        index_root: u32,
117        /// The index's name, for the plan description.
118        index_name: Vec<u8>,
119        /// The equality prefix, one value per leading index column.
120        equalities: Vec<BoundExpr>,
121        /// The positions in `equalities` whose value the seek compares without
122        /// converting it to the column's affinity.
123        ///
124        /// **The comparison decides this, and only the planner sees the
125        /// comparison (task-2083).** A seek converts the probe value to the
126        /// indexed column's affinity, except when both sides of the `=` have
127        /// an affinity and neither is numeric: then `WHERE` converts nothing
128        /// and neither may the seek. That is SQLite's `codeAllEqualityTerms`.
129        /// The executor used to decide it from the probe expression alone,
130        /// and a correlated subquery replaces the outer column with a
131        /// parameter before planning. The parameter has no affinity, so
132        /// `(SELECT id FROM h WHERE h.a = s.k)` with `h.a TEXT` and `s.k`
133        /// untyped converted the number 3 to `'3'` and found a row SQLite
134        /// does not.
135        ///
136        /// A list of positions rather than a flag per equality because it is
137        /// almost always empty, and an empty `Vec` does not allocate. A flag per
138        /// equality cost two allocations to compile `WHERE email = ?1`, which
139        /// `inillucent::budget` counts.
140        unconverted: Vec<usize>,
141        /// A range on the column after the equality prefix.
142        low: Option<RangeBound>,
143        /// The upper end of that range.
144        high: Option<RangeBound>,
145        /// The collation of each index column used, in order.
146        collations: Vec<Collation>,
147        /// Whether the index columns used are stored descending.
148        descending: Vec<bool>,
149        /// Which table column each index column holds.
150        ///
151        /// `None` for a key the index *computes*: an index on `lower(a)` holds
152        /// a value no column of the table carries, and the probe value takes no
153        /// column affinity because there is no column to take it from - which
154        /// is SQLite's rule and the reason this is an `Option` rather than a
155        /// position that would have to be invented. A key on a virtual generated
156        /// column is the exception: it is computed, and it names the column, so
157        /// the probe takes the column's affinity.
158        columns: Vec<Option<u16>>,
159        /// Whether the table has no rowid, so the index key holds the key.
160        without_rowid: bool,
161        /// Where in each entry the row's primary key sits, for a `WITHOUT
162        /// ROWID` table read through a *secondary* index.
163        ///
164        /// Such an entry ends with the primary key where a rowid table's would
165        /// end with a rowid, and that is how the row is then found. Empty for a
166        /// rowid table, and empty when the index is the table's own key - then
167        /// the entry the seek landed on already is the row.
168        key_entry_slots: Vec<usize>,
169        /// Where in the index entry every column the query reads sits, when the
170        /// index holds all of them.
171        ///
172        /// An index entry is the indexed columns followed by the row's key, so
173        /// a query that reads only those columns never has to go to the table
174        /// at all - which halves the descents and, on a range, is the whole
175        /// difference between a search and a scan. `None` means the query needs
176        /// something the entry does not carry, and the row is fetched.
177        ///
178        /// The pairs are `(record slot in the table, slot in the index entry)`.
179        /// The rowid is not in the list: it is always the entry's last field
180        /// for a rowid table, and the compiler reads it with `IdxRowid`.
181        covering: Option<Vec<(u16, usize)>>,
182    },
183    /// One row per key, found by rowid - several of
184    /// [`RowidSeek`](Self::RowidSeek), concatenated.
185    ///
186    /// What `WHERE rowid IN (a, b, c)` plans to on a rowid table: every branch
187    /// is the same one-row lookup `RowidSeek` uses alone, so the union is
188    /// nothing more than that lookup run once per key. A rowid is unique by
189    /// construction, so the only way two branches can name the same row is a
190    /// repeated key - a literal list is de-duplicated once, here, at plan
191    /// time; a key that is not a literal (a parameter, a correlated column)
192    /// cannot be compared this way, so the executor still checks each key
193    /// against the ones already probed before it seeks.
194    RowidSeekUnion {
195        /// The table B-tree's root page.
196        root: u32,
197        /// The keys to look up, in the order they are probed.
198        keys: Vec<BoundExpr>,
199    },
200    /// Rows found through one index - several seeks over the same tree,
201    /// concatenated.
202    ///
203    /// The branches are what a disjunction's terms become once each is
204    /// individually seekable: `x IN (a, b, c)` is every branch a bare
205    /// equality on the same column, and a keyset page's
206    /// `(a=? AND b>?) OR a>?` is two branches over the same composite index,
207    /// one an equality followed by a range and the other a range alone. The
208    /// fields outside `branches` describe the one index and table every
209    /// branch reads, because those never vary between branches - only the
210    /// equality prefix and the range do, which is exactly what a term of a
211    /// disjunction can differ in.
212    IndexSeekUnion {
213        /// The table B-tree's root page.
214        table_root: u32,
215        /// The index B-tree's root page.
216        index_root: u32,
217        /// The index's name, for the plan description.
218        index_name: Vec<u8>,
219        /// One seek per branch, in the order they run.
220        branches: Vec<IndexSeekBranch>,
221        /// The collation of each index column a branch can reach, in order.
222        ///
223        /// Sized to the deepest branch - the one whose equality prefix and
224        /// range together reach furthest into the index - because a
225        /// shallower branch simply does not read the columns past its own
226        /// depth.
227        collations: Vec<Collation>,
228        /// Whether each of those columns is stored descending.
229        descending: Vec<bool>,
230        /// Which table column each of those index columns holds.
231        columns: Vec<Option<u16>>,
232        /// Whether the table has no rowid, so the index key holds the key.
233        without_rowid: bool,
234        /// Where in each entry the row's primary key sits, for a `WITHOUT
235        /// ROWID` table read through a *secondary* index. Empty for a rowid
236        /// table, and empty when the index is the table's own key.
237        key_entry_slots: Vec<usize>,
238        /// Where in the index entry every column the query reads sits, when
239        /// the index holds all of them.
240        covering: Option<Vec<(u16, usize)>>,
241        /// Whether a row this union finds can also be found by a different
242        /// branch, and so has to be checked against the rows already
243        /// emitted before it is.
244        ///
245        /// `false` only when the branches are proven disjoint by
246        /// construction - the keyset-range shape, where each branch's
247        /// equality prefix pins a value no other branch's range can reach -
248        /// which is what lets that shape stream straight through a `LIMIT`
249        /// with nothing held back to be deduplicated. An `IN` list is always
250        /// `true`: a non-literal value (a parameter, a correlated column)
251        /// cannot be proven distinct from another at plan time, so the
252        /// executor has to check.
253        dedup: bool,
254    },
255    /// Rows produced by a nested query, materialised and then scanned.
256    Subquery {
257        /// The plan that fills the store.
258        plan: Box<PhysicalPlan>,
259        /// How many columns a materialised row holds.
260        width: usize,
261        /// Whether the nested block reads a FROM term outside itself, and so
262        /// has to be rebuilt for every row of the query that encloses it.
263        correlated: bool,
264        /// The affinity each column of the rows is stored with.
265        ///
266        /// SQLite writes a derived table's rows into a table of its own, whose
267        /// columns take the affinity of the query's: a REAL column of the first
268        /// arm of a compound makes the integer 7 of the second arm 7.0. Blob
269        /// stands for no affinity.
270        affinities: Vec<inillucent_value::Affinity>,
271    },
272    /// Rows produced by a recursive CTE, filled by walking its own queue.
273    Recursive {
274        /// The arms that do not reference the CTE, in order.
275        seeds: Vec<(CompoundOp, PhysicalPlan)>,
276        /// The arms that do.
277        steps: Vec<(CompoundOp, PhysicalPlan)>,
278        /// How many columns a row holds.
279        width: usize,
280        /// How the queue is ordered and where the recursion stops.
281        queue: RecursiveQueue,
282    },
283    /// The one row of a recursive CTE's queue the fill loop is on.
284    RecursiveSelf {
285        /// The FROM term whose store holds the queue.
286        cte: usize,
287    },
288    /// The k nearest vectors, from an index a module owns.
289    ///
290    /// **A `TopN` over a distance is a different question from a scan.** The
291    /// rows are chosen by the index rather than filtered out of a walk, so the
292    /// path carries the probe and the depth rather than a range: the module is
293    /// asked for `k` candidates and the plan's own `ORDER BY` then rescores
294    /// them exactly, over an `ORDER BY` function matching the index's metric.
295    VectorProbe {
296        /// The table's root page, whose rows the candidates name.
297        root: u32,
298        /// The store holding the vectors, by the name the index was created
299        /// with.
300        index: Vec<u8>,
301        /// The vector to measure against, which reads no column of this query.
302        probe: Box<BoundExpr>,
303        /// How many candidates to ask the index for.
304        depth: usize,
305    },
306    /// Rows produced by a virtual table's module.
307    VirtualScan {
308        /// The module and the arguments its `CREATE` gave it.
309        module: crate::vtab::ModuleRef,
310        /// The constraints offered to `best_index`, in the order the module
311        /// will see them.
312        offer: Vec<VirtualConstraint>,
313        /// The ordering offered to `best_index`.
314        order_by: Vec<crate::vtab::OrderSpec>,
315        /// What the module answered, once it has been asked.
316        ///
317        /// It is `None` while the plan is still the planner's, and filled in by
318        /// a pass that runs before compilation. Keeping the two apart is what
319        /// lets the planner stay a pure function of the SQL and one catalog
320        /// generation while the program still carries a real plan.
321        chosen: Option<VirtualChoice>,
322    },
323}
324
325/// What a module answered when it was shown the offer.
326#[derive(Clone, Debug, PartialEq)]
327pub struct VirtualChoice {
328    /// The plan number, passed back to the module's `filter`.
329    pub index_number: i32,
330    /// The plan string, passed back to the module's `filter`.
331    pub index_string: String,
332    /// The offer positions whose values feed `filter`, in argument order.
333    pub arguments: Vec<usize>,
334    /// The offer positions the engine must still test for itself.
335    ///
336    /// Everything the module did not take, and everything it took without
337    /// promising to apply. A module that says `omit` is promising; anything
338    /// else and the predicate is tested twice, which is the safe direction.
339    pub recheck: Vec<usize>,
340    /// Whether the module will produce the requested order by itself.
341    pub ordered: bool,
342}
343
344/// One predicate offered to a module, with what it was made of.
345///
346/// The predicate is kept whole beside the constraint because the compiler may
347/// have to test it after all: a module that used the constraint without
348/// promising to apply it leaves the engine responsible for the answer.
349#[derive(Clone, Debug, PartialEq)]
350pub struct VirtualConstraint {
351    /// The constraint as the module is shown it.
352    pub spec: crate::vtab::ConstraintSpec,
353    /// The value on the other side, which becomes an argument to `filter`.
354    pub value: BoundExpr,
355    /// The whole predicate, for the compiler to re-test when it must.
356    pub predicate: BoundExpr,
357    /// The values of `column IN (a, b, c)`, when that is what this constraint
358    /// came from, and empty otherwise.
359    ///
360    /// **An `IN` list is offered as `=`, and the engine runs the module's
361    /// `filter` once for each distinct value**, which is what SQLite does for a
362    /// virtual table. Before it was offered at all, `DELETE FROM docs WHERE rowid
363    /// IN (...)` on an `inillucent_search` table read every row of the table and
364    /// tested each one against the whole list: 4.94 s to delete 2,000 rows of
365    /// 120,000, where deleting them one statement at a time took a fraction of a
366    /// second. `value` holds the first item, so a module asked about the
367    /// constraint sees an ordinary `=`. The term stays among the statement's own
368    /// filters, so a module that ignores the constraint, or one the engine cannot
369    /// drive this way, still returns the right rows.
370    pub in_list: Vec<BoundExpr>,
371}
372
373impl AccessPath {
374    /// Returns a one-line description, which is what `EXPLAIN QUERY PLAN`
375    /// renders and what a performance test asserts on.
376    pub fn describe(&self, table: &str) -> String {
377        self.describe_over(table, None)
378    }
379
380    /// Returns the same line, naming the columns an index seek compares.
381    ///
382    /// **`(a=?)` rather than `(?=?)`.** The reference names the key column, and
383    /// it is the one part of the line a reader uses to tell "this index" from
384    /// "the other index on the same table". The declaration is passed in
385    /// because an access path carries the index's *name* and not its columns -
386    /// which is the right thing for a plan to carry, and the wrong thing to
387    /// render a description from.
388    ///
389    /// @param table - the name the query calls the term
390    /// @param info - the table's declaration, when the caller has it
391    pub fn describe_over(&self, table: &str, info: Option<&TableInfo>) -> String {
392        match self {
393            AccessPath::TableScan { .. } => format!("SCAN {table}"),
394            AccessPath::RowidSeek { .. } => {
395                format!("SEARCH {table} USING INTEGER PRIMARY KEY (rowid=?)")
396            }
397            AccessPath::RowidRange { low, high, .. } => {
398                let bounds = range_detail("rowid", low.is_some(), high.is_some());
399                format!("SEARCH {table} USING INTEGER PRIMARY KEY ({bounds})")
400            }
401            // Every branch is the same one-row lookup, so one line describes
402            // all of them - which is also how a plain equality reads, and an
403            // `IN` list is nothing else once it has been turned into this.
404            AccessPath::RowidSeekUnion { .. } => {
405                format!("SEARCH {table} USING INTEGER PRIMARY KEY (rowid=?)")
406            }
407            AccessPath::Recursive { .. } => format!("SCAN {table} USING RECURSIVE QUEUE"),
408            AccessPath::RecursiveSelf { .. } => format!("SCAN {table}"),
409            AccessPath::VectorProbe { index, depth, .. } => format!(
410                "SEARCH {table} USING VECTOR INDEX {} (k={depth})",
411                String::from_utf8_lossy(index)
412            ),
413            AccessPath::VirtualScan { .. } => format!("SCAN {table} VIRTUAL TABLE INDEX"),
414            AccessPath::Subquery { correlated, .. } => {
415                if *correlated {
416                    format!("CORRELATED SCALAR SUBQUERY {table}")
417                } else {
418                    format!("SCAN {table}")
419                }
420            }
421            AccessPath::IndexSeek {
422                index_name,
423                equalities,
424                low,
425                high,
426                covering,
427                ..
428            } => {
429                let kind = if covering.is_some() {
430                    "COVERING INDEX"
431                } else {
432                    "INDEX"
433                };
434                // A walk with nothing to seek used to be covering by
435                // construction, so this line said so unconditionally. A
436                // partial index and an `INDEXED BY` are walked whole while a
437                // lookup per entry fetches the row, and SQLite says `USING
438                // INDEX` for that: `SELECT * FROM h INDEXED BY h_a` is
439                // `SCAN h USING INDEX h_a` in the pinned 3.53.4 shell.
440                if equalities.is_empty() && low.is_none() && high.is_none() {
441                    return format!(
442                        "SCAN {table} USING {kind} {}",
443                        String::from_utf8_lossy(index_name)
444                    );
445                }
446                let detail = index_seek_detail(
447                    index_name,
448                    info,
449                    equalities.len(),
450                    (low.is_some(), high.is_some()),
451                );
452                format!(
453                    "SEARCH {table} USING {kind} {} ({detail})",
454                    String::from_utf8_lossy(index_name)
455                )
456            }
457            AccessPath::IndexSeekUnion {
458                index_name,
459                branches,
460                covering,
461                ..
462            } => {
463                let kind = if covering.is_some() {
464                    "COVERING INDEX"
465                } else {
466                    "INDEX"
467                };
468                // A branch with the same shape as one already rendered - the
469                // same equality-prefix depth and the same presence of a range
470                // - reads identically, so an `IN` list (every branch the same
471                // bare equality) collapses to the one line a plain equality
472                // would render. A genuine disjunction of differently shaped
473                // branches - the keyset-range case - gets one line per shape,
474                // in the order the branches run.
475                let mut lines: Vec<String> = Vec::new();
476                for branch in branches {
477                    let detail = index_seek_detail(
478                        index_name,
479                        info,
480                        branch.equalities.len(),
481                        (branch.low.is_some(), branch.high.is_some()),
482                    );
483                    let line = format!(
484                        "SEARCH {table} USING {kind} {} ({detail})",
485                        String::from_utf8_lossy(index_name)
486                    );
487                    if !lines.contains(&line) {
488                        lines.push(line);
489                    }
490                }
491                lines.join(" OR ")
492            }
493        }
494    }
495}
496
497/// One FROM term with the path chosen for it.
498#[derive(Clone, Debug, PartialEq)]
499pub struct PlannedSource {
500    /// What the planner estimated this term's path would cost.
501    ///
502    /// It is kept so that a test can assert on the *reason* a plan was chosen
503    /// rather than only on the plan, which is the difference between catching a
504    /// cost-model regression and catching it two releases later.
505    pub cost: f64,
506    /// How many rows the path is estimated to produce.
507    pub rows: f64,
508    /// The statement-wide number every bound expression refers to it by.
509    pub id: usize,
510    /// The table.
511    pub table: TableInfo,
512    /// The name the query calls it.
513    pub alias: Vec<u8>,
514    /// The schema the query wrote before the name, which `EXPLAIN QUERY PLAN` repeats.
515    pub written_schema: Option<Vec<u8>>,
516    /// How its rows are produced.
517    pub path: AccessPath,
518    /// The join that attached it to the term before it.
519    pub join: JoinKind,
520    /// The `ON` condition, when the join is an outer one.
521    ///
522    /// An inner join's condition is an ordinary predicate and is distributed
523    /// with the rest; an outer join's is not, because a row that fails it is
524    /// still emitted, null-extended. Keeping it here rather than in the
525    /// residual list is what stops the two being confused.
526    pub on: Option<BoundExpr>,
527    /// Whether the path this term is read by enforces the whole `ON` condition.
528    ///
529    /// **What decides whether an outer join can be an index nested loop.**
530    /// That operator probes the inner tree by a key and
531    /// null-extends when the probe finds nothing; it has nowhere to test a
532    /// condition the key did not capture, so it may only be used when there is
533    /// nothing left to test. When the key is the whole condition - which
534    /// `ON b.k = a.k` over an index on `b(k)` is - the probe's answer and the
535    /// condition's answer are the same answer.
536    ///
537    /// False for every inner join, where the condition is distributed into the
538    /// statement's terms and re-tested as a residual, and false for an outer
539    /// join whose condition says more than its key does.
540    pub on_enforced: bool,
541    /// A condition the rows entering this `RIGHT` or `FULL` join must meet.
542    ///
543    /// It is the `ON` of the inner joins written before this join that no
544    /// access path enforced. It is tested on the rows of the terms before this
545    /// one, before they are joined, because after the join a null extended row
546    /// would fail it. `None` for every other join.
547    pub before: Option<BoundExpr>,
548}
549
550/// How the rows are grouped and aggregated.
551#[derive(Clone, Copy, Debug, PartialEq, Eq)]
552pub enum AggregationMode {
553    /// No aggregation at all.
554    None,
555    /// One group for the whole input, which produces exactly one row.
556    Whole,
557    /// One group per distinct `GROUP BY` key, produced by sorting first.
558    Grouped,
559}
560
561/// A physical plan for a read-only statement.
562#[derive(Clone, Debug, PartialEq)]
563pub struct PhysicalPlan {
564    /// The FROM terms, in the order the nested loops visit them.
565    pub sources: Vec<PlannedSource>,
566    /// The predicates the loops must still evaluate, one per nesting level.
567    ///
568    /// A predicate is attached to the innermost term it reads, so it is tested
569    /// as soon as it can be rather than after every loop has been entered.
570    pub residuals: Vec<Option<BoundExpr>>,
571    /// A predicate over no columns at all, tested once before the loops.
572    pub constant_filter: Option<BoundExpr>,
573    /// The bound statement the plan came from.
574    pub select: BoundSelect,
575    /// How the rows are aggregated.
576    pub aggregation: AggregationMode,
577    /// Whether the results have to pass through a sorter.
578    pub needs_sort: bool,
579    /// Whether the outermost term is walked backwards.
580    ///
581    /// A B-tree read from its last entry to its first produces exactly the
582    /// reverse of what it produces read forwards, so a descending `ORDER BY`
583    /// over an ascending structure is a direction rather than a sort. Only ever
584    /// set when [`needs_sort`](Self::needs_sort) is false: a plan that sorts
585    /// does not care which way its input arrived.
586    pub reverse: bool,
587    /// Whether the walk already brings the rows of each group together.
588    ///
589    /// Grouping needs adjacency, not order: if every row of a group arrives
590    /// before the next group starts, the aggregate can be finished and emitted
591    /// as the key changes and nothing has to be collected first. A walk whose
592    /// leading keys are exactly the `GROUP BY` columns delivers that, whichever
593    /// direction it runs in.
594    pub grouped_walk: bool,
595    /// Whether the walk already brings duplicate result rows together.
596    ///
597    /// The same property for `DISTINCT`: adjacent duplicates can be dropped by
598    /// comparing each row with the one before it, where a set has to remember
599    /// every row it has seen.
600    pub distinct_walk: bool,
601    /// The later arms of a compound, each with the operator that joined it.
602    pub compounds: Vec<(CompoundOp, PhysicalPlan)>,
603    /// Which optimizations were on when this plan was chosen.
604    ///
605    /// Carried on the plan rather than looked up by the executor, because a
606    /// plan is *cached* and a lever that changed after it was built must not
607    /// change what it does - a plan that consulted the connection at execution
608    /// time would answer one way today and another tomorrow with no
609    /// recompilation in between. The connection throws its compiled statements
610    /// away when a lever moves, which is what makes this field the truth.
611    pub levers: Levers,
612    /// Whether any expression in this plan holds a subquery used as a value.
613    ///
614    /// Decided here because it is a property of the *statement* and not of the
615    /// data, and because the alternative was deciding it per execution: the
616    /// executor folds uncorrelated subqueries on the way into each run, and it
617    /// has to ask this question first every time. Walking the expression tree
618    /// to ask it cost about 0.07 us per execution - measurable against a
619    /// `point.rowid` that takes 0.78 - because `BoundExpr::children` allocates
620    /// a vector per node. Asked once per compiled statement instead, it costs
621    /// nothing a statement runs.
622    pub subqueries: bool,
623}
624
625impl PhysicalPlan {
626    /// Returns the highest statement-wide source id anywhere in the plan.
627    ///
628    /// The compiler sizes its cursor map from this, so a nested block's cursor
629    /// has a slot before the block that encloses it is compiled.
630    pub fn max_source_id(&self) -> usize {
631        let mut highest = 0usize;
632        for source in &self.sources {
633            highest = highest.max(source.id);
634            match &source.path {
635                AccessPath::Subquery { plan, .. } => {
636                    highest = highest.max(plan.max_source_id());
637                }
638                AccessPath::Recursive { seeds, steps, .. } => {
639                    for (_, arm) in seeds.iter().chain(steps.iter()) {
640                        highest = highest.max(arm.max_source_id());
641                    }
642                }
643                _ => {}
644            }
645        }
646        for (_, arm) in &self.compounds {
647            highest = highest.max(arm.max_source_id());
648        }
649        highest
650    }
651
652    /// Returns the lines for the search that finds the rows an `UPDATE` or `DELETE` changes.
653    ///
654    /// SQLite never reports a covering index for a write, so `USING COVERING INDEX` reads `USING INDEX`.
655    pub fn describe_write(&self) -> Vec<String> {
656        describe::write_lines(self)
657    }
658
659    /// Returns the `EXPLAIN QUERY PLAN` lines this plan renders as.
660    pub fn describe(&self) -> Vec<String> {
661        describe::plan_lines(self)
662    }
663}
664
665/// Which planner optimizations are switched on.
666///
667/// An optimization that cannot be switched off cannot be measured. The claim
668/// "the covering-index path made range reads thirty times faster" is a
669/// comparison, and without an arm to compare against it is a comparison with a
670/// build that no longer exists - which is an argument, not evidence.
671///
672/// The shape is SQLite's. `sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS)`
673/// takes a bitmask of optimizations to *disable*, reached through a control
674/// channel rather than through SQL, for exactly this reason: a knob on the SQL
675/// surface is a knob applications start depending on, and then it is not a
676/// measurement device any more, it is a feature with a compatibility story.
677///
678/// Disabling is what the mask names, so zero is the shipped engine and the
679/// default everywhere. A lever added later defaults to on without anybody
680/// having to remember to turn it on.
681#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
682pub struct Levers {
683    /// The optimizations that are turned *off*.
684    disabled: u32,
685}
686
687impl Levers {
688    /// Read a term's columns from the index entry, without fetching the row.
689    pub const COVERING_INDEX: u32 = 1;
690    /// Find the rows an UPDATE or DELETE touches through an index or a rowid,
691    /// rather than by scanning the table.
692    pub const INDEXED_WRITE: u32 = 2;
693    /// Answer an `ORDER BY` by walking a B-tree in its own key order, forwards
694    /// or backwards, instead of sorting every row and throwing most away.
695    pub const ORDERED_WALK: u32 = 4;
696    /// Group and de-duplicate as the rows arrive, when the walk already brings
697    /// equal keys together, instead of collecting every row into a sorter or a
698    /// set first.
699    pub const STREAMING_GROUP: u32 = 8;
700    /// Fold a value written into a scratch register and immediately copied
701    /// into the one instruction that writes it where it was going.
702    pub const FUSED_BYTECODE: u32 = 16;
703
704    /// Reusing a compiled program for SQL text already prepared.
705    ///
706    /// The rearchitecture's design puts a plan cache in the new engine's
707    /// prepare path, and measures it here, on the existing one, first - so the
708    /// mechanism is proved independently of the new storage. It is a lever
709    /// rather than a constant because a speedup that cannot be switched off
710    /// cannot be measured, and because "the cache made prepare six times
711    /// faster" needs an arm to be a claim rather than an assertion.
712    pub const PLAN_CACHE: u32 = 32;
713    /// Build a throwaway structure over an unindexed inner side of a join,
714    /// rather than walking it once per outer row.
715    ///
716    /// What `PRAGMA automatic_index` switches. It is a lever rather than a
717    /// constant for the same reason the others are - an optimisation that
718    /// cannot be switched off cannot be measured - and because SQLite exposes
719    /// exactly this switch under exactly this name, so an application that
720    /// turns it off there has somewhere to turn it off here.
721    pub const AUTOMATIC_INDEX: u32 = 64;
722    /// Walk a scan with no `ORDER BY` from its first entry to its last.
723    ///
724    /// What `PRAGMA reverse_unordered_selects` switches, turned around so that
725    /// zero disabled levers is still the shipped engine: the pragma being ON
726    /// disables this one, and the outermost scan of a query with no `ORDER BY`
727    /// then runs backwards, as SQLite's does.
728    pub const FORWARD_UNORDERED: u32 = 128;
729    /// Every lever this build has.
730    pub const EVERY: u32 = Levers::FORWARD_UNORDERED
731        | Levers::PLAN_CACHE
732        | Levers::COVERING_INDEX
733        | Levers::INDEXED_WRITE
734        | Levers::ORDERED_WALK
735        | Levers::STREAMING_GROUP
736        | Levers::FUSED_BYTECODE
737        | Levers::AUTOMATIC_INDEX;
738
739    /// Returns the shipped configuration: everything on.
740    pub fn all() -> Levers {
741        Levers { disabled: 0 }
742    }
743
744    /// Returns a configuration with the named levers turned off.
745    /// @param mask - the levers to disable
746    pub fn without(mask: u32) -> Levers {
747        Levers {
748            disabled: mask & Levers::EVERY,
749        }
750    }
751
752    /// Returns whether one lever is on.
753    /// @param lever - the lever to ask about
754    pub fn has(self, lever: u32) -> bool {
755        self.disabled & lever == 0
756    }
757
758    /// Returns the mask of what is off, which is what a report prints.
759    pub fn disabled(self) -> u32 {
760        self.disabled
761    }
762
763    /// Returns the names of the levers that are off, for a report.
764    pub fn names_disabled(self) -> Vec<&'static str> {
765        let mut names = Vec::new();
766        if !self.has(Levers::COVERING_INDEX) {
767            names.push("covering-index");
768        }
769        if !self.has(Levers::INDEXED_WRITE) {
770            names.push("indexed-write");
771        }
772        if !self.has(Levers::ORDERED_WALK) {
773            names.push("ordered-walk");
774        }
775        if !self.has(Levers::STREAMING_GROUP) {
776            names.push("streaming-group");
777        }
778        if !self.has(Levers::FUSED_BYTECODE) {
779            names.push("fused-bytecode");
780        }
781        names
782    }
783}
784
785/// Plans a bound SELECT with some optimizations switched off.
786///
787/// The levers travel with the recursion rather than being read from anywhere
788/// global, so a subquery is planned under the same arm as the statement that
789/// contains it. An arm that applied to the outer block and not the inner one
790/// would measure a mixture and report it as one number.
791/// @param select - the bound statement
792/// @param levers - which optimizations are on
793pub fn plan_select_with(select: BoundSelect, levers: Levers) -> PhysicalPlan {
794    let mut select = select;
795    pushdown::push_into_derived_tables(&mut select);
796    let compound_arms = core::mem::take(&mut select.compounds);
797    let (terms, owners) = hint::statement_terms_with_owners(&select);
798    let held_before = hint::terms_held_before_a_right_join(&select, &owners);
799    // The order the terms are visited in is chosen before their paths are, and
800    // then the paths are chosen in that order - because a path may use a value
801    // from a term visited earlier, and which terms those are is exactly what the
802    // order decides.
803    let order = choose_order(&select, &terms, levers);
804    let ordered: Vec<usize> = order.clone();
805    let ids: Vec<usize> = ordered
806        .iter()
807        .filter_map(|position| select.sources.get(*position))
808        .map(|source| source.id)
809        .collect();
810    let mut consumed = vec![false; terms.len()];
811    let mut sources = Vec::with_capacity(select.sources.len());
812    for (level, position) in ordered.iter().enumerate() {
813        let Some(source) = select.sources.get(*position) else {
814            continue;
815        };
816        // **An outer term's rows are not filtered on the way in.** A `WHERE`
817        // predicate over the null-extendable side is applied *after* the join,
818        // because a row that fails it must still produce a null-extended pair
819        // rather than vanish. Letting `choose_path` turn such a predicate into
820        // a seek would do both wrong things at once: filter the rows before the
821        // null extension, and mark the term consumed so it is never re-tested.
822        //
823        // **Its own `ON` condition is a different question.**
824        // An outer join's `ON` decides which inner rows *match*, and a row with
825        // no match is null-extended by the join itself - so seeking the inner
826        // side by an equality the `ON` states returns exactly the matches and
827        // nothing the join needed is lost. Refusing that made
828        // `LEFT JOIN chunk c ON c.document_id = d.id` scan a 60,000-row table
829        // where the same join written `JOIN` seeks it: 138.2 ms against 0.5 ms
830        // for the same ten rows, and the gap grows with the table.
831        //
832        // Two things keep it honest. The `ON` terms are collected into a list
833        // of their own, so nothing in the statement's `WHERE` can be turned
834        // into a seek here and nothing in the statement's `consumed` is marked.
835        // And `on` below still carries the whole condition, so the join re-tests
836        // it - a seek narrows the rows the test runs over and never stands in
837        // for it.
838        //
839        // A subquery, a recursive CTE and a virtual table each still resolve to
840        // what they are, because those are not access-path choices - they are
841        // what the term *is*.
842        let TermPath {
843            path,
844            on_enforced,
845            remaining_on,
846        } = choose_term_path(level, &ids, source, &select, &terms, &mut consumed, levers);
847        let (cost, rows) = path_cost(source, &path);
848        sources.push(PlannedSource {
849            cost,
850            rows,
851            id: source.id,
852            table: (*source.table).clone(),
853            alias: source.alias.clone(),
854            written_schema: source.written_schema.clone(),
855            path,
856            join: source.join,
857            on: match remaining_on {
858                Some(remaining) => remaining,
859                None => is_outer(source.join)
860                    .then(|| source.constraint.clone())
861                    .flatten(),
862            },
863            on_enforced,
864            before: None,
865        });
866    }
867    hold_terms_before_right_joins(&select, &terms, &held_before, &mut consumed, &mut sources);
868    let (residuals, constant_filter) = distribute_residuals(&terms, &consumed, &ids);
869    let aggregation = if !select.group_by.is_empty() {
870        AggregationMode::Grouped
871    } else if !select.aggregates.is_empty() {
872        AggregationMode::Whole
873    } else {
874        AggregationMode::None
875    };
876    // The sort is only needed when the outer term's path does not already
877    // produce the order that was asked for. Walking a B-tree *is* walking it in
878    // key order, and a statement asking for that order has been answered by the
879    // walk - which is the difference between reading fifty rows and reading,
880    // sorting and throwing away six hundred thousand.
881    // A window function sorts the rows into its own order to compute over them,
882    // so whatever order the walk delivered is not the order the result comes
883    // out in - which is why `windows` disqualifies a statement here even though
884    // it has nothing to do with the access path.
885    // Adjacency is a weaker property than order, so it is asked first and for a
886    // wider set of statements: a grouped aggregate can be streamed whether or
887    // not it also answers an ORDER BY.
888    let adjacent = levers.has(Levers::STREAMING_GROUP)
889        && sources.len() == 1
890        && select.windows.is_empty()
891        && select.compounds.is_empty();
892    let outer = sources.first();
893    let grouped_walk = adjacent
894        && aggregation == AggregationMode::Grouped
895        && outer.is_some_and(|outer| grouped_by_walk(&select, outer));
896    let distinct_walk = adjacent && outer.is_some_and(|outer| distinct_by_walk(&select, outer));
897    // A statement that streams its grouping or its de-duplication still comes
898    // out in the order the walk delivered: the rows of a key arrive together,
899    // one output row is emitted per key, and the keys arrive in key order. So
900    // the walk answers the ORDER BY for these too.
901    //
902    // It did not used to. `SELECT DISTINCT category FROM main_table ORDER BY
903    // category` walked the covering index on `(category, key)` - which is
904    // already in `category` order - de-duplicated as the rows arrived, and then
905    // sorted the thirty-two answers through a temporary B-tree anyway. SQLite
906    // reads the same index and does not sort, which is the whole of a 26x
907    // difference on that workload. The same applied to every
908    // `GROUP BY x ORDER BY x`.
909    //
910    // The two are kept apart rather than merged: a statement that is both
911    // grouped and DISTINCT is left to sort, because the de-duplication then
912    // runs on the aggregate output rather than on the walk and the walk's order
913    // is no longer the result's.
914    let streamed_in_order = (grouped_walk && !select.distinct)
915        || (distinct_walk && aggregation == AggregationMode::None);
916    let single = levers.has(Levers::ORDERED_WALK)
917        && sources.len() == 1
918        && select.windows.is_empty()
919        && select.compounds.is_empty()
920        && ((aggregation == AggregationMode::None && !select.distinct) || streamed_in_order);
921    let provided = if single {
922        sources
923            .first()
924            .and_then(|outer| ordering_provided(&select, outer.id, &outer.table, &outer.path))
925    } else {
926        None
927    };
928    let needs_sort = !select.order_by.is_empty() && provided.is_none();
929    let reverse = provided.unwrap_or(false) || reverses_unordered_scan(&select, &sources, levers);
930    let compounds: Vec<(CompoundOp, PhysicalPlan)> = compound_arms
931        .into_iter()
932        .map(|(op, arm)| (op, plan_select_with(arm, levers)))
933        .collect();
934    let subqueries = holds_subquery(&select)
935        || residuals.iter().flatten().any(expression_holds_subquery)
936        || constant_filter
937            .as_ref()
938            .is_some_and(expression_holds_subquery)
939        || compounds.iter().any(|(_op, arm)| arm.subqueries);
940    PhysicalPlan {
941        sources,
942        residuals,
943        constant_filter,
944        select,
945        aggregation,
946        needs_sort,
947        reverse,
948        grouped_walk,
949        distinct_walk,
950        compounds,
951        subqueries,
952        levers,
953    }
954}
955
956/// Returns whether `PRAGMA reverse_unordered_selects` makes the outer scan run
957/// backwards.
958///
959/// SQLite reverses every scan whose direction nothing asks for. This reverses
960/// the outermost term of a query with no `ORDER BY`, no `GROUP BY` and no
961/// `DISTINCT` (a plain aggregate such as `group_concat(a)` is included, because
962/// the order the rows reach it in is visible), which is the order the rows leave in when the inner terms are
963/// seeks that find one row each. A statement that sorts anyway, or whose outer
964/// term is not a scan of a table or an index, is left alone.
965///
966/// @param select - the bound statement
967/// @param sources - the planned FROM terms
968/// @param levers - the optimizations that were on when the plan was chosen
969fn reverses_unordered_scan(
970    select: &BoundSelect,
971    sources: &[PlannedSource],
972    levers: Levers,
973) -> bool {
974    !levers.has(Levers::FORWARD_UNORDERED)
975        && select.order_by.is_empty()
976        && !select.distinct
977        && select.group_by.is_empty()
978        && select.windows.is_empty()
979        && select.compounds.is_empty()
980        && sources.first().is_some_and(|outer| {
981            matches!(
982                outer.path,
983                AccessPath::TableScan { .. }
984                    | AccessPath::RowidRange { .. }
985                    | AccessPath::IndexSeek { .. }
986            )
987        })
988}
989
990/// Returns whether a select holds a subquery used as a value.
991///
992/// Compound arms are not walked here: `plan_select_with` has already taken them
993/// out of `select.compounds` and planned them, and each arm carries its own
994/// answer. A subquery's *block* is not walked either - finding one is enough to
995/// say the plan has one.
996///
997/// @param select - the query to look through
998fn holds_subquery(select: &BoundSelect) -> bool {
999    select.filter.iter().any(expression_holds_subquery)
1000        || select.group_by.iter().any(expression_holds_subquery)
1001        || select.having.iter().any(expression_holds_subquery)
1002        || select
1003            .columns
1004            .iter()
1005            .any(|column| expression_holds_subquery(&column.expr))
1006        || select
1007            .order_by
1008            .iter()
1009            .any(|term| expression_holds_subquery(&term.expr))
1010        || select.limit.iter().any(expression_holds_subquery)
1011        || select.offset.iter().any(expression_holds_subquery)
1012        || select
1013            .values
1014            .iter()
1015            .flatten()
1016            .any(expression_holds_subquery)
1017        // **The aggregate's `FILTER` and its inner `ORDER BY` are walked here
1018        // too as of task-1932 (M6).** This flag is the cheap question
1019        // `subquery::fold` asks before it walks anything, so a statement it
1020        // answers `false` for never folds - and a subquery in an aggregate's
1021        // `FILTER` was therefore left in an unfilled slot, which `translate`
1022        // reports as `unsupported("a correlated subquery used as a value")`.
1023        // The windows below already had all four of theirs.
1024        || select.aggregates.iter().any(|aggregate| {
1025            aggregate.arguments.iter().any(expression_holds_subquery)
1026                || aggregate.filter.iter().any(expression_holds_subquery)
1027                || aggregate
1028                    .order_by
1029                    .iter()
1030                    .any(|term| expression_holds_subquery(&term.expr))
1031        })
1032        || select.windows.iter().any(|window| {
1033            window.arguments.iter().any(expression_holds_subquery)
1034                || window.filter.iter().any(expression_holds_subquery)
1035                || window.partition_by.iter().any(expression_holds_subquery)
1036                || window
1037                    .order_by
1038                    .iter()
1039                    .any(|term| expression_holds_subquery(&term.expr))
1040        })
1041        || select.sources.iter().any(|source| {
1042            source.constraint.iter().any(expression_holds_subquery)
1043                || matches!(&source.rows, SourceRows::Subquery(block) if holds_subquery(block))
1044        })
1045}
1046
1047/// Returns whether an expression holds a subquery, anywhere beneath it.
1048///
1049/// Public because the *write* paths need the same answer and cannot get it from
1050/// a plan: a `VALUES` list and an `UPDATE`'s assignments are evaluated without
1051/// one. They ask this once when the statement is compiled, for the same reason
1052/// `PhysicalPlan::subqueries` is decided once - the question is about the
1053/// statement, and asking it per execution walks a tree and allocates.
1054///
1055/// @param expr - the expression to look through
1056pub fn expression_holds_subquery(expr: &BoundExpr) -> bool {
1057    matches!(expr, BoundExpr::Subquery { .. })
1058        || expr
1059            .children()
1060            .iter()
1061            .any(|child| expression_holds_subquery(child))
1062}
1063
1064/// Returns whether the walk brings the rows of each `GROUP BY` key together.
1065///
1066/// Grouping needs adjacency rather than order, so the direction does not
1067/// matter: what matters is that the walk's leading keys are exactly the group
1068/// columns. Exactly, not merely a superset - a walk ordered by `(a, b)` groups
1069/// `a` and groups `(a, b)`, and does not group `b`.
1070///
1071/// The collation does matter. Grouping compares keys with the result collation
1072/// and the walk compares them with the structure's, so a `NOCASE` index does
1073/// not group a `BINARY` key: it would put `Ada` and `ADA` next to each other
1074/// and the grouping would then treat them as one.
1075/// @param select - the bound statement
1076/// @param outer - the planned outer term
1077fn grouped_by_walk(select: &BoundSelect, outer: &PlannedSource) -> bool {
1078    if select.group_by.is_empty() {
1079        return false;
1080    }
1081    let Some(key) = path_ordering(&outer.table, &outer.path) else {
1082        return false;
1083    };
1084    let mut wanted: Vec<(OrderedBy, Collation)> = Vec::new();
1085    for expr in &select.group_by {
1086        let Some(named) = walk_key_of(expr, outer.id, &outer.table) else {
1087            return false;
1088        };
1089        let collation = crate::bind::result_collation(expr);
1090        if !wanted.iter().any(|(held, _)| *held == named) {
1091            wanted.push((named, collation));
1092        }
1093    }
1094    covers_prefix(&key, &wanted)
1095}
1096
1097/// Returns whether the walk brings duplicate result rows together.
1098///
1099/// The same rule as [`grouped_by_walk`], over the result columns rather than
1100/// the group ones - and it is only asked when there is no grouping, because a
1101/// `DISTINCT` over aggregates is distinct over values the walk never saw.
1102/// @param select - the bound statement
1103/// @param outer - the planned outer term
1104fn distinct_by_walk(select: &BoundSelect, outer: &PlannedSource) -> bool {
1105    if !select.distinct || !select.group_by.is_empty() || !select.aggregates.is_empty() {
1106        return false;
1107    }
1108    let Some(key) = path_ordering(&outer.table, &outer.path) else {
1109        return false;
1110    };
1111    let mut wanted: Vec<(OrderedBy, Collation)> = Vec::new();
1112    for column in &select.columns {
1113        let Some(named) = walk_key_of(&column.expr, outer.id, &outer.table) else {
1114            return false;
1115        };
1116        let collation = crate::bind::result_collation(&column.expr);
1117        if !wanted.iter().any(|(held, _)| *held == named) {
1118            wanted.push((named, collation));
1119        }
1120    }
1121    covers_prefix(&key, &wanted)
1122}
1123
1124/// Returns whether a set of keys is exactly the walk's leading keys.
1125///
1126/// A key an equality pinned counts as held: it has one value for every row the
1127/// walk returns, so it is constant across the whole scan and cannot separate
1128/// two rows that are otherwise equal.
1129/// @param key - what the walk is ordered by
1130/// @param wanted - the keys that have to arrive together, with their collations
1131fn covers_prefix(key: &PathOrdering, wanted: &[(OrderedBy, Collation)]) -> bool {
1132    let free: Vec<&(OrderedBy, Collation)> = wanted
1133        .iter()
1134        .filter(|(named, _)| !key.pinned.contains(named))
1135        .collect();
1136    if free.len() > key.columns.len() {
1137        return false;
1138    }
1139    let prefix = match key.columns.get(..free.len()) {
1140        Some(prefix) => prefix,
1141        None => return false,
1142    };
1143    free.iter().all(|(named, collation)| {
1144        prefix
1145            .iter()
1146            .any(|(held, _, held_collation)| held == named && held_collation == collation)
1147    })
1148}
1149
1150/// Returns which of the walk's keys an expression names, if it names one.
1151/// @param expr - the expression to resolve
1152/// @param id - the outer term's source id
1153/// @param table - the table being walked
1154fn walk_key_of(expr: &BoundExpr, id: usize, table: &TableInfo) -> Option<OrderedBy> {
1155    let mut expr = expr;
1156    while let BoundExpr::Collate { operand, .. } = expr {
1157        expr = operand;
1158    }
1159    match expr {
1160        BoundExpr::Column { source, column, .. } if *source == id => {
1161            Some(named_key(table, OrderedBy::Column(*column)))
1162        }
1163        BoundExpr::Rowid { source } if *source == id => Some(OrderedBy::Rowid),
1164        _ => None,
1165    }
1166}
1167
1168/// What a term of an `ORDER BY` names.
1169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1170enum OrderedBy {
1171    /// A column of the table, by its declared position.
1172    Column(u16),
1173    /// The row's key.
1174    Rowid,
1175}
1176
1177/// The order one access path's own walk produces.
1178struct PathOrdering {
1179    /// What the walk is ordered by, in order, with each key's direction and the
1180    /// collation the structure compares it with.
1181    columns: Vec<(OrderedBy, bool, Collation)>,
1182    /// What an equality has pinned to a single value, which therefore holds
1183    /// still across the whole walk and cannot affect its order.
1184    pinned: Vec<OrderedBy>,
1185}
1186
1187/// Returns whether the outer term's path already produces the `ORDER BY`, and
1188/// whether it has to be walked backwards to do it.
1189///
1190/// `None` means it does not and the rows have to go through a sorter.
1191/// `Some(false)` means a forward walk answers it, `Some(true)` a backward one.
1192///
1193/// The rules are narrow on purpose, because getting this wrong returns rows in
1194/// the wrong order and nothing about the result looks wrong:
1195///
1196/// - every `ORDER BY` term is a plain column of the outer term, or its rowid;
1197/// - the path is one whose order is a key's - every rowid path, and an index
1198///   seek over whatever columns the equalities did not pin;
1199/// - the directions agree, all with the structure or all against it, because a
1200///   B-tree can be read either way but not both at once;
1201/// - the collation is the one the structure holds the column in;
1202/// - the NULLs land where the structure puts them, which for the SQL defaults
1203///   they already do: first ascending, last descending, exactly as an index
1204///   holds them.
1205///
1206/// A column an equality pinned is skipped rather than matched: it holds one
1207/// value for every row the path returns, so ordering by it changes nothing.
1208/// @param select - the bound statement, for its ORDER BY
1209/// @param outer - the planned outer term
1210fn ordering_provided(
1211    select: &BoundSelect,
1212    id: usize,
1213    table: &TableInfo,
1214    path: &AccessPath,
1215) -> Option<bool> {
1216    if select.order_by.is_empty() {
1217        return Some(false);
1218    }
1219    let key = path_ordering(table, path)?;
1220    let mut reverse: Option<bool> = None;
1221    let mut at = 0usize;
1222    for term in &select.order_by {
1223        // `ORDER BY name COLLATE NOCASE` binds to a `Collate` around the
1224        // column, and the collation it names is already on the term - so the
1225        // wrapper is unwrapped rather than refused, or the one case an index
1226        // exists precisely to answer would be the one case that sorted.
1227        let mut expr = &term.expr;
1228        while let BoundExpr::Collate { operand, .. } = expr {
1229            expr = operand;
1230        }
1231        let named = match expr {
1232            BoundExpr::Column { source, column, .. } if *source == id => {
1233                named_key(table, OrderedBy::Column(*column))
1234            }
1235            BoundExpr::Rowid { source } if *source == id => OrderedBy::Rowid,
1236            _ => return None,
1237        };
1238        let descending = matches!(term.order, SortOrder::Descending);
1239        // The binder has already defaulted this, so what is left is a written
1240        // placement - and only the one the structure already produces can be
1241        // answered by a walk: an index holds NULLs first, so a forward walk is
1242        // NULLS FIRST and a backward one is NULLS LAST.
1243        let natural = match term.nulls {
1244            NullOrder::First => !descending,
1245            NullOrder::Last => descending,
1246        };
1247        if !natural {
1248            return None;
1249        }
1250        if key.pinned.contains(&named) {
1251            continue;
1252        }
1253        let (held, held_descending, held_collation) = key.columns.get(at).copied()?;
1254        if held != named || held_collation != term.collation {
1255            return None;
1256        }
1257        let walk = descending != held_descending;
1258        match reverse {
1259            None => reverse = Some(walk),
1260            Some(existing) if existing == walk => {}
1261            Some(_) => return None,
1262        }
1263        at = at.saturating_add(1);
1264    }
1265    Some(reverse.unwrap_or(false))
1266}
1267
1268/// Returns the order one access path's walk produces, if it produces one.
1269/// @param table - the table being read
1270/// @param path - the chosen path
1271fn path_ordering(table: &TableInfo, path: &AccessPath) -> Option<PathOrdering> {
1272    match path {
1273        // A table B-tree is keyed by rowid, and a range over it is a slice of
1274        // that same walk.
1275        AccessPath::TableScan { .. } | AccessPath::RowidRange { .. } => Some(PathOrdering {
1276            columns: rowid_key(table),
1277            pinned: Vec::new(),
1278        }),
1279        // One row is in every order at once.
1280        AccessPath::RowidSeek { .. } => Some(PathOrdering {
1281            columns: Vec::new(),
1282            pinned: Vec::new(),
1283        }),
1284        AccessPath::IndexSeek {
1285            index_name,
1286            equalities,
1287            ..
1288        } => {
1289            let index = table
1290                .indexes
1291                .iter()
1292                .find(|candidate| candidate.name == *index_name)?;
1293            let mut columns: Vec<(OrderedBy, bool, Collation)> = Vec::new();
1294            let mut pinned: Vec<OrderedBy> = Vec::new();
1295            for (at, key_column) in index.columns.iter().enumerate() {
1296                // An expression key orders by something no ORDER BY term here
1297                // can name, so the walk stops describing itself at that point.
1298                let Some(column) = key_column.plain_column() else {
1299                    break;
1300                };
1301                let named = named_key(table, OrderedBy::Column(column));
1302                let collation = collation_of(&key_column.collation);
1303                if at < equalities.len() {
1304                    pinned.push(named);
1305                    continue;
1306                }
1307                columns.push((named, key_column.descending, collation));
1308            }
1309            // Every index entry ends with the row's key, so the walk is a total
1310            // order even where the indexed columns tie.
1311            columns.push((OrderedBy::Rowid, false, Collation::Binary));
1312            Some(PathOrdering { columns, pinned })
1313        }
1314        // A branch that needs de-duplicating (an `IN` list) has no order:
1315        // list values are probed in whatever order they were written, not
1316        // index order. A branch that does not - the keyset-range shape,
1317        // proven disjoint at plan time - runs each branch in the index's own
1318        // order and the branches themselves in that same order, so the whole
1319        // union reads exactly as an unconstrained walk of the index would: no
1320        // column is pinned, because no column has one value across every
1321        // branch.
1322        AccessPath::IndexSeekUnion {
1323            index_name,
1324            dedup: false,
1325            ..
1326        } => {
1327            let index = table
1328                .indexes
1329                .iter()
1330                .find(|candidate| candidate.name == *index_name)?;
1331            let mut columns: Vec<(OrderedBy, bool, Collation)> = Vec::new();
1332            for key_column in &index.columns {
1333                let Some(column) = key_column.plain_column() else {
1334                    break;
1335                };
1336                let named = named_key(table, OrderedBy::Column(column));
1337                let collation = collation_of(&key_column.collation);
1338                columns.push((named, key_column.descending, collation));
1339            }
1340            columns.push((OrderedBy::Rowid, false, Collation::Binary));
1341            Some(PathOrdering {
1342                columns,
1343                pinned: Vec::new(),
1344            })
1345        }
1346        _ => None,
1347    }
1348}
1349
1350/// Returns the ordering a rowid walk produces.
1351/// @param table - the table being walked
1352fn rowid_key(table: &TableInfo) -> Vec<(OrderedBy, bool, Collation)> {
1353    let _ = table;
1354    vec![(OrderedBy::Rowid, false, Collation::Binary)]
1355}
1356
1357/// Returns the one name a key goes by.
1358///
1359/// `INTEGER PRIMARY KEY` is the rowid under another name, so a statement that
1360/// ordered by the declared column and one that ordered by `rowid` asked for the
1361/// same walk. Folding the two spellings into one here is what lets the rest of
1362/// the comparison be an equality.
1363/// @param table - the table the column belongs to
1364/// @param named - the key as the statement or the index spelled it
1365fn named_key(table: &TableInfo, named: OrderedBy) -> OrderedBy {
1366    match named {
1367        OrderedBy::Column(column) if table.rowid_alias == Some(column) => OrderedBy::Rowid,
1368        other => other,
1369    }
1370}
1371
1372/// Returns the order the FROM terms are visited in.
1373///
1374/// The legality rule is the whole of the difficulty. An outer join's rows
1375/// depend on the terms it was written against: a `LEFT JOIN` cannot be visited
1376/// before the term it null-extends, and neither side of one can cross it. A
1377/// `CROSS JOIN` is SQLite's documented instruction not to reorder at all. So a
1378/// term may only move within the run of ordinary joins it belongs to, and the
1379/// enumeration is over those runs rather than over the whole list.
1380///
1381/// Inside a run the search is exhaustive while that is affordable - the runs
1382/// that occur in practice are two to five terms - and falls back to the written
1383/// order beyond, because a greedy answer that is worse than the written order
1384/// is worse than not reordering at all.
1385fn choose_order(select: &BoundSelect, terms: &[BoundExpr], levers: Levers) -> Vec<usize> {
1386    let count = select.sources.len();
1387    if count < 2 {
1388        return (0..count).collect();
1389    }
1390    let mut order = Vec::with_capacity(count);
1391    let mut run: Vec<usize> = Vec::new();
1392    for position in 0..count {
1393        let pins = select
1394            .sources
1395            .get(position)
1396            .is_some_and(|source| matches!(source.join, JoinKind::Cross) || is_outer(source.join));
1397        if pins {
1398            order.extend(best_order(select, terms, &run, levers));
1399            run.clear();
1400            order.push(position);
1401            continue;
1402        }
1403        run.push(position);
1404    }
1405    order.extend(best_order(select, terms, &run, levers));
1406    order
1407}
1408
1409/// Returns the cheapest visiting order for one run of reorderable terms.
1410fn best_order(
1411    select: &BoundSelect,
1412    terms: &[BoundExpr],
1413    run: &[usize],
1414    levers: Levers,
1415) -> Vec<usize> {
1416    // Eight terms is 40,320 orders, which is milliseconds; beyond that the
1417    // written order stands rather than a guess being substituted for it.
1418    if run.len() < 2 || run.len() > 8 {
1419        return run.to_vec();
1420    }
1421    let mut best: Option<(f64, Vec<usize>)> = None;
1422    let mut candidate = run.to_vec();
1423    permute(&mut candidate, 0, &mut |order| {
1424        let cost = order_cost(select, terms, order, levers);
1425        let better = best
1426            .as_ref()
1427            .is_none_or(|(existing, _)| cost < *existing - 1e-9);
1428        if better {
1429            best = Some((cost, order.to_vec()));
1430        }
1431    });
1432    best.map(|(_, order)| order).unwrap_or_else(|| run.to_vec())
1433}
1434
1435/// Calls a closure with every permutation of a slice.
1436fn permute(order: &mut Vec<usize>, at: usize, visit: &mut impl FnMut(&[usize])) {
1437    if at >= order.len() {
1438        visit(order);
1439        return;
1440    }
1441    for index in at..order.len() {
1442        order.swap(at, index);
1443        permute(order, at.saturating_add(1), visit);
1444        order.swap(at, index);
1445    }
1446}
1447
1448/// Returns what one visiting order is estimated to cost.
1449///
1450/// The loops are nested, so each term's cost is multiplied by the rows every
1451/// term before it produced - which is the whole reason the order matters, and
1452/// why putting the most selective term first is usually right and sometimes
1453/// spectacularly wrong.
1454fn order_cost(select: &BoundSelect, terms: &[BoundExpr], order: &[usize], levers: Levers) -> f64 {
1455    let ids: Vec<usize> = order
1456        .iter()
1457        .filter_map(|position| select.sources.get(*position))
1458        .map(|source| source.id)
1459        .collect();
1460    let mut consumed = vec![false; terms.len()];
1461    let mut total = 0.0f64;
1462    let mut outer_rows = 1.0f64;
1463    for (level, position) in order.iter().enumerate() {
1464        let Some(source) = select.sources.get(*position) else {
1465            continue;
1466        };
1467        let path = choose_path(level, &ids, source, select, terms, &mut consumed, levers);
1468        let (cost, rows) = path_cost(source, &path);
1469        total += outer_rows * cost;
1470        outer_rows *= rows.max(1.0);
1471    }
1472    total
1473}
1474
1475/// Returns the vector probe a `TopN` over a distance can use, when it can.
1476///
1477/// **Every condition here is a way the rewrite would change the answer.** The
1478/// index returns `k` candidates and nothing else, so the query has to be asking
1479/// for the nearest `k` of *this* table by *this* measure and by nothing else:
1480///
1481/// - one FROM term, because a join's other side may multiply or drop rows and
1482///   the k the index was asked for would then be the wrong k;
1483/// - the only `ORDER BY` term, ascending, so the index's order and the query's
1484///   are the same order;
1485/// - a `LIMIT` that is a literal, because the index has to be told how deep to
1486///   go before the statement runs;
1487/// - no `OFFSET`, no `GROUP BY`, no aggregate and no `DISTINCT`, each of which
1488///   reads rows the top k does not contain;
1489/// - and a probe that reads no column, because a per-row probe is a different
1490///   query - the index answers one question, not one per row;
1491/// - and the `ORDER BY` function names the distance the index minimises
1492///   (`IndexInfo::metric`), or it falls back to scan-and-sort instead.
1493///
1494/// A `WHERE` clause is *allowed*: the residual is tested over the candidates,
1495/// which is what SQLite does with a partial index and what pgvector's own
1496/// documentation warns about - a narrow filter over an approximate index can
1497/// return fewer than `k` rows. It is the caller's query and this does not
1498/// second-guess it.
1499///
1500/// @param id - the FROM term's statement-wide number
1501/// @param position - where it sits in the join order
1502/// @param source - the term
1503/// @param select - the whole statement, for its `ORDER BY` and `LIMIT`
1504fn vector_path(
1505    id: usize,
1506    position: usize,
1507    source: &BoundSource,
1508    select: &BoundSelect,
1509) -> Option<AccessPath> {
1510    if position != 0 || select.sources.len() != 1 {
1511        return None;
1512    }
1513    if select.distinct
1514        || !select.group_by.is_empty()
1515        || !select.aggregates.is_empty()
1516        || select.offset.is_some()
1517        || !select.compounds.is_empty()
1518    {
1519        return None;
1520    }
1521    let [term] = select.order_by.as_slice() else {
1522        return None;
1523    };
1524    if term.order != crate::ast::SortOrder::Ascending {
1525        return None;
1526    }
1527    let Some(BoundExpr::Integer(depth)) = select.limit.as_ref() else {
1528        return None;
1529    };
1530    let depth = usize::try_from(*depth).ok().filter(|held| *held > 0)?;
1531    let BoundExpr::Function {
1532        func, arguments, ..
1533    } = &term.expr
1534    else {
1535        return None;
1536    };
1537    let wanted = match func {
1538        crate::function::ScalarFunc::VectorDistanceCos => crate::catalog_view::IndexMetric::Cosine,
1539        crate::function::ScalarFunc::VectorDistanceL2 => crate::catalog_view::IndexMetric::L2,
1540        _ => return None,
1541    };
1542    let [BoundExpr::Column {
1543        source: held,
1544        column,
1545        ..
1546    }, probe] = arguments.as_slice()
1547    else {
1548        return None;
1549    };
1550    if *held != id || terms::reads_a_column(probe) {
1551        return None;
1552    }
1553    let index = source.table.indexes.iter().find(|held| {
1554        held.origin == crate::catalog_view::IndexOrigin::Module
1555            && held.metric == Some(wanted)
1556            && held
1557                .columns
1558                .first()
1559                .is_some_and(|first| first.column == Some(*column))
1560    })?;
1561    Some(AccessPath::VectorProbe {
1562        root: source.table.root,
1563        index: index.name.clone(),
1564        probe: Box::new(probe.clone()),
1565        depth,
1566    })
1567}
1568
1569/// Returns what one term's path costs, and how many rows it produces.
1570fn path_cost(source: &BoundSource, path: &AccessPath) -> (f64, f64) {
1571    let rows = estimated_rows(&source.table);
1572    match path {
1573        AccessPath::TableScan { .. } => (cost::scan_cost(rows), rows),
1574        // A module prices its own scan, and the planner cannot ask it here
1575        // without making the plan depend on run-time state. What it can do is
1576        // reward an offer: a virtual table that was given a constraint will be
1577        // cheaper than one that was not, whatever the module then says.
1578        AccessPath::VirtualScan { offer, .. } => {
1579            let usable = offer.iter().filter(|item| item.spec.usable).count();
1580            let rows = if usable == 0 {
1581                rows
1582            } else {
1583                rows / (usable as f64 * 8.0)
1584            };
1585            (cost::scan_cost(rows.max(1.0)), rows.max(1.0))
1586        }
1587        // The index returns `depth` rows and the walk visits exactly those, so
1588        // the cost is a descent per candidate and the row count is the depth -
1589        // which is what makes it beat a scan on a table of any size and lose to
1590        // one on a table smaller than `k`.
1591        AccessPath::VectorProbe { depth, .. } => {
1592            let matches = (*depth as f64).min(rows).max(1.0);
1593            (cost::search_cost(rows, matches, true), matches)
1594        }
1595        AccessPath::RowidSeek { .. } => (cost::search_cost(rows, 1.0, true), 1.0),
1596        // A search per key, each a descent of the same tree - which is
1597        // exactly what running them one after another actually costs, and is
1598        // why a list long enough to be worth a scan instead gets priced that
1599        // way on its own, with no separate penalty needed for how many
1600        // branches there are.
1601        AccessPath::RowidSeekUnion { keys, .. } => {
1602            let branches = keys.len().max(1) as f64;
1603            (cost::search_cost(rows, 1.0, true) * branches, branches)
1604        }
1605        AccessPath::RowidRange { low, high, .. } => {
1606            let bounds = usize::from(low.is_some()) + usize::from(high.is_some());
1607            let mut matches = rows;
1608            for _ in 0..bounds {
1609                matches /= cost::RANGE_SHARE;
1610            }
1611            let matches = matches.max(1.0);
1612            (cost::search_cost(rows, matches, true), matches)
1613        }
1614        AccessPath::IndexSeek {
1615            index_name,
1616            equalities,
1617            low,
1618            high,
1619            covering,
1620            ..
1621        } => {
1622            let bounds = usize::from(low.is_some()) + usize::from(high.is_some());
1623            index_seek_cost(
1624                source,
1625                rows,
1626                index_name,
1627                equalities.len(),
1628                bounds,
1629                covering.is_some(),
1630            )
1631        }
1632        // A union is priced by summing one branch's cost over every branch -
1633        // each is a full descent of the same tree, so the total is exactly
1634        // what running them one after another costs, and a branch count large
1635        // enough to make that expensive is a branch count large enough for a
1636        // scan to win the comparison on its own.
1637        AccessPath::IndexSeekUnion {
1638            index_name,
1639            branches,
1640            covering,
1641            ..
1642        } => {
1643            let mut total_cost = 0.0f64;
1644            let mut total_matches = 0.0f64;
1645            for branch in branches {
1646                let bounds = usize::from(branch.low.is_some()) + usize::from(branch.high.is_some());
1647                let (branch_cost, branch_matches) = index_seek_cost(
1648                    source,
1649                    rows,
1650                    index_name,
1651                    branch.equalities.len(),
1652                    bounds,
1653                    covering.is_some(),
1654                );
1655                total_cost += branch_cost;
1656                total_matches += branch_matches;
1657            }
1658            (total_cost, total_matches.max(1.0))
1659        }
1660        // A materialised term is built once and then scanned; the build is
1661        // charged where it happens, which is the block that fills it.
1662        AccessPath::Subquery { .. } | AccessPath::Recursive { .. } => (cost::scan_cost(rows), rows),
1663        AccessPath::RecursiveSelf { .. } => (1.0, 1.0),
1664    }
1665}
1666
1667/// Returns what one seek over an index costs, and how many rows it produces.
1668///
1669/// Shared by [`AccessPath::IndexSeek`] and each branch of an
1670/// [`AccessPath::IndexSeekUnion`], which price identically - a union is
1671/// priced by summing this over its branches.
1672/// @param source - the FROM term the index belongs to
1673/// @param rows - the table's estimated row count
1674/// @param index_name - the index being priced
1675/// @param equalities - how many leading columns the seek's equality prefix pins
1676/// @param bounds - how many range bounds the seek carries after the prefix
1677/// @param covering - whether the seek reads entries rather than fetching rows
1678fn index_seek_cost(
1679    source: &BoundSource,
1680    rows: f64,
1681    index_name: &[u8],
1682    equalities: usize,
1683    bounds: usize,
1684    covering: bool,
1685) -> (f64, f64) {
1686    let index = source
1687        .table
1688        .indexes
1689        .iter()
1690        .find(|candidate| candidate.name == index_name);
1691    let matches = index_matches(index, rows, equalities, bounds);
1692    let Some(index) = index else {
1693        return (cost::search_cost(rows, matches, false), matches);
1694    };
1695    if !covering {
1696        return (cost::search_cost(rows, matches, false), matches);
1697    }
1698    // A covering path reads entries rather than rows, and an entry is the
1699    // indexed columns plus the key rather than the whole row. Cost is bytes
1700    // touched, so the narrower shape is the saving - and it is the whole
1701    // reason a covering scan of a two-column index beats a table scan of a
1702    // five-column table when there is no predicate at all to narrow either of
1703    // them.
1704    let width = cost::entry_share(index.columns.len(), source.table.columns.len());
1705    (cost::search_cost(rows, matches * width, true), matches)
1706}
1707
1708/// Returns how many rows a table is estimated to hold.
1709fn estimated_rows(table: &TableInfo) -> f64 {
1710    match table.analysed_rows {
1711        Some(rows) if rows > 0 => rows as f64,
1712        // A measured zero is a real answer, and so is an unmeasured table: the
1713        // first is empty and the second is assumed large. Collapsing them would
1714        // make an `ANALYZE` on an empty table look like no `ANALYZE` at all.
1715        Some(_) => 1.0,
1716        None => cost::DEFAULT_ROWS,
1717    }
1718}
1719
1720/// Returns how many rows an index search is estimated to return.
1721fn index_matches(index: Option<&IndexInfo>, rows: f64, equalities: usize, bounds: usize) -> f64 {
1722    // **A partial index walked whole returns what it holds.**
1723    // With nothing to seek to, every other arm below prices this as a walk of
1724    // the table - which is what it would be for an ordinary index, and is not
1725    // what it is for one holding only the rows a predicate accepted.
1726    if equalities == 0 && bounds == 0 {
1727        if let Some(index) = index {
1728            if index.partial_sql.is_some() {
1729                if let Some(held) = index.analysed_rows {
1730                    return (held as f64).max(1.0);
1731                }
1732            }
1733        }
1734    }
1735    let mut matches = match index {
1736        // Measured: the average number of rows sharing the prefix the search
1737        // pinned down. This is the number `ANALYZE` exists to supply.
1738        Some(index) if !index.prefix_rows.is_empty() && equalities > 0 => index
1739            .prefix_rows
1740            .get(equalities.saturating_sub(1))
1741            .copied()
1742            .map(|value| value as f64)
1743            .unwrap_or(rows),
1744        // Unmeasured: a unique index pins one row.
1745        Some(index) if index.unique && equalities >= index.columns.len() => 1.0,
1746        // Unmeasured, not unique: SQLite's own default, which is an absolute
1747        // count rather than a share of the table. A column somebody indexed and
1748        // then compared for equality has many distinct values - that is why it
1749        // was indexed - so the number of rows behind one value does not grow
1750        // with the table the way a fraction does.
1751        Some(_) if equalities > 0 => cost::default_equality_rows(equalities, rows),
1752        _ => {
1753            let mut estimate = rows;
1754            for _ in 0..equalities {
1755                estimate /= cost::EQUALITY_SHARE;
1756            }
1757            estimate
1758        }
1759    };
1760    // Once per bound, not once per range. SQLite reduces the estimate by a
1761    // factor for the lower bound and again for the upper, which is why
1762    // `BETWEEN` is treated as sixteen times more selective than a bare `>` -
1763    // and treating them alike made a two-sided range look like a quarter of the
1764    // table, which is a quarter no join order can beat a scan with.
1765    for _ in 0..bounds {
1766        matches /= cost::RANGE_SHARE;
1767    }
1768    matches.max(1.0)
1769}
1770
1771/// Returns whether a join keeps rows that match nothing on the other side.
1772pub fn is_outer(join: JoinKind) -> bool {
1773    matches!(join, JoinKind::Left | JoinKind::Right | JoinKind::Full)
1774}
1775
1776/// Splits `a AND b AND c` into its terms.
1777///
1778/// Only `AND` is split. Splitting an `OR` would produce terms that are not
1779/// individually true of every row the expression accepts, which is the classic
1780/// way to lose rows.
1781pub fn split_conjunction(expr: &BoundExpr, into: &mut Vec<BoundExpr>) {
1782    match expr {
1783        BoundExpr::And(left, right) => {
1784            split_conjunction(left, into);
1785            split_conjunction(right, into);
1786        }
1787        // `x BETWEEN a AND b` *is* `x >= a AND x <= b`, so splitting it lets an
1788        // index range be found where otherwise the whole thing sat in the
1789        // residual and the table was scanned. It is split only when `x` is a
1790        // column, which is both the case that can drive an index and the case
1791        // where evaluating the operand twice cannot change an answer: a
1792        // volatile expression tested twice is a different question.
1793        BoundExpr::Between {
1794            negated: false,
1795            operand,
1796            low,
1797            high,
1798            low_affinity,
1799            low_collation,
1800            high_affinity,
1801            high_collation,
1802        } if matches!(
1803            **operand,
1804            BoundExpr::Column { .. } | BoundExpr::Rowid { .. }
1805        ) =>
1806        {
1807            // Each half keeps the affinity and collation of its own bound,
1808            // which is what SQLite's two comparisons use (task-2088). The
1809            // `between-index*` cases in `differential-part8/task2088.cases`
1810            // grade this path with and without `INDEXED BY`.
1811            into.push(BoundExpr::Compare {
1812                op: BinaryOp::GreaterEqual,
1813                left: operand.clone(),
1814                right: low.clone(),
1815                affinity: *low_affinity,
1816                collation: *low_collation,
1817            });
1818            into.push(BoundExpr::Compare {
1819                op: BinaryOp::LessEqual,
1820                left: operand.clone(),
1821                right: high.clone(),
1822                affinity: *high_affinity,
1823                collation: *high_collation,
1824            });
1825        }
1826        other => into.push(other.clone()),
1827    }
1828}
1829
1830/// Attaches each unconsumed predicate to the innermost term it reads.
1831///
1832/// A predicate that reads only FROM terms belonging to an *enclosing* block is
1833/// constant for the whole of this block: the outer cursors are positioned
1834/// before it starts and do not move while it runs, so it is tested once before
1835/// the loops rather than once per row.
1836fn distribute_residuals(
1837    terms: &[BoundExpr],
1838    consumed: &[bool],
1839    ids: &[usize],
1840) -> (Vec<Option<BoundExpr>>, Option<BoundExpr>) {
1841    let levels = ids.len();
1842    let mut residuals: Vec<Option<BoundExpr>> = vec![None; levels];
1843    let mut constant: Option<BoundExpr> = None;
1844    for (index, term) in terms.iter().enumerate() {
1845        if consumed.get(index).copied().unwrap_or(false) {
1846            continue;
1847        }
1848        let mut used = Vec::new();
1849        term.sources_used(&mut used);
1850        let level = used
1851            .iter()
1852            .filter_map(|source| ids.iter().position(|id| id == source))
1853            .max();
1854        // A term that reads no table but calls `random()` or another function
1855        // whose answer changes between calls is tested for every row, as SQLite
1856        // does. Treated as a constant it would be tested once for the statement.
1857        let level = match level {
1858            None if levels > 0 && pushdown::calls_a_volatile_function(term) => Some(levels - 1),
1859            other => other,
1860        };
1861        match level {
1862            Some(level) if level < levels => {
1863                if let Some(slot) = residuals.get_mut(level) {
1864                    *slot = Some(match slot.take() {
1865                        Some(existing) => {
1866                            BoundExpr::And(Box::new(existing), Box::new(term.clone()))
1867                        }
1868                        None => term.clone(),
1869                    });
1870                }
1871            }
1872            _ => {
1873                constant = Some(match constant.take() {
1874                    Some(existing) => BoundExpr::And(Box::new(existing), Box::new(term.clone())),
1875                    None => term.clone(),
1876                });
1877            }
1878        }
1879    }
1880    (residuals, constant)
1881}
1882
1883/// Chooses the access path for one FROM term.
1884fn choose_path(
1885    position: usize,
1886    ids: &[usize],
1887    source: &BoundSource,
1888    select: &BoundSelect,
1889    terms: &[BoundExpr],
1890    consumed: &mut [bool],
1891    levers: Levers,
1892) -> AccessPath {
1893    match &source.rows {
1894        SourceRows::Subquery(block) => {
1895            let width = block.columns.len();
1896            let correlated = !block.correlations.is_empty();
1897            let affinities = derived_affinities(block);
1898            return AccessPath::Subquery {
1899                plan: Box::new(plan_select_with((**block).clone(), levers)),
1900                width,
1901                correlated,
1902                affinities,
1903            };
1904        }
1905        SourceRows::Recursive(body) => {
1906            let width = body.seeds.first().map_or(0, |(_, arm)| arm.columns.len());
1907            return AccessPath::Recursive {
1908                seeds: body
1909                    .seeds
1910                    .iter()
1911                    .map(|(op, arm)| (*op, plan_select_with(arm.clone(), levers)))
1912                    .collect(),
1913                steps: body
1914                    .steps
1915                    .iter()
1916                    .map(|(op, arm)| (*op, plan_select_with(arm.clone(), levers)))
1917                    .collect(),
1918                width,
1919                queue: RecursiveQueue {
1920                    order_by: body.order_by.clone(),
1921                    limit: body.limit.clone(),
1922                    offset: body.offset.clone(),
1923                },
1924            };
1925        }
1926        SourceRows::RecursiveSelf { cte } => {
1927            return AccessPath::RecursiveSelf { cte: *cte };
1928        }
1929        SourceRows::Table => {}
1930    }
1931    let id = ids.get(position).copied().unwrap_or(position);
1932    let table = &source.table;
1933    // **The k nearest, when the query asked exactly that.** Tried before the
1934    // b-tree paths because none of them apply: an index a module owns has no
1935    // key to seek and no range to walk, and the shape it answers - a distance
1936    // ordered ascending with a `LIMIT` - is one no other path can improve on.
1937    let forced = match &source.index_hint {
1938        crate::bind::IndexChoice::Only(wanted) => Some(wanted.as_slice()),
1939        _ => None,
1940    };
1941    if let Some(path) = vector_path(id, position, source, select) {
1942        // `INDEXED BY` a b-tree index rules the probe out like every other
1943        // path; `INDEXED BY` the probe's own index is the one way to keep it.
1944        let named = match &path {
1945            AccessPath::VectorProbe { index, .. } => table
1946                .indexes
1947                .iter()
1948                .find(|held| &held.name == index)
1949                .map(|held| held.folded.as_slice()),
1950            _ => None,
1951        };
1952        if forced.is_none() || forced == named {
1953            return path;
1954        }
1955    }
1956    if let Some(module) = table.module.clone() {
1957        return virtual_path(id, position, ids, source, select, module, terms, consumed);
1958    }
1959    if forced.is_some() {
1960        return forced_path(id, position, ids, source, select, terms, consumed, levers);
1961    }
1962    // Every candidate is built against a *copy* of the consumed list, because a
1963    // path that is not chosen must not leave its predicates marked as handled.
1964    // It did: when a scan beat an index range, the range's own comparison had
1965    // already been struck off the residual list and the scan then returned
1966    // every row of the table, silently.
1967    let mut candidates: Vec<(AccessPath, Vec<bool>)> = Vec::new();
1968    let mut trial = consumed.to_vec();
1969    if let Some(path) = rowid_path(id, position, ids, table, terms, &mut trial) {
1970        candidates.push((path, trial));
1971    }
1972    // **`NOT INDEXED` removes the index candidates and nothing else.** SQLite's
1973    // rule is that the clause prohibits every index on the table while leaving
1974    // the INTEGER PRIMARY KEY usable, which is why `rowid_path` above is
1975    // unconditional and this is the one candidate the hint takes away.
1976    //
1977    // `crates/inillucent-cli/src/diagnose.rs` is what this is for. Its integrity
1978    // digest reads every table `SELECT * FROM "t" NOT INDEXED`, and its comment
1979    // says that is what makes the digest a fact about the rows - which was not
1980    // true while the hint was dropped, because a corrupt index would then be
1981    // read in place of the table it was meant to be checked against.
1982    if source.index_hint != crate::bind::IndexChoice::NotIndexed {
1983        let mut trial = consumed.to_vec();
1984        let needed = select.columns_read(id);
1985        if let Some(path) = index_path(
1986            id, position, ids, source, terms, &mut trial, &needed, levers,
1987        ) {
1988            candidates.push((path, trial));
1989        }
1990    }
1991    candidates.push((
1992        AccessPath::TableScan { root: table.root },
1993        consumed.to_vec(),
1994    ));
1995
1996    // A scan beats a search that returns most of the table: an index that has
1997    // to fetch every row costs a second descent per row on top of the scan it
1998    // was meant to avoid. And a path that already produces the ORDER BY beats
1999    // one that does not by the whole cost of the sort it saves, which is how a
2000    // `LIMIT 50` over six hundred thousand rows becomes fifty rows read rather
2001    // than six hundred thousand read, sorted and thrown away.
2002    let sort = sort_penalty(select, position, source, levers);
2003    let mut best: Option<(f64, AccessPath, Vec<bool>)> = None;
2004    for (path, trial) in candidates {
2005        let (mut cost, _) = path_cost(source, &path);
2006        if !levers.has(Levers::ORDERED_WALK)
2007            || ordering_provided(select, id, table, &path).is_none()
2008        {
2009            cost += sort;
2010        }
2011        if best
2012            .as_ref()
2013            .is_none_or(|(existing, _, _)| cost < *existing - 1e-9)
2014        {
2015            best = Some((cost, path, trial));
2016        }
2017    }
2018    match best {
2019        Some((_, path, trial)) => {
2020            consumed.copy_from_slice(&trial);
2021            path
2022        }
2023        None => AccessPath::TableScan { root: table.root },
2024    }
2025}
2026
2027/// Returns what a sort would cost this term, or nothing when no path could
2028/// avoid one anyway.
2029///
2030/// Only the outermost term of a single-term statement can answer an `ORDER BY`
2031/// by walking: an inner loop restarts for every outer row, and the order it
2032/// produces inside one of those runs is not the order of the result. Charging
2033/// the sort anywhere else would tilt a plan towards an index for a saving it
2034/// would not make.
2035/// @param select - the bound statement
2036/// @param position - which visiting position this term is at
2037/// @param source - the term being priced
2038fn sort_penalty(
2039    select: &BoundSelect,
2040    position: usize,
2041    source: &BoundSource,
2042    levers: Levers,
2043) -> f64 {
2044    // A grouped or DISTINCT statement that streams over the walk answers its
2045    // ORDER BY the same way an ungrouped one does, so it is priced the same
2046    // way. Charging it the sort regardless would hide the saving that makes the
2047    // index path worth taking.
2048    let streams = levers.has(Levers::STREAMING_GROUP)
2049        && ((!select.group_by.is_empty() && !select.distinct)
2050            || (select.distinct && select.group_by.is_empty() && select.aggregates.is_empty()));
2051    let answerable = levers.has(Levers::ORDERED_WALK)
2052        && position == 0
2053        && select.sources.len() == 1
2054        && select.windows.is_empty()
2055        && select.compounds.is_empty()
2056        && !select.order_by.is_empty()
2057        && ((select.group_by.is_empty() && select.aggregates.is_empty() && !select.distinct)
2058            || streams);
2059    if !answerable {
2060        return 0.0;
2061    }
2062    cost::sort_cost(estimated_rows(&source.table))
2063}
2064
2065/// Builds the offer a virtual table's module will be shown.
2066///
2067/// Every predicate that compares one of this term's columns - or its rowid - to
2068/// something is offered, whether or not the value is available yet: a
2069/// constraint the loop order has put out of reach is offered as *not usable*,
2070/// which is what lets one answer serve every position the term could take.
2071fn virtual_path(
2072    id: usize,
2073    position: usize,
2074    ids: &[usize],
2075    source: &BoundSource,
2076    select: &BoundSelect,
2077    module: crate::vtab::ModuleRef,
2078    terms: &[BoundExpr],
2079    consumed: &mut [bool],
2080) -> AccessPath {
2081    let table = &source.table;
2082    let mut offer = Vec::new();
2083    for (index, term) in terms.iter().enumerate() {
2084        if consumed.get(index).copied().unwrap_or(false) {
2085            continue;
2086        }
2087        let Some((column, op, value)) = virtual_constraint(id, table, term) else {
2088            continue;
2089        };
2090        let usable = is_available(position, ids, &value);
2091        offer.push(VirtualConstraint {
2092            spec: crate::vtab::ConstraintSpec { column, op, usable },
2093            value,
2094            predicate: term.clone(),
2095            in_list: Vec::new(),
2096        });
2097        // **Only a usable constraint is this term's to answer.** In `FROM
2098        // json_each(...) s, json_each(s.value) r`, `s` took `s.value = r.json`
2099        // as its own, which left `r` with no document and failed. SQLite offers
2100        // such a constraint as not usable and tests it at the later loop.
2101        if let Some(slot) = consumed.get_mut(index).filter(|_| usable) {
2102            *slot = true;
2103        }
2104    }
2105    // **One `IN` list of constant values, offered as `=` and not consumed.** See
2106    // `VirtualConstraint::in_list`. Only one, because the engine drives the
2107    // module once per value of one list; two lists would need every pair.
2108    let lists: Vec<(i32, Vec<BoundExpr>, &BoundExpr)> = terms
2109        .iter()
2110        .enumerate()
2111        .filter(|(index, _)| !consumed.get(*index).copied().unwrap_or(false))
2112        .filter_map(|(_, term)| {
2113            constant_in_list(id, table, term).map(|(column, list)| (column, list, term))
2114        })
2115        .collect();
2116    for (column, list, term) in lists {
2117        if let Some(first) = list.first().cloned() {
2118            // A list written as a subquery has no first value to show the
2119            // module; its values are read when the scan runs.
2120            let first = if matches!(first, BoundExpr::Subquery { .. }) {
2121                BoundExpr::Null
2122            } else {
2123                first
2124            };
2125            offer.push(VirtualConstraint {
2126                spec: crate::vtab::ConstraintSpec {
2127                    column,
2128                    op: crate::vtab::ConstraintOp::Eq,
2129                    usable: true,
2130                },
2131                value: first,
2132                predicate: term.clone(),
2133                in_list: list,
2134            });
2135        }
2136    }
2137    let order_by = order_offer(id, position, select);
2138    AccessPath::VirtualScan {
2139        module,
2140        offer,
2141        order_by,
2142        chosen: None,
2143    }
2144}
2145
2146/// Returns the `ORDER BY` a module may be able to satisfy for itself.
2147///
2148/// Only the outermost loop is offered one. An inner loop restarts for every row
2149/// of the loops around it, so an ordering it produced would be an ordering
2150/// within each of those restarts - which is not the statement's ordering and
2151/// would let the sorter be skipped wrongly.
2152fn order_offer(id: usize, position: usize, select: &BoundSelect) -> Vec<crate::vtab::OrderSpec> {
2153    // An aggregate with no GROUP BY returns one row, so its ORDER BY orders
2154    // nothing the module scans: `SELECT group_concat(value) FROM
2155    // generate_series(10, 1, -3) ORDER BY value` walks the series in its own
2156    // order, as SQLite does.
2157    if position != 0 || (!select.aggregates.is_empty() && select.group_by.is_empty()) {
2158        return Vec::new();
2159    }
2160    let mut offer = Vec::new();
2161    for term in &select.order_by {
2162        let column = match &term.expr {
2163            BoundExpr::Column { source, column, .. } if *source == id => i32::from(*column),
2164            BoundExpr::Rowid { source } if *source == id => crate::vtab::ROWID_COLUMN,
2165            _ => return Vec::new(),
2166        };
2167        offer.push(crate::vtab::OrderSpec {
2168            column,
2169            descending: term.order == crate::ast::SortOrder::Descending,
2170        });
2171    }
2172    offer
2173}
2174
2175/// Splits a predicate into the conjunction the offer is built from.
2176pub fn conjunction(filter: &BoundExpr) -> Vec<BoundExpr> {
2177    let mut terms = Vec::new();
2178    split_conjunction(filter, &mut terms);
2179    terms
2180}
2181
2182/// Returns how an UPDATE or a DELETE should find the rows it touches, with some
2183/// optimizations switched off.
2184/// @param table - the table being written
2185/// @param source_id - the source the filter's columns are bound to
2186/// @param filter - the WHERE clause, when there is one
2187/// @param levers - which optimizations are on
2188pub fn write_path_with(
2189    table: &TableInfo,
2190    source_id: usize,
2191    filter: Option<&BoundExpr>,
2192    levers: Levers,
2193) -> AccessPath {
2194    if !levers.has(Levers::INDEXED_WRITE) {
2195        return AccessPath::TableScan { root: table.root };
2196    }
2197    let scan = AccessPath::TableScan { root: table.root };
2198    if table.module.is_some() || table.without_rowid {
2199        return scan;
2200    }
2201    let Some(filter) = filter else {
2202        return scan;
2203    };
2204    let mut terms = Vec::new();
2205    split_conjunction(filter, &mut terms);
2206    let ids = [source_id];
2207    let mut consumed = vec![false; terms.len()];
2208    if let Some(path) = rowid_path(source_id, 0, &ids, table, &terms, &mut consumed) {
2209        return path;
2210    }
2211    let source = BoundSource {
2212        index_hint: crate::bind::IndexChoice::Any,
2213        id: source_id,
2214        rows: SourceRows::Table,
2215        table: std::rc::Rc::new(table.clone()),
2216        alias: table.name.clone(),
2217        join: JoinKind::Inner,
2218        constraint: None,
2219        suppressed: Vec::new(),
2220        index_exprs: Vec::new(),
2221        written_schema: None,
2222    };
2223    let mut consumed = vec![false; terms.len()];
2224    // A write reads the whole row it is about to change, so no index covers it.
2225    let needed = ColumnUse {
2226        opaque: true,
2227        ..ColumnUse::default()
2228    };
2229    let Some(path) = index_path(
2230        source_id,
2231        0,
2232        &ids,
2233        &source,
2234        &terms,
2235        &mut consumed,
2236        &needed,
2237        levers,
2238    ) else {
2239        return scan;
2240    };
2241    // The same crossover the read planner uses: an index that has to fetch most
2242    // of the table costs a second descent per row on top of the scan it was
2243    // meant to replace.
2244    let (index_cost, _) = path_cost(&source, &path);
2245    let (scan_cost, _) = path_cost(&source, &scan);
2246    if index_cost <= scan_cost {
2247        return path;
2248    }
2249    scan
2250}
2251
2252/// Returns a rowid equality or range path, when the predicates allow one.
2253fn rowid_path(
2254    id: usize,
2255    position: usize,
2256    ids: &[usize],
2257    table: &TableInfo,
2258    terms: &[BoundExpr],
2259    consumed: &mut [bool],
2260) -> Option<AccessPath> {
2261    if !table.has_rowid() {
2262        return None;
2263    }
2264    for (index, term) in terms.iter().enumerate() {
2265        if consumed.get(index).copied().unwrap_or(false) {
2266            continue;
2267        }
2268        let Some((op, value)) = comparison_against_rowid(id, term) else {
2269            continue;
2270        };
2271        if op != BinaryOp::Equal || !is_available(position, ids, &value) {
2272            continue;
2273        }
2274        if let Some(slot) = consumed.get_mut(index) {
2275            *slot = true;
2276        }
2277        return Some(AccessPath::RowidSeek {
2278            root: table.root,
2279            key: value,
2280        });
2281    }
2282    if let Some(path) = seek_union::rowid_in_list_path(id, position, ids, table, terms, consumed) {
2283        return Some(path);
2284    }
2285    // **A range is an outermost-term path only.** The physical pass drives an
2286    // inner term either by probing it per outer row or by reading it once into
2287    // a buffer, and neither of those is a walk between two bounds - so a range
2288    // chosen here for an inner term was refused downstream with "the physical
2289    // pass does not handle a rowid range as an inner join term yet", which is
2290    // what `SELECT x.id, y.id FROM t x JOIN t y ON y.a = x.a AND y.id > x.id`
2291    // hit. Not choosing it is better than refusing it: the bound stays
2292    // unconsumed, so it is tested as a residual over the pair and the self join
2293    // answers. The equality half above is unaffected, because a seek per outer
2294    // row *is* what an index nested loop does.
2295    if position != 0 {
2296        return None;
2297    }
2298    let mut low = None;
2299    let mut high = None;
2300    let mut used = Vec::new();
2301    for (index, term) in terms.iter().enumerate() {
2302        if consumed.get(index).copied().unwrap_or(false) {
2303            continue;
2304        }
2305        let Some((op, value)) = comparison_against_rowid(id, term) else {
2306            continue;
2307        };
2308        if !is_available(position, ids, &value) {
2309            continue;
2310        }
2311        match op {
2312            BinaryOp::Greater if low.is_none() => {
2313                low = Some(RangeBound {
2314                    kind: BoundKind::Greater,
2315                    value,
2316                    unconverted: false,
2317                });
2318                used.push(index);
2319            }
2320            BinaryOp::GreaterEqual if low.is_none() => {
2321                low = Some(RangeBound {
2322                    kind: BoundKind::GreaterEqual,
2323                    value,
2324                    unconverted: false,
2325                });
2326                used.push(index);
2327            }
2328            BinaryOp::Less if high.is_none() => {
2329                high = Some(RangeBound {
2330                    kind: BoundKind::Less,
2331                    value,
2332                    unconverted: false,
2333                });
2334                used.push(index);
2335            }
2336            BinaryOp::LessEqual if high.is_none() => {
2337                high = Some(RangeBound {
2338                    kind: BoundKind::LessEqual,
2339                    value,
2340                    unconverted: false,
2341                });
2342                used.push(index);
2343            }
2344            _ => {}
2345        }
2346    }
2347    if low.is_none() && high.is_none() {
2348        return None;
2349    }
2350    for index in used {
2351        if let Some(slot) = consumed.get_mut(index) {
2352            *slot = true;
2353        }
2354    }
2355    Some(AccessPath::RowidRange {
2356        root: table.root,
2357        low,
2358        high,
2359    })
2360}
2361
2362/// Returns an index path over an equality prefix, when one is usable.
2363fn index_path(
2364    id: usize,
2365    position: usize,
2366    ids: &[usize],
2367    source: &BoundSource,
2368    terms: &[BoundExpr],
2369    consumed: &mut [bool],
2370    needed: &ColumnUse,
2371    levers: Levers,
2372) -> Option<AccessPath> {
2373    let table = &source.table;
2374    let forced = match &source.index_hint {
2375        crate::bind::IndexChoice::Only(wanted) => Some(wanted.as_slice()),
2376        _ => None,
2377    };
2378    let context = CandidateContext {
2379        id,
2380        position,
2381        ids,
2382        table,
2383        terms,
2384        consumed,
2385        needed,
2386        levers,
2387        forced: forced.is_some(),
2388    };
2389    let mut best: Option<(f64, AccessPath, Vec<usize>)> = None;
2390    for (at, index) in table.indexes.iter().enumerate() {
2391        // An index a module owns is not a b-tree: it has no root to seek into
2392        // and no key order to walk. `vector_path` is the only path that can use
2393        // one, and it was tried before this.
2394        if index.origin == crate::catalog_view::IndexOrigin::Module {
2395            continue;
2396        }
2397        if forced.is_some_and(|wanted| wanted != index.folded.as_slice()) {
2398            continue;
2399        }
2400        // The expressions this index needs, when the binder could bind them.
2401        // `None` for every ordinary index, and for one whose schema text did
2402        // not bind - which leaves a partial index unusable and an expression
2403        // key unmatched, both the conservative answer.
2404        let computed = source.index_exprs.iter().find(|held| held.position == at);
2405        let usable = index_usable(source, at, index, terms);
2406        if !usable && index.partial_sql.is_some() {
2407            // **A partial index only holds the rows its predicate accepts.**
2408            // Using one over a query that does not imply the predicate would
2409            // lose rows - silently, and only the rows the predicate excludes -
2410            // so the index is skipped unless the implication is *proved*.
2411            //
2412            // The proof is SQLite's own, and it is deliberately the crudest one
2413            // that is sound: the predicate appears, unchanged, as a conjunct of
2414            // the statement's `WHERE`. `WHERE b > 5 AND a = 1` therefore uses an
2415            // index declared `WHERE b > 5`, and `WHERE b > 6` does not, even
2416            // though it implies it. A cleverer test would answer more queries
2417            // and would be a place for a wrong answer to live.
2418            continue;
2419        }
2420        // Three different candidates can come from the same index: the
2421        // ordinary equality-prefix-and-range seek, a union of equality seeks
2422        // when a disjunction is an `IN` list on the leading column, and a
2423        // union of range seeks when a disjunction is a keyset page's tuple
2424        // comparison. None of them rules another out - a statement can only
2425        // ever use one of them here, but which one is cheapest is a cost
2426        // question, so every one that matches is tried and the best kept.
2427        if let Some((path, used)) = index_candidate(&context, index, computed, usable) {
2428            consider_index_candidate(source, &mut best, path, used);
2429        }
2430        if let Some((path, used)) = seek_union::in_list_union_path(&context, index, usable) {
2431            consider_index_candidate(source, &mut best, path, used);
2432        }
2433        if let Some((path, used)) = seek_union::keyset_range_union_path(&context, index, usable) {
2434            consider_index_candidate(source, &mut best, path, used);
2435        }
2436    }
2437    let (_, path, used) = best?;
2438    for index in used {
2439        if let Some(slot) = consumed.get_mut(index) {
2440            *slot = true;
2441        }
2442    }
2443    Some(path)
2444}
2445
2446/// What every index candidate for one FROM term is chosen from.
2447///
2448/// **A type rather than ten arguments (task-1962, A9).** `index_candidate`,
2449/// [`seek_union::in_list_union_path`] and [`seek_union::keyset_range_union_path`]
2450/// each took the same ten, in the same order, and two of them carried
2451/// `#[allow(clippy::too_many_arguments)]` to say so. Ten positional arguments of
2452/// which three are slices of different things is a call nobody can read and a
2453/// call site nobody can check.
2454pub(crate) struct CandidateContext<'a> {
2455    /// The FROM term being planned.
2456    pub(crate) id: usize,
2457    /// Its position in the FROM list; zero drives the pipeline.
2458    pub(crate) position: usize,
2459    /// Every FROM term's id, so a correlated reference can be recognised.
2460    pub(crate) ids: &'a [usize],
2461    /// The table the term reads.
2462    pub(crate) table: &'a TableInfo,
2463    /// The statement's `WHERE` terms, bound.
2464    pub(crate) terms: &'a [BoundExpr],
2465    /// Which of those an earlier path has already consumed.
2466    pub(crate) consumed: &'a [bool],
2467    /// What the statement reads of this term, which decides covering.
2468    pub(crate) needed: &'a ColumnUse,
2469    /// The planner's tuning knobs.
2470    pub(crate) levers: Levers,
2471    /// The term was written `INDEXED BY`, so the one index left must produce a
2472    /// path even when nothing seeks it: a walk of every entry.
2473    pub(crate) forced: bool,
2474}
2475
2476/// Folds one more index candidate into whichever is cheapest so far.
2477///
2478/// The choice between candidates is a cost, not a count of consumed terms:
2479/// two candidates that each satisfy one equality consume the same number of
2480/// terms and can differ by orders of magnitude in how many rows they return -
2481/// and taking the first one found made a query constrained on both a
2482/// two-valued column and a four-hundred-valued one search the two-valued one.
2483/// A tie goes to the later candidate, which is what the reference does - it
2484/// keeps a candidate that is no worse than the one it holds, so the last
2485/// equal one wins, which matters because a query with no `ORDER BY` returns
2486/// rows in whatever order its path produces.
2487fn consider_index_candidate(
2488    source: &BoundSource,
2489    best: &mut Option<(f64, AccessPath, Vec<usize>)>,
2490    path: AccessPath,
2491    used: Vec<usize>,
2492) {
2493    let (cost, _) = path_cost(source, &path);
2494    let better = best
2495        .as_ref()
2496        .is_none_or(|(existing, _, _)| cost <= *existing + 1e-9);
2497    if better {
2498        *best = Some((cost, path, used));
2499    }
2500}
2501
2502/// Builds the best path over one index, or `None` if it cannot be used.
2503fn index_candidate(
2504    context: &CandidateContext<'_>,
2505    index: &IndexInfo,
2506    computed: Option<&crate::dml::BoundIndexExprs>,
2507    usable: bool,
2508) -> Option<(AccessPath, Vec<usize>)> {
2509    let CandidateContext {
2510        id,
2511        position,
2512        ids,
2513        table,
2514        terms,
2515        consumed,
2516        needed,
2517        levers,
2518        forced,
2519    } = *context;
2520    let mut equalities = Vec::new();
2521    let mut unconverted = Vec::new();
2522    let mut used = Vec::new();
2523    let mut collations = Vec::new();
2524    let mut descending = Vec::new();
2525    let mut columns: Vec<Option<u16>> = Vec::new();
2526    let mut key = 0usize;
2527    while let Some(key_column) = index.columns.get(key) {
2528        let collation = collation_of(&key_column.collation);
2529        let found = match key_column.plain_column() {
2530            Some(column) => {
2531                find_equality(id, position, ids, column, collation, terms, consumed, &used)
2532                    .map(|(term_index, value)| (term_index, value, Some(column)))
2533            }
2534            // **A key the index computes.** `CREATE INDEX ix ON t(lower(a))`
2535            // answers `WHERE lower(a) = 'ab'` and nothing else: the entry holds
2536            // the expression's value, so the only predicate it can seek on is
2537            // one whose own side is that same expression. The comparison is
2538            // between *bound* expressions, which is why the binder puts them on
2539            // the FROM term - see `BoundSource::index_exprs`.
2540            None => computed
2541                .and_then(|held| held.keys.get(key).cloned().flatten())
2542                .and_then(|wanted| {
2543                    find_expr_equality(position, ids, &wanted, collation, terms, consumed, &used)
2544                })
2545                // A key on a virtual generated column still names that column, and
2546                // the probe takes the column's affinity: the entries hold the value
2547                // after the column's affinity was applied.
2548                .map(|(term_index, value)| (term_index, value, key_column.column)),
2549        };
2550        let Some((term_index, value, column)) = found else {
2551            break;
2552        };
2553        if terms.get(term_index).is_some_and(compares_unconverted) {
2554            unconverted.push(equalities.len());
2555        }
2556        equalities.push(value);
2557        used.push(term_index);
2558        collations.push(collation);
2559        descending.push(key_column.descending);
2560        columns.push(column);
2561        key = key.saturating_add(1);
2562    }
2563    // **A range is an outermost-term path only**, the rule `rowid_path` and
2564    // `seek_union` already follow and this candidate did not. The physical
2565    // pass refuses an inner index seek with a bound, so
2566    // `SELECT count(*) FROM s CROSS JOIN h WHERE h.b > 595` was refused with
2567    // exit code 3 on the release build, with no hint anywhere (task-2078).
2568    // Left unconsumed, the bound is a residual over the pair, which answers.
2569    let range = match index.columns.get(key) {
2570        Some(key_column) if position == 0 => range::key_range(context, key_column, &mut used),
2571        _ => None,
2572    };
2573    let (low, high) = match range {
2574        Some(found) => {
2575            collations.push(found.collation);
2576            descending.push(found.descending);
2577            columns.push(Some(found.column));
2578            (found.low, found.high)
2579        }
2580        None => (None, None),
2581    };
2582    // A partial index whose predicate the query repeats does not need that predicate's columns.
2583    let beyond = if usable {
2584        partial::needed_beyond_predicate(id, needed, terms, computed)
2585    } else {
2586        None
2587    };
2588    let covering_need = beyond.as_ref().map_or(needed, |(reduced, _)| reduced);
2589    let covering = levers
2590        .has(Levers::COVERING_INDEX)
2591        .then(|| covering_slots(table, index, covering_need, usable))
2592        .flatten();
2593    if let Some((_, implied)) = &beyond {
2594        for term in implied {
2595            if !used.contains(term) {
2596                used.push(*term);
2597            }
2598        }
2599    }
2600    // **A partial index whose predicate the query implies is worth walking whole.**
2601    // It holds only the rows its predicate accepted, so reading
2602    // every entry of it reads exactly the rows the query asked for - even with
2603    // nothing to seek to and even when a lookup per entry is needed, which is
2604    // the case a covering test cannot see.
2605    //
2606    // `CREATE INDEX document_pending_idx ON document (indexed_at) WHERE
2607    // indexed_at IS NULL` over `SELECT id FROM document WHERE indexed_at IS
2608    // NULL` is the shape: 120 of 6,000 documents, and `id` is not in the index,
2609    // so the covering test said no and the whole candidate was dropped. The
2610    // plan was `SCAN document`, over a table whose rows carry nine kilobytes of
2611    // body each, and the equivalent query on the real corpus was thousands of
2612    // times slower than the same question asked of PostgreSQL.
2613    //
2614    // It is offered rather than taken: `path_cost` compares it against the scan
2615    // with the index's own entry count, which `ANALYZE` now writes for a partial
2616    // index instead of the table's.
2617    let partial_walk = usable && index.partial_sql.is_some();
2618    if equalities.is_empty()
2619        && low.is_none()
2620        && high.is_none()
2621        && covering.is_none()
2622        && !partial_walk
2623        && !(forced && usable)
2624    {
2625        // Nothing to seek to and nothing to save by reading the entries: this
2626        // index has no part in answering the query.
2627        return None;
2628    }
2629    Some((
2630        AccessPath::IndexSeek {
2631            table_root: table.root,
2632            index_root: index.root,
2633            index_name: index.name.clone(),
2634            equalities,
2635            unconverted,
2636            low,
2637            high,
2638            collations,
2639            descending,
2640            columns,
2641            without_rowid: table.without_rowid,
2642            key_entry_slots: if table.without_rowid && index.root != table.root {
2643                let leading = index.columns.len();
2644                (0..table.primary_key().len())
2645                    .map(|offset| leading.saturating_add(offset))
2646                    .collect()
2647            } else {
2648                Vec::new()
2649            },
2650            covering,
2651        },
2652        used,
2653    ))
2654}
2655
2656/// The entry slot that stands for the row's own key rather than a field.
2657///
2658/// An index entry over a rowid table ends with the rowid, and the machine reads
2659/// it with `IdxRowid` rather than out of the entry's record - so a column that
2660/// *is* the rowid needs a marker rather than a slot number. It is the largest
2661/// `usize` because no entry can have that many fields, and because a number
2662/// that could also be a real slot would be a silent misread.
2663pub const ROWID_ENTRY_SLOT: usize = usize::MAX;
2664
2665/// Finds an equality predicate on one column with a matching collation.
2666fn find_equality(
2667    id: usize,
2668    position: usize,
2669    ids: &[usize],
2670    column: u16,
2671    collation: Collation,
2672    terms: &[BoundExpr],
2673    consumed: &[bool],
2674    used: &[usize],
2675) -> Option<(usize, BoundExpr)> {
2676    for (index, term) in terms.iter().enumerate() {
2677        if consumed.get(index).copied().unwrap_or(false) || used.contains(&index) {
2678            continue;
2679        }
2680        let Some((op, value)) = indexable_comparison(id, column, term) else {
2681            continue;
2682        };
2683        if op != BinaryOp::Equal || !is_available(position, ids, &value) {
2684            continue;
2685        }
2686        if comparison_collation(term) != collation {
2687            continue;
2688        }
2689        return Some((index, value));
2690    }
2691    None
2692}
2693
2694/// Finds an equality against an expression the index computes.
2695///
2696/// The mirror of [`find_equality`] for a key that is not a column: the term has
2697/// to compare the index's own key expression against something the join has
2698/// already produced, under the collation the key is ordered by.
2699///
2700/// @param position - the FROM term's position among the ones already joined
2701/// @param ids - the FROM terms joined so far
2702/// @param wanted - the index's bound key expression
2703/// @param collation - the collation the key is ordered under
2704/// @param terms - the statement's `WHERE` conjuncts
2705/// @param consumed - which terms an earlier stage already used
2706/// @param used - which terms this candidate has already used
2707fn find_expr_equality(
2708    position: usize,
2709    ids: &[usize],
2710    wanted: &BoundExpr,
2711    collation: Collation,
2712    terms: &[BoundExpr],
2713    consumed: &[bool],
2714    used: &[usize],
2715) -> Option<(usize, BoundExpr)> {
2716    for (index, term) in terms.iter().enumerate() {
2717        if consumed.get(index).copied().unwrap_or(false) || used.contains(&index) {
2718            continue;
2719        }
2720        let BoundExpr::Compare {
2721            op, left, right, ..
2722        } = term
2723        else {
2724            continue;
2725        };
2726        if *op != BinaryOp::Equal || comparison_collation(term) != collation {
2727            continue;
2728        }
2729        let value = if left.as_ref() == wanted {
2730            right.as_ref().clone()
2731        } else if right.as_ref() == wanted {
2732            left.as_ref().clone()
2733        } else {
2734            continue;
2735        };
2736        if !is_available(position, ids, &value) {
2737            continue;
2738        }
2739        return Some((index, value));
2740    }
2741    None
2742}
2743
2744/// Returns whether a value can be computed before entering a loop level.
2745///
2746/// A seek key may only read terms *outside* the loop it drives. Reading the
2747/// term's own columns would be circular, and reading an inner term's columns
2748/// would read a cursor that has not been positioned yet.
2749fn is_available(position: usize, ids: &[usize], value: &BoundExpr) -> bool {
2750    let mut used = Vec::new();
2751    value.sources_used(&mut used);
2752    used.iter().all(|source| {
2753        // A term this block does not own belongs to an enclosing one, whose
2754        // cursor is positioned before this block runs at all - so it is
2755        // available at every level, including the first.
2756        ids.iter()
2757            .position(|id| id == source)
2758            .is_none_or(|level| level < position)
2759    })
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764    use super::*;
2765    use crate::bind::BoundExpr;
2766
2767    /// A conjunction splits into its terms; a disjunction does not, because a
2768    /// term of an OR is not true of every row the OR accepts.
2769    #[test]
2770    fn only_conjunctions_split() {
2771        let expr = BoundExpr::And(
2772            Box::new(BoundExpr::Integer(1)),
2773            Box::new(BoundExpr::Or(
2774                Box::new(BoundExpr::Integer(2)),
2775                Box::new(BoundExpr::Integer(3)),
2776            )),
2777        );
2778        let mut terms = Vec::new();
2779        split_conjunction(&expr, &mut terms);
2780        assert_eq!(terms.len(), 2);
2781        assert!(matches!(terms.get(1), Some(BoundExpr::Or(_, _))));
2782    }
2783
2784    /// A seek key may read only terms outside its own loop.
2785    #[test]
2786    fn a_seek_key_may_only_read_outer_terms() {
2787        let outer = BoundExpr::Column {
2788            source: 0,
2789            column: 0,
2790            slot: 0,
2791            affinity: inillucent_value::Affinity::Integer,
2792            collation: Collation::Binary,
2793        };
2794        let ids = [0usize, 1usize];
2795        assert!(is_available(1, &ids, &outer));
2796        assert!(!is_available(0, &ids, &outer));
2797        assert!(is_available(0, &ids, &BoundExpr::Integer(5)));
2798        // A term the block does not own belongs to an enclosing block, whose
2799        // cursor is already positioned, so it is available at every level.
2800        assert!(is_available(0, &[7usize], &outer));
2801    }
2802}