1use std::sync::Arc;
16
17use crate::executor::base::MemoryTracker;
18use crate::executor::streaming::chunk::DataChunk;
19use crate::executor::streaming::executor::StreamingExecutor;
20use crate::executor::streaming::operators::intersect_build::IntersectBuild;
21use crate::executor::streaming::operators::source_operator::OperatorConfig;
22use crate::executor::streaming::runtime::ExecutionRuntime;
23use crate::executor::streaming::slot::SlotLayout;
24use linkrs_core::error::QueryError;
25use linkrs_core::value::NullType;
26use linkrs_core::Value;
27
28use super::intersect::WcoIntersectExecutor;
29use super::spec::WcoSpec;
30
31#[derive(Debug, Clone, Default)]
33struct BuildLayout {
34 bound_col: usize,
35 intersect_col: usize,
36 col_names: Vec<String>,
37}
38
39#[derive(Debug)]
46pub struct WcoIntersectOperator {
47 spec: WcoSpec,
48 builds: Vec<IntersectBuild>,
49 build_layouts: Vec<BuildLayout>,
50 build_done: bool,
51 memory_tracker: MemoryTracker,
52 runtime: Option<Arc<ExecutionRuntime>>,
53 output_layout: Arc<SlotLayout>,
54 config: OperatorConfig,
55}
56
57impl WcoIntersectOperator {
58 pub fn new(spec: WcoSpec, output_layout: Arc<SlotLayout>) -> Self {
59 Self {
60 spec,
61 builds: Vec::new(),
62 build_layouts: Vec::new(),
63 build_done: false,
64 memory_tracker: MemoryTracker::new(crate::executor::base::MemoryBudget::new(
65 crate::executor::base::MemoryBudget::DEFAULT_MAX,
66 )),
67 runtime: None,
68 output_layout,
69 config: OperatorConfig::default(),
70 }
71 }
72
73 pub fn from_spec(
74 spec: &WcoSpec,
75 memory_budget: &crate::executor::base::MemoryBudget,
76 output_layout: Arc<SlotLayout>,
77 ) -> Self {
78 let mut op = Self::new(spec.clone(), output_layout);
79 op.memory_tracker = MemoryTracker::new(memory_budget.clone());
80 op
81 }
82
83 pub fn inject_context(
86 &mut self,
87 runtime: Option<&Arc<ExecutionRuntime>>,
88 config: OperatorConfig,
89 ) {
90 if let Some(rt) = runtime {
91 self.runtime = Some(rt.clone());
92 }
93 self.config = config;
94 }
95
96 pub fn memory_tracker(&self) -> &MemoryTracker {
97 &self.memory_tracker
98 }
99
100 pub fn open(
101 &mut self,
102 probe: &mut StreamingExecutor,
103 builds: &mut [StreamingExecutor],
104 ) -> Result<(), QueryError> {
105 probe.open()?;
106 for build in builds.iter_mut() {
107 build.open()?;
108 }
109 Ok(())
110 }
111
112 pub fn next(
113 &mut self,
114 probe: &mut StreamingExecutor,
115 builds: &mut [StreamingExecutor],
116 ) -> Result<Option<DataChunk>, QueryError> {
117 if builds.len() != self.spec.num_builds() {
118 return Err(QueryError::execution(format!(
119 "WcoIntersect planned {} build sides but received {} inputs",
120 self.spec.num_builds(),
121 builds.len()
122 )));
123 }
124 if !self.build_done {
125 self.drain_builds(builds)?;
126 }
127 let output_layout = Arc::clone(&self.output_layout);
128 while let Some(probe_chunk) = probe.advance()? {
129 if let Some(rt) = self.runtime.as_ref() {
130 rt.ensure_not_cancelled()?;
131 }
132 let chunk_names = probe_chunk.col_names();
133 let mut bound_cols = Vec::with_capacity(self.spec.bound_names.len());
134 for name in &self.spec.bound_names {
135 bound_cols.push(chunk_names.iter().position(|c| c == name).ok_or_else(|| {
136 QueryError::execution(format!(
137 "WcoIntersect probe side is missing bound column `{name}`"
138 ))
139 })?);
140 }
141 let builds = std::mem::take(&mut self.builds);
144 let executor = WcoIntersectExecutor::new(builds, bound_cols);
145 let mut result_rows = Vec::new();
146 for row_idx in probe_chunk.visible_indices() {
147 for wide in executor.probe_row(&probe_chunk.rows[row_idx]) {
148 result_rows.push(self.assemble_output_row(&wide, &chunk_names));
149 }
150 }
151 self.builds = executor.into_builds();
152 if !result_rows.is_empty() {
153 return Ok(Some(DataChunk::new_with_layout(result_rows, output_layout)));
154 }
155 }
156 Ok(None)
157 }
158
159 pub fn reset(
162 &mut self,
163 probe: &mut StreamingExecutor,
164 builds: &mut [StreamingExecutor],
165 ) -> Result<bool, QueryError> {
166 self.builds.clear();
167 self.build_layouts.clear();
168 self.build_done = false;
169 self.memory_tracker.reset();
170 probe.reset()?;
171 for build in builds.iter_mut() {
172 build.reset()?;
173 }
174 Ok(false)
175 }
176
177 pub fn stop(&mut self) -> Result<(), QueryError> {
178 Ok(())
179 }
180
181 pub fn close(&mut self) -> Result<(), QueryError> {
182 self.memory_tracker.reset();
183 for build in &mut self.builds {
184 build.clear();
185 }
186 Ok(())
187 }
188
189 pub fn spill_with_manager(
190 &mut self,
191 _sm: &crate::executor::streaming::spill::SpillManager,
192 ) -> Result<(), QueryError> {
193 Ok(())
194 }
195
196 pub fn spilled_bytes(&self) -> u64 {
197 0
198 }
199
200 pub fn spilled_rows(&self) -> u64 {
201 0
202 }
203
204 fn drain_builds(&mut self, builds: &mut [StreamingExecutor]) -> Result<(), QueryError> {
206 self.builds = Vec::with_capacity(builds.len());
207 self.build_layouts = Vec::with_capacity(builds.len());
208 for (side, build) in builds.iter_mut().enumerate() {
209 let mut table: Option<IntersectBuild> = None;
210 let mut layout = BuildLayout::default();
211 let mut col_names = Vec::new();
212 while let Some(mut chunk) = build.advance()? {
213 if let Some(rt) = self.runtime.as_ref() {
214 rt.ensure_not_cancelled()?;
215 }
216 chunk.normalize_for_opaque("WcoBuild");
217 if col_names.is_empty() {
218 col_names = chunk.col_names();
219 layout = self.resolve_build_layout(side, &col_names)?;
220 }
221 for row in chunk.rows.iter() {
222 self.memory_tracker.try_reserve_row(row)?;
223 }
224 let table = table.get_or_insert_with(|| {
225 IntersectBuild::new(layout.bound_col, layout.intersect_col)
226 });
227 for row in chunk.rows.iter().cloned() {
228 table.append_row(row);
229 }
230 }
231 let mut table = table.unwrap_or_else(|| IntersectBuild::new(0, 0));
234 table.finish();
235 layout.col_names = col_names;
236 self.builds.push(table);
237 self.build_layouts.push(layout);
238 }
239 self.build_done = true;
240 Ok(())
241 }
242
243 fn resolve_build_layout(
245 &self,
246 side: usize,
247 col_names: &[String],
248 ) -> Result<BuildLayout, QueryError> {
249 let bound_name = &self.spec.bound_names[side];
250 let bound_col = col_names
251 .iter()
252 .position(|c| c == bound_name)
253 .ok_or_else(|| {
254 QueryError::execution(format!(
255 "WcoIntersect build side {side} is missing bound column `{bound_name}`"
256 ))
257 })?;
258 let intersect_name = &self.spec.intersect_name;
259 let intersect_col = col_names
260 .iter()
261 .position(|c| c == intersect_name)
262 .ok_or_else(|| {
263 QueryError::execution(format!(
264 "WcoIntersect build side {side} is missing intersect column `{intersect_name}`"
265 ))
266 })?;
267 Ok(BuildLayout {
268 bound_col,
269 intersect_col,
270 col_names: Vec::new(),
271 })
272 }
273
274 fn assemble_output_row(&self, wide: &[Value], probe_names: &[String]) -> Vec<Value> {
277 let probe_len = probe_names.len();
280 let mut build_offsets = Vec::with_capacity(self.build_layouts.len());
281 let mut offset = probe_len + 1;
282 for layout in &self.build_layouts {
283 build_offsets.push(offset);
284 offset += layout.col_names.len();
285 }
286 let rep = wide
287 .get(probe_len)
288 .cloned()
289 .unwrap_or(Value::Null(NullType::Null));
290 let mut out = Vec::with_capacity(self.spec.output_col_names.len());
291 for name in &self.spec.output_col_names {
292 if name == &self.spec.intersect_name {
293 out.push(rep.clone());
294 continue;
295 }
296 if let Some(idx) = probe_names.iter().position(|c| c == name) {
297 out.push(
298 wide.get(idx)
299 .cloned()
300 .unwrap_or(Value::Null(NullType::Null)),
301 );
302 continue;
303 }
304 let mut pushed = false;
305 for (side, layout) in self.build_layouts.iter().enumerate() {
306 if let Some(idx) = layout.col_names.iter().position(|c| c == name) {
307 let pos = build_offsets[side] + idx;
308 out.push(
309 wide.get(pos)
310 .cloned()
311 .unwrap_or(Value::Null(NullType::Null)),
312 );
313 pushed = true;
314 break;
315 }
316 }
317 if !pushed {
318 out.push(Value::Null(NullType::Null));
319 }
320 }
321 out
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::executor::streaming::operators::source_operator::{
329 SourceOperator, SourceOperatorKind,
330 };
331 use crate::executor::streaming::slot::SlotLayout;
332 use linkrs_core::Value;
333
334 fn source(rows: Vec<Vec<Value>>, col_names: Vec<String>) -> StreamingExecutor {
335 let layout = Arc::new(SlotLayout::from_names(&col_names));
336 let base = crate::executor::streaming::operators::base::OperatorBase::new(1)
337 .with_output_layout(layout.clone());
338 let op = SourceOperator::new(
339 SourceOperatorKind::ScanVertices {
340 buffer: rows,
341 current_index: 0,
342 col_names,
343 },
344 layout,
345 );
346 StreamingExecutor::Source(base, op)
347 }
348
349 fn triangle_spec() -> WcoSpec {
350 WcoSpec {
351 bound_names: vec!["a".to_string(), "b".to_string()],
352 intersect_name: "c".to_string(),
353 output_col_names: vec![
354 "a".to_string(),
355 "b".to_string(),
356 "e1".to_string(),
357 "c".to_string(),
358 "e2".to_string(),
359 "e3".to_string(),
360 ],
361 }
362 }
363
364 #[test]
367 fn triangle_probes_common_adjacency() {
368 let layout = Arc::new(SlotLayout::from_names(&triangle_spec().output_col_names));
369 let mut op = WcoIntersectOperator::from_spec(
370 &triangle_spec(),
371 &crate::executor::base::MemoryBudget::new(
372 crate::executor::base::MemoryBudget::DEFAULT_MAX,
373 ),
374 layout.clone(),
375 );
376 let mut probe = source(
377 vec![vec![Value::Int(1), Value::Int(2), Value::Int(7)]],
378 vec!["a".to_string(), "b".to_string(), "e1".to_string()],
379 );
380 let mut builds = vec![
381 source(
382 vec![
383 vec![Value::Int(1), Value::Int(10), Value::Int(11)],
384 vec![Value::Int(1), Value::Int(20), Value::Int(12)],
385 ],
386 vec!["a".to_string(), "c".to_string(), "e2".to_string()],
387 ),
388 source(
389 vec![
390 vec![Value::Int(2), Value::Int(20), Value::Int(13)],
391 vec![Value::Int(2), Value::Int(40), Value::Int(14)],
392 ],
393 vec!["b".to_string(), "c".to_string(), "e3".to_string()],
394 ),
395 ];
396 op.open(&mut probe, &mut builds).expect("open");
397 let chunk = op
398 .next(&mut probe, &mut builds)
399 .expect("next")
400 .expect("one chunk");
401 assert_eq!(chunk.rows.len(), 1);
402 assert_eq!(
403 chunk.rows[0],
404 vec![
405 Value::Int(1),
406 Value::Int(2),
407 Value::Int(7),
408 Value::Int(20),
409 Value::Int(12),
410 Value::Int(13),
411 ]
412 );
413 assert!(op.next(&mut probe, &mut builds).expect("drain").is_none());
414 op.close().expect("close");
415 }
416
417 #[test]
418 fn disjoint_adjacency_emits_nothing() {
419 let layout = Arc::new(SlotLayout::from_names(&triangle_spec().output_col_names));
420 let mut op = WcoIntersectOperator::from_spec(
421 &triangle_spec(),
422 &crate::executor::base::MemoryBudget::new(
423 crate::executor::base::MemoryBudget::DEFAULT_MAX,
424 ),
425 layout,
426 );
427 let mut probe = source(
428 vec![vec![Value::Int(2), Value::Int(2), Value::Int(7)]],
429 vec!["a".to_string(), "b".to_string(), "e1".to_string()],
430 );
431 let mut builds = vec![
432 source(
433 vec![vec![Value::Int(2), Value::Int(30), Value::Int(11)]],
434 vec!["a".to_string(), "c".to_string(), "e2".to_string()],
435 ),
436 source(
437 vec![vec![Value::Int(2), Value::Int(40), Value::Int(13)]],
438 vec!["b".to_string(), "c".to_string(), "e3".to_string()],
439 ),
440 ];
441 op.open(&mut probe, &mut builds).expect("open");
442 assert!(op.next(&mut probe, &mut builds).expect("next").is_none());
443 op.close().expect("close");
444 }
445
446 #[test]
447 fn missing_bound_column_is_loud_error() {
448 let layout = Arc::new(SlotLayout::from_names(&triangle_spec().output_col_names));
449 let mut op = WcoIntersectOperator::from_spec(
450 &triangle_spec(),
451 &crate::executor::base::MemoryBudget::new(
452 crate::executor::base::MemoryBudget::DEFAULT_MAX,
453 ),
454 layout,
455 );
456 let mut probe = source(
457 vec![vec![Value::Int(1), Value::Int(2), Value::Int(7)]],
458 vec!["a".to_string(), "b".to_string(), "e1".to_string()],
459 );
460 let mut builds = vec![
462 source(
463 vec![vec![Value::Int(1), Value::Int(11)]],
464 vec!["a".to_string(), "e2".to_string()],
465 ),
466 source(
467 vec![vec![Value::Int(2), Value::Int(20), Value::Int(13)]],
468 vec!["b".to_string(), "c".to_string(), "e3".to_string()],
469 ),
470 ];
471 op.open(&mut probe, &mut builds).expect("open");
472 let err = op.next(&mut probe, &mut builds).expect_err("must fail");
473 assert!(err.to_string().contains("intersect column"));
474 }
475
476 #[test]
477 fn reset_rebuilds_and_replays() {
478 let layout = Arc::new(SlotLayout::from_names(&triangle_spec().output_col_names));
479 let mut op = WcoIntersectOperator::from_spec(
480 &triangle_spec(),
481 &crate::executor::base::MemoryBudget::new(
482 crate::executor::base::MemoryBudget::DEFAULT_MAX,
483 ),
484 layout,
485 );
486 let mut probe = source(
487 vec![vec![Value::Int(1), Value::Int(2), Value::Int(7)]],
488 vec!["a".to_string(), "b".to_string(), "e1".to_string()],
489 );
490 let mut builds = vec![
491 source(
492 vec![vec![Value::Int(1), Value::Int(20), Value::Int(12)]],
493 vec!["a".to_string(), "c".to_string(), "e2".to_string()],
494 ),
495 source(
496 vec![vec![Value::Int(2), Value::Int(20), Value::Int(13)]],
497 vec!["b".to_string(), "c".to_string(), "e3".to_string()],
498 ),
499 ];
500 op.open(&mut probe, &mut builds).expect("open");
501 let first = op
502 .next(&mut probe, &mut builds)
503 .expect("next")
504 .expect("rows");
505 assert_eq!(first.rows.len(), 1);
506 op.reset(&mut probe, &mut builds).expect("reset");
507 let replayed = op
508 .next(&mut probe, &mut builds)
509 .expect("next")
510 .expect("rows");
511 assert_eq!(replayed.rows, first.rows);
512 op.close().expect("close");
513 }
514}