linkrs_query/executor/streaming/operators/
intersect.rs1use std::collections::HashMap;
16
17use linkrs_core::value::NullType;
18use linkrs_core::Value;
19
20use super::intersect_build::{IntersectBuild, IntersectKey};
21
22pub fn two_way_sorted_intersect(
27 left: &[IntersectKey],
28 right: &[IntersectKey],
29) -> Vec<IntersectKey> {
30 let mut out = Vec::new();
31 let (mut i, mut j) = (0, 0);
32 while i < left.len() && j < right.len() {
33 match left[i].cmp(&right[j]) {
34 std::cmp::Ordering::Less => i += 1,
35 std::cmp::Ordering::Greater => j += 1,
36 std::cmp::Ordering::Equal => {
37 if out.last() != Some(&left[i]) {
38 out.push(left[i].clone());
39 }
40 i += 1;
41 j += 1;
42 }
43 }
44 }
45 out
46}
47
48pub fn multiway_sorted_intersect(lists: &[Vec<IntersectKey>]) -> Vec<IntersectKey> {
52 if lists.is_empty() || lists.iter().any(Vec::is_empty) {
53 return Vec::new();
54 }
55 let mut order: Vec<usize> = (0..lists.len()).collect();
56 order.sort_by_key(|i| lists[*i].len());
57 let mut acc = lists[order[0]].clone();
58 for idx in order.into_iter().skip(1) {
59 acc = two_way_sorted_intersect(&acc, &lists[idx]);
60 if acc.is_empty() {
61 break;
62 }
63 }
64 acc
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct IntersectDataInfo {
70 pub bound_col: usize,
71}
72
73#[derive(Debug, Default)]
80pub struct WcoIntersectExecutor {
81 builds: Vec<IntersectBuild>,
82 bound_cols: Vec<usize>,
83}
84
85impl WcoIntersectExecutor {
86 pub fn new(builds: Vec<IntersectBuild>, bound_cols: Vec<usize>) -> Self {
89 assert_eq!(
90 builds.len(),
91 bound_cols.len(),
92 "one probe bound column per build side"
93 );
94 assert!(
95 !builds.is_empty(),
96 "WCO intersect needs at least one build side"
97 );
98 Self { builds, bound_cols }
99 }
100
101 pub fn num_builds(&self) -> usize {
102 self.builds.len()
103 }
104
105 pub fn into_builds(self) -> Vec<IntersectBuild> {
109 self.builds
110 }
111
112 pub fn probe_row(&self, probe_row: &[Value]) -> Vec<Vec<Value>> {
114 let mut adjacencies: Vec<&[Vec<Value>]> = Vec::with_capacity(self.builds.len());
116 for (build, bound_col) in self.builds.iter().zip(self.bound_cols.iter()) {
117 let Some(bound) = probe_row.get(*bound_col) else {
118 return Vec::new();
119 };
120 let rows = build.lookup(bound);
121 if rows.is_empty() {
122 return Vec::new();
123 }
124 adjacencies.push(rows);
125 }
126
127 let mut key_lists: Vec<Vec<IntersectKey>> = Vec::with_capacity(adjacencies.len());
130 let mut groups: Vec<HashMap<IntersectKey, Vec<usize>>> =
131 Vec::with_capacity(adjacencies.len());
132 let mut representatives: HashMap<IntersectKey, Value> = HashMap::new();
133 for (build_idx, rows) in adjacencies.iter().enumerate() {
134 let intersect_col = self.builds[build_idx].intersect_col();
135 let mut group: HashMap<IntersectKey, Vec<usize>> = HashMap::new();
136 let mut keys: Vec<IntersectKey> = Vec::new();
137 for (row_idx, row) in rows.iter().enumerate() {
138 let Some(value) = row.get(intersect_col) else {
139 continue;
140 };
141 let key = IntersectKey::from(value);
142 if !group.contains_key(&key) {
143 keys.push(key.clone());
144 if build_idx == 0 {
145 representatives
146 .entry(key.clone())
147 .or_insert_with(|| value.clone());
148 }
149 }
150 group.entry(key).or_default().push(row_idx);
151 }
152 if keys.is_empty() {
153 return Vec::new();
154 }
155 key_lists.push(keys);
156 groups.push(group);
157 }
158
159 let common = multiway_sorted_intersect(&key_lists);
161 if common.is_empty() {
162 return Vec::new();
163 }
164
165 let mut out = Vec::new();
167 for key in &common {
168 let rep = representatives
169 .get(key)
170 .cloned()
171 .unwrap_or(Value::Null(NullType::Null));
172 let base: Vec<Value> = probe_row
173 .iter()
174 .cloned()
175 .chain(std::iter::once(rep))
176 .collect();
177 let mut partials = vec![base];
178 for (build_idx, group) in groups.iter().enumerate() {
179 let Some(matched) = group.get(key) else {
180 partials.clear();
181 break;
182 };
183 let rows = adjacencies[build_idx];
184 let mut next = Vec::with_capacity(partials.len() * matched.len());
185 for partial in &partials {
186 for row_idx in matched {
187 let mut row = partial.clone();
188 row.extend(rows[*row_idx].iter().cloned());
189 next.push(row);
190 }
191 }
192 partials = next;
193 }
194 out.extend(partials);
195 }
196 out
197 }
198
199 pub fn probe_rows(&self, probe_rows: &[Vec<Value>]) -> Vec<Vec<Value>> {
201 let mut out = Vec::new();
202 for row in probe_rows {
203 out.extend(self.probe_row(row));
204 }
205 out
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn k(v: i32) -> IntersectKey {
214 IntersectKey::Int(v as i64)
215 }
216
217 #[test]
218 fn two_way_merge_basics() {
219 assert_eq!(
220 two_way_sorted_intersect(&[k(1), k(2), k(3)], &[k(2), k(3), k(4)]),
221 vec![k(2), k(3)]
222 );
223 assert!(two_way_sorted_intersect(&[k(1)], &[k(2)]).is_empty());
224 assert!(two_way_sorted_intersect(&[], &[k(1)]).is_empty());
225 }
226
227 #[test]
228 fn two_way_merge_collapses_duplicates() {
229 assert_eq!(
230 two_way_sorted_intersect(&[k(1), k(1), k(2)], &[k(1), k(2), k(2)]),
231 vec![k(1), k(2)]
232 );
233 }
234
235 #[test]
236 fn multiway_smallest_first() {
237 let lists = vec![
238 vec![k(1), k(2), k(3), k(4)],
239 vec![k(2), k(4)],
240 vec![k(1), k(2), k(4)],
241 ];
242 assert_eq!(multiway_sorted_intersect(&lists), vec![k(2), k(4)]);
243 assert!(multiway_sorted_intersect(&[vec![k(1)], vec![]]).is_empty());
244 assert!(multiway_sorted_intersect(&[]).is_empty());
245 }
246
247 fn triangle_executor() -> WcoIntersectExecutor {
248 let mut b1 = IntersectBuild::new(0, 1);
250 b1.append_rows(&[
251 vec![Value::Int(1), Value::Int(10)],
252 vec![Value::Int(1), Value::Int(20)],
253 vec![Value::Int(2), Value::Int(30)],
254 ]);
255 b1.finish();
256 let mut b2 = IntersectBuild::new(0, 1);
257 b2.append_rows(&[
258 vec![Value::Int(2), Value::Int(20)],
259 vec![Value::Int(2), Value::Int(40)],
260 ]);
261 b2.finish();
262 WcoIntersectExecutor::new(vec![b1, b2], vec![0, 1])
263 }
264
265 #[test]
266 fn triangle_probe_emits_common_adjacency() {
267 let exec = triangle_executor();
268 let out = exec.probe_row(&[Value::Int(1), Value::Int(2)]);
269 assert_eq!(out.len(), 1);
271 assert_eq!(
272 out[0],
273 vec![
274 Value::Int(1),
275 Value::Int(2),
276 Value::Int(20),
277 Value::Int(1),
278 Value::Int(20),
279 Value::Int(2),
280 Value::Int(20),
281 ]
282 );
283 }
284
285 #[test]
286 fn probe_without_common_adjacency_emits_nothing() {
287 let exec = triangle_executor();
288 assert!(exec.probe_row(&[Value::Int(2), Value::Int(2)]).is_empty());
290 assert!(exec.probe_row(&[Value::Int(9), Value::Int(2)]).is_empty());
292 }
293
294 #[test]
295 fn duplicate_adjacency_fans_out() {
296 let mut b1 = IntersectBuild::new(0, 1);
297 b1.append_rows(&[
298 vec![Value::Int(1), Value::Int(7), Value::string("x")],
299 vec![Value::Int(1), Value::Int(7), Value::string("y")],
300 ]);
301 b1.finish();
302 let mut b2 = IntersectBuild::new(0, 1);
303 b2.append_rows(&[vec![Value::Int(1), Value::Int(7), Value::string("z")]]);
304 b2.finish();
305 let exec = WcoIntersectExecutor::new(vec![b1, b2], vec![0, 0]);
306 let out = exec.probe_row(&[Value::Int(1)]);
307 assert_eq!(out.len(), 2);
308 }
309}