Skip to main content

akar_main/
query_result.rs

1//! QueryResult — encapsulates the result of a query execution.
2
3use akar_common::vector::DataChunk;
4use std::fmt;
5use std::time::Duration;
6
7/// Timing summary for a query execution.
8#[derive(Debug, Clone)]
9pub struct QuerySummary {
10    /// Total wall-clock time from query submission to result.
11    pub elapsed: Duration,
12    /// Time spent in compilation (parse + bind + plan + optimize).
13    pub compile_time: Duration,
14    /// Time spent in execution (physical operator execution).
15    pub execution_time: Duration,
16}
17
18impl Default for QuerySummary {
19    fn default() -> Self {
20        Self {
21            elapsed: Duration::ZERO,
22            compile_time: Duration::ZERO,
23            execution_time: Duration::ZERO,
24        }
25    }
26}
27
28impl fmt::Display for QuerySummary {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(
31            f,
32            "Query executed in {:.2}ms (compile: {:.2}ms, execution: {:.2}ms)",
33            self.elapsed.as_secs_f64() * 1000.0,
34            self.compile_time.as_secs_f64() * 1000.0,
35            self.execution_time.as_secs_f64() * 1000.0,
36        )
37    }
38}
39
40/// The result of executing a Cypher query.
41///
42/// Contains result chunks as column-major [`DataChunk`] vectors, metadata
43/// (row/column counts), and optional timing summary.
44///
45/// # Examples
46///
47/// ```no_run
48/// # use akar_main::database::{Database, SystemConfig};
49/// # use akar_main::connection::Connection;
50/// # let db = std::sync::Arc::new(Database::new("./db", SystemConfig::default())?);
51/// # let conn = Connection::new(&db);
52/// let result = conn.query("MATCH (n) RETURN n LIMIT 5")?;
53/// println!("Rows: {}, Columns: {}", result.num_rows, result.num_columns);
54/// for chunk in &result.chunks {
55///     for field_idx in 0..chunk.fields.len() {
56///         for row in 0..chunk.size {
57///             if let Some(val) = chunk.get_value(field_idx, row) {
58///                 println!("  {:?}", val);
59///             }
60///         }
61///     }
62/// }
63/// # Ok::<(), String>(())
64/// ```
65#[derive(Debug, Clone)]
66pub struct QueryResult {
67    pub chunks: Vec<DataChunk>,
68    pub num_rows: usize,
69    pub num_columns: usize,
70    pub success: bool,
71    pub error_message: Option<String>,
72    /// Human-readable message (e.g. "Table created" for DDL).
73    pub message: Option<String>,
74    /// Timing summary for the query.
75    pub summary: Option<QuerySummary>,
76}
77
78impl QueryResult {
79    pub fn new(chunks: Vec<DataChunk>) -> Self {
80        let num_rows = chunks.iter().map(|c| c.size).sum();
81        let num_columns = chunks.first().map(|c| c.num_fields()).unwrap_or(0);
82        Self {
83            chunks,
84            num_rows,
85            num_columns,
86            success: true,
87            error_message: None,
88            message: None,
89            summary: None,
90        }
91    }
92
93    /// Create a success result with a human-readable message (no data chunks).
94    pub fn success_message(msg: String) -> Self {
95        Self {
96            chunks: Vec::new(),
97            num_rows: 0,
98            num_columns: 0,
99            success: true,
100            error_message: None,
101            message: Some(msg),
102            summary: None,
103        }
104    }
105
106    /// Attach timing summary to this result.
107    pub fn with_summary(mut self, summary: QuerySummary) -> Self {
108        self.summary = Some(summary);
109        self
110    }
111
112    pub fn error(msg: String) -> Self {
113        Self {
114            chunks: Vec::new(),
115            num_rows: 0,
116            num_columns: 0,
117            success: false,
118            error_message: Some(msg),
119            message: None,
120            summary: None,
121        }
122    }
123
124    pub fn is_success(&self) -> bool {
125        self.success
126    }
127
128    pub fn num_rows(&self) -> usize {
129        self.num_rows
130    }
131
132    pub fn num_columns(&self) -> usize {
133        self.num_columns
134    }
135
136    /// Get a human-readable summary of the result.
137    pub fn result_summary(&self) -> String {
138        if let Some(head) = result_summary_head(
139            self.message.as_deref(),
140            self.success,
141            self.error_message.as_deref(),
142            self.num_rows != 0,
143        ) {
144            return head;
145        }
146        format!("Returned {} rows in {} columns", self.num_rows, self.num_columns)
147    }
148}
149
150/// Shared head logic for result summaries (local [`QueryResult`] and remote
151/// [`crate::remote::WireResponse`], DRY P51.43).
152///
153/// Precedence: message → error → empty-result marker. Returns `Some(summary)`
154/// when the result carries no data rows; `None` means the caller should append
155/// its row-count summary (or render rows).
156pub(crate) fn result_summary_head(
157    message: Option<&str>,
158    success: bool,
159    error_message: Option<&str>,
160    has_rows: bool,
161) -> Option<String> {
162    if let Some(msg) = message {
163        return Some(msg.to_string());
164    }
165    if !success {
166        return Some(format!("Error: {}", error_message.unwrap_or("Unknown error")));
167    }
168    if !has_rows {
169        return Some("(empty result)".into());
170    }
171    None
172}
173
174impl fmt::Display for QueryResult {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        if let Some(head) = result_summary_head(
177            self.message.as_deref(),
178            self.success,
179            self.error_message.as_deref(),
180            !self.chunks.is_empty(),
181        ) {
182            return write!(f, "{head}");
183        }
184        for (i, chunk) in self.chunks.iter().enumerate() {
185            if i > 0 {
186                writeln!(f)?;
187            }
188            write!(f, "Chunk {}: {} rows, {} columns", i, chunk.size, chunk.num_fields())?;
189        }
190        Ok(())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_query_result_summary_cases() {
200        // Message takes precedence over data.
201        let mut r = QueryResult::new(Vec::new());
202        r.message = Some("Table created".into());
203        assert_eq!(r.result_summary(), "Table created");
204
205        // Error rendering.
206        let err = QueryResult::error("boom".into());
207        assert_eq!(err.result_summary(), "Error: boom");
208
209        // Empty result marker.
210        assert_eq!(QueryResult::new(Vec::new()).result_summary(), "(empty result)");
211
212        // Row-count summary.
213        let mut rows = QueryResult::new(Vec::new());
214        rows.num_rows = 3;
215        rows.num_columns = 2;
216        assert_eq!(rows.result_summary(), "Returned 3 rows in 2 columns");
217    }
218
219    fn remote(
220        success: bool,
221        message: Option<&str>,
222        error: Option<&str>,
223        columns: usize,
224        row_count: usize,
225    ) -> crate::remote::WireResponse {
226        crate::remote::WireResponse {
227            success,
228            message: message.map(str::to_string),
229            error_message: error.map(str::to_string),
230            column_names: (0..columns).map(|i| format!("c{i}")).collect(),
231            rows: vec![Vec::new(); row_count],
232            stats: None,
233        }
234    }
235
236    /// Local and remote results must produce byte-identical summaries for the
237    /// same logical outcome (P51.43 — single shared head logic).
238    #[test]
239    fn test_local_remote_summary_parity() {
240        // Message case.
241        assert_eq!(
242            QueryResult::success_message("Table created".into()).result_summary(),
243            remote(true, Some("Table created"), None, 0, 0).result_summary()
244        );
245        // Error case.
246        assert_eq!(
247            QueryResult::error("boom".into()).result_summary(),
248            remote(false, None, Some("boom"), 0, 0).result_summary()
249        );
250        // Empty case.
251        assert_eq!(
252            QueryResult::new(Vec::new()).result_summary(),
253            remote(true, None, None, 0, 0).result_summary()
254        );
255        // Data case.
256        let mut local = QueryResult::new(Vec::new());
257        local.num_rows = 2;
258        local.num_columns = 3;
259        assert_eq!(local.result_summary(), remote(true, None, None, 3, 2).result_summary());
260    }
261}