Skip to main content

spg_engine/
brin.rs

1//! v7.38.11 — read a range predicate's bounds so a BRIN index can say
2//! which slots the scan may skip.
3//!
4//! Lives in its own module, and is `#[cold]` `#[inline(never)]`, for a
5//! measured reason: v7.38.8's conjunct reorder cost two shapes it did
6//! not even touch 17 % and 20 % purely by being compiled into the file
7//! that holds the row loop. This runs once per plan; it has no business
8//! near that loop's code layout.
9
10use alloc::vec::Vec;
11use core::sync::atomic::AtomicU64;
12
13/// Diagnostic only: how often the reader ran, and how often it produced
14/// a slot list. Cheap enough to leave in — one relaxed add per plan.
15pub static PROBE_ENTERED: AtomicU64 = AtomicU64::new(0);
16/// See [`PROBE_ENTERED`].
17pub static PROBE_PRUNED: AtomicU64 = AtomicU64::new(0);
18use core::ops::Range;
19
20use spg_sql::ast::{BinOp, Expr};
21use spg_storage::{Table, Value};
22
23/// The slots a BRIN index cannot rule out for `where_`, or `None` when
24/// nothing about this query and table lets it rule anything out.
25///
26/// `None` means "no opinion" and the caller scans as before. It is
27/// returned for a table with no BRIN index, a predicate with no bound
28/// on a BRIN column, and — deliberately — for a predicate this reader
29/// does not fully understand. Declining is always safe; the danger is
30/// only ever in claiming a range can be skipped.
31#[cold]
32#[inline(never)]
33pub(crate) fn candidate_slots(where_: &Expr, table: &Table) -> Option<Vec<Range<usize>>> {
34    PROBE_ENTERED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
35    for col_pos in table.brin_columns() {
36        let name = table.schema().columns.get(col_pos)?.name.as_str();
37        let (lo, hi) = bounds_on(where_, name);
38        if lo.is_none() && hi.is_none() {
39            continue;
40        }
41        if let Some(slots) = table.brin_candidate_slots(col_pos, lo, hi) {
42            PROBE_PRUNED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
43            return Some(slots);
44        }
45    }
46    None
47}
48
49/// Walk the AND-chain and collect the tightest lower and upper bound on
50/// `col_pos`.
51///
52/// Only conjuncts joined by AND count. An OR anywhere above a bound
53/// makes it unusable — `a < 5 OR b > 9` does not restrict `a` — so this
54/// simply does not descend into one.
55fn bounds_on(e: &Expr, col: &str) -> (Option<i64>, Option<i64>) {
56    let mut lo: Option<i64> = None;
57    let mut hi: Option<i64> = None;
58    let mut stack = alloc::vec![e];
59    while let Some(cur) = stack.pop() {
60        match cur {
61            Expr::Binary {
62                lhs,
63                op: BinOp::And,
64                rhs,
65            } => {
66                stack.push(lhs);
67                stack.push(rhs);
68            }
69            Expr::Binary { lhs, op, rhs } => {
70                let Some((op, lit)) = normalise(lhs, *op, rhs, col) else {
71                    continue;
72                };
73                let Some(k) = spg_storage::brin_scalar(&lit) else {
74                    continue;
75                };
76                match op {
77                    // `x > k` cannot be tightened to `k + 1` here: the
78                    // summary comparison is `>=`-shaped and a range
79                    // whose max IS k still has to be visited so the
80                    // predicate itself can reject it.
81                    BinOp::Gt | BinOp::GtEq => lo = Some(lo.map_or(k, |p: i64| p.max(k))),
82                    BinOp::Lt | BinOp::LtEq => hi = Some(hi.map_or(k, |p: i64| p.min(k))),
83                    BinOp::Eq => {
84                        lo = Some(lo.map_or(k, |p: i64| p.max(k)));
85                        hi = Some(hi.map_or(k, |p: i64| p.min(k)));
86                    }
87                    _ => {}
88                }
89            }
90            _ => {}
91        }
92    }
93    (lo, hi)
94}
95
96/// `(op, literal)` with the column on the left, or `None` if this is
97/// not a comparison between THIS column and a literal.
98fn normalise(lhs: &Expr, op: BinOp, rhs: &Expr, col: &str) -> Option<(BinOp, Value<'static>)> {
99    let lit_of = |e: &Expr| match e {
100        Expr::Literal(l) => Some(crate::eval::literal_to_value(l)),
101        _ => None,
102    };
103    let is_col = |e: &Expr| matches!(e, Expr::Column(c) if c.name == col);
104    if is_col(lhs) {
105        return lit_of(rhs).map(|v| (op, v));
106    }
107    if is_col(rhs) {
108        // `5 < x` is `x > 5`.
109        let flipped = match op {
110            BinOp::Lt => BinOp::Gt,
111            BinOp::LtEq => BinOp::GtEq,
112            BinOp::Gt => BinOp::Lt,
113            BinOp::GtEq => BinOp::LtEq,
114            other => other,
115        };
116        return lit_of(lhs).map(|v| (flipped, v));
117    }
118    None
119}