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