Rindle docs and package mapSkip to main content

rindle/op/
skip.rs

1//! `Skip` (`ivm/skip.ts`, spec `07` §5) — drop rows before a start bound. Also the
2//! **worked template** for the operator fan-out seam (see [`crate::op`]).
3//!
4//! A faithful port of `skip.ts`: a stateless, ordered operator that sets the
5//! pipeline's start position. On `fetch` it pushes the bound down into the input's
6//! `FetchRequest::start` (merging it with any incoming start via [`Skip::get_start`],
7//! exactly as `#getStart`) and, on a reverse scan, trims the far end with a
8//! `take_while`. On `push` it gates Add/Remove/Child on the bound and splits an
9//! `Edit` that crosses it (`maybe-split-and-push-edit-change.ts`).
10
11use std::cell::Cell;
12use std::cmp::Ordering;
13
14use crate::change::{Basis, Change, FetchRequest, NodeStream, OutEdge, Start};
15use crate::graph::{Graph, NodeId};
16use crate::value::{compare_rows, OwnedRow as Row, Sort};
17
18/// `Skip`: a stateless, ordered operator that drops rows up to `bound` (the AST
19/// `start`). Stateless ⇒ no [`StorageId`](crate::graph::StorageId).
20pub struct Skip {
21    /// Upstream input operator (always the source connection in the built subset —
22    /// `start` is lowered right after the source, mirroring `buildPipelineInternal`).
23    pub input: NodeId,
24    /// The start bound: rows at/after it survive, per its [`Start::basis`]
25    /// (`Basis::At` includes the bound row, `Basis::After` excludes it). The
26    /// builder lowers the AST `start` (a name-keyed partial row + `exclusive`) to
27    /// this positional [`Start`]; the comparator only reads the sort columns.
28    pub bound: Start,
29    /// The single downstream edge as a **port-carrying** [`OutEdge`] (like a
30    /// [`Join`](crate::graph)'s): `Port::Single` to a terminal sink, or
31    /// `Port::JoinParent` when this `Skip` feeds the parent port of a relationship
32    /// join stacked above it. Wired two-phase via
33    /// [`Graph::set_output`](crate::graph::Graph::set_output) (Single) or
34    /// [`Graph::set_out_edge`](crate::graph::Graph::set_out_edge) (explicit port).
35    pub output: Cell<Option<OutEdge>>,
36}
37
38/// The resolved start for the input fetch — `#getStart`'s return (`skip.ts:107`).
39/// `Empty` means the request can produce no rows; `Use(None)` means "no start
40/// gate" (JS `undefined`); `Use(Some(s))` pushes `s` down to the input.
41enum Resolved {
42    Empty,
43    Use(Option<Start>),
44}
45
46impl Skip {
47    pub fn new(input: NodeId, bound: Start) -> Skip {
48        Skip {
49            input,
50            bound,
51            output: Cell::new(None),
52        }
53    }
54
55    /// `#shouldBePresent` (`skip.ts:83`): `row` survives iff the bound sorts before
56    /// it, or sorts equal and the bound is inclusive (`Basis::At`).
57    fn should_be_present(&self, sort: &Sort, row: &Row) -> bool {
58        match compare_rows(sort, &self.bound.row, row) {
59            Ordering::Less => true,
60            Ordering::Equal => matches!(self.bound.basis, Basis::At),
61            Ordering::Greater => false,
62        }
63    }
64
65    /// `#getStart` (`skip.ts:107`): merge the Skip bound with the request's own
66    /// start into the single start to push down to the input. Handles forward and
67    /// reverse scans and the equal/exclusive boundary straddles.
68    fn get_start(&self, sort: &Sort, req: &FetchRequest) -> Resolved {
69        let exclusive = matches!(self.bound.basis, Basis::After);
70        let bound_start = || self.bound.clone();
71        let after_bound = || Start {
72            row: self.bound.row.clone(),
73            basis: Basis::After,
74        };
75
76        let Some(rs) = &req.start else {
77            // No incoming start: forward uses the bound; reverse needs no gate here
78            // (the far-end trim happens in `fetch`'s `take_while`).
79            return if req.reverse {
80                Resolved::Use(None)
81            } else {
82                Resolved::Use(Some(bound_start()))
83            };
84        };
85
86        let cmp = compare_rows(sort, &self.bound.row, &rs.row);
87        if !req.reverse {
88            match cmp {
89                // Bound is after the requested start → the requested start is moot.
90                Ordering::Greater => Resolved::Use(Some(bound_start())),
91                // Equal: if either side is exclusive, exclude the shared row.
92                Ordering::Equal => {
93                    if exclusive || matches!(rs.basis, Basis::After) {
94                        Resolved::Use(Some(after_bound()))
95                    } else {
96                        Resolved::Use(Some(bound_start()))
97                    }
98                }
99                // Bound is before the requested start → honor the request's start.
100                Ordering::Less => Resolved::Use(Some(rs.clone())),
101            }
102        } else {
103            match cmp {
104                // Reverse scan whose start is *below* the bound → no rows survive.
105                Ordering::Greater => Resolved::Empty,
106                Ordering::Equal => {
107                    // Only the single shared row survives, and only if both inclusive.
108                    if !exclusive && matches!(rs.basis, Basis::At) {
109                        Resolved::Use(Some(bound_start()))
110                    } else {
111                        Resolved::Empty
112                    }
113                }
114                Ordering::Less => Resolved::Use(Some(rs.clone())),
115            }
116        }
117    }
118
119    /// Lazy pull (`skip.ts:53`): merge the bound into the input start and fetch.
120    /// Forward scans pass the input through (the source's start gate did the work);
121    /// reverse scans additionally `take_while` rows that are still at/after the
122    /// bound, stopping at the first that falls below it.
123    pub fn fetch<'g>(&'g self, g: &'g Graph, req: &FetchRequest) -> NodeStream<'g> {
124        let sort = g.input_sort(self.input);
125        let start = match self.get_start(&sort, req) {
126            Resolved::Empty => return Box::new(std::iter::empty()),
127            Resolved::Use(s) => s,
128        };
129        let adjusted = FetchRequest {
130            start,
131            ..req.clone()
132        };
133        let nodes = g.fetch(self.input, &adjusted);
134        if req.reverse {
135            Box::new(nodes.take_while(move |node| self.should_be_present(&sort, &node.row)))
136        } else {
137            nodes
138        }
139    }
140
141    /// Eager push (`skip.ts:88`): forward Add/Remove/Child iff their row is at/after
142    /// the bound; split an `Edit` that crosses the bound into Remove(old)/Add(new)
143    /// (`maybe-split-and-push-edit-change.ts`). Forwards on the edge's own port so a
144    /// `Skip` can feed a relationship join's parent port.
145    pub fn push<'g>(&'g self, g: &'g Graph, change: Change<'g>) {
146        let out = self.output.get().expect("Skip output not wired");
147        let sort = g.input_sort(self.input);
148        match change {
149            Change::Edit { node, old } => {
150                match (
151                    self.should_be_present(&sort, &old.row),
152                    self.should_be_present(&sort, &node.row),
153                ) {
154                    (true, true) => g.push(out.node, Change::Edit { node, old }, out.port),
155                    (true, false) => g.push(out.node, Change::Remove(old), out.port),
156                    (false, true) => g.push(out.node, Change::Add(node), out.port),
157                    (false, false) => {}
158                }
159            }
160            Change::Add(n) => {
161                if self.should_be_present(&sort, &n.row) {
162                    g.push(out.node, Change::Add(n), out.port);
163                }
164            }
165            Change::Remove(n) => {
166                if self.should_be_present(&sort, &n.row) {
167                    g.push(out.node, Change::Remove(n), out.port);
168                }
169            }
170            Change::Child { node, rel, child } => {
171                if self.should_be_present(&sort, &node.row) {
172                    g.push(out.node, Change::Child { node, rel, child }, out.port);
173                }
174            }
175        }
176    }
177}