1use std::fmt;
53use std::sync::{Arc, Mutex};
54
55use async_trait::async_trait;
56use datafusion::arrow::array::{new_empty_array, Array, ArrayRef, BooleanArray, Float64Array};
57use datafusion::arrow::buffer::BooleanBuffer;
58use datafusion::arrow::compute::SortOptions;
59use datafusion::arrow::compute::{cast, cast_with_options, concat, CastOptions};
60use datafusion::arrow::compute::{filter, filter_record_batch};
61use datafusion::arrow::datatypes::{DataType, SchemaRef};
62use datafusion::arrow::record_batch::RecordBatch;
63use datafusion::catalog::Session;
64use datafusion::common::config::ConfigOptions;
65use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
66use datafusion::common::ScalarValue;
67use datafusion::error::{DataFusionError, Result};
68use datafusion::execution::context::QueryPlanner;
69use datafusion::execution::TaskContext;
70use datafusion::logical_expr::physical_planning_context::PhysicalPlanningContext;
71use datafusion::logical_expr::{LogicalPlan, UserDefinedLogicalNode};
72use datafusion::physical_expr::expressions::Column;
73use datafusion::physical_expr::{EquivalenceProperties, PhysicalExpr};
74use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr};
75use datafusion::physical_optimizer::PhysicalOptimizerRule;
76use datafusion::physical_plan::aggregates::AggregateExec;
77use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
78use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
79use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder, MetricsSet};
80use datafusion::physical_plan::repartition::RepartitionExec;
81use datafusion::physical_plan::replace_children_if_necessary;
82use datafusion::physical_plan::sorts::sort::SortExec;
83use datafusion::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
84use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
85use datafusion::physical_plan::union::{InterleaveExec, UnionExec};
86use datafusion::physical_plan::{
87 DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, Partitioning,
88 PlanProperties, SendableRecordBatchStream, StatisticsArgs, StatisticsContext,
89};
90use datafusion::physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner};
91use futures::{FutureExt, StreamExt, TryStreamExt};
92use ursa_core::{EdgeInterner, IdError, Topology};
93
94use super::index::{
95 file_identities, fingerprint, portable_in, BuildInfo, FileIdentity, GraphIndex, IndexCache,
96 Weights,
97};
98use super::kernels;
99use super::node::{
100 weight_column, GraphAlgorithmNode, GraphTraversalNode, Output, WEIGHTED_DESTINATION,
101 WEIGHTED_SOURCE,
102};
103use super::path_search::{self, PathColumns, PathSearchNode, Search};
104use super::traversal::{self, Traversal, TraversalParam, TraversalParamType};
105
106#[derive(Debug)]
108pub struct GraphPlanner;
109
110#[async_trait]
111impl ExtensionPlanner for GraphPlanner {
112 async fn plan_extension(
113 &self,
114 _planner: &dyn PhysicalPlanner,
115 node: &dyn UserDefinedLogicalNode,
116 logical_inputs: &[&LogicalPlan],
117 physical_inputs: &[Arc<dyn ExecutionPlan>],
118 _session_state: &dyn Session,
119 _planning_ctx: &PhysicalPlanningContext,
120 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
121 if let Some(node) = node.as_any().downcast_ref::<GraphTraversalNode>() {
122 return plan_traversal(node, logical_inputs, physical_inputs).map(Some);
123 }
124 if let Some(node) = node.as_any().downcast_ref::<PathSearchNode>() {
125 return plan_path_search(node, logical_inputs, physical_inputs).map(Some);
126 }
127 let Some(node) = node.as_any().downcast_ref::<GraphAlgorithmNode>() else {
128 return Ok(None);
129 };
130 let (source, destination) = node.endpoint_indices()?;
131 let mut next = 1;
132 let weighted = match (
133 &node.weighted,
134 logical_inputs.get(next),
135 physical_inputs.get(next),
136 ) {
137 (Some(_), Some(logical), Some(physical)) => {
138 next += 1;
139 Some(weighted_input(logical, physical, node.weights.len())?)
140 }
141 _ => None,
142 };
143 let vertices = vertex_input(
144 node.vertices.is_some(),
145 logical_inputs,
146 physical_inputs,
147 next,
148 )?;
149 let values = node
151 .values
152 .as_ref()
153 .and_then(|_| physical_inputs.last().cloned());
154 Ok(Some(Arc::new(GraphAlgorithmExec::new(
155 Shape {
156 edges: Edges {
157 graph: node.graph.clone(),
158 endpoints: (source, destination),
159 id_type: node.id_type.clone(),
160 fingerprint: fingerprint(logical_inputs[0]),
161 vertices,
162 },
163 outputs: node.outputs.clone(),
164 per_node: node.per_node,
165 display: node.display_outputs(),
166 schema: Arc::new(node.schema().as_arrow().clone()),
167 },
168 Arc::clone(&physical_inputs[0]),
169 weighted,
170 values,
171 ))))
172 }
173}
174
175fn vertex_input(
177 has: bool,
178 logical_inputs: &[&LogicalPlan],
179 physical_inputs: &[Arc<dyn ExecutionPlan>],
180 at: usize,
181) -> Result<Option<VertexInput>> {
182 if !has {
183 return Ok(None);
184 }
185 match (logical_inputs.get(at), physical_inputs.get(at)) {
186 (Some(logical), Some(physical)) => Ok(Some(VertexInput {
187 plan: Arc::clone(physical),
188 fingerprint: fingerprint(logical),
189 })),
190 _ => Err(DataFusionError::Internal(
191 "a graph node's vertex keys were not planned".to_string(),
192 )),
193 }
194}
195
196fn weighted_input(
199 logical: &LogicalPlan,
200 physical: &Arc<dyn ExecutionPlan>,
201 n_weights: usize,
202) -> Result<WeightedInput> {
203 let schema = logical.schema();
204 let column = |name: &str| schema.index_of_column_by_name(None, name);
205 let missing =
206 |name: &str| DataFusionError::Internal(format!("the weighted edge plan has no {name}"));
207 let weights = (0..n_weights)
208 .map(|i| {
209 let name = weight_column(i);
210 column(&name).ok_or_else(|| missing(&name))
211 })
212 .collect::<Result<Vec<_>>>()?;
213 Ok(WeightedInput {
214 plan: Arc::clone(physical),
215 source: column(WEIGHTED_SOURCE).ok_or_else(|| missing(WEIGHTED_SOURCE))?,
216 destination: column(WEIGHTED_DESTINATION).ok_or_else(|| missing(WEIGHTED_DESTINATION))?,
217 weights,
218 fingerprint: fingerprint(logical),
219 })
220}
221
222fn plan_traversal(
224 node: &GraphTraversalNode,
225 logical_inputs: &[&LogicalPlan],
226 physical_inputs: &[Arc<dyn ExecutionPlan>],
227) -> Result<Arc<dyn ExecutionPlan>> {
228 let expected = 1
229 + usize::from(node.weighted.is_some())
230 + usize::from(node.vertices.is_some())
231 + node.nodes.len();
232 if logical_inputs.len() != expected || physical_inputs.len() != expected {
233 return Err(DataFusionError::Internal(format!(
234 "GraphTraversal has {expected} inputs; planned {}",
235 physical_inputs.len()
236 )));
237 }
238 let (source, destination) = node.endpoint_indices()?;
239 let mut next = 1;
240 let weighted = match node.weighted {
241 Some(_) => {
242 next += 1;
243 Some(weighted_input(logical_inputs[1], &physical_inputs[1], 1)?)
244 }
245 None => None,
246 };
247 let vertices = vertex_input(
248 node.vertices.is_some(),
249 logical_inputs,
250 physical_inputs,
251 next,
252 )?;
253 next += usize::from(vertices.is_some());
254 Ok(Arc::new(GraphTraversalExec::new(
255 TraversalShape {
256 edges: Edges {
257 graph: node.graph.clone(),
258 endpoints: (source, destination),
259 id_type: node.id_type.clone(),
260 fingerprint: fingerprint(logical_inputs[0]),
261 vertices,
262 },
263 traversal: node.traversal,
264 display: node.display_call(),
265 schema: Arc::new(node.schema().as_arrow().clone()),
266 },
267 Arc::clone(&physical_inputs[0]),
268 weighted,
269 physical_inputs[next..].to_vec(),
270 )))
271}
272
273fn plan_path_search(
274 node: &PathSearchNode,
275 logical_inputs: &[&LogicalPlan],
276 physical_inputs: &[Arc<dyn ExecutionPlan>],
277) -> Result<Arc<dyn ExecutionPlan>> {
278 let expected = 2 + usize::from(node.vertices.is_some()) + usize::from(node.targets.is_some());
279 if physical_inputs.len() != expected || logical_inputs.len() != expected {
280 return Err(DataFusionError::Internal(format!(
281 "PathSearch has {expected} inputs; planned {}",
282 physical_inputs.len()
283 )));
284 }
285 let vertices = vertex_input(node.vertices.is_some(), logical_inputs, physical_inputs, 1)?;
286 let starts = 1 + usize::from(vertices.is_some());
287 Ok(Arc::new(PathSearchExec::new(
288 PathShape {
289 edges: Edges {
290 graph: node.graph.clone(),
291 endpoints: node.endpoint_indices()?,
292 id_type: node.id_type.clone(),
293 fingerprint: fingerprint(logical_inputs[0]),
294 vertices,
295 },
296 search: node.search,
297 columns: node.columns,
298 known_only: node.known_only,
299 schema: Arc::new(node.schema().as_arrow().clone()),
300 },
301 Arc::clone(&physical_inputs[0]),
302 Arc::clone(&physical_inputs[starts]),
303 node.targets
304 .as_ref()
305 .map(|_| Arc::clone(&physical_inputs[starts + 1])),
306 )))
307}
308
309#[derive(Debug, Clone)]
310struct PathShape {
311 edges: Edges,
312 search: Search,
313 columns: PathColumns,
314 known_only: bool,
315 schema: SchemaRef,
316}
317
318#[derive(Debug)]
323pub struct PathSearchExec {
324 shape: PathShape,
325 input: Arc<dyn ExecutionPlan>,
326 starts: Arc<dyn ExecutionPlan>,
327 targets: Option<Arc<dyn ExecutionPlan>>,
328 properties: Arc<PlanProperties>,
329 metrics: ExecutionPlanMetricsSet,
330}
331
332impl PathSearchExec {
333 fn new(
334 shape: PathShape,
335 input: Arc<dyn ExecutionPlan>,
336 starts: Arc<dyn ExecutionPlan>,
337 targets: Option<Arc<dyn ExecutionPlan>>,
338 ) -> Self {
339 let properties = Arc::new(PlanProperties::new(
340 EquivalenceProperties::new(Arc::clone(&shape.schema)),
341 Partitioning::UnknownPartitioning(1),
342 EmissionType::Final,
343 Boundedness::Bounded,
344 ));
345 Self {
346 shape,
347 input,
348 starts,
349 targets,
350 properties,
351 metrics: ExecutionPlanMetricsSet::new(),
352 }
353 }
354}
355
356impl DisplayAs for PathSearchExec {
357 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
358 write!(
359 f,
360 "PathSearchExec: graph={}, search={}{}",
361 self.shape.edges.graph,
362 self.shape.search,
363 if self.targets.is_some() {
364 ", targets"
365 } else {
366 ""
367 }
368 )
369 }
370}
371
372impl ExecutionPlan for PathSearchExec {
373 fn name(&self) -> &str {
374 "PathSearchExec"
375 }
376
377 fn properties(&self) -> &Arc<PlanProperties> {
378 &self.properties
379 }
380
381 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
382 let mut children = vec![&self.input];
383 children.extend(self.shape.edges.vertices.as_ref().map(|v| &v.plan));
384 children.push(&self.starts);
385 children.extend(self.targets.as_ref());
386 children
387 }
388
389 fn metrics(&self) -> Option<MetricsSet> {
390 Some(self.metrics.clone_inner())
391 }
392
393 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
394 vec![false; self.children().len()]
395 }
396
397 fn apply_expressions(
398 &self,
399 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
400 ) -> Result<TreeNodeRecursion> {
401 Ok(TreeNodeRecursion::Continue)
402 }
403
404 fn with_new_children(
405 self: Arc<Self>,
406 children: Vec<Arc<dyn ExecutionPlan>>,
407 ) -> Result<Arc<dyn ExecutionPlan>> {
408 let expected = self.children().len();
409 if children.len() != expected {
410 return Err(DataFusionError::Internal(format!(
411 "PathSearchExec has {expected} children; got {}",
412 children.len()
413 )));
414 }
415 let mut children = children.into_iter();
416 let input = children.next().expect("checked above");
417 let mut shape = self.shape.clone();
418 shape.edges.vertices = shape.edges.vertices.map(|v| VertexInput {
419 plan: children.next().expect("checked above"),
420 ..v
421 });
422 let starts = children.next().expect("checked above");
423 let targets = self.targets.as_ref().and_then(|_| children.next());
424 Ok(Arc::new(Self::new(shape, input, starts, targets)))
425 }
426
427 fn execute(
428 &self,
429 partition: usize,
430 context: Arc<TaskContext>,
431 ) -> Result<SendableRecordBatchStream> {
432 if partition != 0 {
433 return Err(DataFusionError::Internal(format!(
434 "PathSearchExec has one partition; asked for {partition}"
435 )));
436 }
437 let shape = self.shape.clone();
438 let input = Arc::clone(&self.input);
439 let starts = Arc::clone(&self.starts);
440 let targets = self.targets.clone();
441 let schema = Arc::clone(&shape.schema);
442 let expanded = MetricBuilder::new(&self.metrics).counter("expanded", partition);
443 let run = async move {
444 let index = unweighted_index(&shape.edges, input, &context).await?;
445 let starts = search_keys(&starts, "start nodes", &shape, &context).await?;
446 let targets = match &targets {
447 Some(t) => Some(search_keys(t, "target nodes", &shape, &context).await?),
448 None => None,
449 };
450 let out_schema = Arc::clone(&shape.schema);
451 let (batches, n) = crate::run_blocking(move || {
452 path_search::run(
453 &index.topology,
454 &index.ids,
455 &|e| index.edge_row(e),
456 &shape.search,
457 &starts,
458 targets.as_ref(),
459 shape.known_only,
460 shape.columns,
461 out_schema,
462 )
463 })
464 .await
465 .map_err(|e| DataFusionError::Execution(format!("a path search failed: {e}")))??;
466 expanded.add(n);
467 Ok::<_, DataFusionError>(futures::stream::iter(batches.into_iter().map(Ok)))
468 };
469 Ok(Box::pin(RecordBatchStreamAdapter::new(
470 schema,
471 futures::stream::once(run).try_flatten(),
472 )))
473 }
474}
475
476async fn search_keys(
479 plan: &Arc<dyn ExecutionPlan>,
480 what: &str,
481 shape: &PathShape,
482 context: &Arc<TaskContext>,
483) -> Result<ArrayRef> {
484 let batches = drain(plan, &[0], context).await?;
485 let id_type = &shape.edges.id_type;
486 let mut columns = Vec::with_capacity(batches.len());
487 for batch in &batches {
488 columns.push(cast(batch[0].as_ref(), id_type).map_err(|_| {
489 DataFusionError::Plan(format!(
490 "in GRAPH_TABLE: the {what}' keys ({}) cannot be read as graph '{}''s node ids ({id_type})",
491 batch[0].data_type(),
492 shape.edges.graph
493 ))
494 })?);
495 }
496 Ok(if columns.is_empty() {
497 new_empty_array(id_type)
498 } else {
499 let refs: Vec<&dyn Array> = columns.iter().map(|c| c.as_ref()).collect();
500 concat(&refs)?
501 })
502}
503
504#[derive(Debug)]
506pub struct UrsaQueryPlanner;
507
508#[async_trait]
509impl QueryPlanner for UrsaQueryPlanner {
510 async fn create_physical_plan(
511 &self,
512 logical_plan: &LogicalPlan,
513 session_state: &dyn Session,
514 ) -> Result<Arc<dyn ExecutionPlan>> {
515 DefaultPhysicalPlanner::with_extension_planners(vec![Arc::new(GraphPlanner)])
516 .create_physical_plan(logical_plan, session_state)
517 .await
518 }
519}
520
521#[derive(Debug, Clone)]
524struct Edges {
525 graph: String,
526 endpoints: (usize, usize),
528 id_type: DataType,
529 fingerprint: Option<String>,
531 vertices: Option<VertexInput>,
533}
534
535#[derive(Debug, Clone)]
537struct VertexInput {
538 plan: Arc<dyn ExecutionPlan>,
539 fingerprint: Option<String>,
541}
542
543#[derive(Debug, Clone)]
545struct Shape {
546 edges: Edges,
547 outputs: Vec<Output>,
548 per_node: bool,
549 display: String,
551 schema: SchemaRef,
552}
553
554#[derive(Debug, Clone)]
556struct WeightedInput {
557 plan: Arc<dyn ExecutionPlan>,
558 source: usize,
559 destination: usize,
560 weights: Vec<usize>,
562 fingerprint: Option<String>,
564}
565
566#[derive(Debug)]
569pub struct GraphAlgorithmExec {
570 shape: Shape,
571 input: Arc<dyn ExecutionPlan>,
572 weighted: Option<WeightedInput>,
573 values: Option<Arc<dyn ExecutionPlan>>,
575 properties: Arc<PlanProperties>,
576}
577
578impl GraphAlgorithmExec {
579 fn new(
580 shape: Shape,
581 input: Arc<dyn ExecutionPlan>,
582 weighted: Option<WeightedInput>,
583 values: Option<Arc<dyn ExecutionPlan>>,
584 ) -> Self {
585 let properties = Arc::new(PlanProperties::new(
586 EquivalenceProperties::new(Arc::clone(&shape.schema)),
587 Partitioning::UnknownPartitioning(1),
588 EmissionType::Final,
589 Boundedness::Bounded,
590 ));
591 Self {
592 shape,
593 input,
594 weighted,
595 values,
596 properties,
597 }
598 }
599}
600
601impl DisplayAs for GraphAlgorithmExec {
602 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
603 write!(
604 f,
605 "GraphAlgorithmExec: graph={}, outputs=[{}]",
606 self.shape.edges.graph, self.shape.display
607 )
608 }
609}
610
611impl ExecutionPlan for GraphAlgorithmExec {
612 fn name(&self) -> &str {
613 "GraphAlgorithmExec"
614 }
615
616 fn properties(&self) -> &Arc<PlanProperties> {
617 &self.properties
618 }
619
620 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
621 let mut children = vec![&self.input];
622 children.extend(self.weighted.as_ref().map(|w| &w.plan));
623 children.extend(self.shape.edges.vertices.as_ref().map(|v| &v.plan));
624 children.extend(self.values.as_ref());
625 children
626 }
627
628 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
631 vec![false; self.children().len()]
632 }
633
634 fn apply_expressions(
635 &self,
636 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
637 ) -> Result<TreeNodeRecursion> {
638 Ok(TreeNodeRecursion::Continue)
639 }
640
641 fn with_new_children(
642 self: Arc<Self>,
643 children: Vec<Arc<dyn ExecutionPlan>>,
644 ) -> Result<Arc<dyn ExecutionPlan>> {
645 let expected = self.children().len();
646 if children.len() != expected {
647 return Err(DataFusionError::Internal(format!(
648 "GraphAlgorithmExec has {expected} children; got {}",
649 children.len()
650 )));
651 }
652 let mut children = children.into_iter();
653 let input = children.next().expect("checked above");
654 let weighted = self.weighted.clone().map(|w| WeightedInput {
655 plan: children.next().expect("checked above"),
656 ..w
657 });
658 let mut shape = self.shape.clone();
659 shape.edges.vertices = shape.edges.vertices.map(|v| VertexInput {
660 plan: children.next().expect("checked above"),
661 ..v
662 });
663 let values = self
664 .values
665 .as_ref()
666 .map(|_| children.next().expect("checked above"));
667 Ok(Arc::new(Self::new(shape, input, weighted, values)))
668 }
669
670 fn execute(
671 &self,
672 partition: usize,
673 context: Arc<TaskContext>,
674 ) -> Result<SendableRecordBatchStream> {
675 if partition != 0 {
676 return Err(DataFusionError::Internal(format!(
677 "GraphAlgorithmExec has one partition; asked for {partition}"
678 )));
679 }
680 let shape = self.shape.clone();
681 let input = Arc::clone(&self.input);
682 let weighted = self.weighted.clone();
683 let values_plan = self.values.clone();
684 let schema = Arc::clone(&shape.schema);
685 let run = async move {
686 let (index, weights) = match weighted {
687 None => (unweighted_index(&shape.edges, input, &context).await?, None),
688 Some(w) => {
689 let (index, weights) = weighted_index(&shape.edges, input, w, &context).await?;
690 (index, Some(weights))
691 }
692 };
693 let values = match values_plan {
694 Some(plan) => {
695 let batches = drain(&plan, &[0, 1], &context).await?;
696 Some(kernels::node_values(&index, &shape.edges.graph, batches)?)
697 }
698 None => None,
699 };
700 crate::run_blocking(move || {
703 let weights = weights.as_deref().map_or(&[][..], Vec::as_slice);
704 kernels::batch(
705 &index,
706 &shape.outputs,
707 weights,
708 values.as_ref(),
709 shape.per_node,
710 shape.schema,
711 )
712 })
713 .await
714 .map_err(|e| DataFusionError::Execution(format!("a graph kernel failed: {e}")))?
715 };
716 Ok(Box::pin(RecordBatchStreamAdapter::new(
717 schema,
718 futures::stream::once(run),
719 )))
720 }
721}
722
723#[derive(Debug, Clone)]
725struct TraversalShape {
726 edges: Edges,
727 traversal: Traversal,
728 display: String,
730 schema: SchemaRef,
731}
732
733#[derive(Debug)]
736pub struct GraphTraversalExec {
737 shape: TraversalShape,
738 input: Arc<dyn ExecutionPlan>,
739 weighted: Option<WeightedInput>,
740 nodes: Vec<Arc<dyn ExecutionPlan>>,
742 properties: Arc<PlanProperties>,
743}
744
745impl GraphTraversalExec {
746 fn new(
747 shape: TraversalShape,
748 input: Arc<dyn ExecutionPlan>,
749 weighted: Option<WeightedInput>,
750 nodes: Vec<Arc<dyn ExecutionPlan>>,
751 ) -> Self {
752 let properties = Arc::new(PlanProperties::new(
753 EquivalenceProperties::new(Arc::clone(&shape.schema)),
754 Partitioning::UnknownPartitioning(1),
755 EmissionType::Final,
756 Boundedness::Bounded,
757 ));
758 Self {
759 shape,
760 input,
761 weighted,
762 nodes,
763 properties,
764 }
765 }
766}
767
768impl DisplayAs for GraphTraversalExec {
769 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
770 write!(
771 f,
772 "GraphTraversalExec: graph={}, call={}",
773 self.shape.edges.graph, self.shape.display
774 )
775 }
776}
777
778impl ExecutionPlan for GraphTraversalExec {
779 fn name(&self) -> &str {
780 "GraphTraversalExec"
781 }
782
783 fn properties(&self) -> &Arc<PlanProperties> {
784 &self.properties
785 }
786
787 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
788 let mut children = vec![&self.input];
789 children.extend(self.weighted.as_ref().map(|w| &w.plan));
790 children.extend(self.shape.edges.vertices.as_ref().map(|v| &v.plan));
791 children.extend(self.nodes.iter());
792 children
793 }
794
795 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
799 vec![false; self.children().len()]
800 }
801
802 fn apply_expressions(
803 &self,
804 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
805 ) -> Result<TreeNodeRecursion> {
806 Ok(TreeNodeRecursion::Continue)
807 }
808
809 fn with_new_children(
810 self: Arc<Self>,
811 children: Vec<Arc<dyn ExecutionPlan>>,
812 ) -> Result<Arc<dyn ExecutionPlan>> {
813 let expected = self.children().len();
814 if children.len() != expected {
815 return Err(DataFusionError::Internal(format!(
816 "GraphTraversalExec has {expected} children; got {}",
817 children.len()
818 )));
819 }
820 let mut children = children.into_iter();
821 let input = children.next().expect("checked above");
822 let weighted = self.weighted.clone().map(|w| WeightedInput {
823 plan: children.next().expect("checked above"),
824 ..w
825 });
826 let mut shape = self.shape.clone();
827 shape.edges.vertices = shape.edges.vertices.map(|v| VertexInput {
828 plan: children.next().expect("checked above"),
829 ..v
830 });
831 Ok(Arc::new(Self::new(
832 shape,
833 input,
834 weighted,
835 children.collect(),
836 )))
837 }
838
839 fn execute(
840 &self,
841 partition: usize,
842 context: Arc<TaskContext>,
843 ) -> Result<SendableRecordBatchStream> {
844 if partition != 0 {
845 return Err(DataFusionError::Internal(format!(
846 "GraphTraversalExec has one partition; asked for {partition}"
847 )));
848 }
849 let shape = self.shape.clone();
850 let input = Arc::clone(&self.input);
851 let weighted = self.weighted.clone();
852 let node_plans = self.nodes.clone();
853 let schema = Arc::clone(&shape.schema);
854 let run = async move {
855 let (index, weights) = match weighted {
856 None => (unweighted_index(&shape.edges, input, &context).await?, None),
857 Some(w) => {
858 let (index, weights) = weighted_index(&shape.edges, input, w, &context).await?;
859 (index, Some(weights))
860 }
861 };
862 let spec = shape.traversal.spec();
863 let mut nodes = Vec::with_capacity(node_plans.len());
864 for (param, plan) in spec.node_params().zip(&node_plans) {
865 let batches = drain(plan, &[0], &context).await?;
866 nodes.push(node_ids(&shape, param, &index, batches)?);
867 }
868 crate::run_blocking(move || {
869 let weights = weights.as_ref().map(|w| w[0].as_slice());
870 traversal::batch(
871 &index.topology,
872 &index.ids,
873 &shape.traversal,
874 &nodes,
875 weights,
876 shape.schema,
877 )
878 })
879 .await
880 .map_err(|e| DataFusionError::Execution(format!("a graph kernel failed: {e}")))?
881 };
882 Ok(Box::pin(RecordBatchStreamAdapter::new(
883 schema,
884 futures::stream::once(run),
885 )))
886 }
887}
888
889fn node_ids(
892 shape: &TraversalShape,
893 param: &TraversalParam,
894 index: &GraphIndex,
895 batches: Vec<Vec<ArrayRef>>,
896) -> Result<Vec<Option<u32>>> {
897 let (function, name, graph) = (shape.traversal.name(), param.name, &shape.edges.graph);
898 let id_type = index.ids.user_type();
899 let options = CastOptions {
900 safe: false,
901 ..CastOptions::default()
902 };
903 let arrays = batches
904 .iter()
905 .map(|b| {
906 if let Some(row) = traversal::first_refused_node_id(&b[0], &id_type) {
909 let got = sql_text(&b[0], row)
910 .unwrap_or_else(|| format!("a value of type {}", b[0].data_type()));
911 return Err(DataFusionError::Execution(traversal::not_node_ids(
912 function, param, graph, &id_type, &got,
913 )));
914 }
915 cast_with_options(&b[0], &id_type, &options).map_err(|e| {
916 let got = first_uncast(&b[0], &id_type).unwrap_or_else(|| {
917 crate::user_message(&DataFusionError::ArrowError(Box::new(e), None))
918 });
919 DataFusionError::Execution(traversal::not_node_ids(
920 function, param, graph, &id_type, &got,
921 ))
922 })
923 })
924 .collect::<Result<Vec<_>>>()?;
925 let refs: Vec<&dyn Array> = arrays.iter().map(|a| a.as_ref()).collect();
926 let ids = match refs.as_slice() {
927 [] => new_empty_array(&id_type),
928 _ => concat(&refs)?,
929 };
930 if param.ty == TraversalParamType::Node && ids.len() > 1 {
931 return Err(DataFusionError::Execution(format!(
932 "{function}({name} =>) must be one node id; its query returned {} rows",
933 ids.len()
934 )));
935 }
936 index
937 .ids
938 .dense_from_array(ids.as_ref())
939 .map_err(|e| DataFusionError::Execution(format!("{function}({name} =>): {e}")))
940}
941
942fn first_uncast(values: &ArrayRef, id_type: &DataType) -> Option<String> {
945 let lenient = cast(values, id_type).ok()?;
946 let row = (0..values.len()).find(|&i| values.is_valid(i) && lenient.is_null(i))?;
947 sql_text(values, row)
948}
949
950fn sql_text(values: &ArrayRef, row: usize) -> Option<String> {
952 let mut value = ScalarValue::try_from_array(values, row).ok()?;
953 while let ScalarValue::Dictionary(_, inner) = value {
954 value = *inner;
955 }
956 Some(match value.try_as_str() {
957 Some(Some(s)) => format!("'{}'", s.replace('\'', "''")),
958 _ => value.to_string(),
959 })
960}
961
962#[derive(Debug)]
987pub struct KeepEdgeOrder;
988
989impl PhysicalOptimizerRule for KeepEdgeOrder {
990 fn optimize(
991 &self,
992 plan: Arc<dyn ExecutionPlan>,
993 _config: &ConfigOptions,
994 ) -> Result<Arc<dyn ExecutionPlan>> {
995 plan.transform_up(|node| {
996 let keys: Vec<Vec<usize>> =
998 if let Some(exec) = node.downcast_ref::<GraphAlgorithmExec>() {
999 let mut keys = vec![endpoint_keys(exec.shape.edges.endpoints)];
1000 keys.extend(exec.weighted.as_ref().map(weighted_keys));
1001 keys.extend(exec.shape.edges.vertices.as_ref().map(|_| vec![0]));
1002 keys.extend(exec.values.as_ref().map(|_| vec![]));
1005 keys
1006 } else if let Some(exec) = node.downcast_ref::<GraphTraversalExec>() {
1007 let mut keys = vec![endpoint_keys(exec.shape.edges.endpoints)];
1008 keys.extend(exec.weighted.as_ref().map(weighted_keys));
1009 keys.extend(exec.shape.edges.vertices.as_ref().map(|_| vec![0]));
1010 keys.extend(exec.nodes.iter().map(|_| vec![0]));
1011 keys
1012 } else if let Some(exec) = node.downcast_ref::<PathSearchExec>() {
1013 let mut keys = vec![endpoint_keys(exec.shape.edges.endpoints)];
1014 keys.extend(exec.shape.edges.vertices.as_ref().map(|_| vec![0]));
1015 keys.push(vec![0]);
1016 keys.extend(exec.targets.as_ref().map(|_| vec![]));
1018 keys
1019 } else {
1020 return Ok(Transformed::no(node));
1021 };
1022 let children = node
1023 .children()
1024 .into_iter()
1025 .zip(keys)
1026 .map(|(child, keys)| {
1027 let child = without_round_robin(Arc::clone(child))?;
1028 if preserves_order(&child) {
1029 Ok(child)
1030 } else {
1031 sorted(child, &keys)
1032 }
1033 })
1034 .collect::<Result<Vec<_>>>()?;
1035 Ok(Transformed::yes(replace_children_if_necessary(
1036 Arc::clone(&node),
1037 children,
1038 )?))
1039 })
1040 .map(|t| t.data)
1041 }
1042
1043 fn name(&self) -> &str {
1044 "keep_edge_order"
1045 }
1046
1047 fn schema_check(&self) -> bool {
1048 true
1049 }
1050}
1051
1052fn without_round_robin(plan: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> {
1054 plan.transform_up(|node| {
1055 if let Some(repartition) = node.downcast_ref::<RepartitionExec>() {
1056 if matches!(repartition.partitioning(), Partitioning::RoundRobinBatch(_)) {
1057 return Ok(Transformed::yes(Arc::clone(repartition.input())));
1058 }
1059 }
1060 Ok(Transformed::no(node))
1061 })
1062 .map(|t| t.data)
1063}
1064
1065fn endpoint_keys((source, destination): (usize, usize)) -> Vec<usize> {
1067 vec![source, destination]
1068}
1069
1070fn weighted_keys(weighted: &WeightedInput) -> Vec<usize> {
1073 let mut keys = endpoint_keys((weighted.source, weighted.destination));
1074 keys.extend(&weighted.weights);
1075 keys
1076}
1077
1078fn preserves_order(plan: &Arc<dyn ExecutionPlan>) -> bool {
1083 let mut preserves = true;
1084 let _ = plan.apply(|node| {
1085 if node.downcast_ref::<GraphAlgorithmExec>().is_some()
1086 || node.downcast_ref::<GraphTraversalExec>().is_some()
1087 || node.downcast_ref::<PathSearchExec>().is_some()
1088 {
1089 return Ok(TreeNodeRecursion::Jump);
1090 }
1091 let reorders = node.downcast_ref::<RepartitionExec>().is_some()
1092 || node.downcast_ref::<CoalescePartitionsExec>().is_some()
1093 || node.downcast_ref::<AggregateExec>().is_some()
1094 || node.downcast_ref::<UnionExec>().is_some()
1095 || node.downcast_ref::<InterleaveExec>().is_some()
1096 || node.name().contains("Join");
1097 if reorders {
1098 preserves = false;
1099 return Ok(TreeNodeRecursion::Stop);
1100 }
1101 Ok(TreeNodeRecursion::Continue)
1102 });
1103 preserves
1104}
1105
1106fn sorted(plan: Arc<dyn ExecutionPlan>, keys: &[usize]) -> Result<Arc<dyn ExecutionPlan>> {
1109 let schema = plan.schema();
1110 let exprs = keys.iter().map(|&i| {
1111 PhysicalSortExpr::new(
1112 Arc::new(Column::new(schema.field(i).name(), i)) as Arc<dyn PhysicalExpr>,
1113 SortOptions {
1114 descending: false,
1115 nulls_first: false,
1116 },
1117 )
1118 });
1119 let Some(ordering) = LexOrdering::new(exprs) else {
1120 return Ok(plan);
1121 };
1122 let partitions = plan.output_partitioning().partition_count();
1123 let sort =
1124 Arc::new(SortExec::new(ordering.clone(), plan).with_preserve_partitioning(partitions > 1));
1125 Ok(if partitions > 1 {
1126 Arc::new(SortPreservingMergeExec::new(ordering, sort))
1127 } else {
1128 sort
1129 })
1130}
1131
1132async fn index_key(
1136 shape: &Edges,
1137 input: &Arc<dyn ExecutionPlan>,
1138 context: &TaskContext,
1139) -> Result<Option<(String, Vec<FileIdentity>)>> {
1140 let Some(edges) = &shape.fingerprint else {
1141 return Ok(None);
1142 };
1143 let mut files = file_identities(input, context).await?;
1144 let fingerprint = match &shape.vertices {
1145 None => edges.clone(),
1146 Some(VertexInput {
1147 fingerprint: None, ..
1148 }) => return Ok(None),
1149 Some(VertexInput {
1150 plan,
1151 fingerprint: Some(vertices),
1152 }) => {
1153 files.extend(file_identities(plan, context).await?);
1154 files.sort();
1155 files.dedup();
1156 format!("{edges}\n-- vertex keys --\n{vertices}")
1157 }
1158 };
1159 Ok(Some((fingerprint, files)))
1160}
1161
1162async fn unweighted_index(
1164 shape: &Edges,
1165 input: Arc<dyn ExecutionPlan>,
1166 context: &Arc<TaskContext>,
1167) -> Result<Arc<GraphIndex>> {
1168 let build = {
1169 let (shape, input, context) = (shape.clone(), Arc::clone(&input), Arc::clone(context));
1170 async move {
1171 let mut no_weights = WeightReader::new(&[], 0);
1172 build_streamed(&shape, &input, shape.endpoints, &context, &mut no_weights).await
1173 }
1174 };
1175 match context.session_config().get_extension::<IndexCache>() {
1176 Some(cache) => {
1177 let key = index_key(shape, &input, context).await?;
1178 cache
1179 .get_or_build_for(
1180 context.session_config(),
1181 key,
1182 &build_info(shape, &input),
1183 build.boxed(),
1184 )
1185 .await
1186 }
1187 None => build.await.map(Arc::new),
1188 }
1189}
1190
1191async fn weighted_index(
1194 shape: &Edges,
1195 input: Arc<dyn ExecutionPlan>,
1196 weighted: WeightedInput,
1197 context: &Arc<TaskContext>,
1198) -> Result<(Arc<GraphIndex>, Weights)> {
1199 let cache = context.session_config().get_extension::<IndexCache>();
1200 let key = match &cache {
1201 Some(_) => index_key(shape, &input, context).await?,
1202 None => None,
1203 };
1204 let weights_key = match &weighted.fingerprint {
1205 Some(f) => Some(format!(
1206 "{}\n{:?}",
1207 portable_in(f, context.session_config()),
1208 file_identities(&weighted.plan, context).await?
1209 )),
1210 None => None,
1211 };
1212 let n_weights = weighted.weights.len();
1213 if let (Some(cache), Some(key), Some(weights_key)) = (&cache, &key, &weights_key) {
1215 if let Some(index) = cache.get(key) {
1216 if let Some(weights) = known_weights(&index, weights_key, n_weights).await {
1217 return Ok((index, weights));
1218 }
1219 }
1220 }
1221
1222 let (index, weights, shared) = match &cache {
1223 Some(cache) => {
1224 let slot = Arc::new(Mutex::new(None));
1226 let build = {
1227 let (shape, weighted) = (shape.clone(), weighted.clone());
1228 let (context, slot) = (Arc::clone(context), Arc::clone(&slot));
1229 async move {
1230 let (index, weights) = build_weighted(&shape, &weighted, &context).await?;
1231 *slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(weights);
1232 Ok(index)
1233 }
1234 };
1235 let index = cache
1236 .get_or_build_for(
1237 context.session_config(),
1238 key,
1239 &build_info(shape, &weighted.plan),
1240 build.boxed(),
1241 )
1242 .await?;
1243 let built_here = slot.lock().unwrap_or_else(|e| e.into_inner()).take();
1244 if built_here.is_none() {
1246 if let Some(weights_key) = &weights_key {
1247 if let Some(weights) = known_weights(&index, weights_key, n_weights).await {
1248 return Ok((index, weights));
1249 }
1250 }
1251 }
1252 match built_here {
1253 Some(weights) => (index, weights, true),
1254 None => {
1255 let mut weights =
1256 WeightReader::new(&weighted.weights, index.topology.n_edges());
1257 if read_aligned(&index, &weighted, shape, context, &mut weights).await? {
1258 (index, weights, true)
1259 } else {
1260 cache.record_build();
1263 let (private, weights) = build_weighted(shape, &weighted, context).await?;
1264 (Arc::new(private), weights, false)
1265 }
1266 }
1267 }
1268 }
1269 None => {
1270 let (index, weights) = build_weighted(shape, &weighted, context).await?;
1271 (Arc::new(index), weights, false)
1272 }
1273 };
1274
1275 let weights: Weights = Arc::new(weights.finish()?.into_iter().map(Arc::new).collect());
1276 if let (true, Some(weights_key)) = (shared, weights_key) {
1277 index.store_weights(weights_key, Arc::clone(&weights));
1278 }
1279 Ok((index, weights))
1280}
1281
1282async fn known_weights(index: &Arc<GraphIndex>, key: &str, n_weights: usize) -> Option<Weights> {
1285 if let Some(weights) = index.weights(key) {
1286 return Some(weights);
1287 }
1288 if !index.has_stored_weights(key) {
1289 return None;
1290 }
1291 let (index, key) = (Arc::clone(index), key.to_string());
1292 crate::run_blocking(move || index.stored_weights(&key, n_weights))
1294 .await
1295 .ok()
1296 .flatten()
1297}
1298
1299async fn build_weighted(
1302 shape: &Edges,
1303 weighted: &WeightedInput,
1304 context: &Arc<TaskContext>,
1305) -> Result<(GraphIndex, WeightReader)> {
1306 let mut weights = WeightReader::new(&weighted.weights, row_hint(&weighted.plan));
1307 let endpoints = (weighted.source, weighted.destination);
1308 let index = build_streamed(shape, &weighted.plan, endpoints, context, &mut weights).await?;
1309 Ok((index, weights))
1310}
1311
1312async fn read_aligned(
1316 index: &GraphIndex,
1317 weighted: &WeightedInput,
1318 shape: &Edges,
1319 context: &Arc<TaskContext>,
1320 weights: &mut WeightReader,
1321) -> Result<bool> {
1322 let id_type = &shape.id_type;
1323 let mut rows = RowCheck::new(index);
1324 let plan = &weighted.plan;
1325 for partition in 0..plan.output_partitioning().partition_count() {
1326 let mut stream = plan.execute(partition, Arc::clone(context))?;
1327 while let Some(batch) = stream.next().await {
1328 let mut batch = batch?;
1329 let mut src = cast(batch.column(weighted.source), id_type)?;
1330 let mut dst = cast(batch.column(weighted.destination), id_type)?;
1331 if shape.vertices.is_some() {
1334 let keep = between_nodes(index, &src, &dst)?;
1335 if keep.true_count() < keep.len() {
1336 batch = filter_record_batch(&batch, &keep)?;
1337 src = filter(&src, &keep)?;
1338 dst = filter(&dst, &keep)?;
1339 }
1340 }
1341 if batch.num_rows() == 0 {
1342 continue;
1343 }
1344 if !rows.check(&src, &dst)? {
1345 return Ok(false);
1346 }
1347 weights.push(&batch);
1348 }
1349 }
1350 Ok(rows.complete())
1351}
1352
1353fn between_nodes(index: &GraphIndex, src: &ArrayRef, dst: &ArrayRef) -> Result<BooleanArray> {
1356 let lookup = |a: &ArrayRef| {
1357 index
1358 .ids
1359 .dense_from_array(a.as_ref())
1360 .map_err(|e| DataFusionError::Execution(e.to_string()))
1361 };
1362 let (s, d) = (lookup(src)?, lookup(dst)?);
1363 Ok(s.iter()
1364 .zip(&d)
1365 .map(|(s, d)| Some(s.is_some() && d.is_some()))
1366 .collect())
1367}
1368
1369struct WeightReader {
1374 columns: Vec<usize>,
1375 values: Vec<Vec<f64>>,
1376 error: Option<DataFusionError>,
1377}
1378
1379impl WeightReader {
1380 fn new(columns: &[usize], rows: usize) -> Self {
1382 Self {
1383 columns: columns.to_vec(),
1384 values: columns.iter().map(|_| Vec::with_capacity(rows)).collect(),
1385 error: None,
1386 }
1387 }
1388
1389 fn push(&mut self, batch: &RecordBatch) {
1390 if self.error.is_some() {
1391 return;
1392 }
1393 for (&c, values) in self.columns.iter().zip(&mut self.values) {
1394 if let Err(e) = extend_weights(batch.column(c), values) {
1395 self.error = Some(e);
1396 return;
1397 }
1398 }
1399 }
1400
1401 fn keep(&mut self, kept: &BooleanBuffer) {
1403 for values in &mut self.values {
1404 let mut bits = kept.iter();
1405 values.retain(|_| bits.next().unwrap_or(false));
1406 }
1407 }
1408
1409 fn finish(self) -> Result<Vec<Vec<f64>>> {
1411 match self.error {
1412 Some(e) => Err(e),
1413 None => Ok(self.values),
1414 }
1415 }
1416}
1417
1418async fn drain(
1421 input: &Arc<dyn ExecutionPlan>,
1422 columns: &[usize],
1423 context: &Arc<TaskContext>,
1424) -> Result<Vec<Vec<ArrayRef>>> {
1425 let mut out = Vec::new();
1426 let partitions = input.output_partitioning().partition_count();
1427 for partition in 0..partitions {
1428 let mut stream = input.execute(partition, Arc::clone(context))?;
1429 while let Some(batch) = stream.next().await {
1430 let batch = batch?;
1431 if batch.num_rows() == 0 {
1432 continue;
1433 }
1434 out.push(
1435 columns
1436 .iter()
1437 .map(|&c| Arc::clone(batch.column(c)))
1438 .collect(),
1439 );
1440 }
1441 }
1442 Ok(out)
1443}
1444
1445const INTERN_ROWS: usize = 1 << 17;
1451
1452#[derive(Default)]
1454struct Gather {
1455 src: Vec<ArrayRef>,
1456 dst: Vec<ArrayRef>,
1457 rows: usize,
1458}
1459
1460impl Gather {
1461 fn push(&mut self, src: ArrayRef, dst: ArrayRef) -> Result<Option<(ArrayRef, ArrayRef)>> {
1463 self.rows += src.len();
1464 self.src.push(src);
1465 self.dst.push(dst);
1466 if self.rows >= INTERN_ROWS {
1467 self.take()
1468 } else {
1469 Ok(None)
1470 }
1471 }
1472
1473 fn take(&mut self) -> Result<Option<(ArrayRef, ArrayRef)>> {
1475 self.rows = 0;
1476 let (src, dst) = (std::mem::take(&mut self.src), std::mem::take(&mut self.dst));
1477 Ok(match src.len() {
1478 0 => None,
1479 1 => src.into_iter().zip(dst).next(),
1480 _ => {
1481 let join = |arrays: &[ArrayRef]| {
1482 let refs: Vec<&dyn Array> = arrays.iter().map(|a| a.as_ref()).collect();
1483 concat(&refs)
1484 };
1485 Some((join(&src)?, join(&dst)?))
1486 }
1487 })
1488 }
1489}
1490
1491fn finish_index(mut interner: EdgeInterner, id_type: &DataType) -> GraphIndex {
1494 let (n_dropped, kept) = (interner.n_dropped(), interner.kept_rows());
1495 let (ids, source, destination) = interner.finish();
1496 if ids.is_empty() {
1497 let (topology, ids) = ursa_io::topology::empty_topology(id_type);
1498 return GraphIndex::new(topology, ids).with_dropped(n_dropped, kept);
1499 }
1500 let topology = Topology::build(ids.len(), source, destination);
1501 GraphIndex::new(Arc::new(topology), Arc::new(ids)).with_dropped(n_dropped, kept)
1502}
1503
1504fn row_hint(plan: &Arc<dyn ExecutionPlan>) -> usize {
1507 StatisticsContext::new()
1508 .compute(plan.as_ref(), &StatisticsArgs::new())
1509 .ok()
1510 .and_then(|s| s.num_rows.get_value().copied())
1511 .unwrap_or(0)
1512}
1513
1514fn build_info(shape: &Edges, input: &Arc<dyn ExecutionPlan>) -> BuildInfo {
1517 let rows = StatisticsContext::new()
1518 .compute(input.as_ref(), &StatisticsArgs::new())
1519 .ok()
1520 .and_then(|s| {
1521 let exact = s.num_rows.is_exact().unwrap_or(false);
1522 s.num_rows.get_value().map(|n| (*n as u64, exact))
1523 });
1524 BuildInfo {
1525 graph: Some(shape.graph.clone()),
1526 edges: rows,
1527 }
1528}
1529
1530enum Chunk {
1532 Nodes(ArrayRef),
1534 NodesDone,
1536 Edges(ArrayRef, ArrayRef),
1537 End,
1539}
1540
1541fn take_chunk(
1544 interner: &mut EdgeInterner,
1545 chunk: Chunk,
1546 graph: &str,
1547 id_type: &DataType,
1548) -> Result<Option<GraphIndex>> {
1549 match chunk {
1550 Chunk::Nodes(keys) => interner
1551 .push_nodes(keys.as_ref())
1552 .map_err(|e| build_error(graph, e))?,
1553 Chunk::NodesDone => interner.close_nodes(),
1554 Chunk::Edges(src, dst) => interner
1555 .push(src.as_ref(), dst.as_ref())
1556 .map_err(|e| build_error(graph, e))?,
1557 Chunk::End => {
1558 let interner = std::mem::replace(interner, EdgeInterner::with_capacity(0));
1559 return Ok(Some(finish_index(interner, id_type)));
1560 }
1561 }
1562 Ok(None)
1563}
1564
1565async fn build_streamed(
1576 shape: &Edges,
1577 input: &Arc<dyn ExecutionPlan>,
1578 (source, destination): (usize, usize),
1579 context: &Arc<TaskContext>,
1580 weights: &mut WeightReader,
1581) -> Result<GraphIndex> {
1582 let (graph, id_type) = (shape.graph.clone(), shape.id_type.clone());
1583 let hint = row_hint(input);
1584 let (tx, mut rx) = tokio::sync::mpsc::channel::<Chunk>(2);
1585
1586 #[cfg(not(target_arch = "wasm32"))]
1590 let interning = tokio::task::spawn_blocking(move || -> Result<Option<GraphIndex>> {
1591 let mut interner = EdgeInterner::with_capacity(hint);
1592 while let Some(chunk) = rx.blocking_recv() {
1593 if let Some(index) = take_chunk(&mut interner, chunk, &graph, &id_type)? {
1594 return Ok(Some(index));
1595 }
1596 }
1597 Ok(None)
1599 });
1600 #[cfg(target_arch = "wasm32")]
1605 let interning = async move {
1606 let mut interner = EdgeInterner::with_capacity(hint);
1607 while let Some(chunk) = rx.recv().await {
1608 if let Some(index) = take_chunk(&mut interner, chunk, &graph, &id_type)? {
1609 return Ok(Some(index));
1610 }
1611 }
1612 Ok::<_, DataFusionError>(None)
1613 };
1614
1615 let read = async {
1616 let tx = tx;
1619 if let Some(vertices) = &shape.vertices {
1620 let keys = &vertices.plan;
1621 for partition in 0..keys.output_partitioning().partition_count() {
1622 let mut stream = keys.execute(partition, Arc::clone(context))?;
1623 while let Some(batch) = stream.next().await {
1624 let batch = batch?;
1625 if batch.num_rows() == 0 {
1626 continue;
1627 }
1628 let keys = cast(batch.column(0), &shape.id_type)?;
1629 if tx.send(Chunk::Nodes(keys)).await.is_err() {
1630 return Ok(());
1631 }
1632 }
1633 }
1634 if tx.send(Chunk::NodesDone).await.is_err() {
1635 return Ok(());
1636 }
1637 }
1638 let mut gather = Gather::default();
1639 for partition in 0..input.output_partitioning().partition_count() {
1640 let mut stream = input.execute(partition, Arc::clone(context))?;
1641 while let Some(batch) = stream.next().await {
1642 let batch = batch?;
1643 if batch.num_rows() == 0 {
1644 continue;
1645 }
1646 let src = cast(batch.column(source), &shape.id_type)?;
1647 let dst = cast(batch.column(destination), &shape.id_type)?;
1648 weights.push(&batch);
1649 drop(batch);
1650 if let Some((src, dst)) = gather.push(src, dst)? {
1651 if tx.send(Chunk::Edges(src, dst)).await.is_err() {
1652 return Ok(());
1654 }
1655 }
1656 }
1657 }
1658 if let Some((src, dst)) = gather.take()? {
1659 if tx.send(Chunk::Edges(src, dst)).await.is_err() {
1660 return Ok(());
1661 }
1662 }
1663 let _ = tx.send(Chunk::End).await;
1664 Ok::<_, DataFusionError>(())
1665 };
1666 #[cfg(not(target_arch = "wasm32"))]
1667 let (read, built) = {
1668 let read = read.await;
1669 let built = interning.await.map_err(|e| {
1670 DataFusionError::Execution(format!("building graph '{}' failed: {e}", shape.graph))
1671 })?;
1672 (read, built)
1673 };
1674 #[cfg(target_arch = "wasm32")]
1675 let (read, built) = futures::join!(read, interning);
1676 read?;
1677 let index = built?.ok_or_else(|| {
1678 DataFusionError::Internal(format!(
1679 "building graph '{}' stopped before the last edge",
1680 shape.graph
1681 ))
1682 })?;
1683 if let Some(kept) = index.kept_rows() {
1684 weights.keep(kept);
1685 }
1686 Ok(index)
1687}
1688
1689struct RowCheck<'a> {
1692 index: &'a GraphIndex,
1693 source: Vec<u32>,
1696 destination: Vec<u32>,
1697 start: usize,
1699}
1700
1701impl<'a> RowCheck<'a> {
1702 fn new(index: &'a GraphIndex) -> Self {
1703 let topology = &index.topology;
1704 let n = topology.n_edges();
1705 let out = topology.out();
1706 let (mut source, mut destination) = (vec![0u32; n], vec![0u32; n]);
1707 for u in 0..topology.n_nodes() {
1708 let slots = out.offsets[u] as usize..out.offsets[u + 1] as usize;
1709 for slot in slots {
1710 let row = out.edge_ids[slot] as usize;
1711 source[row] = u as u32;
1712 destination[row] = out.targets[slot];
1713 }
1714 }
1715 Self {
1716 index,
1717 source,
1718 destination,
1719 start: 0,
1720 }
1721 }
1722
1723 fn check(&mut self, src: &ArrayRef, dst: &ArrayRef) -> Result<bool> {
1726 let rows = self.start..self.start + src.len();
1727 if rows.end > self.source.len() {
1728 return Ok(false);
1729 }
1730 self.start = rows.end;
1731 let ids = &self.index.ids;
1732 let user_type = ids.user_type();
1733 Ok(
1734 ids.gather_user(&self.source[rows.clone()]).as_ref() == cast(src, &user_type)?.as_ref()
1735 && ids.gather_user(&self.destination[rows]).as_ref()
1736 == cast(dst, &user_type)?.as_ref(),
1737 )
1738 }
1739
1740 fn complete(&self) -> bool {
1742 self.start == self.source.len()
1743 }
1744}
1745
1746#[cfg(test)]
1748fn aligned(index: &GraphIndex, endpoints: &[(ArrayRef, ArrayRef)]) -> Result<bool> {
1749 let mut rows = RowCheck::new(index);
1750 for (src, dst) in endpoints {
1751 if !rows.check(src, dst)? {
1752 return Ok(false);
1753 }
1754 }
1755 Ok(rows.complete())
1756}
1757
1758#[cfg(test)]
1761fn edge_weights(columns: &[ArrayRef]) -> Result<Vec<f64>> {
1762 let mut out = Vec::with_capacity(columns.iter().map(|c| c.len()).sum());
1763 for column in columns {
1764 extend_weights(column, &mut out)?;
1765 }
1766 Ok(out)
1767}
1768
1769fn extend_weights(column: &ArrayRef, out: &mut Vec<f64>) -> Result<()> {
1772 let column = cast(column, &DataType::Float64)?;
1773 let column = column
1774 .as_any()
1775 .downcast_ref::<Float64Array>()
1776 .expect("cast to Float64 yields Float64Array");
1777 if column.null_count() > 0 {
1778 return Err(DataFusionError::Execution(
1779 "weight expression produced a null value; weights must be non-null".into(),
1780 ));
1781 }
1782 let values = column.values();
1783 if let Some(&w) = values.iter().find(|&&w| w < 0.0) {
1784 return Err(DataFusionError::Execution(format!(
1785 "weight expression produced a negative value ({w}); weights must be \
1786 non-negative"
1787 )));
1788 }
1789 out.extend_from_slice(values);
1790 Ok(())
1791}
1792
1793fn build_error(graph: &str, e: IdError) -> DataFusionError {
1794 DataFusionError::Execution(match e {
1795 IdError::Null => format!(
1796 "graph '{graph}': an edge has a null source or destination; filter those rows out \
1797 of the edge table (for example, with a view that keeps rows WHERE both are NOT NULL)"
1798 ),
1799 other => format!("graph '{graph}': {other}"),
1800 })
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805 use super::*;
1806 use datafusion::arrow::array::{Int64Array, StringArray};
1807
1808 fn index(src: &dyn Array, dst: &dyn Array) -> GraphIndex {
1809 let (topology, ids) = ursa_io::build_topology_batches(&[(src, dst)]).unwrap();
1810 GraphIndex::new(topology, ids)
1811 }
1812
1813 fn ints(v: &[i64]) -> ArrayRef {
1814 Arc::new(Int64Array::from(v.to_vec()))
1815 }
1816
1817 #[test]
1818 fn weights_line_up_only_with_the_rows_the_index_was_built_from() {
1819 let (src, dst) = (ints(&[10, 20, 10, 30]), ints(&[20, 30, 30, 10]));
1820 let built = index(src.as_ref(), dst.as_ref());
1821 let split = vec![
1823 (src.slice(0, 1), dst.slice(0, 1)),
1824 (src.slice(1, 3), dst.slice(1, 3)),
1825 ];
1826 assert!(aligned(&built, &split).unwrap());
1827 let swapped = vec![(ints(&[20, 10, 10, 30]), ints(&[30, 20, 30, 10]))];
1829 assert!(!aligned(&built, &swapped).unwrap());
1830 assert!(!aligned(&built, &split[1..]).unwrap());
1832
1833 let names = |v: &[&str]| Arc::new(StringArray::from(v.to_vec())) as ArrayRef;
1834 let (s, d) = (names(&["a", "b"]), names(&["b", "c"]));
1835 let built = index(s.as_ref(), d.as_ref());
1836 assert!(aligned(&built, &[(Arc::clone(&s), Arc::clone(&d))]).unwrap());
1837 assert!(!aligned(&built, &[(d, s)]).unwrap());
1838 }
1839
1840 #[test]
1841 fn weights_are_checked_as_ursa_plan_checks_them() {
1842 let ok = edge_weights(&[ints(&[1, 2]), ints(&[0])]).unwrap();
1843 assert_eq!(ok, vec![1.0, 2.0, 0.0]);
1844 let negative = edge_weights(&[ints(&[1, -1])]).unwrap_err();
1845 assert_eq!(
1846 crate::user_message(&negative),
1847 "weight expression produced a negative value (-1); weights must be non-negative"
1848 );
1849 let null: ArrayRef = Arc::new(Int64Array::from(vec![Some(1), None]));
1850 assert_eq!(
1851 crate::user_message(&edge_weights(&[null]).unwrap_err()),
1852 "weight expression produced a null value; weights must be non-null"
1853 );
1854 }
1855}