1use std::{fmt, str::FromStr};
7
8use crate::GfError;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum AlgorithmVerb {
13 Rank,
15 Cluster,
17 Similar,
19 Paths,
21 Analyze,
23}
24
25impl AlgorithmVerb {
26 #[must_use]
28 pub const fn as_str(self) -> &'static str {
29 match self {
30 Self::Rank => "rank",
31 Self::Cluster => "cluster",
32 Self::Similar => "similar",
33 Self::Paths => "paths",
34 Self::Analyze => "analyze",
35 }
36 }
37}
38
39macro_rules! algorithm_enum {
40 (
41 $name:ident, $default:ident, $verb:expr,
42 { $($variant:ident => $wire:literal),+ $(,)? }
43 $(, aliases { $($alias:literal => $alias_variant:ident),+ $(,)? })?
44 ) => {
45 #[doc = concat!("Typed `by=` values for `", stringify!($name), "`.")]
46 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47 pub enum $name {
48 $(
49 #[doc = concat!("`", $wire, "`.")]
50 $variant,
51 )+
52 }
53
54 impl $name {
55 pub const ALL: &'static [Self] = &[$(Self::$variant),+];
57
58 #[must_use]
60 pub const fn as_str(self) -> &'static str {
61 match self { $(Self::$variant => $wire),+ }
62 }
63
64 #[must_use]
66 pub const fn verb(self) -> AlgorithmVerb { $verb }
67 }
68
69 impl fmt::Display for $name {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 formatter.write_str(self.as_str())
72 }
73 }
74
75 impl FromStr for $name {
76 type Err = GfError;
77
78 fn from_str(value: &str) -> Result<Self, Self::Err> {
79 match value {
80 $($wire => Ok(Self::$variant),)+
81 $($($alias => Ok(Self::$alias_variant),)+)?
82 _ => Err(unknown_algorithm($verb, value)),
83 }
84 }
85 }
86
87 impl Default for $name {
88 fn default() -> Self { Self::$default }
89 }
90 };
91}
92
93algorithm_enum!(RankAlgorithm, PageRank, AlgorithmVerb::Rank, {
94 PageRank => "pagerank",
95 Betweenness => "betweenness",
96 Closeness => "closeness",
97 HarmonicCloseness => "harmonic_closeness",
98 Degree => "degree",
99 Eigenvector => "eigenvector",
100 ArticleRank => "article_rank",
101 HitsHub => "hits_hub",
102 HitsAuthority => "hits_authority",
103 Celf => "celf",
104 ClusteringCoefficient => "clustering_coefficient",
105 Triangles => "triangles",
106 KCore => "k_core",
107 PreferentialAttachment => "preferential_attachment",
108 AdamicAdar => "adamic_adar",
109 CommonNeighbors => "common_neighbors",
110 ResourceAllocation => "resource_allocation",
111 TotalNeighbors => "total_neighbors",
112}, aliases {
113 "local_clustering_coefficient" => ClusteringCoefficient,
114});
115
116algorithm_enum!(ClusterAlgorithm, Louvain, AlgorithmVerb::Cluster, {
117 Louvain => "louvain",
118 Leiden => "leiden",
119 LabelPropagation => "label_propagation",
120 SpeakerListener => "speaker_listener",
121 GirvanNewman => "girvan_newman",
122 ModularityOptimization => "modularity_optimization",
123 FastGreedy => "fastgreedy",
124 InfoMap => "infomap",
125 LeadingEigenvector => "leading_eigenvector",
126 Walktrap => "walktrap",
127 Spinglass => "spinglass",
128 Hdbscan => "hdbscan",
129 KMeans => "k_means",
130 ApproximateMaxKCut => "approximate_max_k_cut",
131 Components => "components",
132 StronglyConnected => "strongly_connected",
133 Biconnected => "biconnected",
134 KCoreDecomposition => "k_core_decomposition",
135});
136
137impl ClusterAlgorithm {
138 #[must_use]
140 pub const fn respects_direction(self) -> bool {
141 match self {
142 Self::InfoMap
143 | Self::LeadingEigenvector
144 | Self::Walktrap
145 | Self::Components
146 | Self::StronglyConnected => true,
147 Self::Louvain
148 | Self::Leiden
149 | Self::LabelPropagation
150 | Self::SpeakerListener
151 | Self::GirvanNewman
152 | Self::ModularityOptimization
153 | Self::FastGreedy
154 | Self::Spinglass
155 | Self::Hdbscan
156 | Self::KMeans
157 | Self::ApproximateMaxKCut
158 | Self::Biconnected
159 | Self::KCoreDecomposition => false,
160 }
161 }
162}
163
164algorithm_enum!(SimilarAlgorithm, NodeSimilarity, AlgorithmVerb::Similar, {
165 NodeSimilarity => "node_similarity",
166 Knn => "knn",
167 FilteredKnn => "filtered_knn",
168 FilteredNodeSimilarity => "filtered_node_similarity",
169 Cosine => "cosine",
170});
171
172algorithm_enum!(PathAlgorithm, Bfs, AlgorithmVerb::Paths, {
173 Bfs => "bfs",
174 Dijkstra => "dijkstra",
175 DijkstraAllPairs => "dijkstra_all_pairs",
176 AStar => "astar",
177 BellmanFord => "bellman_ford",
178 FloydWarshall => "floyd_warshall",
179 DeltaStepping => "delta_stepping",
180 Yens => "yens",
181 MaxFlow => "max_flow",
182 MaxFlowEdges => "max_flow_edges",
183 MinCut => "min_cut",
184 MinCutEdges => "min_cut_edges",
185 MinCostMaxFlow => "min_cost_max_flow",
186 MinCostMaxFlowEdges => "min_cost_max_flow_edges",
187 GomoryHuTree => "gomory_hu_tree",
188 MinSteinerTree => "min_steiner_tree",
189 PrizeCollectingSteinerTree => "prize_collecting_steiner_tree",
190 Dfs => "dfs",
191 RandomWalk => "random_walk",
192 TransitiveClosure => "transitive_closure",
193});
194
195algorithm_enum!(AnalyzeAlgorithm, IsDag, AlgorithmVerb::Analyze, {
196 MinimumSpanningTree => "minimum_spanning_tree",
197 MaximumSpanningTree => "maximum_spanning_tree",
198 MinimumKSpanningTree => "minimum_k_spanning_tree",
199 TopologicalSort => "topological_sort",
200 IsDag => "is_dag",
201 FindCycles => "find_cycles",
202 DagLongestPath => "dag_longest_path",
203 DagLongestPathWeighted => "dag_longest_path_weighted",
204 NodeColoring => "node_coloring",
205 EdgeColoring => "edge_coloring",
206 ChromaticNumber => "chromatic_number",
207 K1Coloring => "k1_coloring",
208 MaxWeightMatching => "max_weight_matching",
209 MaxCardinalityMatching => "max_cardinality_matching",
210 MaxBipartiteMatching => "max_bipartite_matching",
211 EulerCircuit => "euler_circuit",
212 EulerPath => "euler_path",
213 HasEulerCircuit => "has_euler_circuit",
214 HasEulerPath => "has_euler_path",
215 IsPlanar => "is_planar",
216 ArticulationPoints => "articulation_points",
217 Bridges => "bridges",
218 TriangleCount => "triangle_count",
219 Conductance => "conductance",
220 Modularity => "modularity",
221 Transitivity => "transitivity",
222 TriadCensus => "triad_census",
223 DyadCensus => "dyad_census",
224 CountAutomorphisms => "count_automorphisms",
225 Node2Vec => "node2vec",
226 GraphSage => "graphsage",
227 FastRandomProjection => "fast_random_projection",
228 HashGnn => "hashgnn",
229});
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
233pub enum Algorithm {
234 Rank(RankAlgorithm),
236 Cluster(ClusterAlgorithm),
238 Similar(SimilarAlgorithm),
240 Paths(PathAlgorithm),
242 Analyze(AnalyzeAlgorithm),
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248pub enum AlgorithmFieldType {
249 Uuid,
251 UuidList,
253 Float32List,
255 Utf8,
257 Boolean,
259 UInt64,
261 Int64,
263 Float64,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
269pub struct AlgorithmField {
270 pub name: &'static str,
272 pub data_type: AlgorithmFieldType,
274 pub nullable: bool,
276}
277
278const fn field(
279 name: &'static str,
280 data_type: AlgorithmFieldType,
281 nullable: bool,
282) -> AlgorithmField {
283 AlgorithmField {
284 name,
285 data_type,
286 nullable,
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub struct AlgorithmResultSchema {
293 pub fields: &'static [AlgorithmField],
295 pub includes_node_properties: bool,
297}
298
299const fn schema(fields: &'static [AlgorithmField]) -> AlgorithmResultSchema {
300 AlgorithmResultSchema {
301 fields,
302 includes_node_properties: false,
303 }
304}
305
306const fn node_schema(fields: &'static [AlgorithmField]) -> AlgorithmResultSchema {
307 AlgorithmResultSchema {
308 fields,
309 includes_node_properties: true,
310 }
311}
312
313const NODE_SCORE: &[AlgorithmField] = &[
314 field("node_uuid", AlgorithmFieldType::Uuid, false),
315 field("score", AlgorithmFieldType::Float64, false),
316];
317const NODE_COMMUNITY: &[AlgorithmField] = &[
318 field("node_uuid", AlgorithmFieldType::Uuid, false),
319 field("community_id", AlgorithmFieldType::Int64, false),
320];
321const SIMILARITY: &[AlgorithmField] = &[
322 field("node1_uuid", AlgorithmFieldType::Uuid, false),
323 field("node2_uuid", AlgorithmFieldType::Uuid, false),
324 field("similarity", AlgorithmFieldType::Float64, false),
325];
326const PATH: &[AlgorithmField] = &[
327 field("source_uuid", AlgorithmFieldType::Uuid, false),
328 field("target_uuid", AlgorithmFieldType::Uuid, false),
329 field("cost", AlgorithmFieldType::Float64, false),
330 field("path", AlgorithmFieldType::UuidList, false),
331];
332const RANKED_PATH: &[AlgorithmField] = &[
333 field("source_uuid", AlgorithmFieldType::Uuid, false),
334 field("target_uuid", AlgorithmFieldType::Uuid, false),
335 field("rank", AlgorithmFieldType::UInt64, false),
336 field("cost", AlgorithmFieldType::Float64, false),
337 field("path", AlgorithmFieldType::UuidList, false),
338];
339const FLOW: &[AlgorithmField] = &[
340 field("source_uuid", AlgorithmFieldType::Uuid, false),
341 field("sink_uuid", AlgorithmFieldType::Uuid, false),
342 field("flow", AlgorithmFieldType::Float64, false),
343];
344const FLOW_EDGES: &[AlgorithmField] = &[
345 field("edge_uuid", AlgorithmFieldType::Uuid, false),
346 field("source_uuid", AlgorithmFieldType::Uuid, false),
347 field("target_uuid", AlgorithmFieldType::Uuid, false),
348 field("flow", AlgorithmFieldType::Float64, false),
349];
350const COSTED_FLOW: &[AlgorithmField] = &[
351 field("source_uuid", AlgorithmFieldType::Uuid, false),
352 field("sink_uuid", AlgorithmFieldType::Uuid, false),
353 field("flow", AlgorithmFieldType::Float64, false),
354 field("cost", AlgorithmFieldType::Float64, false),
355];
356const COSTED_FLOW_EDGES: &[AlgorithmField] = &[
357 field("edge_uuid", AlgorithmFieldType::Uuid, false),
358 field("source_uuid", AlgorithmFieldType::Uuid, false),
359 field("target_uuid", AlgorithmFieldType::Uuid, false),
360 field("flow", AlgorithmFieldType::Float64, false),
361 field("unit_cost", AlgorithmFieldType::Float64, false),
362 field("flow_cost", AlgorithmFieldType::Float64, false),
363];
364const MIN_CUT: &[AlgorithmField] = &[
365 field("source_uuid", AlgorithmFieldType::Uuid, false),
366 field("sink_uuid", AlgorithmFieldType::Uuid, false),
367 field("cut_value", AlgorithmFieldType::Float64, false),
368];
369const MIN_CUT_EDGES: &[AlgorithmField] = &[
370 field("edge_uuid", AlgorithmFieldType::Uuid, false),
371 field("source_uuid", AlgorithmFieldType::Uuid, false),
372 field("target_uuid", AlgorithmFieldType::Uuid, false),
373 field("capacity", AlgorithmFieldType::Float64, false),
374];
375const CUT: &[AlgorithmField] = &[
376 field("source_uuid", AlgorithmFieldType::Uuid, false),
377 field("target_uuid", AlgorithmFieldType::Uuid, false),
378 field("cut_value", AlgorithmFieldType::Float64, false),
379];
380const EDGE_LIST: &[AlgorithmField] = &[
381 field("edge_uuid", AlgorithmFieldType::Uuid, false),
382 field("source_uuid", AlgorithmFieldType::Uuid, false),
383 field("target_uuid", AlgorithmFieldType::Uuid, false),
384 field("weight", AlgorithmFieldType::Float64, true),
385];
386const STEINER_EDGE_LIST: &[AlgorithmField] = &[
387 field("edge_uuid", AlgorithmFieldType::Uuid, false),
388 field("source_uuid", AlgorithmFieldType::Uuid, false),
389 field("target_uuid", AlgorithmFieldType::Uuid, false),
390 field("weight", AlgorithmFieldType::Float64, false),
391];
392const UNWEIGHTED_EDGE_LIST: &[AlgorithmField] = &[
393 field("edge_uuid", AlgorithmFieldType::Uuid, false),
394 field("source_uuid", AlgorithmFieldType::Uuid, false),
395 field("target_uuid", AlgorithmFieldType::Uuid, false),
396];
397const K_EDGE_LIST: &[AlgorithmField] = &[
398 field("tree_id", AlgorithmFieldType::UInt64, false),
399 field("edge_uuid", AlgorithmFieldType::Uuid, false),
400 field("source_uuid", AlgorithmFieldType::Uuid, false),
401 field("target_uuid", AlgorithmFieldType::Uuid, false),
402 field("weight", AlgorithmFieldType::Float64, false),
403];
404const TRAVERSAL: &[AlgorithmField] = &[
405 field("node_uuid", AlgorithmFieldType::Uuid, false),
406 field("depth", AlgorithmFieldType::UInt64, false),
407 field("order", AlgorithmFieldType::UInt64, false),
408];
409const WALK: &[AlgorithmField] = &[
410 field("start_uuid", AlgorithmFieldType::Uuid, false),
411 field("walk", AlgorithmFieldType::UuidList, false),
412];
413const PAIR: &[AlgorithmField] = &[
414 field("source_uuid", AlgorithmFieldType::Uuid, false),
415 field("target_uuid", AlgorithmFieldType::Uuid, false),
416];
417const NODE_ORDER: &[AlgorithmField] = &[
418 field("node_uuid", AlgorithmFieldType::Uuid, false),
419 field("order", AlgorithmFieldType::UInt64, false),
420];
421const NODE: &[AlgorithmField] = &[field("node_uuid", AlgorithmFieldType::Uuid, false)];
422const NODE_COLOR: &[AlgorithmField] = &[
423 field("node_uuid", AlgorithmFieldType::Uuid, false),
424 field("color", AlgorithmFieldType::UInt64, false),
425];
426const EDGE_COLOR: &[AlgorithmField] = &[
427 field("edge_uuid", AlgorithmFieldType::Uuid, false),
428 field("color", AlgorithmFieldType::UInt64, false),
429];
430const EULER_TRAIL: &[AlgorithmField] = &[
431 field("node_path", AlgorithmFieldType::UuidList, false),
432 field("edge_path", AlgorithmFieldType::UuidList, false),
433];
434const CYCLE: &[AlgorithmField] = &[field("cycle", AlgorithmFieldType::UuidList, false)];
435const COST_PATH: &[AlgorithmField] = &[
436 field("cost", AlgorithmFieldType::Float64, false),
437 field("path", AlgorithmFieldType::UuidList, false),
438];
439const IS_DAG: &[AlgorithmField] = &[field("is_dag", AlgorithmFieldType::Boolean, false)];
440const HAS_EULER_CIRCUIT: &[AlgorithmField] = &[field(
441 "has_euler_circuit",
442 AlgorithmFieldType::Boolean,
443 false,
444)];
445const HAS_EULER_PATH: &[AlgorithmField] =
446 &[field("has_euler_path", AlgorithmFieldType::Boolean, false)];
447const IS_PLANAR: &[AlgorithmField] = &[field("is_planar", AlgorithmFieldType::Boolean, false)];
448const CHROMATIC_NUMBER: &[AlgorithmField] =
449 &[field("chromatic_number", AlgorithmFieldType::UInt64, false)];
450const TRIANGLE_COUNT: &[AlgorithmField] =
451 &[field("triangle_count", AlgorithmFieldType::UInt64, false)];
452const AUTOMORPHISM_COUNT: &[AlgorithmField] = &[field("count", AlgorithmFieldType::UInt64, false)];
453const CONDUCTANCE: &[AlgorithmField] = &[
454 field("partition_id", AlgorithmFieldType::Utf8, false),
455 field("conductance", AlgorithmFieldType::Float64, false),
456];
457const MODULARITY: &[AlgorithmField] = &[field("modularity", AlgorithmFieldType::Float64, false)];
458const TRANSITIVITY: &[AlgorithmField] =
459 &[field("transitivity", AlgorithmFieldType::Float64, false)];
460const TRIAD_CENSUS: &[AlgorithmField] = &[
461 field("triad_type", AlgorithmFieldType::Utf8, false),
462 field("count", AlgorithmFieldType::UInt64, false),
463];
464const DYAD_CENSUS: &[AlgorithmField] = &[
465 field("dyad_type", AlgorithmFieldType::Utf8, false),
466 field("count", AlgorithmFieldType::UInt64, false),
467];
468const EMBEDDING: &[AlgorithmField] = &[
469 field("node_uuid", AlgorithmFieldType::Uuid, false),
470 field("embedding", AlgorithmFieldType::Float32List, false),
471];
472
473impl Algorithm {
474 pub fn parse(verb: AlgorithmVerb, value: &str) -> Result<Self, GfError> {
476 match verb {
477 AlgorithmVerb::Rank => value.parse().map(Self::Rank),
478 AlgorithmVerb::Cluster => value.parse().map(Self::Cluster),
479 AlgorithmVerb::Similar => value.parse().map(Self::Similar),
480 AlgorithmVerb::Paths => value.parse().map(Self::Paths),
481 AlgorithmVerb::Analyze => value.parse().map(Self::Analyze),
482 }
483 }
484
485 #[must_use]
487 pub const fn verb(self) -> AlgorithmVerb {
488 match self {
489 Self::Rank(_) => AlgorithmVerb::Rank,
490 Self::Cluster(_) => AlgorithmVerb::Cluster,
491 Self::Similar(_) => AlgorithmVerb::Similar,
492 Self::Paths(_) => AlgorithmVerb::Paths,
493 Self::Analyze(_) => AlgorithmVerb::Analyze,
494 }
495 }
496
497 #[must_use]
499 pub const fn as_str(self) -> &'static str {
500 match self {
501 Self::Rank(value) => value.as_str(),
502 Self::Cluster(value) => value.as_str(),
503 Self::Similar(value) => value.as_str(),
504 Self::Paths(value) => value.as_str(),
505 Self::Analyze(value) => value.as_str(),
506 }
507 }
508
509 #[must_use]
511 pub const fn result_schema(self) -> AlgorithmResultSchema {
512 match self {
513 Self::Rank(_) => node_schema(NODE_SCORE),
514 Self::Cluster(_) => node_schema(NODE_COMMUNITY),
515 Self::Similar(_) => schema(SIMILARITY),
516 Self::Paths(value) => path_schema(value),
517 Self::Analyze(value) => analyze_schema(value),
518 }
519 }
520}
521
522const fn path_schema(value: PathAlgorithm) -> AlgorithmResultSchema {
523 match value {
524 PathAlgorithm::Yens => schema(RANKED_PATH),
525 PathAlgorithm::MaxFlow => schema(FLOW),
526 PathAlgorithm::MaxFlowEdges => schema(FLOW_EDGES),
527 PathAlgorithm::MinCut => schema(MIN_CUT),
528 PathAlgorithm::MinCutEdges => schema(MIN_CUT_EDGES),
529 PathAlgorithm::MinCostMaxFlow => schema(COSTED_FLOW),
530 PathAlgorithm::MinCostMaxFlowEdges => schema(COSTED_FLOW_EDGES),
531 PathAlgorithm::GomoryHuTree => schema(CUT),
532 PathAlgorithm::MinSteinerTree | PathAlgorithm::PrizeCollectingSteinerTree => {
533 schema(STEINER_EDGE_LIST)
534 }
535 PathAlgorithm::Dfs => schema(TRAVERSAL),
536 PathAlgorithm::RandomWalk => schema(WALK),
537 PathAlgorithm::TransitiveClosure => schema(PAIR),
538 _ => schema(PATH),
539 }
540}
541
542const fn analyze_schema(value: AnalyzeAlgorithm) -> AlgorithmResultSchema {
543 match value {
544 AnalyzeAlgorithm::MinimumKSpanningTree => schema(K_EDGE_LIST),
545 AnalyzeAlgorithm::MinimumSpanningTree
546 | AnalyzeAlgorithm::MaximumSpanningTree
547 | AnalyzeAlgorithm::MaxWeightMatching => schema(EDGE_LIST),
548 AnalyzeAlgorithm::MaxCardinalityMatching
549 | AnalyzeAlgorithm::MaxBipartiteMatching
550 | AnalyzeAlgorithm::Bridges => schema(UNWEIGHTED_EDGE_LIST),
551 AnalyzeAlgorithm::TopologicalSort => schema(NODE_ORDER),
552 AnalyzeAlgorithm::ArticulationPoints => schema(NODE),
553 AnalyzeAlgorithm::IsDag => schema(IS_DAG),
554 AnalyzeAlgorithm::FindCycles => schema(CYCLE),
555 AnalyzeAlgorithm::DagLongestPath | AnalyzeAlgorithm::DagLongestPathWeighted => {
556 schema(COST_PATH)
557 }
558 AnalyzeAlgorithm::NodeColoring | AnalyzeAlgorithm::K1Coloring => schema(NODE_COLOR),
559 AnalyzeAlgorithm::EdgeColoring => schema(EDGE_COLOR),
560 AnalyzeAlgorithm::ChromaticNumber => schema(CHROMATIC_NUMBER),
561 AnalyzeAlgorithm::EulerCircuit | AnalyzeAlgorithm::EulerPath => schema(EULER_TRAIL),
562 AnalyzeAlgorithm::HasEulerCircuit => schema(HAS_EULER_CIRCUIT),
563 AnalyzeAlgorithm::HasEulerPath => schema(HAS_EULER_PATH),
564 AnalyzeAlgorithm::IsPlanar => schema(IS_PLANAR),
565 AnalyzeAlgorithm::TriangleCount => schema(TRIANGLE_COUNT),
566 AnalyzeAlgorithm::Conductance => schema(CONDUCTANCE),
567 AnalyzeAlgorithm::Modularity => schema(MODULARITY),
568 AnalyzeAlgorithm::Transitivity => schema(TRANSITIVITY),
569 AnalyzeAlgorithm::TriadCensus => schema(TRIAD_CENSUS),
570 AnalyzeAlgorithm::DyadCensus => schema(DYAD_CENSUS),
571 AnalyzeAlgorithm::CountAutomorphisms => schema(AUTOMORPHISM_COUNT),
572 AnalyzeAlgorithm::Node2Vec
573 | AnalyzeAlgorithm::GraphSage
574 | AnalyzeAlgorithm::FastRandomProjection
575 | AnalyzeAlgorithm::HashGnn => schema(EMBEDDING),
576 }
577}
578
579fn unknown_algorithm(verb: AlgorithmVerb, value: &str) -> GfError {
580 GfError::Validation(format!(
581 "unknown {} algorithm `{value}`; use a catalogued `by=` value",
582 verb.as_str()
583 ))
584}
585
586#[cfg(test)]
587mod tests {
588 use std::collections::HashSet;
589
590 use super::*;
591
592 #[test]
593 fn every_catalog_is_unique_and_round_trips() {
594 let rank = RankAlgorithm::ALL
595 .iter()
596 .map(|value| value.as_str())
597 .collect::<Vec<_>>();
598 let cluster = ClusterAlgorithm::ALL
599 .iter()
600 .map(|value| value.as_str())
601 .collect::<Vec<_>>();
602 let similar = SimilarAlgorithm::ALL
603 .iter()
604 .map(|value| value.as_str())
605 .collect::<Vec<_>>();
606 let paths = PathAlgorithm::ALL
607 .iter()
608 .map(|value| value.as_str())
609 .collect::<Vec<_>>();
610 let analyze = AnalyzeAlgorithm::ALL
611 .iter()
612 .map(|value| value.as_str())
613 .collect::<Vec<_>>();
614 let catalogs: &[(&[&str], AlgorithmVerb)] = &[
615 (&rank, AlgorithmVerb::Rank),
616 (&cluster, AlgorithmVerb::Cluster),
617 (&similar, AlgorithmVerb::Similar),
618 (&paths, AlgorithmVerb::Paths),
619 (&analyze, AlgorithmVerb::Analyze),
620 ];
621
622 for (names, verb) in catalogs {
623 let unique: HashSet<_> = names.iter().copied().collect();
624 assert_eq!(unique.len(), names.len(), "duplicate in {}", verb.as_str());
625 for name in *names {
626 let parsed = Algorithm::parse(*verb, name).expect("catalog entry must parse");
627 assert_eq!(parsed.as_str(), *name);
628 assert_eq!(parsed.verb(), *verb);
629 assert!(!parsed.result_schema().fields.is_empty());
630 }
631 }
632 }
633
634 #[test]
635 fn rank_alias_has_one_canonical_owner() {
636 let alias: RankAlgorithm = "local_clustering_coefficient".parse().unwrap();
637 assert_eq!(alias, RankAlgorithm::ClusteringCoefficient);
638 assert_eq!(alias.as_str(), "clustering_coefficient");
639 assert!(
640 !RankAlgorithm::ALL
641 .iter()
642 .any(|value| value.as_str() == "local_clustering_coefficient")
643 );
644 }
645
646 #[test]
647 fn invalid_name_is_a_stable_validation_error() {
648 let error = Algorithm::parse(AlgorithmVerb::Similar, "pagerank").unwrap_err();
649 assert!(matches!(error, GfError::Validation(_)));
650 assert_eq!(
651 error.to_string(),
652 "validation error: unknown similar algorithm `pagerank`; use a catalogued `by=` value"
653 );
654 }
655
656 #[test]
657 fn vertical_proof_schemas_are_stable_and_uuid_only() {
658 let cases = [
659 (
660 Algorithm::Rank(RankAlgorithm::Degree),
661 &["node_uuid", "score"][..],
662 ),
663 (
664 Algorithm::Cluster(ClusterAlgorithm::Components),
665 &["node_uuid", "community_id"],
666 ),
667 (
668 Algorithm::Similar(SimilarAlgorithm::NodeSimilarity),
669 &["node1_uuid", "node2_uuid", "similarity"],
670 ),
671 (
672 Algorithm::Paths(PathAlgorithm::Bfs),
673 &["source_uuid", "target_uuid", "cost", "path"],
674 ),
675 (Algorithm::Analyze(AnalyzeAlgorithm::IsDag), &["is_dag"]),
676 ];
677 for (algorithm, names) in cases {
678 let fields = algorithm.result_schema().fields;
679 assert_eq!(
680 fields.iter().map(|field| field.name).collect::<Vec<_>>(),
681 names
682 );
683 assert!(fields.iter().all(|field| {
684 ![
685 "node_id",
686 "edge_id",
687 "src_id",
688 "dst_id",
689 "source_id",
690 "target_id",
691 ]
692 .contains(&field.name)
693 }));
694 }
695 }
696
697 #[test]
698 fn max_cardinality_matching_has_an_unweighted_edge_schema() {
699 let fields = Algorithm::Analyze(AnalyzeAlgorithm::MaxCardinalityMatching)
700 .result_schema()
701 .fields;
702
703 assert_eq!(
704 fields
705 .iter()
706 .map(|field| (field.name, field.data_type, field.nullable))
707 .collect::<Vec<_>>(),
708 [
709 ("edge_uuid", AlgorithmFieldType::Uuid, false),
710 ("source_uuid", AlgorithmFieldType::Uuid, false),
711 ("target_uuid", AlgorithmFieldType::Uuid, false),
712 ]
713 );
714 }
715
716 #[test]
717 fn max_bipartite_matching_has_an_unweighted_edge_schema() {
718 let fields = Algorithm::Analyze(AnalyzeAlgorithm::MaxBipartiteMatching)
719 .result_schema()
720 .fields;
721
722 assert_eq!(
723 fields
724 .iter()
725 .map(|field| (field.name, field.data_type, field.nullable))
726 .collect::<Vec<_>>(),
727 [
728 ("edge_uuid", AlgorithmFieldType::Uuid, false),
729 ("source_uuid", AlgorithmFieldType::Uuid, false),
730 ("target_uuid", AlgorithmFieldType::Uuid, false),
731 ]
732 );
733 }
734
735 #[test]
736 fn max_flow_catalog_values_have_separate_stable_schemas() {
737 let scalar: PathAlgorithm = "max_flow".parse().unwrap();
738 let edges: PathAlgorithm = "max_flow_edges".parse().unwrap();
739
740 assert_eq!(scalar, PathAlgorithm::MaxFlow);
741 assert_eq!(scalar.as_str(), "max_flow");
742 assert_eq!(edges, PathAlgorithm::MaxFlowEdges);
743 assert_eq!(edges.as_str(), "max_flow_edges");
744 assert_eq!(
745 Algorithm::Paths(scalar)
746 .result_schema()
747 .fields
748 .iter()
749 .map(|field| (field.name, field.data_type, field.nullable))
750 .collect::<Vec<_>>(),
751 [
752 ("source_uuid", AlgorithmFieldType::Uuid, false),
753 ("sink_uuid", AlgorithmFieldType::Uuid, false),
754 ("flow", AlgorithmFieldType::Float64, false),
755 ]
756 );
757 assert_eq!(
758 Algorithm::Paths(edges)
759 .result_schema()
760 .fields
761 .iter()
762 .map(|field| (field.name, field.data_type, field.nullable))
763 .collect::<Vec<_>>(),
764 [
765 ("edge_uuid", AlgorithmFieldType::Uuid, false),
766 ("source_uuid", AlgorithmFieldType::Uuid, false),
767 ("target_uuid", AlgorithmFieldType::Uuid, false),
768 ("flow", AlgorithmFieldType::Float64, false),
769 ]
770 );
771 }
772
773 #[test]
774 fn min_cost_flow_catalog_values_have_separate_stable_schemas() {
775 let scalar: PathAlgorithm = "min_cost_max_flow".parse().unwrap();
776 let edges: PathAlgorithm = "min_cost_max_flow_edges".parse().unwrap();
777 assert_eq!(scalar, PathAlgorithm::MinCostMaxFlow);
778 assert_eq!(edges, PathAlgorithm::MinCostMaxFlowEdges);
779 assert_eq!(
780 Algorithm::Paths(scalar)
781 .result_schema()
782 .fields
783 .iter()
784 .map(|field| field.name)
785 .collect::<Vec<_>>(),
786 ["source_uuid", "sink_uuid", "flow", "cost"]
787 );
788 assert_eq!(
789 Algorithm::Paths(edges)
790 .result_schema()
791 .fields
792 .iter()
793 .map(|field| field.name)
794 .collect::<Vec<_>>(),
795 [
796 "edge_uuid",
797 "source_uuid",
798 "target_uuid",
799 "flow",
800 "unit_cost",
801 "flow_cost",
802 ]
803 );
804 assert!(
805 Algorithm::Paths(edges)
806 .result_schema()
807 .fields
808 .iter()
809 .all(|field| !field.nullable)
810 );
811 }
812
813 #[test]
814 fn min_cut_catalog_values_have_distinct_stable_uuid_schemas() {
815 let scalar: PathAlgorithm = "min_cut".parse().unwrap();
816 let edges: PathAlgorithm = "min_cut_edges".parse().unwrap();
817
818 assert_eq!(scalar, PathAlgorithm::MinCut);
819 assert_eq!(scalar.as_str(), "min_cut");
820 assert_eq!(scalar.to_string(), "min_cut");
821 assert_eq!(edges, PathAlgorithm::MinCutEdges);
822 assert_eq!(edges.as_str(), "min_cut_edges");
823 assert_eq!(edges.to_string(), "min_cut_edges");
824
825 let scalar_schema = Algorithm::Paths(scalar).result_schema();
826 assert_eq!(
827 scalar_schema
828 .fields
829 .iter()
830 .map(|field| (field.name, field.data_type, field.nullable))
831 .collect::<Vec<_>>(),
832 [
833 ("source_uuid", AlgorithmFieldType::Uuid, false),
834 ("sink_uuid", AlgorithmFieldType::Uuid, false),
835 ("cut_value", AlgorithmFieldType::Float64, false),
836 ]
837 );
838
839 let edge_schema = Algorithm::Paths(edges).result_schema();
840 assert_eq!(
841 edge_schema
842 .fields
843 .iter()
844 .map(|field| (field.name, field.data_type, field.nullable))
845 .collect::<Vec<_>>(),
846 [
847 ("edge_uuid", AlgorithmFieldType::Uuid, false),
848 ("source_uuid", AlgorithmFieldType::Uuid, false),
849 ("target_uuid", AlgorithmFieldType::Uuid, false),
850 ("capacity", AlgorithmFieldType::Float64, false),
851 ]
852 );
853 assert_ne!(scalar_schema, edge_schema);
854 assert!(scalar_schema.fields.iter().all(|field| {
855 !["node_id", "edge_id", "source_id", "sink_id", "target_id"].contains(&field.name)
856 }));
857 assert!(edge_schema.fields.iter().all(|field| {
858 !["node_id", "edge_id", "source_id", "sink_id", "target_id"].contains(&field.name)
859 }));
860 }
861
862 #[test]
863 fn euler_constructions_share_aligned_non_null_identity_schema() {
864 for algorithm in [AnalyzeAlgorithm::EulerCircuit, AnalyzeAlgorithm::EulerPath] {
865 assert_eq!(
866 Algorithm::Analyze(algorithm)
867 .result_schema()
868 .fields
869 .iter()
870 .map(|field| (field.name, field.data_type, field.nullable))
871 .collect::<Vec<_>>(),
872 [
873 ("node_path", AlgorithmFieldType::UuidList, false),
874 ("edge_path", AlgorithmFieldType::UuidList, false),
875 ]
876 );
877 }
878 }
879
880 #[test]
881 fn steiner_algorithms_share_dedicated_non_null_weighted_edge_schema() {
882 let ordinary_edge_list =
883 Algorithm::Analyze(AnalyzeAlgorithm::MinimumSpanningTree).result_schema();
884 assert_eq!(
885 ordinary_edge_list
886 .fields
887 .iter()
888 .map(|field| (field.name, field.data_type, field.nullable))
889 .collect::<Vec<_>>(),
890 [
891 ("edge_uuid", AlgorithmFieldType::Uuid, false),
892 ("source_uuid", AlgorithmFieldType::Uuid, false),
893 ("target_uuid", AlgorithmFieldType::Uuid, false),
894 ("weight", AlgorithmFieldType::Float64, true),
895 ]
896 );
897 assert!(!ordinary_edge_list.includes_node_properties);
898 for algorithm in [
899 PathAlgorithm::MinSteinerTree,
900 PathAlgorithm::PrizeCollectingSteinerTree,
901 ] {
902 let steiner = Algorithm::Paths(algorithm).result_schema();
903 assert_eq!(
904 steiner
905 .fields
906 .iter()
907 .map(|field| (field.name, field.data_type, field.nullable))
908 .collect::<Vec<_>>(),
909 [
910 ("edge_uuid", AlgorithmFieldType::Uuid, false),
911 ("source_uuid", AlgorithmFieldType::Uuid, false),
912 ("target_uuid", AlgorithmFieldType::Uuid, false),
913 ("weight", AlgorithmFieldType::Float64, false),
914 ]
915 );
916 assert!(!steiner.includes_node_properties);
917 assert_ne!(steiner, ordinary_edge_list);
918 }
919 }
920}