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    /// RRF fusion was requested in point-computation context where no global
120    /// ranking is available, so it degenerated to equal-weight fusion.
121    RrfPointContext,
122    /// Generic warning message.
123    Other(String),
124}
125
126impl std::fmt::Display for QueryWarning {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            QueryWarning::IndexUnavailable {
130                label,
131                index_name,
132                reason,
133            } => {
134                write!(
135                    f,
136                    "Index '{}' on label '{}' is unavailable: {}",
137                    index_name, label, reason
138                )
139            }
140            QueryWarning::NoIndexForFilter { label, property } => {
141                write!(
142                    f,
143                    "No index available for filter on {}.{}, using full scan",
144                    label, property
145                )
146            }
147            QueryWarning::RrfPointContext => {
148                write!(
149                    f,
150                    "RRF fusion degenerated to equal-weight fusion in point-computation context \
151                     (no global ranking available). Consider using method: 'weighted' with explicit weights."
152                )
153            }
154            QueryWarning::Other(msg) => write!(f, "{}", msg),
155        }
156    }
157}
158
159/// Collection of query result rows.
160#[derive(Debug)]
161pub struct QueryResult {
162    /// Column names shared across all rows.
163    pub columns: Arc<Vec<String>>,
164    /// Result rows.
165    pub rows: Vec<Row>,
166    /// Warnings emitted during query execution.
167    pub warnings: Vec<QueryWarning>,
168}
169
170impl QueryResult {
171    /// Returns the column names.
172    pub fn columns(&self) -> &[String] {
173        &self.columns
174    }
175
176    /// Returns the number of rows.
177    pub fn len(&self) -> usize {
178        self.rows.len()
179    }
180
181    /// Returns `true` if there are no rows.
182    pub fn is_empty(&self) -> bool {
183        self.rows.is_empty()
184    }
185
186    /// Returns all rows.
187    pub fn rows(&self) -> &[Row] {
188        &self.rows
189    }
190
191    /// Consumes the result, returning the rows.
192    pub fn into_rows(self) -> Vec<Row> {
193        self.rows
194    }
195
196    /// Returns an iterator over the rows.
197    pub fn iter(&self) -> impl Iterator<Item = &Row> {
198        self.rows.iter()
199    }
200
201    /// Returns warnings emitted during execution.
202    pub fn warnings(&self) -> &[QueryWarning] {
203        &self.warnings
204    }
205
206    /// Returns `true` if the query produced any warnings.
207    pub fn has_warnings(&self) -> bool {
208        !self.warnings.is_empty()
209    }
210}
211
212impl IntoIterator for QueryResult {
213    type Item = Row;
214    type IntoIter = std::vec::IntoIter<Row>;
215
216    fn into_iter(self) -> Self::IntoIter {
217        self.rows.into_iter()
218    }
219}
220
221/// Result of a write operation (CREATE, SET, DELETE, etc.).
222#[derive(Debug)]
223pub struct ExecuteResult {
224    /// Number of entities affected.
225    pub affected_rows: usize,
226}
227
228/// Cursor-based result streaming for large result sets.
229pub struct QueryCursor {
230    /// Column names shared across all rows.
231    pub columns: Arc<Vec<String>>,
232    /// Async stream of row batches.
233    pub stream: Pin<Box<dyn Stream<Item = Result<Vec<Row>>> + Send>>,
234}
235
236impl QueryCursor {
237    /// Returns the column names.
238    pub fn columns(&self) -> &[String] {
239        &self.columns
240    }
241
242    /// Fetches the next batch of rows.
243    pub async fn next_batch(&mut self) -> Option<Result<Vec<Row>>> {
244        use futures::StreamExt;
245        self.stream.next().await
246    }
247
248    /// Consumes all remaining rows into a single vector.
249    ///
250    /// # Errors
251    ///
252    /// Returns the first error encountered while streaming.
253    pub async fn collect_remaining(mut self) -> Result<Vec<Row>> {
254        use futures::StreamExt;
255        let mut rows = Vec::new();
256        while let Some(batch_res) = self.stream.next().await {
257            rows.extend(batch_res?);
258        }
259        Ok(rows)
260    }
261}