Skip to main content

ursa_engine/graph/
exec.rs

1//! Executing a graph node.
2//!
3//! [`GraphPlanner`] maps a [`GraphAlgorithmNode`] to a [`GraphAlgorithmExec`]
4//! whose children are the planned edge plans. When it executes, it looks up
5//! the index in the session's [`IndexCache`]; on a miss it reads a child,
6//! partition by partition, and builds the index as the rows arrive. Then it runs the calls and
7//! emits one batch.
8//!
9//! Row order matters to the index: dense node ids are given in first-seen
10//! order, and they decide component labels and the order floating-point sums
11//! run in. The child's partitions are read in order, the node asks for no
12//! repartitioning of its input, and the session turns off file-scan work
13//! stealing, so a file's edges arrive in the file's own order on any thread
14//! count, as `ursa-io` reads them for the Python library.
15//!
16//! ## A vertex table
17//!
18//! A graph with a vertex table has its keys as its nodes (#311). Its node has
19//! one more child, the keys ([`GraphAlgorithmNode::vertices`]), which the
20//! build reads first: each key takes the next dense id, in the table's order,
21//! so a node on no edge is a node all the same. The edges are read after,
22//! and an edge whose source or destination is not a key is dropped, as an
23//! inner join of the edges with the vertex table drops it (DuckPGQ's CSR is
24//! built that way). The index counts them ([`GraphIndex::n_dropped`]), for
25//! `describe`, and keeps which rows it kept, so weights read row by row line
26//! up with the kept edges and a path's edges are still the edge table's rows.
27//! The keys' plan is part of the index's cache key: its fingerprint, and the
28//! identity of the files it reads, so a changed vertex table builds afresh.
29//!
30//! ## Weights
31//!
32//! Weights are looked up by edge row, so they must be in the order of the
33//! rows the index was built from. A weighted node reads only its weighted
34//! child (the endpoints and every weight expression, in one pass, evaluating
35//! the weights a batch at a time as it goes), and:
36//!
37//! - on an index-cache miss, builds the index from those same rows, so the
38//!   weights line up by construction;
39//! - on a hit, checks that the rows' endpoints are the index's, row for row
40//!   (an edge plan over a join need not produce its rows in the same order
41//!   twice). If they are not, it reads the child again and builds a private
42//!   index from those rows rather than mis-assign a weight.
43//!
44//! Each distinct weight expression is evaluated once per node, cast to
45//! `Float64`, and checked as `ursa-plan` checks weights (no nulls, no negative
46//! values, with the same messages). Weights that line up with a cached index
47//! are stored on it, keyed by the weighted plan's fingerprint and file
48//! identities, so the next query with the same weights reads no edges at all.
49//! In a database, they are stored with the index on disk too, so a weighted
50//! query over a stored index in a later process reads no edges either.
51
52use 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/// Plans Ursa's graph nodes.
107#[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        // A neighbour aggregation's node values are the last input.
150        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
175/// The vertex keys input at `at`, if the node has one.
176fn 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
196/// The weighted edge plan `physical` (planned from `logical`), with
197/// `n_weights` weight columns.
198fn 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
222/// The exec of a traversal node.
223fn 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/// Builds or fetches a graph index from its first child's edge rows, reads
319/// the start nodes its next child produces (and the targets its last, if it
320/// has them), and searches paths from them (see [`super::path_search`]).
321/// `EXPLAIN ANALYZE` shows how many path prefixes it extended (`expanded`).
322#[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
476/// The one column of keys `plan` gives (a search's `what`: its start or
477/// target nodes), cast to the graph's node id type.
478async 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/// A query planner whose physical planner knows Ursa's graph nodes.
505#[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/// A graph node's edge input: where its endpoints are, and how its index is
522/// cached.
523#[derive(Debug, Clone)]
524struct Edges {
525    graph: String,
526    /// The source and destination columns of the first input.
527    endpoints: (usize, usize),
528    id_type: DataType,
529    /// The edge plan's fingerprint, or `None` if it cannot be cached.
530    fingerprint: Option<String>,
531    /// The graph's vertex keys, if it has a vertex table.
532    vertices: Option<VertexInput>,
533}
534
535/// A graph node's vertex keys: a plan of one column (see the module docs).
536#[derive(Debug, Clone)]
537struct VertexInput {
538    plan: Arc<dyn ExecutionPlan>,
539    /// The keys' plan's fingerprint, or `None` if it cannot be cached.
540    fingerprint: Option<String>,
541}
542
543/// What a [`GraphAlgorithmExec`] computes, besides its children.
544#[derive(Debug, Clone)]
545struct Shape {
546    edges: Edges,
547    outputs: Vec<Output>,
548    per_node: bool,
549    /// The outputs as `EXPLAIN` shows them.
550    display: String,
551    schema: SchemaRef,
552}
553
554/// The weighted edge plan, and where its columns are.
555#[derive(Debug, Clone)]
556struct WeightedInput {
557    plan: Arc<dyn ExecutionPlan>,
558    source: usize,
559    destination: usize,
560    /// One column per weight expression, in the node's order.
561    weights: Vec<usize>,
562    /// The weighted plan's fingerprint, or `None` if it cannot be cached.
563    fingerprint: Option<String>,
564}
565
566/// Builds or fetches a graph index from its child's edge rows, and runs
567/// graph functions over it.
568#[derive(Debug)]
569pub struct GraphAlgorithmExec {
570    shape: Shape,
571    input: Arc<dyn ExecutionPlan>,
572    weighted: Option<WeightedInput>,
573    /// A neighbour aggregation's node values: node ids, then values.
574    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    /// The edges are read partition by partition in order; splitting them
629    /// further would only scramble that order.
630    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            // The kernels are CPU-bound and parallel (rayon): keep them off
701            // the async workers (inline in the browser, which has none).
702            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/// What a [`GraphTraversalExec`] computes, besides its children.
724#[derive(Debug, Clone)]
725struct TraversalShape {
726    edges: Edges,
727    traversal: Traversal,
728    /// The call as `EXPLAIN` shows it.
729    display: String,
730    schema: SchemaRef,
731}
732
733/// Builds or fetches a graph index from its first child's edge rows, reads
734/// the node ids its other children produce, and runs a traversal.
735#[derive(Debug)]
736pub struct GraphTraversalExec {
737    shape: TraversalShape,
738    input: Arc<dyn ExecutionPlan>,
739    weighted: Option<WeightedInput>,
740    /// One plan per node-id parameter, in the spec's order.
741    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    /// The edges are read in order (see [`GraphAlgorithmExec`]), and the node
796    /// ids in the order their query gives them, which sets the order of the
797    /// rows.
798    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
889/// The ids a node-id input produced, as dense indexes of `index` (`None` for a
890/// null or an id not in the graph), cast to the graph's id type first.
891fn 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            // No float, boolean or date, and a string must be exactly an
907            // integer on a graph with integer ids (#271).
908            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
942/// The first value of `values` that does not cast to `id_type`, as SQL writes
943/// it (`'Valjean'`, `2.5`), for a message.
944fn 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
950/// Row `row` of `values` as SQL writes it (`'Valjean'`, `2.5`).
951fn 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/// Keeps a graph node's edge rows in the order the plan produces them, and
963/// gives them a fixed order where the plan has none.
964///
965/// DataFusion's `EnforceDistribution` puts a round-robin repartition under an
966/// operator that benefits from parallel input, such as the `FilterExec` of a
967/// view with a `WHERE` (a subgraph) or the `ProjectionExec` that computes a
968/// weight expression. Draining its partitions in order would then read the
969/// batches out of file order, changing dense ids (and so labels and float
970/// summation order) from what `ursa-io` gives the Python library. This rule
971/// runs after the default rules and removes every round-robin repartition
972/// below a graph node: a round-robin repartition only adds parallelism, so
973/// removing it never changes a result.
974///
975/// What remains may still not preserve order: a join, an aggregate, a hash
976/// repartition, a merge of partitions or a `UNION` produces its rows in an
977/// order that can change between runs and thread counts (#263). Over such a
978/// plan, the rule sorts the edge rows by `(source, destination)`, and a
979/// weighted node's rows by their weights after that, so that rows that tie
980/// are identical in every column the node reads. A traversal's node-id
981/// inputs over such a plan are sorted by value, and so are a graph's vertex
982/// keys, which set the order of its nodes. Order-preserving plans
983/// (scans, views over files, filters, projections) are left as they are,
984/// in file order. The sort is a `SortExec` under the node, so `EXPLAIN`
985/// shows it.
986#[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            // The columns each child is sorted by, if its order is not fixed.
997            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                    // A neighbour aggregation's values give one value per
1003                    // node whatever their order: no sort.
1004                    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                    // The targets are a set: their order does not matter.
1017                    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
1052/// `plan` with its round-robin repartitions removed.
1053fn 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
1065/// The sort key of an edge input: its source, then its destination.
1066fn endpoint_keys((source, destination): (usize, usize)) -> Vec<usize> {
1067    vec![source, destination]
1068}
1069
1070/// The sort key of a weighted edge input: its endpoints, then every weight,
1071/// so rows that tie are the same in every column the node reads.
1072fn 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
1078/// Whether reading `plan`'s partitions in order gives its rows in an order
1079/// that does not depend on the run or the thread count: true unless it has
1080/// a join, an aggregate, a repartition, a merge of partitions or a union. A
1081/// nested graph node emits its rows in a fixed order, whatever its inputs.
1082fn 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
1106/// `plan`'s rows sorted by the columns `keys`, ascending with nulls last,
1107/// as one partition: each partition is sorted, then they are merged.
1108fn 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
1132/// The index cache key for the edges `input` reads, if they can be cached:
1133/// with a vertex table, the keys' plan and files too, so a change to the
1134/// vertex table builds the index afresh.
1135async 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
1162/// The index, cached or built from the endpoint-only input.
1163async 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
1191/// The index and the weight columns, for a node that reads weights (see the
1192/// module docs).
1193async 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    // Weights already evaluated over the cached index's rows: nothing to read.
1214    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            // Weights read by this node's own build, if the cache ran it.
1225            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            // A stored index may have these weights stored with it.
1245            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                        // The cached index's rows are in another order: these
1261                        // weights belong to an index of these rows.
1262                        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
1282/// The weights `index` holds under `key` (with `n_weights` columns), in
1283/// memory or stored with it on disk.
1284async 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    // Reading a file is blocking work.
1293    crate::run_blocking(move || index.stored_weights(&key, n_weights))
1294        .await
1295        .ok()
1296        .flatten()
1297}
1298
1299/// Build the index of the weighted rows, reading their weights in the same
1300/// pass, so they line up by construction.
1301async 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
1312/// Read the weighted rows once, reading their weights, and say whether
1313/// their endpoints are the rows `index` was built from, row for row. Stops
1314/// at the first row that is not.
1315async 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            // With a vertex table, the index holds only the edges between
1332            // keys: skip the others, as the build did.
1333            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
1353/// Which edges `src` -> `dst` (of the index's id type) join two nodes of
1354/// `index`.
1355fn 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
1369/// The weight columns of a weighted node's rows, evaluated a batch at a
1370/// time as the rows are read. The first bad weight is reported once every
1371/// row has been read, after any error in building the index from those
1372/// rows, as when the weights were evaluated after the build.
1373struct WeightReader {
1374    columns: Vec<usize>,
1375    values: Vec<Vec<f64>>,
1376    error: Option<DataFusionError>,
1377}
1378
1379impl WeightReader {
1380    /// A reader of the columns `columns`, sized for `rows` rows.
1381    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    /// Keep only the rows `kept` marks, one bit per row read.
1402    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    /// One `f64` per row read, per column, or the first bad weight.
1410    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
1418/// Every batch of `input`, partition by partition in order, as the columns
1419/// `columns` of each non-empty batch.
1420async 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
1445/// Rows the index build interns at a time. A scan's batches are 8,192 rows,
1446/// and `EdgeInterner` resolves known ids in parallel only in larger chunks,
1447/// so the build gathers batches up to this many rows first: what `ursa-io`'s
1448/// scan does for the Python library (its `SCAN_BATCH_ROWS`). A chunk is
1449/// 2 MiB of `Int64` endpoints.
1450const INTERN_ROWS: usize = 1 << 17;
1451
1452/// Endpoint chunks gathered up to [`INTERN_ROWS`] rows, in row order.
1453#[derive(Default)]
1454struct Gather {
1455    src: Vec<ArrayRef>,
1456    dst: Vec<ArrayRef>,
1457    rows: usize,
1458}
1459
1460impl Gather {
1461    /// Add a chunk; once [`INTERN_ROWS`] rows are gathered, take them.
1462    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    /// The rows gathered so far, as one chunk, if there are any.
1474    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
1491/// The index of everything `interner` was given; an empty graph of
1492/// `id_type` ids if it was given no node and no edge.
1493fn 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
1504/// The number of rows `plan` will produce, if its statistics say: sizes the
1505/// interner, as `ursa-io`'s scan sizes it from the Parquet metadata.
1506fn 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
1514/// What a build over `input` is for, as its notices say: the graph, and the
1515/// edge rows `input`'s statistics promise, if they do.
1516fn 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
1530/// What the reader hands the interning thread.
1531enum Chunk {
1532    /// Vertex keys, before any edge.
1533    Nodes(ArrayRef),
1534    /// Every vertex key has been read: the node set is fixed.
1535    NodesDone,
1536    Edges(ArrayRef, ArrayRef),
1537    /// Every row has been read: build the index.
1538    End,
1539}
1540
1541/// Hand one chunk to `interner` (see [`build_streamed`]); the index once
1542/// every row has been read.
1543fn 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
1565/// Build the index of the edge rows `input` produces, interning each chunk
1566/// as it is read rather than collecting every endpoint array first, so the
1567/// Arrow input never accumulates: the build holds the dense endpoints and a
1568/// chunk or two in flight, as `ursa-io`'s scan does. Reading (here) and
1569/// interning (on a blocking thread) overlap. The rows are interned in the
1570/// order [`drain`] would collect them, so the index is the one
1571/// `ursa_io::build_topology_batches` gives over the collected rows. A
1572/// weighted node's weights are read from the same rows (`weights`), and with
1573/// a vertex table kept only for the edges kept (see the module docs), whose
1574/// keys are read first.
1575async 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    // Native: the interner runs on a blocking thread, so reading and interning
1587    // overlap on two threads. It is spawned here, before the reader starts
1588    // (`run_blocking` would start it only when awaited, after the reader).
1589    #[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        // The reader stopped before the end (its error is the one reported).
1598        Ok(None)
1599    });
1600    // The browser (wasm32) has one thread: the interner is a future polled
1601    // alongside the reader, taking each chunk as it is sent. The channel's
1602    // bound still keeps at most two chunks in flight, and the chunks arrive in
1603    // the same order, so the index is the same (#278).
1604    #[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        // The reader owns the sender, so the interner sees the channel close
1617        // when the reader stops, on an error as at the end.
1618        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                        // The interner stopped on an error: it is reported below.
1653                        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
1689/// Checks, a chunk at a time, that rows are the edge rows an index was
1690/// built from, in order.
1691struct RowCheck<'a> {
1692    index: &'a GraphIndex,
1693    /// Each row's endpoints as dense ids, from the CSR and its row
1694    /// permutation.
1695    source: Vec<u32>,
1696    destination: Vec<u32>,
1697    /// Rows checked so far.
1698    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    /// Whether the next rows are `src` → `dst`. Each chunk's ids are
1724    /// compared on their own, so no column of every row's ids is made.
1725    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    /// Whether every row of the index has been checked.
1741    fn complete(&self) -> bool {
1742        self.start == self.source.len()
1743    }
1744}
1745
1746/// Whether `endpoints` are the edge rows `index` was built from, row for row.
1747#[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/// A weight expression's values, one `f64` per edge row, checked as
1759/// `ursa-plan`'s `evaluate_weight` checks them, with its messages.
1760#[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
1769/// Append one chunk of a weight expression's values to `out`, checked as
1770/// `ursa-plan`'s `evaluate_weight` checks them, with its messages.
1771fn 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        // The same rows, in two batches: aligned.
1822        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        // The same edge set in another order: not.
1828        let swapped = vec![(ints(&[20, 10, 10, 30]), ints(&[30, 20, 30, 10]))];
1829        assert!(!aligned(&built, &swapped).unwrap());
1830        // Fewer rows: not.
1831        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}