Skip to main content

uni_query/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Query result types and value re-exports.
5//!
6//! Core value types ([`Value`], [`Node`], [`Edge`], [`Path`]) are defined in
7//! `uni_common::value` and re-exported here for backward compatibility.
8//! Query-specific types ([`Row`], [`QueryResult`], [`QueryCursor`]) remain here.
9
10use futures::Stream;
11use std::collections::HashMap;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15use uni_common::{Result, UniError};
16
17// Re-export core value types from uni-common.
18#[doc(inline)]
19pub use uni_common::value::{Edge, FromValue, Node, Path, Value};
20
21/// Timing metrics collected during query execution.
22///
23/// All durations default to zero until the execution pipeline populates them.
24#[derive(Debug, Clone, Default)]
25pub struct QueryMetrics {
26    /// Time spent parsing the query string into an AST.
27    pub parse_time: Duration,
28    /// Time spent planning (logical plan generation).
29    pub plan_time: Duration,
30    /// Time spent executing the plan.
31    pub exec_time: Duration,
32    /// Wall-clock time from query submission to result.
33    pub total_time: Duration,
34    /// Number of rows returned to the caller.
35    pub rows_returned: usize,
36    /// Number of rows scanned during execution (0 until executor instrumentation).
37    pub rows_scanned: usize,
38    /// Number of bytes read from storage (0 until storage instrumentation).
39    pub bytes_read: usize,
40    /// Whether the plan was served from cache.
41    pub plan_cache_hit: bool,
42    /// Number of L0 reads during execution (0 until storage instrumentation).
43    pub l0_reads: usize,
44    /// Number of storage reads during execution (0 until storage instrumentation).
45    pub storage_reads: usize,
46    /// Number of cache hits during execution (0 until storage instrumentation).
47    pub cache_hits: usize,
48}
49
50/// Single result row from a query.
51#[derive(Debug, Clone)]
52pub struct Row {
53    /// Column names shared across all rows in a result set.
54    pub(crate) columns: Arc<Vec<String>>,
55    /// Column values for this row.
56    pub(crate) values: Vec<Value>,
57}
58
59impl Row {
60    /// Create a new row from columns and values.
61    pub fn new(columns: Arc<Vec<String>>, values: Vec<Value>) -> Self {
62        Self { columns, values }
63    }
64
65    /// Returns the column names for this row.
66    pub fn columns(&self) -> &[String] {
67        &self.columns
68    }
69
70    /// Returns the column values for this row.
71    pub fn values(&self) -> &[Value] {
72        &self.values
73    }
74
75    /// Consumes the row, returning the column values.
76    pub fn into_values(self) -> Vec<Value> {
77        self.values
78    }
79
80    /// Gets a typed value by column name.
81    ///
82    /// # Errors
83    ///
84    /// Returns `UniError::Query` if the column is missing,
85    /// or `UniError::Type` if it cannot be converted.
86    pub fn get<T: FromValue>(&self, column: &str) -> Result<T> {
87        let idx = self
88            .columns
89            .iter()
90            .position(|c| c == column)
91            .ok_or_else(|| UniError::Query {
92                message: format!("Column '{}' not found", column),
93                query: None,
94            })?;
95        self.get_idx(idx)
96    }
97
98    /// Gets a typed value by column index.
99    ///
100    /// # Errors
101    ///
102    /// Returns `UniError::Query` if the index is out of bounds,
103    /// or `UniError::Type` if it cannot be converted.
104    pub fn get_idx<T: FromValue>(&self, index: usize) -> Result<T> {
105        if index >= self.values.len() {
106            return Err(UniError::Query {
107                message: format!("Column index {} out of bounds", index),
108                query: None,
109            });
110        }
111        T::from_value(&self.values[index])
112    }
113
114    /// Tries to get a typed value, returning `None` on failure.
115    pub fn try_get<T: FromValue>(&self, column: &str) -> Option<T> {
116        self.get(column).ok()
117    }
118
119    /// Gets the raw `Value` by column name.
120    pub fn value(&self, column: &str) -> Option<&Value> {
121        let idx = self.columns.iter().position(|c| c == column)?;
122        self.values.get(idx)
123    }
124
125    /// Returns all column-value pairs as a map.
126    pub fn as_map(&self) -> HashMap<&str, &Value> {
127        self.columns
128            .iter()
129            .zip(&self.values)
130            .map(|(col, val)| (col.as_str(), val))
131            .collect()
132    }
133
134    /// Converts this row to a JSON object.
135    pub fn to_json(&self) -> serde_json::Value {
136        serde_json::to_value(self.as_map()).unwrap_or(serde_json::Value::Null)
137    }
138}
139
140impl std::ops::Index<usize> for Row {
141    type Output = Value;
142    fn index(&self, index: usize) -> &Self::Output {
143        &self.values[index]
144    }
145}
146
147/// Warnings emitted during query execution.
148///
149/// Warnings indicate potential issues but do not prevent the query from
150/// completing.
151#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
152pub enum QueryWarning {
153    /// An index is unavailable (e.g., still being rebuilt).
154    IndexUnavailable {
155        /// The label that the index is for.
156        label: String,
157        /// The name of the unavailable index.
158        index_name: String,
159        /// Reason the index is unavailable.
160        reason: String,
161    },
162    /// A property filter could not use an index.
163    NoIndexForFilter {
164        /// The label being filtered.
165        label: String,
166        /// The property being filtered.
167        property: String,
168    },
169    /// RRF fusion was requested in point-computation context where no global
170    /// ranking is available, so it degenerated to equal-weight fusion.
171    RrfPointContext,
172    /// Generic warning message.
173    Other(String),
174}
175
176impl std::fmt::Display for QueryWarning {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        match self {
179            QueryWarning::IndexUnavailable {
180                label,
181                index_name,
182                reason,
183            } => {
184                write!(
185                    f,
186                    "Index '{}' on label '{}' is unavailable: {}",
187                    index_name, label, reason
188                )
189            }
190            QueryWarning::NoIndexForFilter { label, property } => {
191                write!(
192                    f,
193                    "No index available for filter on {}.{}, using full scan",
194                    label, property
195                )
196            }
197            QueryWarning::RrfPointContext => {
198                write!(
199                    f,
200                    "RRF fusion degenerated to equal-weight fusion in point-computation context \
201                     (no global ranking available). Consider using method: 'weighted' with explicit weights."
202                )
203            }
204            QueryWarning::Other(msg) => write!(f, "{}", msg),
205        }
206    }
207}
208
209/// Collection of query result rows.
210#[derive(Debug)]
211pub struct QueryResult {
212    /// Column names shared across all rows.
213    pub(crate) columns: Arc<Vec<String>>,
214    /// Result rows.
215    pub(crate) rows: Vec<Row>,
216    /// Warnings emitted during query execution.
217    pub(crate) warnings: Vec<QueryWarning>,
218    /// Execution timing metrics.
219    pub(crate) metrics: QueryMetrics,
220}
221
222impl QueryResult {
223    /// Create a new query result.
224    #[doc(hidden)]
225    pub fn new(
226        columns: Arc<Vec<String>>,
227        rows: Vec<Row>,
228        warnings: Vec<QueryWarning>,
229        metrics: QueryMetrics,
230    ) -> Self {
231        Self {
232            columns,
233            rows,
234            warnings,
235            metrics,
236        }
237    }
238
239    /// Returns the column names.
240    pub fn columns(&self) -> &[String] {
241        &self.columns
242    }
243
244    /// Returns the number of rows.
245    pub fn len(&self) -> usize {
246        self.rows.len()
247    }
248
249    /// Returns `true` if there are no rows.
250    pub fn is_empty(&self) -> bool {
251        self.rows.is_empty()
252    }
253
254    /// Returns all rows.
255    pub fn rows(&self) -> &[Row] {
256        &self.rows
257    }
258
259    /// Consumes the result, returning the rows.
260    pub fn into_rows(self) -> Vec<Row> {
261        self.rows
262    }
263
264    /// Returns an iterator over the rows.
265    pub fn iter(&self) -> impl Iterator<Item = &Row> {
266        self.rows.iter()
267    }
268
269    /// Returns warnings emitted during execution.
270    pub fn warnings(&self) -> &[QueryWarning] {
271        &self.warnings
272    }
273
274    /// Returns `true` if the query produced any warnings.
275    pub fn has_warnings(&self) -> bool {
276        !self.warnings.is_empty()
277    }
278
279    /// Returns execution timing metrics.
280    pub fn metrics(&self) -> &QueryMetrics {
281        &self.metrics
282    }
283
284    /// Update the parse timing and total time on the metrics.
285    ///
286    /// Used when the parse phase happens outside `execute_ast_internal` (e.g.,
287    /// in `execute_internal_with_config` which parses first, then delegates).
288    #[doc(hidden)]
289    pub fn update_parse_timing(
290        &mut self,
291        parse_time: std::time::Duration,
292        total_time: std::time::Duration,
293    ) {
294        self.metrics.parse_time = parse_time;
295        self.metrics.total_time = total_time;
296    }
297}
298
299impl IntoIterator for QueryResult {
300    type Item = Row;
301    type IntoIter = std::vec::IntoIter<Row>;
302
303    fn into_iter(self) -> Self::IntoIter {
304        self.rows.into_iter()
305    }
306}
307
308/// Result of a write operation (CREATE, SET, DELETE, etc.).
309#[derive(Debug)]
310pub struct ExecuteResult {
311    /// Number of entities affected.
312    pub(crate) affected_rows: usize,
313    /// Number of nodes created.
314    pub(crate) nodes_created: usize,
315    /// Number of nodes deleted.
316    pub(crate) nodes_deleted: usize,
317    /// Number of relationships created.
318    pub(crate) relationships_created: usize,
319    /// Number of relationships deleted.
320    pub(crate) relationships_deleted: usize,
321    /// Number of properties set.
322    pub(crate) properties_set: usize,
323    /// Number of labels added.
324    pub(crate) labels_added: usize,
325    /// Number of labels removed.
326    pub(crate) labels_removed: usize,
327    /// Execution timing metrics.
328    pub(crate) metrics: QueryMetrics,
329}
330
331impl ExecuteResult {
332    /// Create a new execute result with only an affected row count.
333    ///
334    /// All per-type counters default to zero. Use [`with_details`](Self::with_details)
335    /// to populate detailed mutation statistics.
336    #[doc(hidden)]
337    pub fn new(affected_rows: usize) -> Self {
338        Self {
339            affected_rows,
340            nodes_created: 0,
341            nodes_deleted: 0,
342            relationships_created: 0,
343            relationships_deleted: 0,
344            properties_set: 0,
345            labels_added: 0,
346            labels_removed: 0,
347            metrics: QueryMetrics::default(),
348        }
349    }
350
351    /// Create an execute result with detailed per-type mutation counters and metrics.
352    #[doc(hidden)]
353    pub fn with_details(
354        affected_rows: usize,
355        stats: &uni_store::runtime::l0::MutationStats,
356        metrics: QueryMetrics,
357    ) -> Self {
358        Self {
359            affected_rows,
360            nodes_created: stats.nodes_created,
361            nodes_deleted: stats.nodes_deleted,
362            relationships_created: stats.relationships_created,
363            relationships_deleted: stats.relationships_deleted,
364            properties_set: stats.properties_set,
365            labels_added: stats.labels_added,
366            labels_removed: stats.labels_removed,
367            metrics,
368        }
369    }
370
371    /// Returns the number of affected entities.
372    pub fn affected_rows(&self) -> usize {
373        self.affected_rows
374    }
375
376    /// Returns the number of nodes created.
377    pub fn nodes_created(&self) -> usize {
378        self.nodes_created
379    }
380
381    /// Returns the number of nodes deleted.
382    pub fn nodes_deleted(&self) -> usize {
383        self.nodes_deleted
384    }
385
386    /// Returns the number of relationships created.
387    pub fn relationships_created(&self) -> usize {
388        self.relationships_created
389    }
390
391    /// Returns the number of relationships deleted.
392    pub fn relationships_deleted(&self) -> usize {
393        self.relationships_deleted
394    }
395
396    /// Returns the number of properties set.
397    pub fn properties_set(&self) -> usize {
398        self.properties_set
399    }
400
401    /// Returns the number of labels added.
402    pub fn labels_added(&self) -> usize {
403        self.labels_added
404    }
405
406    /// Returns the number of labels removed.
407    pub fn labels_removed(&self) -> usize {
408        self.labels_removed
409    }
410
411    /// Returns execution timing metrics.
412    pub fn metrics(&self) -> &QueryMetrics {
413        &self.metrics
414    }
415}
416
417/// Cursor-based result streaming for large result sets.
418pub struct QueryCursor {
419    /// Column names shared across all rows.
420    pub(crate) columns: Arc<Vec<String>>,
421    /// Async stream of row batches.
422    pub(crate) stream: Pin<Box<dyn Stream<Item = Result<Vec<Row>>> + Send>>,
423}
424
425impl QueryCursor {
426    /// Create a new query cursor.
427    #[doc(hidden)]
428    pub fn new(
429        columns: Arc<Vec<String>>,
430        stream: Pin<Box<dyn Stream<Item = Result<Vec<Row>>> + Send>>,
431    ) -> Self {
432        Self { columns, stream }
433    }
434
435    /// Returns the column names.
436    pub fn columns(&self) -> &[String] {
437        &self.columns
438    }
439
440    /// Fetches the next batch of rows.
441    pub async fn next_batch(&mut self) -> Option<Result<Vec<Row>>> {
442        use futures::StreamExt;
443        self.stream.next().await
444    }
445
446    /// Consumes all remaining rows into a single vector.
447    ///
448    /// # Errors
449    ///
450    /// Returns the first error encountered while streaming.
451    pub async fn collect_remaining(mut self) -> Result<Vec<Row>> {
452        use futures::StreamExt;
453        let mut rows = Vec::new();
454        while let Some(batch_res) = self.stream.next().await {
455            rows.extend(batch_res?);
456        }
457        Ok(rows)
458    }
459}