linkrs_server/graph_service/
explain.rs1use std::collections::HashMap;
2
3use linkrs_api::api_core::QueryResult;
4
5use crate::storage::{
6 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
7};
8
9use super::error::GraphServiceError;
10use super::GraphService;
11
12impl<
13 S: StorageClient
14 + StorageSchemaContextOps
15 + StorageSyncContextOps
16 + StorageOperationContextOps
17 + Clone
18 + 'static,
19 > GraphService<S>
20{
21 pub async fn estimate_rows(&self, session_id: i64, stmt: &str) -> Option<u64> {
23 let trimmed = stmt.trim();
24 if trimmed.is_empty() {
25 return None;
26 }
27 let explain = if trimmed.len() >= 7 && trimmed[..7].eq_ignore_ascii_case("explain") {
28 trimmed.to_string()
29 } else {
30 format!("EXPLAIN {trimmed}")
31 };
32 let result = self
33 .execute_with_params(session_id, &explain, None, None)
34 .await
35 .ok()?;
36 let text = match result.first_value() {
37 Some(value) => crate::value::to_json(value.clone()),
38 None => return None,
39 };
40 Self::extract_root_estimate(text.as_str()?)
41 }
42
43 pub async fn explain(
45 &self,
46 session_id: i64,
47 stmt: &str,
48 parameters: Option<HashMap<String, linkrs_core::Value>>,
49 session_variables: Option<HashMap<String, linkrs_core::Value>>,
50 ) -> Result<QueryResult, GraphServiceError> {
51 let trimmed = stmt.trim();
52 if trimmed.is_empty() {
53 return Err(GraphServiceError::new("query must not be empty"));
54 }
55 let already_explain = trimmed.len() >= 7 && trimmed[..7].eq_ignore_ascii_case("explain");
56 let explain_stmt = if already_explain {
57 trimmed.to_string()
58 } else {
59 format!("EXPLAIN {trimmed}")
60 };
61 match self
62 .execute_with_params(session_id, &explain_stmt, parameters, session_variables)
63 .await
64 {
65 Ok(result) => Ok(result),
66 Err(e) => {
67 if already_explain {
68 return Err(e);
69 }
70 const PREFIX_LEN: usize = "EXPLAIN ".len();
71 if let Some(pos) = e.position() {
72 if pos.line == 1 && pos.column > PREFIX_LEN {
73 return Err(GraphServiceError::with_position(
74 e.message().to_string(),
75 Some(linkrs_core::types::Position::new(
76 1,
77 pos.column - PREFIX_LEN,
78 )),
79 ));
80 }
81 if pos.line == 1 {
82 return Err(GraphServiceError::new(e.message().to_string()));
83 }
84 }
85 Err(e)
86 }
87 }
88 }
89
90 pub(crate) fn extract_root_estimate(plan_text: &str) -> Option<u64> {
94 const MARKER: &str = "est_rows:";
95 let mut estimate: Option<u64> = None;
96 let mut rest = plan_text;
97 while let Some(pos) = rest.find(MARKER) {
98 rest = &rest[pos + MARKER.len()..];
99 let len = rest
100 .chars()
101 .take_while(|c| c.is_ascii_digit() || *c == '.')
102 .map(|c| c.len_utf8())
103 .sum::<usize>();
104 if len > 0 {
105 if let Ok(parsed) = rest[..len].parse::<f64>() {
106 estimate = Some(parsed.round().max(0.0) as u64);
107 }
108 rest = &rest[len..];
109 } else {
110 match rest.chars().next() {
111 Some(c) => rest = &rest[c.len_utf8()..],
112 None => break,
113 }
114 }
115 }
116 estimate
117 }
118}