1use std::cmp::Ordering;
28use std::fmt;
29use std::hash::{Hash, Hasher};
30use std::sync::Arc;
31
32use datafusion::arrow::datatypes::{DataType, Field, Schema};
33use datafusion::common::{DFSchema, DFSchemaRef};
34use datafusion::error::Result;
35use datafusion::logical_expr::{Expr, ExprSchemable, LogicalPlan, UserDefinedLogicalNodeCore};
36use ursa_core::catalog::Call;
37
38use super::functions::{display_call, plan};
39use super::traversal::Traversal;
40
41pub const WEIGHTED_SOURCE: &str = "__ursa_source";
43pub const WEIGHTED_DESTINATION: &str = "__ursa_destination";
45
46pub fn weight_column(i: usize) -> String {
48 format!("__ursa_weight_{i}")
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub struct Output {
54 pub name: String,
56 pub call: Call,
57 pub field: usize,
59 pub weight: Option<usize>,
62}
63
64#[derive(Debug, Clone)]
66pub struct GraphAlgorithmNode {
67 pub graph: String,
70 pub input: Arc<LogicalPlan>,
72 pub weighted: Option<Arc<LogicalPlan>>,
75 pub values: Option<Arc<LogicalPlan>>,
78 pub source: Expr,
81 pub destination: Expr,
82 pub weights: Vec<String>,
84 pub outputs: Vec<Output>,
86 pub per_node: bool,
88 pub id_type: DataType,
92 schema: DFSchemaRef,
93}
94
95impl GraphAlgorithmNode {
96 #[allow(clippy::too_many_arguments)]
97 pub fn try_new(
98 graph: String,
99 input: Arc<LogicalPlan>,
100 weighted: Option<Arc<LogicalPlan>>,
101 values: Option<Arc<LogicalPlan>>,
102 source: Expr,
103 destination: Expr,
104 weights: Vec<String>,
105 outputs: Vec<Output>,
106 per_node: bool,
107 ) -> Result<Self> {
108 let id_type = endpoint_id_type(&graph, &input, &source, &destination)?;
109 if let Some(values) = &values {
110 let columns = values.schema().fields().len();
111 if columns != 2 {
112 return Err(plan(format!(
113 "the node values of graph '{graph}' must be two columns, node ids then \
114 values; the query returns {columns}"
115 )));
116 }
117 }
118 let mut fields = Vec::new();
119 if per_node {
120 fields.push(Field::new("id", id_type.clone(), false));
121 }
122 for o in &outputs {
123 let spec = o.call.spec().outputs[o.field];
124 fields.push(Field::new(&o.name, spec.ty.data_type(), spec.nullable));
125 }
126 let schema = Arc::new(DFSchema::try_from(Schema::new(fields))?);
127 Ok(Self {
128 graph,
129 input,
130 weighted,
131 values,
132 source,
133 destination,
134 weights,
135 outputs,
136 per_node,
137 id_type,
138 schema,
139 })
140 }
141
142 pub fn endpoint_indices(&self) -> Result<(usize, usize)> {
144 endpoint_positions(&self.graph, &self.input, &self.source, &self.destination)
145 }
146
147 pub(crate) fn display_outputs(&self) -> String {
150 let outputs: Vec<String> = self
151 .outputs
152 .iter()
153 .map(|o| {
154 let weight = o.weight.map(|i| self.weights[i].as_str());
155 let call = display_call(&o.call, weight);
156 let spec = o.call.spec();
157 if spec.outputs.len() == 1 && o.name == spec.outputs[0].name {
158 call
159 } else {
160 format!("{}: {call}", o.name)
161 }
162 })
163 .collect();
164 outputs.join(", ")
165 }
166}
167
168pub(crate) fn endpoint_id_type(
171 graph: &str,
172 input: &LogicalPlan,
173 source: &Expr,
174 destination: &Expr,
175) -> Result<DataType> {
176 let source_type = source.get_type(input.schema())?;
177 let destination_type = destination.get_type(input.schema())?;
178 match (id_type(&source_type), id_type(&destination_type)) {
179 (Some(s), Some(d)) if s == d => Ok(s),
180 (Some(_), Some(_)) => Err(plan(format!(
181 "graph '{graph}': the source and destination columns must have one id type \
182 (both integer or both string); got {source_type} and {destination_type}"
183 ))),
184 (None, _) | (_, None) => {
185 let bad = if id_type(&source_type).is_none() {
186 source_type
187 } else {
188 destination_type
189 };
190 Err(plan(format!(
191 "graph '{graph}': node ids must be an integer or string column; got {bad}"
192 )))
193 }
194 }
195}
196
197fn endpoint_positions(
200 graph: &str,
201 input: &LogicalPlan,
202 source: &Expr,
203 destination: &Expr,
204) -> Result<(usize, usize)> {
205 let index = |e: &Expr| -> Result<usize> {
206 match e {
207 Expr::Column(c) => input.schema().index_of_column(c),
208 other => Err(plan(format!(
209 "graph '{graph}': an edge endpoint must be a column; got {other}"
210 ))),
211 }
212 };
213 Ok((index(source)?, index(destination)?))
214}
215
216pub(crate) fn id_type(t: &DataType) -> Option<DataType> {
220 ursa_io::ids::canonical_id_type(t)
221}
222
223type Key<'a> = (&'a String, &'a Vec<Output>, &'a Vec<String>, bool);
225
226impl GraphAlgorithmNode {
227 fn key(&self) -> Key<'_> {
228 (&self.graph, &self.outputs, &self.weights, self.per_node)
229 }
230}
231
232impl PartialEq for GraphAlgorithmNode {
235 fn eq(&self, other: &Self) -> bool {
236 self.key() == other.key()
237 && self.source == other.source
238 && self.destination == other.destination
239 && self.input == other.input
240 && self.weighted == other.weighted
241 && self.values == other.values
242 }
243}
244
245impl Eq for GraphAlgorithmNode {}
246
247impl Hash for GraphAlgorithmNode {
248 fn hash<H: Hasher>(&self, state: &mut H) {
249 self.key().hash(state);
250 self.source.hash(state);
251 self.destination.hash(state);
252 self.input.hash(state);
253 self.weighted.hash(state);
254 self.values.hash(state);
255 }
256}
257
258impl PartialOrd for GraphAlgorithmNode {
259 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
260 match self.key().cmp(&other.key()) {
261 Ordering::Equal => {}
262 unequal => return Some(unequal),
263 }
264 match self.source.partial_cmp(&other.source)? {
265 Ordering::Equal => {}
266 unequal => return Some(unequal),
267 }
268 match self.destination.partial_cmp(&other.destination)? {
269 Ordering::Equal => {}
270 unequal => return Some(unequal),
271 }
272 match self.input.partial_cmp(&other.input)? {
273 Ordering::Equal => {}
274 unequal => return Some(unequal),
275 }
276 match self.weighted.partial_cmp(&other.weighted)? {
277 Ordering::Equal => {}
278 unequal => return Some(unequal),
279 }
280 self.values.partial_cmp(&other.values)
281 }
282}
283
284impl UserDefinedLogicalNodeCore for GraphAlgorithmNode {
285 fn name(&self) -> &str {
286 "GraphAlgorithm"
287 }
288
289 fn inputs(&self) -> Vec<&LogicalPlan> {
290 let mut inputs = vec![self.input.as_ref()];
291 inputs.extend(self.weighted.as_deref());
292 inputs.extend(self.values.as_deref());
293 inputs
294 }
295
296 fn schema(&self) -> &DFSchemaRef {
297 &self.schema
298 }
299
300 fn expressions(&self) -> Vec<Expr> {
302 vec![self.source.clone(), self.destination.clone()]
303 }
304
305 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
306 write!(
307 f,
308 "GraphAlgorithm: graph={}, edges=({}, {}), outputs=[{}]",
309 self.graph,
310 self.source,
311 self.destination,
312 self.display_outputs()
313 )
314 }
315
316 fn with_exprs_and_inputs(&self, exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> Result<Self> {
317 let [source, destination]: [Expr; 2] = exprs
318 .try_into()
319 .map_err(|_| plan("GraphAlgorithm takes two expressions".into()))?;
320 let expected =
321 1 + usize::from(self.weighted.is_some()) + usize::from(self.values.is_some());
322 if inputs.len() != expected {
323 return Err(plan(format!(
324 "GraphAlgorithm takes {expected} inputs; got {}",
325 inputs.len()
326 )));
327 }
328 let mut inputs = inputs.into_iter().map(Arc::new);
329 let input = inputs.next().expect("checked above");
330 let weighted = self.weighted.as_ref().and_then(|_| inputs.next());
331 let values = self.values.as_ref().and_then(|_| inputs.next());
332 Self::try_new(
333 self.graph.clone(),
334 input,
335 weighted,
336 values,
337 source,
338 destination,
339 self.weights.clone(),
340 self.outputs.clone(),
341 self.per_node,
342 )
343 }
344
345 fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
348 let (source, destination) = self.endpoint_indices().ok()?;
349 let mut needed = vec![source, destination];
350 needed.sort_unstable();
351 needed.dedup();
352 let mut children = vec![needed];
353 if let Some(weighted) = &self.weighted {
354 children.push((0..weighted.schema().fields().len()).collect());
355 }
356 if self.values.is_some() {
357 children.push(vec![0, 1]);
358 }
359 Some(children)
360 }
361
362 fn prevent_predicate_push_down_columns(&self) -> std::collections::HashSet<String> {
364 self.schema
365 .fields()
366 .iter()
367 .map(|f| f.name().clone())
368 .collect()
369 }
370}
371
372#[derive(Debug, Clone)]
388pub struct GraphTraversalNode {
389 pub graph: String,
391 pub traversal: Traversal,
392 pub input: Arc<LogicalPlan>,
394 pub weighted: Option<Arc<LogicalPlan>>,
397 pub weight: Option<String>,
399 pub nodes: Vec<Arc<LogicalPlan>>,
401 pub written: Vec<String>,
403 pub source: Expr,
406 pub destination: Expr,
407 pub id_type: DataType,
409 schema: DFSchemaRef,
410}
411
412impl GraphTraversalNode {
413 #[allow(clippy::too_many_arguments)]
414 pub fn try_new(
415 graph: String,
416 traversal: Traversal,
417 input: Arc<LogicalPlan>,
418 weighted: Option<Arc<LogicalPlan>>,
419 weight: Option<String>,
420 nodes: Vec<Arc<LogicalPlan>>,
421 written: Vec<String>,
422 source: Expr,
423 destination: Expr,
424 ) -> Result<Self> {
425 let id_type = endpoint_id_type(&graph, &input, &source, &destination)?;
426 let spec = traversal.spec();
427 let expected = spec.node_params().count();
428 if nodes.len() != expected || written.len() != expected {
429 return Err(plan(format!(
430 "{}() takes {expected} node-id inputs; got {}",
431 spec.name,
432 nodes.len()
433 )));
434 }
435 if weighted.is_some() != weight.is_some() {
436 return Err(plan(format!(
437 "{}(): a weighted plan needs its weight expression",
438 spec.name
439 )));
440 }
441 for (p, n) in spec.node_params().zip(&nodes) {
442 let columns = n.schema().fields().len();
443 if columns != 1 {
444 return Err(plan(format!(
445 "{}({} =>) must be one column of node ids; the query returns {columns} \
446 columns",
447 spec.name, p.name
448 )));
449 }
450 }
451 let schema = Arc::new(DFSchema::try_from(spec.schema(&id_type))?);
452 Ok(Self {
453 graph,
454 traversal,
455 input,
456 weighted,
457 weight,
458 nodes,
459 written,
460 source,
461 destination,
462 id_type,
463 schema,
464 })
465 }
466
467 pub fn endpoint_indices(&self) -> Result<(usize, usize)> {
469 endpoint_positions(&self.graph, &self.input, &self.source, &self.destination)
470 }
471
472 pub(crate) fn display_call(&self) -> String {
474 self.traversal
475 .display(&self.written, self.weight.as_deref())
476 }
477}
478
479type TraversalKey<'a> = (
481 &'a String,
482 &'a Traversal,
483 &'a Option<String>,
484 &'a Vec<String>,
485);
486
487impl GraphTraversalNode {
488 fn key(&self) -> TraversalKey<'_> {
489 (&self.graph, &self.traversal, &self.weight, &self.written)
490 }
491}
492
493impl PartialEq for GraphTraversalNode {
496 fn eq(&self, other: &Self) -> bool {
497 self.key() == other.key()
498 && self.source == other.source
499 && self.destination == other.destination
500 && self.input == other.input
501 && self.weighted == other.weighted
502 && self.nodes == other.nodes
503 }
504}
505
506impl Eq for GraphTraversalNode {}
507
508impl Hash for GraphTraversalNode {
509 fn hash<H: Hasher>(&self, state: &mut H) {
510 self.key().hash(state);
511 self.source.hash(state);
512 self.destination.hash(state);
513 self.input.hash(state);
514 self.weighted.hash(state);
515 self.nodes.hash(state);
516 }
517}
518
519impl PartialOrd for GraphTraversalNode {
520 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
521 match self.key().cmp(&other.key()) {
522 Ordering::Equal => {}
523 unequal => return Some(unequal),
524 }
525 match self.source.partial_cmp(&other.source)? {
526 Ordering::Equal => {}
527 unequal => return Some(unequal),
528 }
529 match self.destination.partial_cmp(&other.destination)? {
530 Ordering::Equal => {}
531 unequal => return Some(unequal),
532 }
533 match self.input.partial_cmp(&other.input)? {
534 Ordering::Equal => {}
535 unequal => return Some(unequal),
536 }
537 match self.weighted.partial_cmp(&other.weighted)? {
538 Ordering::Equal => {}
539 unequal => return Some(unequal),
540 }
541 self.nodes.partial_cmp(&other.nodes)
542 }
543}
544
545impl UserDefinedLogicalNodeCore for GraphTraversalNode {
546 fn name(&self) -> &str {
547 "GraphTraversal"
548 }
549
550 fn inputs(&self) -> Vec<&LogicalPlan> {
551 let mut inputs = vec![self.input.as_ref()];
552 inputs.extend(self.weighted.as_deref());
553 inputs.extend(self.nodes.iter().map(Arc::as_ref));
554 inputs
555 }
556
557 fn schema(&self) -> &DFSchemaRef {
558 &self.schema
559 }
560
561 fn expressions(&self) -> Vec<Expr> {
563 vec![self.source.clone(), self.destination.clone()]
564 }
565
566 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
567 write!(
568 f,
569 "GraphTraversal: graph={}, edges=({}, {}), call={}",
570 self.graph,
571 self.source,
572 self.destination,
573 self.display_call()
574 )
575 }
576
577 fn with_exprs_and_inputs(&self, exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> Result<Self> {
578 let [source, destination]: [Expr; 2] = exprs
579 .try_into()
580 .map_err(|_| plan("GraphTraversal takes two expressions".into()))?;
581 let weighted = usize::from(self.weighted.is_some());
582 let expected = 1 + weighted + self.nodes.len();
583 if inputs.len() != expected {
584 return Err(plan(format!(
585 "GraphTraversal takes {expected} inputs; got {}",
586 inputs.len()
587 )));
588 }
589 let mut inputs = inputs.into_iter().map(Arc::new);
590 let input = inputs.next().expect("checked above");
591 let weighted = (weighted == 1).then(|| inputs.next().expect("checked above"));
592 Self::try_new(
593 self.graph.clone(),
594 self.traversal,
595 input,
596 weighted,
597 self.weight.clone(),
598 inputs.collect(),
599 self.written.clone(),
600 source,
601 destination,
602 )
603 }
604
605 fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
608 let (source, destination) = self.endpoint_indices().ok()?;
609 let mut needed = vec![source, destination];
610 needed.sort_unstable();
611 needed.dedup();
612 let mut children = vec![needed];
613 if let Some(weighted) = &self.weighted {
614 children.push((0..weighted.schema().fields().len()).collect());
615 }
616 children.extend(self.nodes.iter().map(|_| vec![0]));
617 Some(children)
618 }
619
620 fn prevent_predicate_push_down_columns(&self) -> std::collections::HashSet<String> {
623 self.schema
624 .fields()
625 .iter()
626 .map(|f| f.name().clone())
627 .collect()
628 }
629}