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}