Skip to main content

astraea_server/
handler.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use astraea_core::traits::{GraphOps, VectorIndex};
5use astraea_core::types::*;
6use astraea_query::executor::Executor;
7
8use crate::protocol::{Request, Response};
9
10/// Handles incoming requests by dispatching to the graph engine.
11pub struct RequestHandler {
12    graph: Arc<dyn GraphOps>,
13    executor: Executor,
14    vector_index: Option<Arc<dyn VectorIndex>>,
15}
16
17impl RequestHandler {
18    pub fn new(graph: Arc<dyn GraphOps>, vector_index: Option<Arc<dyn VectorIndex>>) -> Self {
19        let executor = Executor::new(Arc::clone(&graph));
20        Self {
21            graph,
22            executor,
23            vector_index,
24        }
25    }
26
27    /// Process a single request and return a response.
28    pub fn handle(&self, request: Request) -> Response {
29        match request {
30            Request::CreateNode {
31                labels,
32                properties,
33                embedding,
34            } => match self.graph.create_node(labels, properties, embedding) {
35                Ok(id) => Response::ok(serde_json::json!({"node_id": id.0})),
36                Err(e) => Response::error(e.to_string()),
37            },
38
39            Request::CreateEdge {
40                source,
41                target,
42                edge_type,
43                properties,
44                weight,
45                valid_from,
46                valid_to,
47            } => {
48                match self.graph.create_edge(
49                    NodeId(source),
50                    NodeId(target),
51                    edge_type,
52                    properties,
53                    weight,
54                    valid_from,
55                    valid_to,
56                ) {
57                    Ok(id) => Response::ok(serde_json::json!({"edge_id": id.0})),
58                    Err(e) => Response::error(e.to_string()),
59                }
60            }
61
62            Request::GetNode { id } => match self.graph.get_node(NodeId(id)) {
63                Ok(Some(node)) => Response::ok(serde_json::json!({
64                    "id": node.id.0,
65                    "labels": node.labels,
66                    "properties": node.properties,
67                    "has_embedding": node.embedding.is_some(),
68                })),
69                Ok(None) => Response::error(format!("node {id} not found")),
70                Err(e) => Response::error(e.to_string()),
71            },
72
73            Request::GetEdge { id } => match self.graph.get_edge(EdgeId(id)) {
74                Ok(Some(edge)) => Response::ok(serde_json::json!({
75                    "id": edge.id.0,
76                    "source": edge.source.0,
77                    "target": edge.target.0,
78                    "edge_type": edge.edge_type,
79                    "properties": edge.properties,
80                    "weight": edge.weight,
81                    "valid_from": edge.validity.valid_from,
82                    "valid_to": edge.validity.valid_to,
83                })),
84                Ok(None) => Response::error(format!("edge {id} not found")),
85                Err(e) => Response::error(e.to_string()),
86            },
87
88            Request::UpdateNode { id, properties } => {
89                match self.graph.update_node(NodeId(id), properties) {
90                    Ok(()) => Response::ok(serde_json::json!({"updated": true})),
91                    Err(e) => Response::error(e.to_string()),
92                }
93            }
94
95            Request::UpdateEdge { id, properties } => {
96                match self.graph.update_edge(EdgeId(id), properties) {
97                    Ok(()) => Response::ok(serde_json::json!({"updated": true})),
98                    Err(e) => Response::error(e.to_string()),
99                }
100            }
101
102            Request::DeleteNode { id } => match self.graph.delete_node(NodeId(id)) {
103                Ok(()) => Response::ok(serde_json::json!({"deleted": true})),
104                Err(e) => Response::error(e.to_string()),
105            },
106
107            Request::DeleteEdge { id } => match self.graph.delete_edge(EdgeId(id)) {
108                Ok(()) => Response::ok(serde_json::json!({"deleted": true})),
109                Err(e) => Response::error(e.to_string()),
110            },
111
112            Request::Neighbors {
113                id,
114                direction,
115                edge_type,
116            } => {
117                let dir = match direction.as_str() {
118                    "incoming" => Direction::Incoming,
119                    "both" => Direction::Both,
120                    _ => Direction::Outgoing,
121                };
122
123                let result = if let Some(et) = edge_type {
124                    self.graph.neighbors_filtered(NodeId(id), dir, &et)
125                } else {
126                    self.graph.neighbors(NodeId(id), dir)
127                };
128
129                match result {
130                    Ok(neighbors) => {
131                        let items: Vec<serde_json::Value> = neighbors
132                            .into_iter()
133                            .map(|(eid, nid)| {
134                                serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
135                            })
136                            .collect();
137                        Response::ok(serde_json::json!({"neighbors": items}))
138                    }
139                    Err(e) => Response::error(e.to_string()),
140                }
141            }
142
143            Request::Bfs { start, max_depth } => match self.graph.bfs(NodeId(start), max_depth) {
144                Ok(nodes) => {
145                    let items: Vec<serde_json::Value> = nodes
146                        .into_iter()
147                        .map(|(nid, depth)| serde_json::json!({"node_id": nid.0, "depth": depth}))
148                        .collect();
149                    Response::ok(serde_json::json!({"nodes": items}))
150                }
151                Err(e) => Response::error(e.to_string()),
152            },
153
154            Request::ShortestPath { from, to, weighted } => {
155                if weighted {
156                    match self.graph.shortest_path_weighted(NodeId(from), NodeId(to)) {
157                        Ok(Some((path, cost))) => {
158                            let node_ids: Vec<u64> =
159                                path.nodes().into_iter().map(|n| n.0).collect();
160                            Response::ok(
161                                serde_json::json!({"path": node_ids, "cost": cost, "length": path.len()}),
162                            )
163                        }
164                        Ok(None) => Response::ok(serde_json::json!({"path": null})),
165                        Err(e) => Response::error(e.to_string()),
166                    }
167                } else {
168                    match self.graph.shortest_path(NodeId(from), NodeId(to)) {
169                        Ok(Some(path)) => {
170                            let node_ids: Vec<u64> =
171                                path.nodes().into_iter().map(|n| n.0).collect();
172                            Response::ok(
173                                serde_json::json!({"path": node_ids, "length": path.len()}),
174                            )
175                        }
176                        Ok(None) => Response::ok(serde_json::json!({"path": null})),
177                        Err(e) => Response::error(e.to_string()),
178                    }
179                }
180            }
181
182            Request::HybridSearch {
183                anchor,
184                query,
185                max_hops,
186                k,
187                alpha,
188            } => {
189                match self
190                    .graph
191                    .hybrid_search(NodeId(anchor), &query, max_hops, k, alpha)
192                {
193                    Ok(results) => {
194                        let items: Vec<serde_json::Value> = results
195                            .into_iter()
196                            .map(|(nid, score)| {
197                                serde_json::json!({"node_id": nid.0, "score": score})
198                            })
199                            .collect();
200                        Response::ok(serde_json::json!({"results": items}))
201                    }
202                    Err(e) => Response::error(e.to_string()),
203                }
204            }
205
206            Request::SemanticNeighbors {
207                id,
208                concept,
209                direction,
210                k,
211            } => {
212                let dir = match direction.as_str() {
213                    "incoming" => Direction::Incoming,
214                    "both" => Direction::Both,
215                    _ => Direction::Outgoing,
216                };
217                match self.graph.semantic_neighbors(NodeId(id), &concept, dir, k) {
218                    Ok(results) => {
219                        let items: Vec<serde_json::Value> = results
220                            .into_iter()
221                            .map(|(nid, dist)| {
222                                serde_json::json!({"node_id": nid.0, "distance": dist})
223                            })
224                            .collect();
225                        Response::ok(serde_json::json!({"results": items}))
226                    }
227                    Err(e) => Response::error(e.to_string()),
228                }
229            }
230
231            Request::SemanticWalk {
232                start,
233                concept,
234                max_hops,
235            } => match self.graph.semantic_walk(NodeId(start), &concept, max_hops) {
236                Ok(path) => {
237                    let items: Vec<serde_json::Value> = path
238                        .into_iter()
239                        .map(|(nid, dist)| serde_json::json!({"node_id": nid.0, "distance": dist}))
240                        .collect();
241                    Response::ok(serde_json::json!({"path": items}))
242                }
243                Err(e) => Response::error(e.to_string()),
244            },
245
246            Request::VectorSearch { query, k } => {
247                match &self.vector_index {
248                    Some(vi) => match vi.search(&query, k) {
249                        Ok(results) => {
250                            // astraeadb-issues.md #6: `distance` is the
251                            // canonical field name on both transports now
252                            // (the gRPC proto's `VectorSearchResult.score`
253                            // was renamed to `distance` to match). The TCP
254                            // JSON wire format has always used `distance`;
255                            // it also carries a redundant legacy `score`
256                            // key (same value) so existing go/java/python
257                            // JSON clients — which still parse `score` from
258                            // this transport (see e.g.
259                            // go/astraeadb/json_client_test.go,
260                            // python/tests/test_json_client.py,
261                            // java JsonClient.java) — keep working
262                            // unmigrated. New/internal consumers should
263                            // read `distance`; `score` may be dropped once
264                            // those bindings move to `distance` too.
265                            let items: Vec<serde_json::Value> = results
266                                .into_iter()
267                                .map(|r| {
268                                    serde_json::json!({
269                                        "node_id": r.node_id.0,
270                                        "distance": r.distance,
271                                        "score": r.distance,
272                                    })
273                                })
274                                .collect();
275                            Response::ok(serde_json::json!({"results": items}))
276                        }
277                        Err(e) => Response::error(e.to_string()),
278                    },
279                    None => Response::error("vector index not configured"),
280                }
281            }
282
283            Request::Query { gql } => {
284                // Parse the GQL string into an AST.
285                let stmt = match astraea_query::parse(&gql) {
286                    Ok(s) => s,
287                    Err(e) => return Response::error(format!("parse error: {e}")),
288                };
289
290                // Execute the AST against the graph.
291                match self.executor.execute(stmt) {
292                    Ok(result) => Response::ok(serde_json::json!({
293                        "columns": result.columns,
294                        "rows": result.rows,
295                        "stats": {
296                            "nodes_created": result.stats.nodes_created,
297                            "edges_created": result.stats.edges_created,
298                            "nodes_deleted": result.stats.nodes_deleted,
299                            "edges_deleted": result.stats.edges_deleted,
300                        },
301                    })),
302                    Err(e) => Response::error(format!("execution error: {e}")),
303                }
304            }
305
306            Request::ExtractSubgraph {
307                center,
308                hops,
309                max_nodes,
310                format,
311            } => {
312                let text_format = parse_text_format(&format);
313                match astraea_rag::extract_subgraph(&*self.graph, NodeId(center), hops, max_nodes) {
314                    Ok(subgraph) => {
315                        let text = astraea_rag::linearize_subgraph(&subgraph, text_format);
316                        let tokens = astraea_rag::estimate_tokens(&text);
317                        Response::ok(serde_json::json!({
318                            "text": text,
319                            "nodes_count": subgraph.nodes.len(),
320                            "edges_count": subgraph.edges.len(),
321                            "estimated_tokens": tokens,
322                        }))
323                    }
324                    Err(e) => Response::error(e.to_string()),
325                }
326            }
327
328            Request::GraphRag {
329                question,
330                question_embedding,
331                anchor,
332                hops,
333                max_nodes,
334                format,
335            } => {
336                let text_format = parse_text_format(&format);
337
338                let center = if let Some(a) = anchor {
339                    NodeId(a)
340                } else if let Some(emb) = &question_embedding {
341                    match &self.vector_index {
342                        Some(vi) => match vi.search(emb, 1) {
343                            Ok(results) if !results.is_empty() => results[0].node_id,
344                            Ok(_) => return Response::error("no matching nodes found"),
345                            Err(e) => return Response::error(e.to_string()),
346                        },
347                        None => {
348                            return Response::error(
349                                "vector index not configured and no anchor provided",
350                            );
351                        }
352                    }
353                } else {
354                    return Response::error("either anchor or question_embedding must be provided");
355                };
356
357                match astraea_rag::extract_subgraph(&*self.graph, center, hops, max_nodes) {
358                    Ok(subgraph) => {
359                        let context = astraea_rag::linearize_subgraph(&subgraph, text_format);
360                        let tokens = astraea_rag::estimate_tokens(&context);
361                        Response::ok(serde_json::json!({
362                            "anchor_node_id": center.0,
363                            "context": context,
364                            "question": question,
365                            "nodes_in_context": subgraph.nodes.len(),
366                            "edges_in_context": subgraph.edges.len(),
367                            "estimated_tokens": tokens,
368                            "note": "LLM completion requires server-side provider configuration. Use the context with your own LLM."
369                        }))
370                    }
371                    Err(e) => Response::error(e.to_string()),
372                }
373            }
374
375            Request::NeighborsAt {
376                id,
377                direction,
378                timestamp,
379                edge_type,
380            } => {
381                let dir = match direction.as_str() {
382                    "incoming" => Direction::Incoming,
383                    "both" => Direction::Both,
384                    _ => Direction::Outgoing,
385                };
386
387                match self.graph.neighbors_at(NodeId(id), dir, timestamp) {
388                    Ok(neighbors) => {
389                        let items: Vec<serde_json::Value> = if let Some(et) = edge_type {
390                            // Post-filter by edge type (get the edge to check type)
391                            neighbors
392                                .into_iter()
393                                .filter(|(eid, _)| {
394                                    self.graph
395                                        .get_edge(*eid)
396                                        .ok()
397                                        .flatten()
398                                        .is_some_and(|e| e.edge_type == et)
399                                })
400                                .map(|(eid, nid)| {
401                                    serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
402                                })
403                                .collect()
404                        } else {
405                            neighbors
406                                .into_iter()
407                                .map(|(eid, nid)| {
408                                    serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
409                                })
410                                .collect()
411                        };
412                        Response::ok(
413                            serde_json::json!({"neighbors": items, "timestamp": timestamp}),
414                        )
415                    }
416                    Err(e) => Response::error(e.to_string()),
417                }
418            }
419
420            Request::BfsAt {
421                start,
422                max_depth,
423                timestamp,
424            } => match self.graph.bfs_at(NodeId(start), max_depth, timestamp) {
425                Ok(nodes) => {
426                    let items: Vec<serde_json::Value> = nodes
427                        .into_iter()
428                        .map(|(nid, depth)| serde_json::json!({"node_id": nid.0, "depth": depth}))
429                        .collect();
430                    Response::ok(serde_json::json!({"nodes": items, "timestamp": timestamp}))
431                }
432                Err(e) => Response::error(e.to_string()),
433            },
434
435            Request::ShortestPathAt {
436                from,
437                to,
438                timestamp,
439                weighted,
440            } => {
441                if weighted {
442                    match self
443                        .graph
444                        .shortest_path_weighted_at(NodeId(from), NodeId(to), timestamp)
445                    {
446                        Ok(Some((path, cost))) => {
447                            let node_ids: Vec<u64> =
448                                path.nodes().into_iter().map(|n| n.0).collect();
449                            Response::ok(serde_json::json!({
450                                "path": node_ids, "cost": cost, "length": path.len(),
451                                "timestamp": timestamp
452                            }))
453                        }
454                        Ok(None) => {
455                            Response::ok(serde_json::json!({"path": null, "timestamp": timestamp}))
456                        }
457                        Err(e) => Response::error(e.to_string()),
458                    }
459                } else {
460                    match self
461                        .graph
462                        .shortest_path_at(NodeId(from), NodeId(to), timestamp)
463                    {
464                        Ok(Some(path)) => {
465                            let node_ids: Vec<u64> =
466                                path.nodes().into_iter().map(|n| n.0).collect();
467                            Response::ok(serde_json::json!({
468                                "path": node_ids, "length": path.len(),
469                                "timestamp": timestamp
470                            }))
471                        }
472                        Ok(None) => {
473                            Response::ok(serde_json::json!({"path": null, "timestamp": timestamp}))
474                        }
475                        Err(e) => Response::error(e.to_string()),
476                    }
477                }
478            }
479
480            Request::Dfs { start, max_depth } => match self.graph.dfs(NodeId(start), max_depth) {
481                Ok(nodes) => {
482                    let ids: Vec<u64> = nodes.into_iter().map(|n| n.0).collect();
483                    Response::ok(serde_json::json!({"nodes": ids}))
484                }
485                Err(e) => Response::error(e.to_string()),
486            },
487
488            Request::DfsAt {
489                start,
490                max_depth,
491                timestamp,
492            } => {
493                // DFS at a point in time using neighbors_at iteratively.
494                match dfs_at_impl(&*self.graph, NodeId(start), max_depth, timestamp) {
495                    Ok(nodes) => {
496                        let ids: Vec<u64> = nodes.into_iter().map(|n| n.0).collect();
497                        Response::ok(serde_json::json!({"nodes": ids, "timestamp": timestamp}))
498                    }
499                    Err(e) => Response::error(e.to_string()),
500                }
501            }
502
503            Request::FindByLabel { label } => match self.graph.find_by_label(&label) {
504                Ok(ids) => {
505                    let node_ids: Vec<u64> = ids.into_iter().map(|n| n.0).collect();
506                    Response::ok(serde_json::json!({"node_ids": node_ids}))
507                }
508                Err(e) => Response::error(e.to_string()),
509            },
510
511            // astraeadb-issues.md #4. Bulk-delete every node with the
512            // given label (and its edges). Previously clients had to
513            // FindByLabel -> per-node DeleteNode, which round-tripped
514            // once per match.
515            Request::DeleteByLabel { label } => match self.graph.find_by_label(&label) {
516                Ok(ids) => {
517                    let total = ids.len();
518                    let mut deleted = 0u64;
519                    for id in ids {
520                        match self.graph.delete_node(id) {
521                            Ok(()) => deleted += 1,
522                            Err(e) => {
523                                return Response::error(format!(
524                                    "DeleteByLabel '{label}': deleted {deleted}/{total} before error at {id}: {e}"
525                                ));
526                            }
527                        }
528                    }
529                    Response::ok(serde_json::json!({ "deleted": deleted }))
530                }
531                Err(e) => Response::error(e.to_string()),
532            },
533
534            // astraeadb-issues.md #3. Find all edges whose edge_type matches
535            // the given string.
536            Request::FindEdgeByType { edge_type } => {
537                match self.graph.find_edges_by_type(&edge_type) {
538                    Ok(triples) => {
539                        let edges: Vec<serde_json::Value> = triples
540                            .into_iter()
541                            .map(|(eid, src, tgt)| {
542                                serde_json::json!({
543                                    "edge_id": eid.0,
544                                    "source": src.0,
545                                    "target": tgt.0,
546                                })
547                            })
548                            .collect();
549                        Response::ok(serde_json::json!({ "edges": edges }))
550                    }
551                    Err(e) => Response::error(e.to_string()),
552                }
553            }
554
555            Request::RunPageRank {
556                nodes,
557                damping,
558                max_iterations,
559                tolerance,
560            } => {
561                let node_ids = match resolve_node_ids(&*self.graph, nodes) {
562                    Ok(ids) => ids,
563                    Err(e) => return Response::error(e.to_string()),
564                };
565
566                let config = astraea_algorithms::pagerank::PageRankConfig {
567                    damping,
568                    max_iterations,
569                    tolerance,
570                };
571
572                match astraea_algorithms::pagerank::pagerank(&*self.graph, &node_ids, &config) {
573                    Ok(scores) => {
574                        let map: HashMap<String, f64> = scores
575                            .into_iter()
576                            .map(|(k, v)| (k.0.to_string(), v))
577                            .collect();
578                        Response::ok(serde_json::json!({"scores": map}))
579                    }
580                    Err(e) => Response::error(e.to_string()),
581                }
582            }
583
584            Request::RunLouvain { nodes } => {
585                let node_ids = match resolve_node_ids(&*self.graph, nodes) {
586                    Ok(ids) => ids,
587                    Err(e) => return Response::error(e.to_string()),
588                };
589
590                match astraea_algorithms::community::louvain(&*self.graph, &node_ids) {
591                    Ok(communities) => {
592                        let map: HashMap<String, usize> = communities
593                            .into_iter()
594                            .map(|(k, v)| (k.0.to_string(), v))
595                            .collect();
596                        let num_communities = map.values().collect::<HashSet<_>>().len();
597                        Response::ok(serde_json::json!({
598                            "communities": map,
599                            "num_communities": num_communities,
600                        }))
601                    }
602                    Err(e) => Response::error(e.to_string()),
603                }
604            }
605
606            Request::RunConnectedComponents { nodes, strong } => {
607                let node_ids = match resolve_node_ids(&*self.graph, nodes) {
608                    Ok(ids) => ids,
609                    Err(e) => return Response::error(e.to_string()),
610                };
611
612                let result = if strong {
613                    astraea_algorithms::components::strongly_connected_components(
614                        &*self.graph,
615                        &node_ids,
616                    )
617                } else {
618                    astraea_algorithms::components::connected_components(&*self.graph, &node_ids)
619                };
620
621                match result {
622                    Ok(components) => {
623                        let count = components.len();
624                        let comps: Vec<Vec<u64>> = components
625                            .into_iter()
626                            .map(|c| c.into_iter().map(|n| n.0).collect())
627                            .collect();
628                        Response::ok(serde_json::json!({
629                            "components": comps,
630                            "count": count,
631                        }))
632                    }
633                    Err(e) => Response::error(e.to_string()),
634                }
635            }
636
637            Request::RunDegreeCentrality { nodes, direction } => {
638                let node_ids = match resolve_node_ids(&*self.graph, nodes) {
639                    Ok(ids) => ids,
640                    Err(e) => return Response::error(e.to_string()),
641                };
642
643                let dir = parse_direction(&direction);
644
645                match astraea_algorithms::centrality::degree_centrality(
646                    &*self.graph,
647                    &node_ids,
648                    dir,
649                ) {
650                    Ok(scores) => {
651                        let map: HashMap<String, f64> = scores
652                            .into_iter()
653                            .map(|(k, v)| (k.0.to_string(), v))
654                            .collect();
655                        Response::ok(serde_json::json!({"scores": map}))
656                    }
657                    Err(e) => Response::error(e.to_string()),
658                }
659            }
660
661            Request::RunBetweennessCentrality { nodes } => {
662                let node_ids = match resolve_node_ids(&*self.graph, nodes) {
663                    Ok(ids) => ids,
664                    Err(e) => return Response::error(e.to_string()),
665                };
666
667                match astraea_algorithms::centrality::betweenness_centrality(
668                    &*self.graph,
669                    &node_ids,
670                ) {
671                    Ok(scores) => {
672                        let map: HashMap<String, f64> = scores
673                            .into_iter()
674                            .map(|(k, v)| (k.0.to_string(), v))
675                            .collect();
676                        Response::ok(serde_json::json!({"scores": map}))
677                    }
678                    Err(e) => Response::error(e.to_string()),
679                }
680            }
681
682            Request::GraphStats => {
683                match collect_graph_stats(&*self.graph, self.vector_index.as_deref()) {
684                    Ok(stats) => Response::ok(stats),
685                    Err(e) => Response::error(e.to_string()),
686                }
687            }
688
689            Request::GetSubgraph {
690                center,
691                hops,
692                max_nodes,
693            } => {
694                match astraea_rag::extract_subgraph(&*self.graph, NodeId(center), hops, max_nodes) {
695                    Ok(subgraph) => {
696                        let nodes: Vec<serde_json::Value> = subgraph
697                            .nodes
698                            .iter()
699                            .map(|n| {
700                                serde_json::json!({
701                                    "id": n.id.0,
702                                    "labels": n.labels,
703                                    "properties": n.properties,
704                                    "has_embedding": n.embedding.is_some(),
705                                })
706                            })
707                            .collect();
708                        let edges: Vec<serde_json::Value> = subgraph
709                            .edges
710                            .iter()
711                            .map(|e| {
712                                serde_json::json!({
713                                    "id": e.id.0,
714                                    "source": e.source.0,
715                                    "target": e.target.0,
716                                    "edge_type": e.edge_type,
717                                    "properties": e.properties,
718                                    "weight": e.weight,
719                                    "valid_from": e.validity.valid_from,
720                                    "valid_to": e.validity.valid_to,
721                                })
722                            })
723                            .collect();
724                        Response::ok(serde_json::json!({
725                            "nodes": nodes,
726                            "edges": edges,
727                        }))
728                    }
729                    Err(e) => Response::error(e.to_string()),
730                }
731            }
732
733            Request::Ping => {
734                // Expose the vector-index dimension and metric here (in
735                // addition to GraphStats) so clients can check before
736                // attempting an insert with the wrong-size embedding.
737                // astraeadb-issues.md #7.
738                let mut payload = serde_json::json!({
739                    "pong": true,
740                    "version": env!("CARGO_PKG_VERSION"),
741                });
742                if let Some(vi) = self.vector_index.as_deref() {
743                    payload["vector_dim"] = serde_json::json!(vi.dimension());
744                    payload["vector_metric"] = serde_json::json!(format!("{:?}", vi.metric()));
745                }
746                Response::ok(payload)
747            }
748        }
749    }
750}
751
752/// Parse a text format string into a `TextFormat` enum value.
753fn parse_text_format(s: &str) -> astraea_rag::TextFormat {
754    match s.to_lowercase().as_str() {
755        "prose" => astraea_rag::TextFormat::Prose,
756        "triples" => astraea_rag::TextFormat::Triples,
757        "json" => astraea_rag::TextFormat::Json,
758        _ => astraea_rag::TextFormat::Structured,
759    }
760}
761
762/// Parse a direction string into a `Direction` enum value.
763fn parse_direction(s: &str) -> Direction {
764    match s {
765        "incoming" => Direction::Incoming,
766        "both" => Direction::Both,
767        _ => Direction::Outgoing,
768    }
769}
770
771/// Resolve node IDs for algorithm operations.
772///
773/// If `nodes` is `Some`, convert the provided IDs.
774/// If `None`, scan the graph for all existing node IDs (by probing IDs 1..10_000).
775fn resolve_node_ids(
776    graph: &dyn GraphOps,
777    nodes: Option<Vec<u64>>,
778) -> astraea_core::error::Result<Vec<NodeId>> {
779    if let Some(ids) = nodes {
780        Ok(ids.into_iter().map(NodeId).collect())
781    } else {
782        // Scan for existing nodes. This is a simple approach that probes
783        // sequential IDs. For large graphs, a dedicated scan method would be better.
784        let mut found = Vec::new();
785        for id in 1..10_000u64 {
786            if graph.get_node(NodeId(id))?.is_some() {
787                found.push(NodeId(id));
788            }
789        }
790        Ok(found)
791    }
792}
793
794/// Collect graph statistics by scanning node/edge ID space.
795fn collect_graph_stats(
796    graph: &dyn GraphOps,
797    vector_index: Option<&dyn VectorIndex>,
798) -> astraea_core::error::Result<serde_json::Value> {
799    let mut total_nodes: u64 = 0;
800    let mut total_edges: u64 = 0;
801    let mut label_counts: HashMap<String, u64> = HashMap::new();
802    let mut edge_type_counts: HashMap<String, u64> = HashMap::new();
803
804    // Scan nodes
805    for id in 1..10_000u64 {
806        if let Some(node) = graph.get_node(NodeId(id))? {
807            total_nodes += 1;
808            for label in &node.labels {
809                *label_counts.entry(label.clone()).or_insert(0) += 1;
810            }
811        }
812    }
813
814    // Scan edges
815    for id in 1..10_000u64 {
816        if let Some(edge) = graph.get_edge(EdgeId(id))? {
817            total_edges += 1;
818            *edge_type_counts.entry(edge.edge_type.clone()).or_insert(0) += 1;
819        }
820    }
821
822    let mut stats = serde_json::json!({
823        "total_nodes": total_nodes,
824        "total_edges": total_edges,
825        "labels": label_counts,
826        "edge_types": edge_type_counts,
827    });
828
829    if let Some(vi) = vector_index {
830        stats["vector_index"] = serde_json::json!({
831            "dimension": vi.dimension(),
832            "metric": format!("{:?}", vi.metric()),
833            "size": vi.len(),
834        });
835    }
836
837    Ok(stats)
838}
839
840/// DFS traversal at a specific point in time (temporal).
841///
842/// Uses `neighbors_at` to only follow edges valid at the given timestamp.
843fn dfs_at_impl(
844    graph: &dyn GraphOps,
845    start: NodeId,
846    max_depth: usize,
847    timestamp: i64,
848) -> astraea_core::error::Result<Vec<NodeId>> {
849    let mut visited = HashSet::new();
850    let mut result = Vec::new();
851    let mut stack: Vec<(NodeId, usize)> = vec![(start, 0)];
852
853    while let Some((node, depth)) = stack.pop() {
854        if !visited.insert(node) {
855            continue;
856        }
857        result.push(node);
858
859        if depth < max_depth {
860            let neighbors = graph.neighbors_at(node, Direction::Outgoing, timestamp)?;
861            for (_eid, nid) in neighbors {
862                if !visited.contains(&nid) {
863                    stack.push((nid, depth + 1));
864                }
865            }
866        }
867    }
868
869    Ok(result)
870}
871
872#[cfg(test)]
873mod tests {
874    use super::*;
875    use astraea_graph::Graph;
876    use astraea_graph::test_utils::InMemoryStorage;
877    use astraea_vector::HnswVectorIndex;
878
879    /// Helper: create a handler with a vector index.
880    fn handler_with_vector_index(dim: usize) -> (RequestHandler, Arc<dyn VectorIndex>) {
881        let storage = InMemoryStorage::new();
882        let vector_index: Arc<dyn VectorIndex> =
883            Arc::new(HnswVectorIndex::new(dim, DistanceMetric::Cosine));
884        let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
885        let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
886        (handler, vector_index)
887    }
888
889    /// Helper: create a handler without a vector index.
890    fn handler_without_vector_index() -> RequestHandler {
891        let storage = InMemoryStorage::new();
892        let graph = Graph::new(Box::new(storage));
893        RequestHandler::new(Arc::new(graph), None)
894    }
895
896    #[test]
897    fn test_vector_search_basic() {
898        let (handler, vi) = handler_with_vector_index(3);
899
900        // Insert some vectors directly into the index for testing.
901        vi.insert(NodeId(100), &[1.0, 0.0, 0.0]).unwrap();
902        vi.insert(NodeId(200), &[0.0, 1.0, 0.0]).unwrap();
903        vi.insert(NodeId(300), &[0.0, 0.0, 1.0]).unwrap();
904
905        let resp = handler.handle(Request::VectorSearch {
906            query: vec![1.0, 0.0, 0.0],
907            k: 2,
908        });
909
910        match resp {
911            Response::Ok { data } => {
912                let results = data.get("results").unwrap().as_array().unwrap();
913                assert_eq!(results.len(), 2);
914                // The closest should be node 100 (exact match).
915                assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 100);
916            }
917            Response::Error { message } => panic!("expected Ok, got Error: {}", message),
918        }
919    }
920
921    #[test]
922    fn test_vector_search_no_index() {
923        let handler = handler_without_vector_index();
924
925        let resp = handler.handle(Request::VectorSearch {
926            query: vec![1.0, 0.0, 0.0],
927            k: 5,
928        });
929
930        match resp {
931            Response::Error { message } => {
932                assert_eq!(message, "vector index not configured");
933            }
934            Response::Ok { .. } => panic!("expected Error, got Ok"),
935        }
936    }
937
938    #[test]
939    fn test_auto_index_on_create_node() {
940        let (handler, _vi) = handler_with_vector_index(3);
941
942        // Create a node with an embedding via the handler.
943        let create_resp = handler.handle(Request::CreateNode {
944            labels: vec!["TestNode".into()],
945            properties: serde_json::json!({"name": "alpha"}),
946            embedding: Some(vec![0.5, 0.5, 0.0]),
947        });
948        match &create_resp {
949            Response::Ok { data } => {
950                assert!(data.get("node_id").is_some());
951            }
952            Response::Error { message } => panic!("create failed: {}", message),
953        }
954
955        // Create a second node with a different embedding.
956        handler.handle(Request::CreateNode {
957            labels: vec!["TestNode".into()],
958            properties: serde_json::json!({"name": "beta"}),
959            embedding: Some(vec![0.0, 0.0, 1.0]),
960        });
961
962        // Now search for the embedding that matches the first node.
963        let search_resp = handler.handle(Request::VectorSearch {
964            query: vec![0.5, 0.5, 0.0],
965            k: 1,
966        });
967
968        match search_resp {
969            Response::Ok { data } => {
970                let results = data.get("results").unwrap().as_array().unwrap();
971                assert_eq!(results.len(), 1);
972                // The closest should be node 1 (the first created node).
973                assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 1);
974            }
975            Response::Error { message } => panic!("search failed: {}", message),
976        }
977    }
978
979    /// Helper: create a handler with nodes and edges for semantic tests.
980    /// Returns (handler, n1_id, n2_id, n3_id).
981    fn handler_with_semantic_graph() -> RequestHandler {
982        let storage = InMemoryStorage::new();
983        let vector_index: Arc<dyn VectorIndex> =
984            Arc::new(HnswVectorIndex::new(3, DistanceMetric::Euclidean));
985        let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
986        let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
987
988        // Create nodes with embeddings.
989        handler.handle(Request::CreateNode {
990            labels: vec!["Thing".into()],
991            properties: serde_json::json!({"name": "A"}),
992            embedding: Some(vec![1.0, 0.0, 0.0]),
993        }); // id=1
994        handler.handle(Request::CreateNode {
995            labels: vec!["Thing".into()],
996            properties: serde_json::json!({"name": "closeA"}),
997            embedding: Some(vec![0.9, 0.1, 0.0]),
998        }); // id=2
999        handler.handle(Request::CreateNode {
1000            labels: vec!["Thing".into()],
1001            properties: serde_json::json!({"name": "C"}),
1002            embedding: Some(vec![0.0, 0.0, 1.0]),
1003        }); // id=3
1004
1005        // Edges: 1->2, 2->3
1006        handler.handle(Request::CreateEdge {
1007            source: 1,
1008            target: 2,
1009            edge_type: "LINK".into(),
1010            properties: serde_json::json!({}),
1011            weight: 1.0,
1012            valid_from: None,
1013            valid_to: None,
1014        });
1015        handler.handle(Request::CreateEdge {
1016            source: 2,
1017            target: 3,
1018            edge_type: "LINK".into(),
1019            properties: serde_json::json!({}),
1020            weight: 1.0,
1021            valid_from: None,
1022            valid_to: None,
1023        });
1024
1025        handler
1026    }
1027
1028    #[test]
1029    fn test_hybrid_search_handler() {
1030        let handler = handler_with_semantic_graph();
1031
1032        let resp = handler.handle(Request::HybridSearch {
1033            anchor: 1,
1034            query: vec![1.0, 0.0, 0.0],
1035            max_hops: 2,
1036            k: 10,
1037            alpha: 0.5,
1038        });
1039
1040        match resp {
1041            Response::Ok { data } => {
1042                let results = data.get("results").unwrap().as_array().unwrap();
1043                assert!(!results.is_empty());
1044                // Node 2 (closeA) should be the best match (close in both graph and vector).
1045                assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 2);
1046                // Each result should have a "score" field.
1047                assert!(results[0].get("score").is_some());
1048            }
1049            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1050        }
1051    }
1052
1053    #[test]
1054    fn test_semantic_neighbors_handler() {
1055        let handler = handler_with_semantic_graph();
1056
1057        let resp = handler.handle(Request::SemanticNeighbors {
1058            id: 1,
1059            concept: vec![1.0, 0.0, 0.0],
1060            direction: "outgoing".into(),
1061            k: 10,
1062        });
1063
1064        match resp {
1065            Response::Ok { data } => {
1066                let results = data.get("results").unwrap().as_array().unwrap();
1067                assert_eq!(results.len(), 1); // only neighbor of node 1 is node 2
1068                assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 2);
1069                assert!(results[0].get("distance").is_some());
1070            }
1071            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1072        }
1073    }
1074
1075    #[test]
1076    fn test_semantic_walk_handler() {
1077        let handler = handler_with_semantic_graph();
1078
1079        let resp = handler.handle(Request::SemanticWalk {
1080            start: 1,
1081            concept: vec![0.0, 0.0, 1.0],
1082            max_hops: 5,
1083        });
1084
1085        match resp {
1086            Response::Ok { data } => {
1087                let path = data.get("path").unwrap().as_array().unwrap();
1088                // Path should include at least start (1) and end (3).
1089                assert!(path.len() >= 2);
1090                assert_eq!(path[0].get("node_id").unwrap().as_u64().unwrap(), 1);
1091                // Last node should be node 3 (closest to concept [0,0,1]).
1092                assert_eq!(
1093                    path.last()
1094                        .unwrap()
1095                        .get("node_id")
1096                        .unwrap()
1097                        .as_u64()
1098                        .unwrap(),
1099                    3
1100                );
1101                assert!(path.last().unwrap().get("distance").is_some());
1102            }
1103            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1104        }
1105    }
1106
1107    // ---- GraphRAG handler tests ----
1108
1109    /// Helper: create a handler with a graph suitable for GraphRAG tests.
1110    /// Creates: Alice -[KNOWS]-> Bob -[WORKS_AT]-> Acme
1111    fn handler_with_rag_graph() -> RequestHandler {
1112        let storage = InMemoryStorage::new();
1113        let vector_index: Arc<dyn VectorIndex> =
1114            Arc::new(HnswVectorIndex::new(3, DistanceMetric::Euclidean));
1115        let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
1116        let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
1117
1118        // Create nodes with embeddings.
1119        handler.handle(Request::CreateNode {
1120            labels: vec!["Person".into()],
1121            properties: serde_json::json!({"name": "Alice"}),
1122            embedding: Some(vec![1.0, 0.0, 0.0]),
1123        }); // id=1
1124        handler.handle(Request::CreateNode {
1125            labels: vec!["Person".into()],
1126            properties: serde_json::json!({"name": "Bob"}),
1127            embedding: Some(vec![0.0, 1.0, 0.0]),
1128        }); // id=2
1129        handler.handle(Request::CreateNode {
1130            labels: vec!["Company".into()],
1131            properties: serde_json::json!({"name": "Acme"}),
1132            embedding: Some(vec![0.0, 0.0, 1.0]),
1133        }); // id=3
1134
1135        handler.handle(Request::CreateEdge {
1136            source: 1,
1137            target: 2,
1138            edge_type: "KNOWS".into(),
1139            properties: serde_json::json!({"since": 2020}),
1140            weight: 1.0,
1141            valid_from: None,
1142            valid_to: None,
1143        });
1144        handler.handle(Request::CreateEdge {
1145            source: 2,
1146            target: 3,
1147            edge_type: "WORKS_AT".into(),
1148            properties: serde_json::json!({}),
1149            weight: 1.0,
1150            valid_from: None,
1151            valid_to: None,
1152        });
1153
1154        handler
1155    }
1156
1157    #[test]
1158    fn test_extract_subgraph_handler() {
1159        let handler = handler_with_rag_graph();
1160
1161        let resp = handler.handle(Request::ExtractSubgraph {
1162            center: 1,
1163            hops: 2,
1164            max_nodes: 50,
1165            format: "structured".into(),
1166        });
1167
1168        match resp {
1169            Response::Ok { data } => {
1170                assert!(
1171                    data.get("text")
1172                        .unwrap()
1173                        .as_str()
1174                        .unwrap()
1175                        .contains("Alice")
1176                );
1177                let nodes_count = data.get("nodes_count").unwrap().as_u64().unwrap();
1178                assert!(
1179                    nodes_count >= 2,
1180                    "expected at least 2 nodes, got {nodes_count}"
1181                );
1182                let edges_count = data.get("edges_count").unwrap().as_u64().unwrap();
1183                assert!(
1184                    edges_count >= 1,
1185                    "expected at least 1 edge, got {edges_count}"
1186                );
1187                assert!(data.get("estimated_tokens").unwrap().as_u64().unwrap() > 0);
1188            }
1189            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1190        }
1191    }
1192
1193    #[test]
1194    fn test_graph_rag_handler_with_anchor() {
1195        let handler = handler_with_rag_graph();
1196
1197        let resp = handler.handle(Request::GraphRag {
1198            question: "Who does Alice know?".into(),
1199            question_embedding: None,
1200            anchor: Some(1),
1201            hops: 2,
1202            max_nodes: 50,
1203            format: "structured".into(),
1204        });
1205
1206        match resp {
1207            Response::Ok { data } => {
1208                assert_eq!(data.get("anchor_node_id").unwrap().as_u64().unwrap(), 1);
1209                let context = data.get("context").unwrap().as_str().unwrap();
1210                assert!(context.contains("Alice"));
1211                assert_eq!(
1212                    data.get("question").unwrap().as_str().unwrap(),
1213                    "Who does Alice know?"
1214                );
1215                assert!(data.get("nodes_in_context").unwrap().as_u64().unwrap() > 0);
1216                assert!(data.get("edges_in_context").unwrap().as_u64().unwrap() > 0);
1217                assert!(data.get("estimated_tokens").unwrap().as_u64().unwrap() > 0);
1218                assert!(data.get("note").is_some());
1219            }
1220            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1221        }
1222    }
1223
1224    #[test]
1225    fn test_graph_rag_handler_with_embedding() {
1226        let handler = handler_with_rag_graph();
1227
1228        // Query with embedding close to Alice [1,0,0]
1229        let resp = handler.handle(Request::GraphRag {
1230            question: "Tell me about Alice".into(),
1231            question_embedding: Some(vec![1.0, 0.0, 0.0]),
1232            anchor: None,
1233            hops: 2,
1234            max_nodes: 50,
1235            format: "structured".into(),
1236        });
1237
1238        match resp {
1239            Response::Ok { data } => {
1240                // Should anchor on node 1 (Alice, closest to [1,0,0]).
1241                assert_eq!(data.get("anchor_node_id").unwrap().as_u64().unwrap(), 1);
1242                let context = data.get("context").unwrap().as_str().unwrap();
1243                assert!(context.contains("Alice"));
1244            }
1245            Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1246        }
1247    }
1248
1249    #[test]
1250    fn test_graph_rag_handler_no_anchor_no_embedding() {
1251        let handler = handler_with_rag_graph();
1252
1253        let resp = handler.handle(Request::GraphRag {
1254            question: "Some question".into(),
1255            question_embedding: None,
1256            anchor: None,
1257            hops: 2,
1258            max_nodes: 50,
1259            format: "structured".into(),
1260        });
1261
1262        match resp {
1263            Response::Error { message } => {
1264                assert_eq!(
1265                    message,
1266                    "either anchor or question_embedding must be provided"
1267                );
1268            }
1269            Response::Ok { .. } => panic!("expected Error, got Ok"),
1270        }
1271    }
1272
1273    #[test]
1274    fn test_auto_remove_on_delete_node() {
1275        let (handler, _vi) = handler_with_vector_index(3);
1276
1277        // Create a node with an embedding.
1278        let create_resp = handler.handle(Request::CreateNode {
1279            labels: vec!["Temp".into()],
1280            properties: serde_json::json!({}),
1281            embedding: Some(vec![1.0, 0.0, 0.0]),
1282        });
1283        let node_id = match &create_resp {
1284            Response::Ok { data } => data.get("node_id").unwrap().as_u64().unwrap(),
1285            Response::Error { message } => panic!("create failed: {}", message),
1286        };
1287
1288        // Verify the node is searchable.
1289        let search_resp = handler.handle(Request::VectorSearch {
1290            query: vec![1.0, 0.0, 0.0],
1291            k: 1,
1292        });
1293        match &search_resp {
1294            Response::Ok { data } => {
1295                let results = data.get("results").unwrap().as_array().unwrap();
1296                assert_eq!(results.len(), 1);
1297                assert_eq!(
1298                    results[0].get("node_id").unwrap().as_u64().unwrap(),
1299                    node_id
1300                );
1301            }
1302            Response::Error { message } => panic!("search failed before delete: {}", message),
1303        }
1304
1305        // Delete the node.
1306        let del_resp = handler.handle(Request::DeleteNode { id: node_id });
1307        match &del_resp {
1308            Response::Ok { .. } => {}
1309            Response::Error { message } => panic!("delete failed: {}", message),
1310        }
1311
1312        // Verify the node is no longer in vector search results.
1313        let search_resp2 = handler.handle(Request::VectorSearch {
1314            query: vec![1.0, 0.0, 0.0],
1315            k: 10,
1316        });
1317        match search_resp2 {
1318            Response::Ok { data } => {
1319                let results = data.get("results").unwrap().as_array().unwrap();
1320                // Should be empty -- the only node was deleted.
1321                assert!(
1322                    results.is_empty(),
1323                    "expected no results after delete, got: {:?}",
1324                    results
1325                );
1326            }
1327            Response::Error { message } => panic!("search failed after delete: {}", message),
1328        }
1329    }
1330
1331    #[test]
1332    fn test_delete_by_label_removes_matches() {
1333        // astraeadb-issues.md #4.
1334        let (handler, _vi) = handler_with_vector_index(2);
1335
1336        // Create three Widget nodes and one Gadget node.
1337        for _ in 0..3 {
1338            match handler.handle(Request::CreateNode {
1339                labels: vec!["Widget".into()],
1340                properties: serde_json::json!({}),
1341                embedding: None,
1342            }) {
1343                Response::Ok { .. } => {}
1344                Response::Error { message } => panic!("create failed: {message}"),
1345            }
1346        }
1347        match handler.handle(Request::CreateNode {
1348            labels: vec!["Gadget".into()],
1349            properties: serde_json::json!({}),
1350            embedding: None,
1351        }) {
1352            Response::Ok { .. } => {}
1353            Response::Error { message } => panic!("create failed: {message}"),
1354        }
1355
1356        // Delete by Widget — expect 3 deleted.
1357        let resp = handler.handle(Request::DeleteByLabel {
1358            label: "Widget".into(),
1359        });
1360        match resp {
1361            Response::Ok { data } => {
1362                assert_eq!(data.get("deleted").unwrap().as_u64().unwrap(), 3);
1363            }
1364            Response::Error { message } => panic!("DeleteByLabel failed: {message}"),
1365        }
1366
1367        // Widgets gone; Gadget still there.
1368        match handler.handle(Request::FindByLabel {
1369            label: "Widget".into(),
1370        }) {
1371            Response::Ok { data } => {
1372                let ids = data.get("node_ids").unwrap().as_array().unwrap();
1373                assert!(ids.is_empty(), "expected no Widgets, got {:?}", ids);
1374            }
1375            Response::Error { message } => panic!("FindByLabel failed: {message}"),
1376        }
1377        match handler.handle(Request::FindByLabel {
1378            label: "Gadget".into(),
1379        }) {
1380            Response::Ok { data } => {
1381                let ids = data.get("node_ids").unwrap().as_array().unwrap();
1382                assert_eq!(ids.len(), 1, "Gadget should still be present");
1383            }
1384            Response::Error { message } => panic!("FindByLabel failed: {message}"),
1385        }
1386
1387        // DeleteByLabel of an absent label returns deleted=0 cleanly.
1388        match handler.handle(Request::DeleteByLabel {
1389            label: "Nonexistent".into(),
1390        }) {
1391            Response::Ok { data } => {
1392                assert_eq!(data.get("deleted").unwrap().as_u64().unwrap(), 0);
1393            }
1394            Response::Error { message } => panic!("unexpected error: {message}"),
1395        }
1396    }
1397
1398    #[test]
1399    fn test_find_edges_by_type() {
1400        // astraeadb-issues.md #3.
1401        let (handler, _vi) = handler_with_vector_index(2);
1402
1403        // Create two nodes to wire edges between.
1404        let mut node_ids = Vec::new();
1405        for _ in 0..2 {
1406            match handler.handle(Request::CreateNode {
1407                labels: vec![],
1408                properties: serde_json::json!({}),
1409                embedding: None,
1410            }) {
1411                Response::Ok { data } => {
1412                    node_ids.push(data.get("node_id").unwrap().as_u64().unwrap());
1413                }
1414                Response::Error { message } => panic!("create node failed: {message}"),
1415            }
1416        }
1417        let (src, tgt) = (node_ids[0], node_ids[1]);
1418
1419        // Create 3 edges of type T1 and 2 of type T2.
1420        let mut t1_ids: Vec<u64> = Vec::new();
1421        for _ in 0..3 {
1422            match handler.handle(Request::CreateEdge {
1423                source: src,
1424                target: tgt,
1425                edge_type: "T1".into(),
1426                properties: serde_json::json!({}),
1427                weight: 1.0,
1428                valid_from: None,
1429                valid_to: None,
1430            }) {
1431                Response::Ok { data } => {
1432                    t1_ids.push(data.get("edge_id").unwrap().as_u64().unwrap());
1433                }
1434                Response::Error { message } => panic!("create edge T1 failed: {message}"),
1435            }
1436        }
1437        for _ in 0..2 {
1438            match handler.handle(Request::CreateEdge {
1439                source: src,
1440                target: tgt,
1441                edge_type: "T2".into(),
1442                properties: serde_json::json!({}),
1443                weight: 1.0,
1444                valid_from: None,
1445                valid_to: None,
1446            }) {
1447                Response::Ok { .. } => {}
1448                Response::Error { message } => panic!("create edge T2 failed: {message}"),
1449            }
1450        }
1451
1452        // Happy path: FindEdgeByType("T1") must return exactly the 3 T1 edges
1453        // with correct source/target fields.
1454        let resp = handler.handle(Request::FindEdgeByType {
1455            edge_type: "T1".into(),
1456        });
1457        match resp {
1458            Response::Ok { data } => {
1459                let edges = data.get("edges").unwrap().as_array().unwrap();
1460                assert_eq!(edges.len(), 3, "expected 3 T1 edges, got {}", edges.len());
1461                // All returned edges must have the correct source/target.
1462                for e in edges {
1463                    assert_eq!(
1464                        e.get("source").unwrap().as_u64().unwrap(),
1465                        src,
1466                        "wrong source"
1467                    );
1468                    assert_eq!(
1469                        e.get("target").unwrap().as_u64().unwrap(),
1470                        tgt,
1471                        "wrong target"
1472                    );
1473                }
1474                // The edge_ids returned must be exactly the three we created.
1475                let mut returned_ids: Vec<u64> = edges
1476                    .iter()
1477                    .map(|e| e.get("edge_id").unwrap().as_u64().unwrap())
1478                    .collect();
1479                returned_ids.sort_unstable();
1480                let mut expected = t1_ids.clone();
1481                expected.sort_unstable();
1482                assert_eq!(returned_ids, expected, "T1 edge_ids mismatch");
1483            }
1484            Response::Error { message } => panic!("FindEdgeByType T1 failed: {message}"),
1485        }
1486
1487        // Empty result: a nonexistent type returns an empty list, not an error.
1488        match handler.handle(Request::FindEdgeByType {
1489            edge_type: "nonexistent".into(),
1490        }) {
1491            Response::Ok { data } => {
1492                let edges = data.get("edges").unwrap().as_array().unwrap();
1493                assert!(
1494                    edges.is_empty(),
1495                    "expected empty list for unknown type, got {:?}",
1496                    edges
1497                );
1498            }
1499            Response::Error { message } => {
1500                panic!("FindEdgeByType nonexistent returned error: {message}")
1501            }
1502        }
1503    }
1504}