1use std::cmp::Ordering;
30use std::collections::{HashMap, HashSet};
31use std::fmt;
32use std::hash::{Hash, Hasher};
33use std::sync::Arc;
34
35use arrow::array::{
36 new_empty_array, Array, ArrayRef, AsArray, Int64Array, RecordBatch, UInt32Array, UInt64Array,
37};
38use arrow::compute::{cast, filter as filter_array, take};
39use arrow::datatypes::{DataType, Field, Int64Type, Schema, SchemaRef};
40use async_trait::async_trait;
41use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
42use datafusion::common::Column;
43use datafusion::common::{DFSchema, DFSchemaRef};
44use datafusion::execution::context::{QueryPlanner, SessionState};
45use datafusion::execution::{SendableRecordBatchStream, TaskContext};
46use datafusion::logical_expr::utils::{conjunction, split_conjunction};
47use datafusion::logical_expr::{
48 Expr, LogicalPlan, UserDefinedLogicalNode, UserDefinedLogicalNodeCore,
49};
50use datafusion::logical_expr::{Extension, LogicalPlanBuilder};
51use datafusion::optimizer::{OptimizerConfig, OptimizerRule};
52use datafusion::physical_expr::{EquivalenceProperties, PhysicalExpr};
53use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
54use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
55use datafusion::physical_plan::{
56 DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, Partitioning,
57 PlanProperties,
58};
59use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
60use futures::StreamExt;
61use lance::dataset::{Dataset, ProjectionRequest};
62use lance_table::format::Fragment;
63
64pub(crate) async fn cooperative_yield() {
66 let mut yielded = false;
67 std::future::poll_fn(|cx| {
68 if yielded {
69 std::task::Poll::Ready(())
70 } else {
71 yielded = true;
72 cx.waker().wake_by_ref();
73 std::task::Poll::Pending
74 }
75 })
76 .await
77}
78
79const ROW_ADDR: &str = "_rowaddr";
81
82use crate::case_insensitive::qualify_column;
83
84type FastMap<K, V> = HashMap<K, V, ahash::RandomState>;
88use crate::csr_index::{CsrIndex, CsrIndexBuilder};
89use crate::error::{GraphError, Result};
90
91pub struct Adjacency {
123 base: Arc<CsrBase>,
124 added: FastMap<i64, Vec<u64>>,
125 removed: FastMap<i64, Vec<usize>>,
126 added_count: u64,
127 removed_count: u64,
128}
129
130struct CsrBase {
131 dense: FastMap<i64, u64>,
133 csr: CsrIndex,
134}
135
136impl Adjacency {
137 pub async fn build(
142 mut stream: SendableRecordBatchStream,
143 from: &str,
144 to: &str,
145 ) -> Result<Self> {
146 let mut dense: FastMap<i64, u64> = FastMap::default();
147 let mut builder = CsrIndexBuilder::new();
148 while let Some(batch) = stream.next().await {
149 let batch = batch.map_err(|e| exec_error(format!("Failed to read edges: {}", e)))?;
150 let from_col = int64_column(&batch, from)?;
151 let to_col = int64_column(&batch, to)?;
152 for i in 0..batch.num_rows() {
153 if from_col.is_null(i) || to_col.is_null(i) {
154 continue;
155 }
156 let next = dense.len() as u64;
157 let src = *dense.entry(from_col.value(i)).or_insert(next);
158 builder = builder.add_edge(src, to_col.value(i) as u64);
159 }
160 }
161 let csr = builder.with_num_vertices(dense.len() as u64).build();
164 Ok(Self {
165 base: Arc::new(CsrBase { dense, csr }),
166 added: FastMap::default(),
167 removed: FastMap::default(),
168 added_count: 0,
169 removed_count: 0,
170 })
171 }
172
173 pub fn with_changes(&self, added: &[(i64, i64)], removed: &[(i64, i64)]) -> Self {
180 let mut next = Self {
181 base: self.base.clone(),
182 added: self.added.clone(),
183 removed: self.removed.clone(),
184 added_count: self.added_count + added.len() as u64,
185 removed_count: self.removed_count + removed.len() as u64,
186 };
187 for &(from, to) in added {
188 next.added.entry(from).or_default().push(to as u64);
189 }
190 let mut removals: FastMap<i64, FastMap<u64, usize>> = FastMap::default();
191 for &(from, to) in removed {
192 *removals
193 .entry(from)
194 .or_default()
195 .entry(to as u64)
196 .or_default() += 1;
197 }
198 for (from, mut counts) in removals {
199 let base = next.base_neighbors(from);
200 let added = next.added.get(&from).map_or(&[][..], Vec::as_slice);
201 let prior = next.removed.get(&from).map_or(&[][..], Vec::as_slice);
202 let mut prior = prior.iter().copied().peekable();
203 let mut positions = Vec::new();
204 for (position, neighbor) in base.iter().chain(added).enumerate() {
205 if prior.peek() == Some(&position) {
206 prior.next();
207 continue;
208 }
209 if let Some(count) = counts.get_mut(neighbor) {
210 positions.push(position);
211 *count -= 1;
212 if *count == 0 {
213 counts.remove(neighbor);
214 }
215 if counts.is_empty() {
216 break;
217 }
218 }
219 }
220 debug_assert!(counts.is_empty(), "removed edges must be present");
221 let tombstones = next.removed.entry(from).or_default();
222 tombstones.extend(positions);
223 tombstones.sort_unstable();
224 }
225 next
226 }
227
228 pub fn for_each_neighbor(&self, key: i64, f: impl FnMut(u64)) {
230 self.neighbors_from(key, &mut 0).for_each(f);
231 }
232
233 #[inline(always)]
234 fn base_neighbors(&self, key: i64) -> &[u64] {
235 self.base
236 .dense
237 .get(&key)
238 .map_or(&[], |&v| self.base.csr.neighbors(v))
239 }
240
241 #[inline(always)]
243 fn neighbors_from<'a>(
244 &'a self,
245 key: i64,
246 offset: &'a mut usize,
247 ) -> impl Iterator<Item = u64> + 'a {
248 self.candidates_from(key, offset).flatten()
249 }
250
251 #[inline(always)]
253 fn candidates_from<'a>(
254 &'a self,
255 key: i64,
256 offset: &'a mut usize,
257 ) -> impl Iterator<Item = Option<u64>> + 'a {
258 let base = self.base_neighbors(key);
259 let added = self.added.get(&key).map_or(&[][..], Vec::as_slice);
260 let removed = self.removed.get(&key).map_or(&[][..], Vec::as_slice);
261 let mut removed = &removed[removed.partition_point(|&position| position < *offset)..];
262 let added_offset = offset.saturating_sub(base.len());
263 base.get(*offset..)
264 .unwrap_or_default()
265 .iter()
266 .chain(added.get(added_offset..).unwrap_or_default())
267 .map(move |&neighbor| {
268 let position = *offset;
269 *offset += 1;
270 if removed.first() == Some(&position) {
271 removed = &removed[1..];
272 None
273 } else {
274 Some(neighbor)
275 }
276 })
277 }
278
279 pub fn num_edges(&self) -> u64 {
281 self.base.csr.num_edges() + self.added_count - self.removed_count
282 }
283
284 pub fn base_edges(&self) -> u64 {
285 self.base.csr.num_edges()
286 }
287
288 pub fn num_sources(&self) -> usize {
290 self.base.dense.len()
291 }
292
293 pub fn delta_edges(&self) -> u64 {
295 self.added_count
296 }
297
298 pub fn tombstones(&self) -> u64 {
300 self.removed_count
301 }
302}
303
304impl fmt::Debug for Adjacency {
307 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308 f.debug_struct("Adjacency")
309 .field("sources", &self.base.dense.len())
310 .field("edges", &self.num_edges())
311 .field("delta_edges", &self.added_count)
312 .field("tombstones", &self.removed_count)
313 .finish()
314 }
315}
316
317fn int64_column(batch: &RecordBatch, name: &str) -> Result<Int64Array> {
318 let (idx, _) = batch
319 .schema()
320 .fields()
321 .iter()
322 .enumerate()
323 .find(|(_, f)| f.name().eq_ignore_ascii_case(name))
324 .ok_or_else(|| exec_error(format!("Edge column '{}' not found", name)))?;
325 let array = cast(batch.column(idx), &DataType::Int64)
326 .map_err(|e| exec_error(format!("Edge column '{}' is not an integer: {}", name, e)))?;
327 Ok(array.as_primitive::<Int64Type>().clone())
328}
329
330pub fn is_supported_id_type(data_type: &DataType) -> bool {
332 matches!(
333 data_type,
334 DataType::Int8
335 | DataType::Int16
336 | DataType::Int32
337 | DataType::Int64
338 | DataType::UInt8
339 | DataType::UInt16
340 | DataType::UInt32
341 )
342}
343
344#[derive(Debug, Default, PartialEq)]
347pub struct EdgeChanges {
348 pub added: Vec<(i64, i64)>,
349 pub removed: Vec<(i64, i64)>,
350}
351
352impl EdgeChanges {
353 pub fn len(&self) -> usize {
354 self.added.len() + self.removed.len()
355 }
356
357 pub fn is_empty(&self) -> bool {
358 self.len() == 0
359 }
360}
361
362pub async fn edge_changes(
375 old: &Dataset,
376 new: &Dataset,
377 src: &str,
378 dst: &str,
379) -> Result<Option<EdgeChanges>> {
380 let endpoint_ids = |ds: &Dataset| -> Option<[(i32, DataType); 2]> {
381 let field = |name: &str| ds.schema().field(name).map(|f| (f.id, f.data_type()));
382 Some([field(src)?, field(dst)?])
383 };
384 let old_ids = endpoint_ids(old);
385 if old_ids.is_none() || old_ids != endpoint_ids(new) {
386 return Ok(None);
387 }
388 let fields: Vec<i32> = old_ids.unwrap().iter().map(|(id, _)| *id).collect();
389 let touches = |f: &[i32]| f.iter().any(|id| fields.contains(id));
390 let endpoint_data = |frag: &Fragment| {
392 let files: Vec<_> = frag.files.iter().filter(|d| touches(&d.fields)).collect();
393 let overlays: Vec<_> = frag
394 .overlays
395 .iter()
396 .filter(|o| touches(&o.data_file.fields))
397 .collect();
398 format!("{:?}{:?}", files, overlays)
399 };
400
401 let old_frags: HashMap<u64, Fragment> = old
402 .get_fragments()
403 .into_iter()
404 .map(|f| (f.id() as u64, f.metadata().clone()))
405 .collect();
406 let new_frags: HashMap<u64, Fragment> = new
407 .get_fragments()
408 .into_iter()
409 .map(|f| (f.id() as u64, f.metadata().clone()))
410 .collect();
411
412 let mut changes = EdgeChanges::default();
413 let mut removed_whole = Vec::new();
414 let mut added_whole = Vec::new();
415 for (id, old_frag) in &old_frags {
416 let Some(new_frag) = new_frags.get(id) else {
417 removed_whole.push(old_frag.clone());
418 continue;
419 };
420 if endpoint_data(old_frag) != endpoint_data(new_frag) {
421 removed_whole.push(old_frag.clone());
422 added_whole.push(new_frag.clone());
423 } else if old_frag.deletion_file != new_frag.deletion_file {
424 let deleted = |ds: &Dataset| {
425 let frag = ds.get_fragment(*id as usize);
426 async move {
427 match frag {
428 Some(frag) => frag.get_deletion_vector().await.map_err(lance_error),
429 None => Ok(None),
430 }
431 }
432 };
433 let (old_dv, new_dv) = (deleted(old).await?, deleted(new).await?);
434 let newly: HashSet<u32> = new_dv
435 .iter()
436 .flat_map(|dv| dv.to_sorted_iter().collect::<Vec<_>>())
437 .filter(|offset| !old_dv.as_ref().is_some_and(|dv| dv.contains(*offset)))
438 .collect();
439 if !newly.is_empty() {
440 let rows = scan_edges(old, vec![old_frag.clone()], src, dst, true).await?;
441 changes.removed.extend(
442 rows.into_iter()
443 .filter(|(_, addr)| newly.contains(&((addr & 0xFFFF_FFFF) as u32)))
444 .map(|(edge, _)| edge),
445 );
446 }
447 }
448 }
449 for (id, new_frag) in &new_frags {
450 if !old_frags.contains_key(id) {
451 added_whole.push(new_frag.clone());
452 }
453 }
454 if !removed_whole.is_empty() {
455 let rows = scan_edges(old, removed_whole, src, dst, false).await?;
456 changes
457 .removed
458 .extend(rows.into_iter().map(|(edge, _)| edge));
459 }
460 if !added_whole.is_empty() {
461 let rows = scan_edges(new, added_whole, src, dst, false).await?;
462 changes.added.extend(rows.into_iter().map(|(edge, _)| edge));
463 }
464 Ok(Some(changes))
465}
466
467async fn scan_edges(
470 ds: &Dataset,
471 fragments: Vec<Fragment>,
472 src: &str,
473 dst: &str,
474 with_address: bool,
475) -> Result<Vec<((i64, i64), u64)>> {
476 let mut scanner = ds.scan();
477 scanner.with_fragments(fragments);
478 scanner.project(&[src, dst]).map_err(lance_error)?;
479 if with_address {
480 scanner.with_row_address();
481 }
482 let mut stream = scanner.try_into_stream().await.map_err(lance_error)?;
483 let mut out = Vec::new();
484 while let Some(batch) = stream.next().await {
485 let batch = batch.map_err(lance_error)?;
486 let (s, d) = (int64_column(&batch, src)?, int64_column(&batch, dst)?);
487 let addrs = with_address.then(|| {
488 batch
489 .column_by_name(ROW_ADDR)
490 .map(|c| c.as_primitive::<arrow::datatypes::UInt64Type>().clone())
491 });
492 let addrs = match addrs {
493 Some(None) => return Err(exec_error("Edge scan returned no row addresses".into())),
494 Some(Some(a)) => Some(a),
495 None => None,
496 };
497 for i in 0..batch.num_rows() {
498 if s.is_valid(i) && d.is_valid(i) {
499 let addr = addrs.as_ref().map_or(0, |a| a.value(i));
500 out.push(((s.value(i), d.value(i)), addr));
501 }
502 }
503 }
504 Ok(out)
505}
506
507fn lance_error(e: lance::Error) -> GraphError {
508 exec_error(format!("Lance: {}", e))
509}
510
511pub struct NodeRows {
513 positions: FastMap<i64, u64>,
515 data: NodeData,
516 schema: SchemaRef,
517 id_index: usize,
519 #[cfg(test)]
520 fetched_cells: std::sync::atomic::AtomicUsize,
521}
522
523enum NodeData {
524 Lance(Arc<Dataset>),
527 Memory(RecordBatch),
528}
529
530impl NodeRows {
531 pub async fn from_lance(dataset: Arc<Dataset>, id_column: &str) -> Result<Option<Self>> {
536 let schema: SchemaRef = Arc::new(Schema::from(dataset.schema()));
537 let Some((id_index, id_name)) = supported_id_column(&schema, id_column) else {
538 return Ok(None);
539 };
540 let lance_err = |e: lance::Error| exec_error(format!("Failed to scan node ids: {}", e));
541 let mut scanner = dataset.scan();
542 scanner.project(&[id_name.as_str()]).map_err(lance_err)?;
543 scanner.with_row_id();
544 let mut stream = scanner.try_into_stream().await.map_err(lance_err)?;
545 let mut positions = FastMap::default();
546 while let Some(batch) = stream.next().await {
547 let batch = batch.map_err(lance_err)?;
548 let ids = int64_column(&batch, &id_name)?;
549 let row_ids = batch
550 .column_by_name(lance::dataset::ROW_ID)
551 .ok_or_else(|| exec_error("Node scan returned no row ids".to_string()))?
552 .as_primitive::<arrow::datatypes::UInt64Type>()
553 .clone();
554 if !insert_unique(&mut positions, &ids, |i| row_ids.value(i)) {
555 return Ok(None);
556 }
557 }
558 Ok(Some(Self {
559 positions,
560 data: NodeData::Lance(dataset),
561 schema,
562 id_index,
563 #[cfg(test)]
564 fetched_cells: std::sync::atomic::AtomicUsize::new(0),
565 }))
566 }
567
568 pub fn from_batch(batch: RecordBatch, id_column: &str) -> Result<Option<Self>> {
570 let Some((id_index, id_name)) = supported_id_column(&batch.schema(), id_column) else {
571 return Ok(None);
572 };
573 let ids = int64_column(&batch, &id_name)?;
574 let mut positions = FastMap::default();
575 if !insert_unique(&mut positions, &ids, |i| i as u64) {
576 return Ok(None);
577 }
578 Ok(Some(Self {
579 positions,
580 schema: batch.schema(),
581 data: NodeData::Memory(batch),
582 id_index,
583 #[cfg(test)]
584 fetched_cells: std::sync::atomic::AtomicUsize::new(0),
585 }))
586 }
587
588 pub fn num_rows(&self) -> usize {
589 self.positions.len()
590 }
591
592 pub(crate) fn position(&self, id: i64) -> Option<u64> {
593 self.positions.get(&id).copied()
594 }
595
596 async fn take(&self, positions: &[u64], columns: &[usize]) -> Result<Vec<ArrayRef>> {
599 if columns.is_empty() {
600 return Ok(vec![]);
601 }
602 if positions.is_empty() {
603 return Ok(columns
604 .iter()
605 .map(|&c| new_empty_array(self.schema.field(c).data_type()))
606 .collect());
607 }
608 #[cfg(test)]
609 self.fetched_cells.fetch_add(
610 positions.len() * columns.len(),
611 std::sync::atomic::Ordering::Relaxed,
612 );
613 match &self.data {
614 NodeData::Lance(dataset) => {
615 let names = columns.iter().map(|&c| self.schema.field(c).name());
616 let projection = ProjectionRequest::from_columns(names, dataset.schema());
618 let batch = dataset
619 .take_rows(positions, projection)
620 .await
621 .map_err(|e| exec_error(format!("Failed to take target rows: {}", e)))?;
622 Ok(batch.columns().to_vec())
623 }
624 NodeData::Memory(batch) => {
625 let indices = UInt64Array::from(positions.to_vec());
626 columns
627 .iter()
628 .map(|&c| take(batch.column(c).as_ref(), &indices, None))
629 .collect::<std::result::Result<_, _>>()
630 .map_err(|e| exec_error(format!("Failed to take target rows: {}", e)))
631 }
632 }
633 }
634}
635
636impl fmt::Debug for NodeRows {
638 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
639 f.debug_struct("NodeRows")
640 .field("rows", &self.positions.len())
641 .finish()
642 }
643}
644
645fn supported_id_column(schema: &Schema, id_column: &str) -> Option<(usize, String)> {
647 schema
648 .fields()
649 .iter()
650 .enumerate()
651 .find(|(_, f)| f.name().eq_ignore_ascii_case(id_column))
652 .filter(|(_, f)| is_supported_id_type(f.data_type()))
653 .map(|(i, f)| (i, f.name().clone()))
654}
655
656fn insert_unique(
658 positions: &mut FastMap<i64, u64>,
659 ids: &Int64Array,
660 position_of: impl Fn(usize) -> u64,
661) -> bool {
662 for i in 0..ids.len() {
663 if ids.is_valid(i) && positions.insert(ids.value(i), position_of(i)).is_some() {
664 return false;
665 }
666 }
667 true
668}
669
670#[derive(Clone)]
672pub struct TargetLookup {
673 pub variable: String,
674 pub rows: Arc<NodeRows>,
675 fields: Vec<Field>,
677 columns: Vec<usize>,
679}
680
681#[derive(Debug, Clone)]
684struct CompiledTargetFilter {
685 predicate: Arc<dyn PhysicalExpr>,
686 columns: Vec<usize>,
688 schema: SchemaRef,
689}
690
691impl fmt::Debug for TargetLookup {
693 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
694 f.debug_struct("TargetLookup")
695 .field("variable", &self.variable)
696 .field("columns", &self.describe_columns())
697 .finish()
698 }
699}
700
701impl TargetLookup {
702 fn raw_columns(&self) -> HashMap<String, String> {
705 self.rows
706 .schema
707 .fields()
708 .iter()
709 .map(|f| (qualify_column(&self.variable, f.name()), f.name().clone()))
710 .collect()
711 }
712
713 fn compile_filter(
716 &self,
717 filter: &Expr,
718 state: &SessionState,
719 ) -> datafusion::common::Result<CompiledTargetFilter> {
720 let raw = self.raw_columns();
721 let filter = filter
722 .clone()
723 .transform(|e| match &e {
724 Expr::Column(c) => match raw.get(&c.name) {
725 Some(name) => Ok(Transformed::yes(Expr::Column(Column::from_name(name)))),
726 None => Ok(Transformed::no(e)),
727 },
728 _ => Ok(Transformed::no(e)),
729 })?
730 .data;
731 let mut columns: Vec<usize> = filter
732 .column_refs()
733 .iter()
734 .map(|c| self.rows.schema.index_of(&c.name))
735 .collect::<std::result::Result<_, _>>()?;
736 columns.sort_unstable();
737 columns.dedup();
738 let schema = Arc::new(self.rows.schema.project(&columns)?);
739 let predicate =
740 state.create_physical_expr(filter, &DFSchema::try_from(schema.as_ref().clone())?)?;
741 Ok(CompiledTargetFilter {
742 predicate,
743 columns,
744 schema,
745 })
746 }
747
748 fn describe_columns(&self) -> String {
750 let names: Vec<String> = self
751 .columns
752 .iter()
753 .zip(&self.fields)
754 .map(|(&c, f)| {
755 let mark = if c == self.rows.id_index { "*" } else { "" };
756 format!("{}{}", f.name(), mark)
757 })
758 .collect();
759 format!("[{}]", names.join(", "))
760 }
761
762 pub fn new(variable: String, rows: Arc<NodeRows>, fields: Vec<Field>) -> Self {
764 let columns = (0..fields.len()).collect();
765 Self {
766 variable,
767 rows,
768 fields,
769 columns,
770 }
771 }
772}
773
774#[derive(Debug, Clone, Default)]
779pub struct AdjacencyCatalog {
780 entries: HashMap<(String, bool), Arc<Adjacency>>,
781 nodes: HashMap<String, Arc<NodeRows>>,
782}
783
784impl AdjacencyCatalog {
785 pub fn insert(&mut self, rel_type: &str, outgoing: bool, adjacency: Arc<Adjacency>) {
786 self.entries
787 .insert((rel_type.to_lowercase(), outgoing), adjacency);
788 }
789
790 pub fn get(&self, rel_type: &str, outgoing: bool) -> Option<&Arc<Adjacency>> {
791 self.entries.get(&(rel_type.to_lowercase(), outgoing))
792 }
793
794 pub fn insert_nodes(&mut self, label: &str, rows: Arc<NodeRows>) {
795 self.nodes.insert(label.to_lowercase(), rows);
796 }
797
798 pub fn nodes(&self, label: &str) -> Option<&Arc<NodeRows>> {
799 self.nodes.get(&label.to_lowercase())
800 }
801}
802
803#[derive(Debug, Clone)]
809pub struct Neighbors {
810 forward: Arc<Adjacency>,
811 reverse: Option<Arc<Adjacency>>,
812}
813
814#[derive(Clone, Copy, Default)]
816pub(crate) struct NeighborPosition {
817 forward: usize,
818 reverse: usize,
819}
820
821impl Neighbors {
822 pub fn directed(adjacency: Arc<Adjacency>) -> Self {
823 Self {
824 forward: adjacency,
825 reverse: None,
826 }
827 }
828
829 pub fn undirected(outgoing: Arc<Adjacency>, incoming: Arc<Adjacency>) -> Self {
830 Self {
831 forward: outgoing,
832 reverse: Some(incoming),
833 }
834 }
835
836 pub fn for_each_neighbor(&self, key: i64, f: impl FnMut(u64)) {
837 self.neighbors_from(key, &mut NeighborPosition::default())
838 .for_each(f);
839 }
840
841 #[inline(always)]
842 pub(crate) fn neighbors_from<'a>(
843 &'a self,
844 key: i64,
845 position: &'a mut NeighborPosition,
846 ) -> impl Iterator<Item = u64> + 'a {
847 self.candidates_from(key, position).flatten()
848 }
849
850 #[inline(always)]
852 pub(crate) fn candidates_from<'a>(
853 &'a self,
854 key: i64,
855 position: &'a mut NeighborPosition,
856 ) -> impl Iterator<Item = Option<u64>> + 'a {
857 let forward = self.forward.candidates_from(key, &mut position.forward);
858 let reverse = self
859 .reverse
860 .as_ref()
861 .map(|r| r.candidates_from(key, &mut position.reverse));
862 forward.chain(
863 reverse
864 .into_iter()
865 .flatten()
866 .map(move |candidate| candidate.filter(|&n| n as i64 != key)),
867 )
868 }
869
870 pub fn num_edges(&self) -> u64 {
872 self.forward.num_edges() + self.reverse.as_ref().map_or(0, |r| r.num_edges())
873 }
874
875 fn is_undirected(&self) -> bool {
876 self.reverse.is_some()
877 }
878
879 fn mean_degree(&self) -> f64 {
881 let degree = |a: &Adjacency| a.base_edges() as f64 / a.num_sources().max(1) as f64;
882 degree(&self.forward) + self.reverse.as_deref().map_or(0.0, degree)
883 }
884
885 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
886 Arc::ptr_eq(&self.forward, &other.forward)
887 && match (&self.reverse, &other.reverse) {
888 (Some(a), Some(b)) => Arc::ptr_eq(a, b),
889 (None, None) => true,
890 _ => false,
891 }
892 }
893}
894
895#[derive(Debug, Clone)]
897pub struct CsrExpandNode {
898 input: LogicalPlan,
899 key_column: String,
901 key_output: Field,
903 neighbor_output: Field,
904 adjacency: Neighbors,
905 target: Option<TargetLookup>,
907 target_filter: Option<Expr>,
910 closing_column: Option<String>,
913 traversal: Option<Arc<crate::csr_traversal::Traversal>>,
914 schema: DFSchemaRef,
915}
916
917impl CsrExpandNode {
918 pub fn try_new(
919 input: LogicalPlan,
920 key_column: String,
921 key_output: Field,
922 neighbor_output: Field,
923 adjacency: Neighbors,
924 target: Option<TargetLookup>,
925 ) -> Result<Self> {
926 let mut added_fields = vec![key_output.clone(), neighbor_output.clone()];
927 added_fields.extend(target.iter().flat_map(|t| t.fields.iter().cloned()));
928 let added = DFSchema::try_from(Schema::new(added_fields))
929 .map_err(|e| plan_error(format!("CsrExpand schema: {}", e)))?;
930 let schema = input
931 .schema()
932 .join(&added)
933 .map_err(|e| plan_error(format!("CsrExpand schema: {}", e)))?;
934 Ok(Self {
935 input,
936 key_column,
937 key_output,
938 neighbor_output,
939 adjacency,
940 target,
941 target_filter: None,
942 closing_column: None,
943 traversal: None,
944 schema: Arc::new(schema),
945 })
946 }
947
948 pub fn with_closing_column(mut self, column: String) -> Self {
950 self.closing_column = Some(column);
951 self
952 }
953
954 pub(crate) fn with_traversal(mut self, traversal: crate::csr_traversal::Traversal) -> Self {
955 self.traversal = Some(Arc::new(traversal));
956 self
957 }
958
959 fn rebuilt(&self, input: LogicalPlan, target: Option<TargetLookup>) -> Result<Self> {
961 let mut node = Self::try_new(
962 input,
963 self.key_column.clone(),
964 self.key_output.clone(),
965 self.neighbor_output.clone(),
966 self.adjacency.clone(),
967 target,
968 )?;
969 node.target_filter = self.target_filter.clone();
970 node.closing_column = self.closing_column.clone();
971 node.traversal = self.traversal.clone();
972 Ok(node)
973 }
974
975 fn with_target_columns(&self, used: &HashSet<String>) -> Option<Self> {
977 let target = self.target.as_ref()?;
978 let (columns, fields): (Vec<usize>, Vec<Field>) = target
979 .columns
980 .iter()
981 .zip(&target.fields)
982 .filter(|(_, f)| used.contains(f.name()))
983 .map(|(&c, f)| (c, f.clone()))
984 .unzip();
985 if fields.len() == target.fields.len() {
986 return None;
987 }
988 let target = TargetLookup {
989 columns,
990 fields,
991 ..target.clone()
992 };
993 self.rebuilt(self.input.clone(), Some(target)).ok()
994 }
995
996 fn added_columns(&self) -> impl Iterator<Item = &String> {
998 [self.key_output.name(), self.neighbor_output.name()]
999 .into_iter()
1000 .chain(
1001 self.target
1002 .iter()
1003 .flat_map(|t| t.fields.iter().map(|f| f.name())),
1004 )
1005 }
1006}
1007
1008impl PartialEq for CsrExpandNode {
1010 fn eq(&self, other: &Self) -> bool {
1011 self.input == other.input
1012 && self.key_column == other.key_column
1013 && self.key_output == other.key_output
1014 && self.neighbor_output == other.neighbor_output
1015 && self.adjacency.ptr_eq(&other.adjacency)
1016 && match (&self.target, &other.target) {
1017 (Some(a), Some(b)) => {
1018 a.variable == b.variable
1019 && a.columns == b.columns
1020 && Arc::ptr_eq(&a.rows, &b.rows)
1021 }
1022 (None, None) => true,
1023 _ => false,
1024 }
1025 && self.target_filter == other.target_filter
1026 && self.closing_column == other.closing_column
1027 && match (&self.traversal, &other.traversal) {
1028 (Some(a), Some(b)) => Arc::ptr_eq(a, b),
1029 (None, None) => true,
1030 _ => false,
1031 }
1032 }
1033}
1034impl Eq for CsrExpandNode {}
1035impl Hash for CsrExpandNode {
1036 fn hash<H: Hasher>(&self, state: &mut H) {
1037 self.input.hash(state);
1038 self.key_column.hash(state);
1039 self.key_output.name().hash(state);
1040 self.neighbor_output.name().hash(state);
1041 self.target.as_ref().map(|t| &t.variable).hash(state);
1042 self.target_filter.hash(state);
1043 self.closing_column.hash(state);
1044 }
1045}
1046impl PartialOrd for CsrExpandNode {
1047 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1048 (
1049 &self.key_column,
1050 self.key_output.name(),
1051 self.neighbor_output.name(),
1052 )
1053 .partial_cmp(&(
1054 &other.key_column,
1055 other.key_output.name(),
1056 other.neighbor_output.name(),
1057 ))
1058 }
1059}
1060
1061impl UserDefinedLogicalNodeCore for CsrExpandNode {
1062 fn name(&self) -> &str {
1063 "CsrExpand"
1064 }
1065
1066 fn inputs(&self) -> Vec<&LogicalPlan> {
1067 vec![&self.input]
1068 }
1069
1070 fn schema(&self) -> &DFSchemaRef {
1071 &self.schema
1072 }
1073
1074 fn expressions(&self) -> Vec<Expr> {
1075 vec![]
1076 }
1077
1078 fn prevent_predicate_push_down_columns(&self) -> HashSet<String> {
1081 self.added_columns().cloned().collect()
1082 }
1083
1084 fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
1087 let input_width = self.input.schema().fields().len();
1088 let key = self
1089 .input
1090 .schema()
1091 .index_of_column_by_name(None, &self.key_column)?;
1092 let closing = self
1093 .closing_column
1094 .as_deref()
1095 .map(|c| self.input.schema().index_of_column_by_name(None, c));
1096 let closing = match closing {
1097 Some(None) => return None,
1098 Some(Some(i)) => Some(i),
1099 None => None,
1100 };
1101 let mut needed: Vec<usize> = output_columns
1102 .iter()
1103 .copied()
1104 .filter(|&i| i < input_width)
1105 .chain(std::iter::once(key))
1106 .chain(closing)
1107 .collect();
1108 needed.sort_unstable();
1109 needed.dedup();
1110 Some(vec![needed])
1111 }
1112
1113 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1114 write!(
1115 f,
1116 "CsrExpand: key={} -> [{}, {}], edges={}",
1117 self.key_column,
1118 self.key_output.name(),
1119 self.neighbor_output.name(),
1120 self.adjacency.num_edges()
1121 )?;
1122 if self.adjacency.is_undirected() {
1123 write!(f, ", undirected")?;
1124 }
1125 if let Some(target) = &self.target {
1126 write!(f, ", take={}{}", target.variable, target.describe_columns())?;
1127 }
1128 if let Some(filter) = &self.target_filter {
1129 write!(f, ", filter={}", filter)?;
1130 }
1131 if let Some(column) = &self.closing_column {
1132 write!(f, ", match={}", column)?;
1133 }
1134 if let Some(walk) = &self.traversal {
1135 write!(f, ", iterative hops={}..{}", walk.min_hops, walk.max_hops)?;
1136 }
1137 Ok(())
1138 }
1139
1140 fn with_exprs_and_inputs(
1141 &self,
1142 _exprs: Vec<Expr>,
1143 inputs: Vec<LogicalPlan>,
1144 ) -> datafusion::common::Result<Self> {
1145 let input = inputs.into_iter().next().expect("CsrExpand has one input");
1146 self.rebuilt(input, self.target.clone())
1147 .map_err(|e| datafusion::common::DataFusionError::Plan(e.to_string()))
1148 }
1149}
1150
1151#[derive(Debug)]
1153pub struct CsrExpandExec {
1154 input: Arc<dyn ExecutionPlan>,
1155 key_column: String,
1156 adjacency: Neighbors,
1157 target: Option<TargetLookup>,
1158 target_filter: Option<CompiledTargetFilter>,
1160 closing_column: Option<String>,
1161 traversal: Option<Arc<crate::csr_traversal::Traversal>>,
1162 schema: SchemaRef,
1163 properties: Arc<PlanProperties>,
1164}
1165
1166impl CsrExpandExec {
1167 #[allow(clippy::too_many_arguments)]
1168 fn new(
1169 input: Arc<dyn ExecutionPlan>,
1170 key_column: String,
1171 adjacency: Neighbors,
1172 target: Option<TargetLookup>,
1173 target_filter: Option<CompiledTargetFilter>,
1174 closing_column: Option<String>,
1175 traversal: Option<Arc<crate::csr_traversal::Traversal>>,
1176 schema: SchemaRef,
1177 ) -> Self {
1178 let properties = Arc::new(PlanProperties::new(
1179 EquivalenceProperties::new(schema.clone()),
1180 Partitioning::UnknownPartitioning(input.output_partitioning().partition_count()),
1181 EmissionType::Incremental,
1182 Boundedness::Bounded,
1183 ));
1184 Self {
1185 input,
1186 key_column,
1187 adjacency,
1188 target,
1189 target_filter,
1190 closing_column,
1191 traversal,
1192 schema,
1193 properties,
1194 }
1195 }
1196}
1197
1198struct BatchExpansion {
1200 batch: RecordBatch,
1201 keys: Int64Array,
1202 bound: Option<Int64Array>,
1203 row: usize,
1204 position: NeighborPosition,
1205 walk: Option<crate::csr_traversal::TraversalCursor>,
1206}
1207
1208impl BatchExpansion {
1209 fn try_new(
1210 batch: RecordBatch,
1211 key_index: usize,
1212 closing_index: Option<usize>,
1213 ) -> datafusion::common::Result<Self> {
1214 let ints = |i| {
1215 cast(batch.column(i), &DataType::Int64).map(|a| a.as_primitive::<Int64Type>().clone())
1216 };
1217 let keys = ints(key_index)?;
1218 let bound = closing_index.map(ints).transpose()?;
1219 Ok(Self {
1220 batch,
1221 keys,
1222 bound,
1223 row: 0,
1224 position: NeighborPosition::default(),
1225 walk: None,
1226 })
1227 }
1228
1229 async fn next_chunk(
1231 &mut self,
1232 adjacency: &Neighbors,
1233 target: Option<&TargetLookup>,
1234 batch_size: usize,
1235 ) -> ExpandedRows {
1236 let mut chunk = ExpandedRows::default();
1237 loop {
1238 if self.fill_chunk(adjacency, target, batch_size, &mut chunk) {
1239 cooperative_yield().await;
1240 }
1241 if self.row == self.keys.len() || chunk.rows.len() == batch_size.max(1) {
1242 return chunk;
1243 }
1244 }
1245 }
1246
1247 fn fill_chunk(
1249 &mut self,
1250 adjacency: &Neighbors,
1251 target: Option<&TargetLookup>,
1252 batch_size: usize,
1253 chunk: &mut ExpandedRows,
1254 ) -> bool {
1255 let mut position = self.position;
1256 let mut row = self.row;
1257 let mut work = 0_usize;
1258 'rows: while row < self.keys.len() {
1259 let i = row;
1260 let previous_work = work;
1261 if self.keys.is_valid(i) && self.bound.as_ref().is_none_or(|b| b.is_valid(i)) {
1262 let key = self.keys.value(i);
1263 let required = self.bound.as_ref().map(|b| b.value(i));
1264 for candidate in adjacency
1265 .candidates_from(key, &mut position)
1266 .take(1024 - work)
1267 {
1268 work += 1;
1269 let Some(neighbor) = candidate else {
1270 continue;
1271 };
1272 let neighbor = neighbor as i64;
1273 if required.is_some_and(|r| r != neighbor) {
1274 continue;
1275 }
1276 if let Some(target) = target {
1277 let Some(target_row) = target.rows.position(neighbor) else {
1278 continue;
1279 };
1280 chunk.target_positions.push(target_row);
1281 }
1282 chunk.rows.push(i as u32);
1283 chunk.key_values.push(key);
1284 chunk.neighbor_values.push(neighbor);
1285 if chunk.rows.len() == batch_size.max(1) {
1286 break 'rows;
1287 }
1288 }
1289 if work == 1024 {
1290 break;
1291 }
1292 }
1293 if work == previous_work {
1294 work += 1;
1295 }
1296 row += 1;
1297 position = NeighborPosition::default();
1298 if work == 1024 {
1299 break;
1300 }
1301 }
1302 self.row = row;
1303 self.position = position;
1304 work == 1024
1305 }
1306
1307 async fn next_path_chunk(
1308 &mut self,
1309 graph: &crate::csr_traversal::Traversal,
1310 target: Option<&TargetLookup>,
1311 batch_size: usize,
1312 ) -> ExpandedRows {
1313 let mut chunk = ExpandedRows::default();
1314 let mut source_work = 0_usize;
1315 while self.row < self.keys.len() {
1316 source_work += 1;
1317 if source_work.is_multiple_of(1024) {
1318 cooperative_yield().await;
1319 }
1320 let i = self.row;
1321 if self.keys.is_valid(i) && self.bound.as_ref().is_none_or(|b| b.is_valid(i)) {
1322 let key = self.keys.value(i);
1323 let walk = self
1324 .walk
1325 .get_or_insert_with(|| crate::csr_traversal::TraversalCursor::new(graph, key));
1326 let mut candidates = 0usize;
1327 while let Some(neighbor) = walk.next(graph).await {
1328 candidates += 1;
1329 if candidates.is_multiple_of(1024) {
1330 cooperative_yield().await;
1331 }
1332 if self.bound.as_ref().is_some_and(|b| b.value(i) != neighbor) {
1333 continue;
1334 }
1335 if let Some(target) = target {
1336 let Some(position) = target.rows.position(neighbor) else {
1337 continue;
1338 };
1339 chunk.target_positions.push(position);
1340 }
1341 chunk.rows.push(i as u32);
1342 chunk.key_values.push(key);
1343 chunk.neighbor_values.push(neighbor);
1344 if chunk.rows.len() == batch_size.max(1) {
1345 return chunk;
1346 }
1347 }
1348 }
1349 self.row += 1;
1350 self.walk = None;
1351 }
1352 chunk
1353 }
1354}
1355
1356#[derive(Default)]
1359struct ExpandedRows {
1360 rows: Vec<u32>,
1361 key_values: Vec<i64>,
1362 neighbor_values: Vec<i64>,
1363 target_positions: Vec<u64>,
1364}
1365
1366async fn materialize(
1368 batch: &RecordBatch,
1369 expanded: ExpandedRows,
1370 target: Option<&TargetLookup>,
1371 target_filter: Option<&CompiledTargetFilter>,
1372 schema: &SchemaRef,
1373) -> datafusion::common::Result<RecordBatch> {
1374 let ExpandedRows {
1375 mut rows,
1376 mut key_values,
1377 mut neighbor_values,
1378 mut target_positions,
1379 } = expanded;
1380 let mut reused = Vec::new();
1381 if let (Some(target), Some(filter)) = (target, target_filter) {
1382 let fetch: Vec<_> = filter
1385 .columns
1386 .iter()
1387 .copied()
1388 .filter(|&c| c != target.rows.id_index)
1389 .collect();
1390 let mut taken = target
1391 .rows
1392 .take(&target_positions, &fetch)
1393 .await
1394 .map_err(|e| datafusion::common::DataFusionError::External(Box::new(e)))?
1395 .into_iter();
1396 let arrays = filter
1397 .columns
1398 .iter()
1399 .enumerate()
1400 .map(|(i, &c)| {
1401 if c == target.rows.id_index {
1402 cast(
1403 &Int64Array::from(neighbor_values.clone()),
1404 filter.schema.field(i).data_type(),
1405 )
1406 } else {
1407 Ok(taken
1408 .next()
1409 .expect("one fetched array per non-id filter column"))
1410 }
1411 })
1412 .collect::<std::result::Result<Vec<_>, _>>()?;
1413 let candidates = RecordBatch::try_new(filter.schema.clone(), arrays)?;
1414 let mask = filter
1415 .predicate
1416 .evaluate(&candidates)?
1417 .into_array(candidates.num_rows())?;
1418 let mask = mask.as_boolean();
1419 let keep: Vec<bool> = (0..mask.len())
1420 .map(|i| mask.is_valid(i) && mask.value(i))
1421 .collect();
1422 for (i, &column) in filter.columns.iter().enumerate() {
1423 if column != target.rows.id_index && target.columns.contains(&column) {
1424 reused.push((column, filter_array(candidates.column(i), mask)?));
1425 }
1426 }
1427 retain_by_mask(&mut rows, &keep);
1428 retain_by_mask(&mut key_values, &keep);
1429 retain_by_mask(&mut neighbor_values, &keep);
1430 retain_by_mask(&mut target_positions, &keep);
1431 }
1432 let rows = UInt32Array::from(rows);
1433 let mut columns: Vec<ArrayRef> = batch
1434 .columns()
1435 .iter()
1436 .map(|c| take(c.as_ref(), &rows, None))
1437 .collect::<std::result::Result<_, _>>()?;
1438 let width = batch.num_columns();
1439 let neighbor_values = Int64Array::from(neighbor_values);
1440 columns.push(cast(
1441 &Int64Array::from(key_values),
1442 schema.field(width).data_type(),
1443 )?);
1444 columns.push(cast(&neighbor_values, schema.field(width + 1).data_type())?);
1445 if let Some(target) = target {
1446 let id_index = target.rows.id_index;
1447 let fetch: Vec<usize> = target
1448 .columns
1449 .iter()
1450 .copied()
1451 .filter(|&c| c != id_index && !reused.iter().any(|(column, _)| *column == c))
1452 .collect();
1453 let mut fetched = target
1454 .rows
1455 .take(&target_positions, &fetch)
1456 .await
1457 .map_err(|e| datafusion::common::DataFusionError::External(Box::new(e)))?
1458 .into_iter();
1459 for (&c, field) in target.columns.iter().zip(&target.fields) {
1460 columns.push(if c == id_index {
1462 cast(&neighbor_values, field.data_type())?
1463 } else if let Some((_, array)) = reused.iter().find(|(column, _)| *column == c) {
1464 array.clone()
1465 } else {
1466 fetched.next().expect("one fetched array per non-id column")
1467 });
1468 }
1469 }
1470 Ok(RecordBatch::try_new(schema.clone(), columns)?)
1471}
1472
1473fn retain_by_mask<T>(values: &mut Vec<T>, keep: &[bool]) {
1475 let mut keep = keep.iter();
1476 values.retain(|_| *keep.next().expect("one mask entry per value"));
1477}
1478
1479impl DisplayAs for CsrExpandExec {
1480 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
1481 write!(
1482 f,
1483 "CsrExpandExec: key={}, edges={}",
1484 self.key_column,
1485 self.adjacency.num_edges()
1486 )?;
1487 if self.adjacency.is_undirected() {
1488 write!(f, ", undirected")?;
1489 }
1490 if let Some(target) = &self.target {
1491 write!(f, ", take={}{}", target.variable, target.describe_columns())?;
1492 }
1493 if let Some(filter) = &self.target_filter {
1494 write!(f, ", filter={}", filter.predicate)?;
1495 }
1496 if let Some(column) = &self.closing_column {
1497 write!(f, ", match={}", column)?;
1498 }
1499 if let Some(walk) = &self.traversal {
1500 write!(f, ", iterative hops={}..{}", walk.min_hops, walk.max_hops)?;
1501 }
1502 Ok(())
1503 }
1504}
1505
1506impl ExecutionPlan for CsrExpandExec {
1507 fn name(&self) -> &str {
1508 "CsrExpandExec"
1509 }
1510
1511 fn partition_statistics(
1514 &self,
1515 partition: Option<usize>,
1516 ) -> datafusion::common::Result<Arc<datafusion::common::Statistics>> {
1517 use datafusion::common::stats::Precision;
1518 let input = self.input.partition_statistics(partition)?;
1519 let mut stats = datafusion::common::Statistics::new_unknown(&self.schema);
1520 if self.traversal.is_some() {
1521 return Ok(Arc::new(stats));
1522 }
1523 let degree = self.adjacency.mean_degree();
1524 stats.num_rows = match input.num_rows {
1525 Precision::Exact(n) | Precision::Inexact(n) => {
1526 Precision::Inexact((n as f64 * degree).ceil() as usize)
1527 }
1528 Precision::Absent => Precision::Absent,
1529 };
1530 Ok(Arc::new(stats))
1531 }
1532
1533 fn properties(&self) -> &Arc<PlanProperties> {
1534 &self.properties
1535 }
1536
1537 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1538 vec![&self.input]
1539 }
1540
1541 fn with_new_children(
1542 self: Arc<Self>,
1543 children: Vec<Arc<dyn ExecutionPlan>>,
1544 ) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
1545 let input = children
1546 .into_iter()
1547 .next()
1548 .expect("CsrExpandExec has one child");
1549 Ok(Arc::new(Self::new(
1550 input,
1551 self.key_column.clone(),
1552 self.adjacency.clone(),
1553 self.target.clone(),
1554 self.target_filter.clone(),
1555 self.closing_column.clone(),
1556 self.traversal.clone(),
1557 self.schema.clone(),
1558 )))
1559 }
1560
1561 fn execute(
1562 &self,
1563 partition: usize,
1564 context: Arc<TaskContext>,
1565 ) -> datafusion::common::Result<SendableRecordBatchStream> {
1566 let input = self.input.execute(partition, context.clone())?;
1567 let key_index = input.schema().index_of(&self.key_column)?;
1569 let closing_index = self
1570 .closing_column
1571 .as_deref()
1572 .map(|c| input.schema().index_of(c))
1573 .transpose()?;
1574 let adjacency = self.adjacency.clone();
1575 let target = self.target.clone().map(Arc::new);
1576 let target_filter = self.target_filter.clone().map(Arc::new);
1577 let schema = self.schema.clone();
1578 let out_schema = schema.clone();
1579 let batch_size = context.session_config().batch_size();
1580 let traversal = self.traversal.clone();
1581 let stream = futures::stream::try_unfold((input, None), move |(mut input, mut pending)| {
1584 let (adjacency, target, target_filter, schema) = (
1585 adjacency.clone(),
1586 target.clone(),
1587 target_filter.clone(),
1588 schema.clone(),
1589 );
1590 let traversal = traversal.clone();
1591 async move {
1592 loop {
1593 if pending.is_none() {
1594 let Some(batch) = input.next().await else {
1595 return Ok(None);
1596 };
1597 pending = Some(BatchExpansion::try_new(batch?, key_index, closing_index)?);
1598 }
1599 let expansion = pending.as_mut().expect("input batch just set");
1600 let chunk = match &traversal {
1601 Some(walk) => {
1602 expansion
1603 .next_path_chunk(walk, target.as_deref(), batch_size)
1604 .await
1605 }
1606 None => {
1607 expansion
1608 .next_chunk(&adjacency, target.as_deref(), batch_size)
1609 .await
1610 }
1611 };
1612 if chunk.rows.is_empty() {
1613 pending = None;
1614 continue;
1615 }
1616 let batch = materialize(
1617 &expansion.batch,
1618 chunk,
1619 target.as_deref(),
1620 target_filter.as_deref(),
1621 &schema,
1622 )
1623 .await?;
1624 return Ok(Some((batch, (input, pending))));
1625 }
1626 }
1627 });
1628 Ok(Box::pin(RecordBatchStreamAdapter::new(out_schema, stream)))
1629 }
1630}
1631
1632#[derive(Debug, Default)]
1634pub struct CsrExtensionPlanner;
1635
1636#[async_trait]
1637impl ExtensionPlanner for CsrExtensionPlanner {
1638 async fn plan_extension(
1639 &self,
1640 _planner: &dyn PhysicalPlanner,
1641 node: &dyn UserDefinedLogicalNode,
1642 _logical_inputs: &[&LogicalPlan],
1643 physical_inputs: &[Arc<dyn ExecutionPlan>],
1644 session_state: &SessionState,
1645 ) -> datafusion::common::Result<Option<Arc<dyn ExecutionPlan>>> {
1646 if let Some(node) = node.as_any().downcast_ref::<crate::apply::ApplyNode>() {
1647 return Ok(Some(crate::apply::plan_apply(
1648 node,
1649 physical_inputs,
1650 session_state,
1651 )));
1652 }
1653 if let Some(node) = node
1654 .as_any()
1655 .downcast_ref::<crate::path_match::PathMatchNode>()
1656 {
1657 return crate::path_match::plan_path_match(node, physical_inputs, session_state)
1658 .map(Some);
1659 }
1660 if let Some(node) = node
1661 .as_any()
1662 .downcast_ref::<crate::shortest_path::ShortestPathNode>()
1663 {
1664 return Ok(Some(crate::shortest_path::plan_shortest_path(
1665 node,
1666 physical_inputs,
1667 )));
1668 }
1669 let Some(node) = node.as_any().downcast_ref::<CsrExpandNode>() else {
1670 return Ok(None);
1671 };
1672 let target_filter = match (&node.target, &node.target_filter) {
1673 (Some(target), Some(filter)) => Some(target.compile_filter(filter, session_state)?),
1674 _ => None,
1675 };
1676 Ok(Some(Arc::new(CsrExpandExec::new(
1677 physical_inputs[0].clone(),
1678 node.key_column.clone(),
1679 node.adjacency.clone(),
1680 node.target.clone(),
1681 target_filter,
1682 node.closing_column.clone(),
1683 node.traversal.clone(),
1684 Arc::new(node.schema.as_arrow().clone()),
1685 ))))
1686 }
1687}
1688
1689#[derive(Debug, Default)]
1692pub struct CsrQueryPlanner;
1693
1694#[async_trait]
1695impl QueryPlanner for CsrQueryPlanner {
1696 async fn create_physical_plan(
1697 &self,
1698 logical_plan: &LogicalPlan,
1699 session_state: &SessionState,
1700 ) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
1701 DefaultPhysicalPlanner::with_extension_planners(vec![Arc::new(CsrExtensionPlanner)])
1702 .create_physical_plan(logical_plan, session_state)
1703 .await
1704 }
1705}
1706
1707#[derive(Debug, Default)]
1720pub struct PushCsrTargetFilters;
1721
1722impl OptimizerRule for PushCsrTargetFilters {
1723 fn name(&self) -> &str {
1724 "push_csr_target_filters"
1725 }
1726
1727 fn rewrite(
1728 &self,
1729 plan: LogicalPlan,
1730 _config: &dyn OptimizerConfig,
1731 ) -> datafusion::common::Result<Transformed<LogicalPlan>> {
1732 plan.transform_up(|node| {
1733 let LogicalPlan::Filter(filter) = &node else {
1734 return Ok(Transformed::no(node));
1735 };
1736 let mut projections = Vec::new();
1739 let mut below = filter.input.as_ref();
1740 while let LogicalPlan::Projection(p) = below {
1741 if !p.expr.iter().all(|e| matches!(e, Expr::Column(_))) {
1742 return Ok(Transformed::no(node));
1743 }
1744 projections.push(p.expr.clone());
1745 below = p.input.as_ref();
1746 }
1747 let LogicalPlan::Extension(ext) = below else {
1748 return Ok(Transformed::no(node));
1749 };
1750 let Some(csr) = ext.node.as_any().downcast_ref::<CsrExpandNode>() else {
1751 return Ok(Transformed::no(node));
1752 };
1753 let Some(target) = &csr.target else {
1754 return Ok(Transformed::no(node));
1755 };
1756 let target_columns = target.raw_columns();
1757 let (moved, kept): (Vec<&Expr>, Vec<&Expr>) = split_conjunction(&filter.predicate)
1758 .into_iter()
1759 .partition(|e| {
1760 let refs = e.column_refs();
1761 !refs.is_empty()
1762 && !e.is_volatile()
1763 && refs.iter().all(|c| target_columns.contains_key(&c.name))
1764 });
1765 if moved.is_empty() {
1766 return Ok(Transformed::no(node));
1767 }
1768 let mut pushed = csr.clone();
1769 pushed.target_filter = conjunction(
1770 csr.target_filter
1771 .iter()
1772 .cloned()
1773 .chain(moved.into_iter().cloned()),
1774 );
1775 let mut rebuilt = LogicalPlan::Extension(Extension {
1776 node: Arc::new(pushed),
1777 });
1778 for exprs in projections.into_iter().rev() {
1779 rebuilt = LogicalPlanBuilder::from(rebuilt).project(exprs)?.build()?;
1780 }
1781 Ok(Transformed::yes(
1782 match conjunction(kept.into_iter().cloned()) {
1783 Some(rest) => LogicalPlanBuilder::from(rebuilt).filter(rest)?.build()?,
1784 None => rebuilt,
1785 },
1786 ))
1787 })
1788 }
1789}
1790
1791#[derive(Debug, Default)]
1799pub struct PruneCsrTargetColumns;
1800
1801impl OptimizerRule for PruneCsrTargetColumns {
1802 fn name(&self) -> &str {
1803 "prune_csr_target_columns"
1804 }
1805
1806 fn rewrite(
1807 &self,
1808 plan: LogicalPlan,
1809 _config: &dyn OptimizerConfig,
1810 ) -> datafusion::common::Result<Transformed<LogicalPlan>> {
1811 let used = used_columns(&plan)?;
1812 plan.transform_up(|node| {
1813 if let LogicalPlan::Extension(ext) = &node {
1814 if let Some(csr) = ext.node.as_any().downcast_ref::<CsrExpandNode>() {
1815 if let Some(pruned) = csr.with_target_columns(&used) {
1816 return Ok(Transformed::yes(LogicalPlan::Extension(Extension {
1817 node: Arc::new(pruned),
1818 })));
1819 }
1820 }
1821 }
1822 Ok(Transformed::no(node))
1823 })
1824 }
1825}
1826
1827fn used_columns(plan: &LogicalPlan) -> datafusion::common::Result<HashSet<String>> {
1828 let mut used: HashSet<String> = plan
1829 .schema()
1830 .fields()
1831 .iter()
1832 .map(|f| f.name().clone())
1833 .collect();
1834 plan.apply_with_subqueries(|node| {
1835 for expr in node.expressions() {
1836 used.extend(expr.column_refs().into_iter().map(|c| c.name.clone()));
1837 }
1838 let explicit = match node {
1839 LogicalPlan::Extension(ext) => {
1840 match ext.node.as_any().downcast_ref::<CsrExpandNode>() {
1841 Some(csr) => {
1842 used.insert(csr.key_column.clone());
1844 used.extend(csr.closing_column.clone());
1845 true
1846 }
1847 None => false,
1848 }
1849 }
1850 LogicalPlan::Projection(_)
1851 | LogicalPlan::Filter(_)
1852 | LogicalPlan::Aggregate(_)
1853 | LogicalPlan::Sort(_)
1854 | LogicalPlan::Limit(_)
1855 | LogicalPlan::Join(_)
1856 | LogicalPlan::TableScan(_)
1857 | LogicalPlan::SubqueryAlias(_)
1858 | LogicalPlan::Repartition(_)
1859 | LogicalPlan::Window(_)
1860 | LogicalPlan::EmptyRelation(_)
1861 | LogicalPlan::Values(_) => true,
1862 _ => false,
1863 };
1864 if !explicit {
1865 for input in node.inputs() {
1866 used.extend(input.schema().fields().iter().map(|f| f.name().clone()));
1867 }
1868 }
1869 Ok(TreeNodeRecursion::Continue)
1870 })?;
1871 Ok(used)
1872}
1873
1874fn plan_error(message: String) -> GraphError {
1875 GraphError::PlanError {
1876 message,
1877 location: snafu::Location::new(file!(), line!(), column!()),
1878 }
1879}
1880
1881fn exec_error(message: String) -> GraphError {
1882 GraphError::ExecutionError {
1883 message,
1884 location: snafu::Location::new(file!(), line!(), column!()),
1885 }
1886}
1887
1888#[cfg(test)]
1889mod tests {
1890 use super::*;
1891 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1892
1893 fn edge_stream(src: Vec<Option<i64>>, dst: Vec<Option<i64>>) -> SendableRecordBatchStream {
1894 let schema = Arc::new(Schema::new(vec![
1895 Field::new("src", DataType::Int64, true),
1896 Field::new("dst", DataType::Int64, true),
1897 ]));
1898 let batch = RecordBatch::try_new(
1899 schema.clone(),
1900 vec![
1901 Arc::new(Int64Array::from(src)),
1902 Arc::new(Int64Array::from(dst)),
1903 ],
1904 )
1905 .unwrap();
1906 Box::pin(RecordBatchStreamAdapter::new(
1907 schema,
1908 futures::stream::iter(vec![Ok(batch)]),
1909 ))
1910 }
1911
1912 #[tokio::test]
1913 async fn build_keeps_duplicates_skips_nulls_and_handles_large_ids() {
1914 let big = 1_i64 << 40;
1915 let stream = edge_stream(
1916 vec![Some(big), Some(big), Some(7), None, Some(7)],
1917 vec![Some(1), Some(1), Some(-3), Some(5), None],
1918 );
1919 let adj = Adjacency::build(stream, "SRC", "dst").await.unwrap();
1920 assert_eq!(adj.num_edges(), 3);
1921 assert_eq!(neighbors(&adj, big), vec![1, 1]);
1922 assert_eq!(neighbors(&adj, 7), vec![-3]);
1923 assert!(neighbors(&adj, 42).is_empty());
1924 }
1925
1926 #[tokio::test]
1927 async fn undirected_neighbors_visit_both_directions_and_self_loops_once() {
1928 let edges = || {
1929 edge_stream(
1930 vec![Some(1), Some(1), Some(3)],
1931 vec![Some(2), Some(3), Some(3)],
1932 )
1933 };
1934 let out = Adjacency::build(edges(), "src", "dst").await.unwrap();
1935 let inc = Adjacency::build(edges(), "dst", "src").await.unwrap();
1936 let both = Neighbors::undirected(Arc::new(out), Arc::new(inc));
1937 let collect = |key| {
1938 let mut v = Vec::new();
1939 both.for_each_neighbor(key, |n| v.push(n as i64));
1940 v
1941 };
1942 assert_eq!(collect(1), vec![2, 3]);
1943 assert_eq!(collect(2), vec![1]);
1944 assert_eq!(collect(3), vec![3, 1]);
1945 }
1946
1947 fn neighbors(adj: &Adjacency, key: i64) -> Vec<i64> {
1948 let mut out = Vec::new();
1949 adj.for_each_neighbor(key, |n| out.push(n as i64));
1950 out.sort_unstable();
1951 out
1952 }
1953
1954 #[tokio::test]
1955 async fn changes_layer_over_shared_base_with_multiset_tombstones() {
1956 let base = Adjacency::build(
1957 edge_stream(
1958 vec![Some(1), Some(1), Some(1)],
1959 vec![Some(2), Some(2), Some(3)],
1960 ),
1961 "src",
1962 "dst",
1963 )
1964 .await
1965 .unwrap();
1966 let next = base.with_changes(&[(1, 4), (9, 1)], &[(1, 2)]);
1968 assert_eq!(neighbors(&next, 1), vec![2, 3, 4]);
1969 assert_eq!(neighbors(&next, 9), vec![1]);
1970 assert_eq!(
1971 (next.num_edges(), next.delta_edges(), next.tombstones()),
1972 (4, 2, 1)
1973 );
1974 let later = next.with_changes(&[], &[(9, 1)]);
1976 assert!(neighbors(&later, 9).is_empty());
1977 assert_eq!(neighbors(&base, 1), vec![2, 2, 3]);
1978 assert!(Arc::ptr_eq(&base.base, &later.base));
1979 }
1980
1981 #[tokio::test]
1982 async fn resumed_neighbors_cross_the_base_and_delta_boundary() {
1983 let base = Adjacency::build(
1984 edge_stream(vec![Some(1), Some(1)], vec![Some(10), Some(11)]),
1985 "src",
1986 "dst",
1987 )
1988 .await
1989 .unwrap();
1990 let next = base.with_changes(&[(1, 12), (1, 12), (1, 13)], &[(1, 11), (1, 12)]);
1991 for (mut offset, expected) in [
1992 (0, vec![10, 12, 13]),
1993 (1, vec![12, 13]),
1994 (2, vec![12, 13]),
1995 (3, vec![12, 13]),
1996 (4, vec![13]),
1997 (5, vec![]),
1998 (6, vec![]),
1999 ] {
2000 assert_eq!(
2001 next.neighbors_from(1, &mut offset).collect::<Vec<_>>(),
2002 expected
2003 );
2004 }
2005 assert_eq!(neighbors(&base, 1), vec![10, 11]);
2006 }
2007
2008 #[tokio::test]
2009 async fn debug_output_does_not_grow_with_edges() {
2010 let n = 10_000;
2011 let adj = Adjacency::build(
2012 edge_stream((0..n).map(Some).collect(), (0..n).map(Some).collect()),
2013 "src",
2014 "dst",
2015 )
2016 .await
2017 .unwrap();
2018 assert!(format!("{:?}", adj).len() < 100);
2019 }
2020
2021 #[test]
2022 fn node_rows_reject_duplicate_ids_and_skip_nulls() {
2023 let schema = Arc::new(Schema::new(vec![Field::new("ID", DataType::Int32, true)]));
2024 let batch = |ids: Vec<Option<i32>>| {
2025 RecordBatch::try_new(
2026 schema.clone(),
2027 vec![Arc::new(arrow::array::Int32Array::from(ids))],
2028 )
2029 .unwrap()
2030 };
2031 let rows = NodeRows::from_batch(batch(vec![Some(5), None, Some(7)]), "id")
2032 .unwrap()
2033 .unwrap();
2034 assert_eq!(
2035 (rows.num_rows(), rows.position(7), rows.position(9)),
2036 (2, Some(2), None)
2037 );
2038 assert!(NodeRows::from_batch(batch(vec![Some(5), Some(5)]), "id")
2039 .unwrap()
2040 .is_none());
2041 }
2042
2043 #[tokio::test]
2044 async fn many_small_changes_match_a_reference_multiset() {
2045 let mut adj = Adjacency::build(edge_stream(vec![Some(0)], vec![Some(0)]), "src", "dst")
2046 .await
2047 .unwrap();
2048 let mut expected = vec![0_i64];
2049 for step in 1..=200_i64 {
2050 let removed: Vec<(i64, i64)> = if step % 3 == 0 {
2051 expected.pop().map(|n| vec![(0, n)]).unwrap_or_default()
2052 } else {
2053 vec![]
2054 };
2055 adj = adj.with_changes(&[(0, step), (0, step)], &removed);
2056 expected.extend([step, step]);
2057 }
2058 expected.sort_unstable();
2059 assert_eq!(neighbors(&adj, 0), expected);
2060 assert_eq!(adj.num_edges() as usize, expected.len());
2061 }
2062
2063 #[tokio::test]
2064 async fn expansion_repeats_rows_per_neighbor_and_resumes_between_chunks() {
2065 let adj = Adjacency::build(
2066 edge_stream(vec![Some(1), Some(1)], vec![Some(2), Some(3)]),
2067 "src",
2068 "dst",
2069 )
2070 .await
2071 .unwrap();
2072 let input_schema = Arc::new(Schema::new(vec![Field::new(
2073 "a__id",
2074 DataType::Int32,
2075 true,
2076 )]));
2077 let input = RecordBatch::try_new(
2078 input_schema,
2079 vec![Arc::new(arrow::array::Int32Array::from(vec![
2080 Some(1),
2081 None,
2082 Some(9),
2083 ]))],
2084 )
2085 .unwrap();
2086 let out_schema = Arc::new(Schema::new(vec![
2087 Field::new("a__id", DataType::Int32, true),
2088 Field::new("r__src", DataType::Int32, true),
2089 Field::new("r__dst", DataType::Int32, true),
2090 ]));
2091 let adj = Neighbors::directed(Arc::new(adj));
2092 let mut expansion = BatchExpansion::try_new(input.clone(), 0, None).unwrap();
2093 let chunk = expansion.next_chunk(&adj, None, 8192).await;
2094 let out = materialize(&input, chunk, None, None, &out_schema)
2095 .await
2096 .unwrap();
2097 assert_eq!(out.num_rows(), 2);
2098 let dst = out.column(2).as_primitive::<arrow::datatypes::Int32Type>();
2099 assert_eq!(dst.values().to_vec(), vec![2, 3]);
2100 assert!(expansion.next_chunk(&adj, None, 8192).await.rows.is_empty());
2101 let mut expansion = BatchExpansion::try_new(input, 0, None).unwrap();
2103 assert_eq!(
2104 expansion.next_chunk(&adj, None, 1).await.neighbor_values,
2105 vec![2]
2106 );
2107 assert_eq!(expansion.row, 0);
2108 assert_eq!(expansion.position.forward, 1);
2109 assert_eq!(
2110 expansion.next_chunk(&adj, None, 1).await.neighbor_values,
2111 vec![3]
2112 );
2113 assert!(expansion.next_chunk(&adj, None, 1).await.rows.is_empty());
2114 }
2115
2116 #[tokio::test]
2117 async fn cancellation_without_edges_in_variable_paths_yields_between_source_rows() {
2118 let config = crate::config::GraphConfig::builder()
2119 .node_table("Person", "id")
2120 .rel_table("KNOWS", "Person", "Person", "src", "dst")
2121 .build()
2122 .unwrap();
2123 let nodes =
2124 RecordBatch::try_from_iter([("id", Arc::new(Int64Array::from(vec![1])) as ArrayRef)])
2125 .unwrap();
2126 let mut catalog = AdjacencyCatalog::default();
2127 catalog.insert_nodes(
2128 "Person",
2129 Arc::new(NodeRows::from_batch(nodes, "id").unwrap().unwrap()),
2130 );
2131 catalog.insert(
2132 "KNOWS",
2133 true,
2134 Arc::new(
2135 Adjacency::build(edge_stream(vec![], vec![]), "src", "dst")
2136 .await
2137 .unwrap(),
2138 ),
2139 );
2140 let walk = crate::csr_traversal::Traversal::new(
2141 &config,
2142 &catalog,
2143 &["KNOWS".into()],
2144 &crate::ast::RelationshipDirection::Outgoing,
2145 "Person",
2146 "Person",
2147 1,
2148 1,
2149 )
2150 .unwrap();
2151 let input = RecordBatch::try_from_iter([(
2152 "id",
2153 Arc::new(Int64Array::from(vec![1; 100_000])) as ArrayRef,
2154 )])
2155 .unwrap();
2156 let mut expansion = BatchExpansion::try_new(input, 0, None).unwrap();
2157 let mut next = Box::pin(expansion.next_path_chunk(&walk, None, 1));
2158 assert!(futures::poll!(next.as_mut()).is_pending());
2159 drop(next);
2160 assert!(expansion.row > 0 && expansion.row <= 1024);
2161 assert!(expansion
2162 .next_path_chunk(&walk, None, 1)
2163 .await
2164 .rows
2165 .is_empty());
2166 }
2167
2168 #[tokio::test]
2169 async fn cancellation_without_edges_yields_while_skipping_source_rows() {
2170 let adjacency = Neighbors::directed(Arc::new(
2171 Adjacency::build(edge_stream(vec![Some(1)], vec![Some(2)]), "src", "dst")
2172 .await
2173 .unwrap(),
2174 ));
2175 for missing in [Some(99), None] {
2176 let input = RecordBatch::try_from_iter([(
2177 "id",
2178 Arc::new(Int64Array::from(vec![missing; 100_000])) as ArrayRef,
2179 )])
2180 .unwrap();
2181 let mut expansion = BatchExpansion::try_new(input, 0, None).unwrap();
2182 let mut next = Box::pin(expansion.next_chunk(&adjacency, None, 1));
2183 assert!(futures::poll!(next.as_mut()).is_pending());
2184 drop(next);
2185 assert!(expansion.row > 0 && expansion.row <= 1024);
2186 assert!(expansion
2187 .next_chunk(&adjacency, None, 1)
2188 .await
2189 .rows
2190 .is_empty());
2191 }
2192 }
2193
2194 #[tokio::test]
2195 async fn expansion_yields_before_scanning_all_rejected_neighbors() {
2196 let adj = Neighbors::directed(Arc::new(
2197 Adjacency::build(
2198 edge_stream(vec![Some(1); 100_000], (2..100_002).map(Some).collect()),
2199 "src",
2200 "dst",
2201 )
2202 .await
2203 .unwrap(),
2204 ));
2205 let schema = Arc::new(Schema::new(vec![
2206 Field::new("id", DataType::Int64, false),
2207 Field::new("bound", DataType::Int64, false),
2208 ]));
2209 let batch = RecordBatch::try_new(
2210 schema,
2211 vec![
2212 Arc::new(Int64Array::from(vec![1])),
2213 Arc::new(Int64Array::from(vec![-1])),
2214 ],
2215 )
2216 .unwrap();
2217 let mut expansion = BatchExpansion::try_new(batch, 0, Some(1)).unwrap();
2218 let mut next = Box::pin(expansion.next_chunk(&adj, None, 1));
2219 assert!(futures::poll!(next.as_mut()).is_pending());
2220 drop(next);
2221 assert!(expansion.position.forward <= 1024);
2222 assert!(expansion.position.forward > 0);
2223 }
2224
2225 #[tokio::test]
2226 async fn expansion_yields_and_resumes_through_a_long_tombstone_run() {
2227 let n = 5_000;
2228 let base = Adjacency::build(
2229 edge_stream(vec![Some(1); n], (2..n as i64 + 2).map(Some).collect()),
2230 "src",
2231 "dst",
2232 )
2233 .await
2234 .unwrap();
2235 let removed = (2..n as i64 + 1).map(|id| (1, id)).collect::<Vec<_>>();
2236 let adjacency = Neighbors::directed(Arc::new(base.with_changes(&[], &removed)));
2237 let input =
2238 RecordBatch::try_from_iter([("id", Arc::new(Int64Array::from(vec![1])) as ArrayRef)])
2239 .unwrap();
2240 let mut expansion = BatchExpansion::try_new(input, 0, None).unwrap();
2241 let mut next = Box::pin(expansion.next_chunk(&adjacency, None, 1));
2242 assert!(futures::poll!(next.as_mut()).is_pending());
2243 drop(next);
2244 assert_eq!(expansion.position.forward, 1024);
2245 assert_eq!(
2246 expansion
2247 .next_chunk(&adjacency, None, 1)
2248 .await
2249 .neighbor_values,
2250 vec![n as i64 + 1]
2251 );
2252 assert!(expansion
2253 .next_chunk(&adjacency, None, 1)
2254 .await
2255 .rows
2256 .is_empty());
2257 }
2258
2259 #[tokio::test]
2260 async fn high_degree_expansion_visits_only_the_first_chunk() {
2261 let n = 100_000;
2262 let base = Adjacency::build(
2263 edge_stream(vec![Some(0); n], (1..=n as i64).map(Some).collect()),
2264 "src",
2265 "dst",
2266 )
2267 .await
2268 .unwrap();
2269 let removed: Vec<_> = (1..=n as i64).step_by(2).map(|n| (0, n)).collect();
2270 let adj = Neighbors::directed(Arc::new(base.with_changes(&[], &removed)));
2271 let input =
2272 RecordBatch::try_from_iter([("id", Arc::new(Int64Array::from(vec![0])) as ArrayRef)])
2273 .unwrap();
2274 let mut expansion = BatchExpansion::try_new(input, 0, None).unwrap();
2275 let chunk = expansion.next_chunk(&adj, None, 32).await;
2276 assert_eq!(
2277 chunk.neighbor_values,
2278 (2..=64).step_by(2).collect::<Vec<_>>()
2279 );
2280 assert_eq!(chunk.rows.len(), 32);
2281 assert_eq!(expansion.row, 0);
2283 assert_eq!(expansion.position.forward, 64);
2284 assert_eq!(
2285 expansion.next_chunk(&adj, None, 32).await.neighbor_values[0],
2286 66
2287 );
2288 assert_eq!(neighbors(&base, 0).len(), n);
2289 }
2290
2291 #[tokio::test]
2292 async fn chunk_boundaries_preserve_closing_edges_and_missing_target_rows() {
2293 let adjacency = Adjacency::build(
2294 edge_stream(vec![Some(1); 4], vec![Some(2), Some(2), Some(3), Some(999)]),
2295 "src",
2296 "dst",
2297 )
2298 .await
2299 .unwrap();
2300 let adjacency = Neighbors::directed(Arc::new(adjacency));
2301 let input = RecordBatch::try_from_iter([
2302 (
2303 "key",
2304 Arc::new(Int64Array::from(vec![Some(1), Some(1), None, Some(1)])) as ArrayRef,
2305 ),
2306 (
2307 "bound",
2308 Arc::new(Int64Array::from(vec![Some(2), None, Some(2), Some(3)])) as ArrayRef,
2309 ),
2310 ])
2311 .unwrap();
2312 let nodes =
2313 RecordBatch::try_from_iter([("id", Arc::new(Int64Array::from(vec![2])) as ArrayRef)])
2314 .unwrap();
2315 let target = TargetLookup::new(
2316 "b".to_string(),
2317 Arc::new(NodeRows::from_batch(nodes, "id").unwrap().unwrap()),
2318 vec![Field::new("b__id", DataType::Int64, false)],
2319 );
2320 let mut expansion = BatchExpansion::try_new(input, 0, Some(1)).unwrap();
2321 for _ in 0..2 {
2322 let chunk = expansion.next_chunk(&adjacency, Some(&target), 1).await;
2323 assert_eq!(chunk.rows, vec![0]);
2324 assert_eq!(chunk.neighbor_values, vec![2]);
2325 assert_eq!(chunk.target_positions, vec![0]);
2326 }
2327 assert!(expansion
2328 .next_chunk(&adjacency, Some(&target), 1)
2329 .await
2330 .rows
2331 .is_empty());
2332 }
2333
2334 #[tokio::test]
2335 async fn resumed_neighbors_keep_duplicate_deletions_and_self_loops_exact() {
2336 let edges = || edge_stream(vec![Some(1); 4], vec![Some(1), Some(2), Some(2), Some(3)]);
2337 let base = Adjacency::build(edges(), "src", "dst").await.unwrap();
2338 let next = base.with_changes(&[(1, 2), (1, 4)], &[(1, 2), (1, 3)]);
2339 let later = next.with_changes(&[(1, 3)], &[(1, 2), (1, 2)]);
2340 assert_eq!(neighbors(&later, 1), vec![1, 3, 4]);
2341 assert_eq!(neighbors(&next, 1), vec![1, 2, 2, 4]);
2342 assert_eq!(neighbors(&base, 1), vec![1, 2, 2, 3]);
2343
2344 let reverse = Adjacency::build(edges(), "dst", "src").await.unwrap();
2345 let both = Neighbors::undirected(Arc::new(base), Arc::new(reverse));
2346 let mut position = NeighborPosition::default();
2347 let mut got = Vec::new();
2348 while let Some(n) = both.neighbors_from(1, &mut position).next() {
2349 got.push(n);
2350 }
2351 assert_eq!(got, vec![1, 2, 2, 3]);
2352 let mut position = NeighborPosition::default();
2353 assert_eq!(both.neighbors_from(2, &mut position).next(), Some(1));
2354 assert_eq!(both.neighbors_from(2, &mut position).next(), Some(1));
2355 assert_eq!(both.neighbors_from(2, &mut position).next(), None);
2356 }
2357
2358 #[tokio::test]
2359 async fn materialization_reuses_filter_columns_for_memory_and_lance() {
2360 use arrow::array::{RecordBatchIterator, StringArray};
2361 use datafusion::logical_expr::{col, lit};
2362 use std::sync::atomic::Ordering;
2363
2364 let nodes = RecordBatch::try_from_iter([
2365 (
2366 "id",
2367 Arc::new(Int64Array::from(vec![10, 20, 30, 40])) as ArrayRef,
2368 ),
2369 (
2370 "age",
2371 Arc::new(Int64Array::from(vec![Some(2), None, Some(5), Some(1)])) as ArrayRef,
2372 ),
2373 (
2374 "name",
2375 Arc::new(StringArray::from(vec!["ten", "twenty", "thirty", "forty"])) as ArrayRef,
2376 ),
2377 (
2378 "unused",
2379 Arc::new(StringArray::from(vec!["unused payload"; 4])) as ArrayRef,
2380 ),
2381 ])
2382 .unwrap();
2383 let directory = tempfile::tempdir().unwrap();
2384 let path = directory.path().join("nodes.lance");
2385 let reader = RecordBatchIterator::new(vec![Ok(nodes.clone())], nodes.schema());
2386 let dataset = Dataset::write(
2387 reader,
2388 path.to_str().unwrap(),
2389 None::<lance::dataset::WriteParams>,
2390 )
2391 .await
2392 .unwrap();
2393 let sources = [
2394 NodeRows::from_batch(nodes.clone(), "id").unwrap().unwrap(),
2395 NodeRows::from_lance(Arc::new(dataset), "id")
2396 .await
2397 .unwrap()
2398 .unwrap(),
2399 ];
2400 let input = RecordBatch::try_from_iter([(
2401 "a__id",
2402 Arc::new(Int64Array::from(vec![999])) as ArrayRef,
2403 )])
2404 .unwrap();
2405 let state = datafusion::execution::context::SessionContext::new().state();
2406 for source in sources {
2407 let source = Arc::new(source);
2408 for selected in [vec![0, 1, 2], vec![0, 1], vec![2], vec![]] {
2409 let fields: Vec<_> = selected
2410 .iter()
2411 .map(|&i| {
2412 let f = nodes.schema().field(i).clone();
2413 Field::new(
2414 format!("b__{}", f.name()),
2415 f.data_type().clone(),
2416 f.is_nullable(),
2417 )
2418 })
2419 .collect();
2420 let target = TargetLookup {
2421 variable: "b".into(),
2422 rows: source.clone(),
2423 columns: selected.clone(),
2424 fields: fields.clone(),
2425 };
2426 let schema = Arc::new(Schema::new(
2427 [
2428 Field::new("a__id", DataType::Int64, false),
2429 Field::new("edge__src", DataType::Int64, false),
2430 Field::new("edge__dst", DataType::Int64, false),
2431 ]
2432 .into_iter()
2433 .chain(fields)
2434 .collect::<Vec<_>>(),
2435 ));
2436 for threshold in [2, 99] {
2437 source.fetched_cells.store(0, Ordering::Relaxed);
2438 let predicate = col("b__age")
2439 .gt_eq(lit(threshold))
2440 .and(col("b__id").gt_eq(lit(10)));
2441 let filter = target.compile_filter(&predicate, &state).unwrap();
2442 let neighbors = vec![30, 10, 20, 30, 40, 10];
2443 let expanded = ExpandedRows {
2444 rows: vec![0; 6],
2445 key_values: vec![999; 6],
2446 target_positions: neighbors
2447 .iter()
2448 .map(|&n| source.position(n).unwrap())
2449 .collect(),
2450 neighbor_values: neighbors,
2451 };
2452 let out = materialize(&input, expanded, Some(&target), Some(&filter), &schema)
2453 .await
2454 .unwrap();
2455 let expected_rows: Vec<u32> = if threshold == 2 {
2456 vec![2, 0, 2, 0]
2457 } else {
2458 vec![]
2459 };
2460 assert_eq!(out.num_rows(), expected_rows.len());
2461 for (i, &column) in selected.iter().enumerate() {
2462 let expected = take(
2463 nodes.column(column),
2464 &UInt32Array::from(expected_rows.clone()),
2465 None,
2466 )
2467 .unwrap();
2468 assert_eq!(out.column(3 + i).to_data(), expected.to_data());
2469 }
2470 let projection_cells = if selected.contains(&2) {
2472 expected_rows.len()
2473 } else {
2474 0
2475 };
2476 assert_eq!(
2477 source.fetched_cells.load(Ordering::Relaxed),
2478 6 + projection_cells
2479 );
2480 }
2481 }
2482 }
2483 }
2484}