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 uni_common::{Result, UniError};
15
16// Re-export core value types from uni-common.
17#[doc(inline)]
18pub use uni_common::value::{Edge, FromValue, Node, Path, Value};
19
20/// Single result row from a query.
21#[derive(Debug, Clone)]
22pub struct Row {
23    /// Column names shared across all rows in a result set.
24    pub columns: Arc<Vec<String>>,
25    /// Column values for this row.
26    pub values: Vec<Value>,
27}
28
29impl Row {
30    /// Gets a typed value by column name.
31    ///
32    /// # Errors
33    ///
34    /// Returns `UniError::Query` if the column is missing,
35    /// or `UniError::Type` if it cannot be converted.
36    pub fn get<T: FromValue>(&self, column: &str) -> Result<T> {
37        let idx = self
38            .columns
39            .iter()
40            .position(|c| c == column)
41            .ok_or_else(|| UniError::Query {
42                message: format!("Column '{}' not found", column),
43                query: None,
44            })?;
45        self.get_idx(idx)
46    }
47
48    /// Gets a typed value by column index.
49    ///
50    /// # Errors
51    ///
52    /// Returns `UniError::Query` if the index is out of bounds,
53    /// or `UniError::Type` if it cannot be converted.
54    pub fn get_idx<T: FromValue>(&self, index: usize) -> Result<T> {
55        if index >= self.values.len() {
56            return Err(UniError::Query {
57                message: format!("Column index {} out of bounds", index),
58                query: None,
59            });
60        }
61        T::from_value(&self.values[index])
62    }
63
64    /// Tries to get a typed value, returning `None` on failure.
65    pub fn try_get<T: FromValue>(&self, column: &str) -> Option<T> {
66        self.get(column).ok()
67    }
68
69    /// Gets the raw `Value` by column name.
70    pub fn value(&self, column: &str) -> Option<&Value> {
71        let idx = self.columns.iter().position(|c| c == column)?;
72        self.values.get(idx)
73    }
74
75    /// Returns all column-value pairs as a map.
76    pub fn as_map(&self) -> HashMap<&str, &Value> {
77        self.columns
78            .iter()
79            .zip(&self.values)
80            .map(|(col, val)| (col.as_str(), val))
81            .collect()
82    }
83
84    /// Converts this row to a JSON object.
85    pub fn to_json(&self) -> serde_json::Value {
86        serde_json::to_value(self.as_map()).unwrap_or(serde_json::Value::Null)
87    }
88}
89
90impl std::ops::Index<usize> for Row {
91    type Output = Value;
92    fn index(&self, index: usize) -> &Self::Output {
93        &self.values[index]
94    }
95}
96
97/// Warnings emitted during query execution.
98///
99/// Warnings indicate potential issues but do not prevent the query from
100/// completing.
101#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
102pub enum QueryWarning {
103    /// An index is unavailable (e.g., still being rebuilt).
104    IndexUnavailable {
105        /// The label that the index is for.
106        label: String,
107        /// The name of the unavailable index.
108        index_name: String,
109        /// Reason the index is unavailable.
110        reason: String,
111    },
112    /// A property filter could not use an index.
113    NoIndexForFilter {
114        /// The label being filtered.
115        label: String,
116        /// The property being filtered.
117        property: String,
118    },
119    /// Generic warning message.
120    Other(String),
121}
122
123impl std::fmt::Display for QueryWarning {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self {
126            QueryWarning::IndexUnavailable {
127                label,
128                index_name,
129                reason,
130            } => {
131                write!(
132                    f,
133                    "Index '{}' on label '{}' is unavailable: {}",
134                    index_name, label, reason
135                )
136            }
137            QueryWarning::NoIndexForFilter { label, property } => {
138                write!(
139                    f,
140                    "No index available for filter on {}.{}, using full scan",
141                    label, property
142                )
143            }
144            QueryWarning::Other(msg) => write!(f, "{}", msg),
145        }
146    }
147}
148
149/// Collection of query result rows.
150#[derive(Debug)]
151pub struct QueryResult {
152    /// Column names shared across all rows.
153    pub columns: Arc<Vec<String>>,
154    /// Result rows.
155    pub rows: Vec<Row>,
156    /// Warnings emitted during query execution.
157    pub warnings: Vec<QueryWarning>,
158}
159
160impl QueryResult {
161    /// Returns the column names.
162    pub fn columns(&self) -> &[String] {
163        &self.columns
164    }
165
166    /// Returns the number of rows.
167    pub fn len(&self) -> usize {
168        self.rows.len()
169    }
170
171    /// Returns `true` if there are no rows.
172    pub fn is_empty(&self) -> bool {
173        self.rows.is_empty()
174    }
175
176    /// Returns all rows.
177    pub fn rows(&self) -> &[Row] {
178        &self.rows
179    }
180
181    /// Consumes the result, returning the rows.
182    pub fn into_rows(self) -> Vec<Row> {
183        self.rows
184    }
185
186    /// Returns an iterator over the rows.
187    pub fn iter(&self) -> impl Iterator<Item = &Row> {
188        self.rows.iter()
189    }
190
191    /// Returns warnings emitted during execution.
192    pub fn warnings(&self) -> &[QueryWarning] {
193        &self.warnings
194    }
195
196    /// Returns `true` if the query produced any warnings.
197    pub fn has_warnings(&self) -> bool {
198        !self.warnings.is_empty()
199    }
200}
201
202impl IntoIterator for QueryResult {
203    type Item = Row;
204    type IntoIter = std::vec::IntoIter<Row>;
205
206    fn into_iter(self) -> Self::IntoIter {
207        self.rows.into_iter()
208    }
209}
210
211/// Result of a write operation (CREATE, SET, DELETE, etc.).
212#[derive(Debug)]
213pub struct ExecuteResult {
214    /// Number of entities affected.
215    pub affected_rows: usize,
216}
217
218/// Cursor-based result streaming for large result sets.
219pub struct QueryCursor {
220    /// Column names shared across all rows.
221    pub columns: Arc<Vec<String>>,
222    /// Async stream of row batches.
223    pub stream: Pin<Box<dyn Stream<Item = Result<Vec<Row>>> + Send>>,
224}
225
226impl QueryCursor {
227    /// Returns the column names.
228    pub fn columns(&self) -> &[String] {
229        &self.columns
230    }
231
232    /// Fetches the next batch of rows.
233    pub async fn next_batch(&mut self) -> Option<Result<Vec<Row>>> {
234        use futures::StreamExt;
235        self.stream.next().await
236    }
237
238    /// Consumes all remaining rows into a single vector.
239    ///
240    /// # Errors
241    ///
242    /// Returns the first error encountered while streaming.
243    pub async fn collect_remaining(mut self) -> Result<Vec<Row>> {
244        use futures::StreamExt;
245        let mut rows = Vec::new();
246        while let Some(batch_res) = self.stream.next().await {
247            rows.extend(batch_res?);
248        }
249        Ok(rows)
250    }
251}