Skip to main content

uqa_sql/
result.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Result rows returned by `Engine::sql`.
8
9use std::collections::BTreeMap;
10
11use uqa_core::Value;
12
13use crate::ast::ColumnType;
14use crate::RowSchema;
15
16mod labels;
17mod text;
18pub use labels::render_result_enum_labels;
19pub use text::{format_postgres_text, format_postgres_text_with_catalog};
20
21pub type ResultRow = BTreeMap<String, Value>;
22
23/// Whether execution produced a row descriptor, independently of its column or row count.
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub enum SQLResultKind {
26    #[default]
27    Command,
28    Rows,
29    /// An external result source did not provide descriptor information.
30    Unknown,
31}
32
33#[derive(Debug, Clone, Default)]
34pub struct SQLResult {
35    /// Descriptor presence, including zero-column and empty row results.
36    pub kind: SQLResultKind,
37    /// `PostgreSQL` command completion, including its command-specific row count. Execution sets this from the command that actually ran; row constructors leave it absent because rows alone do not identify a SQL command.
38    pub command_tag: Option<String>,
39    /// Column order as the SELECT clause specified.
40    pub columns: Vec<String>,
41    /// Statically bound SQL type for each output position. A missing entry
42    /// represents a type that has not yet been resolved, never a type inferred
43    /// from the first runtime value.
44    pub column_types: Vec<Option<ColumnType>>,
45    /// Complete, densely ordered native output metadata, including selected collations and nested record descriptors. External result sources may omit it.
46    pub row_schema: Option<RowSchema>,
47    /// One row per result document, with the named columns in
48    /// `columns`. Extra columns from `_score` etc. are included here
49    /// too.
50    pub rows: Vec<ResultRow>,
51    /// Positional values for result sets whose output contains repeated column
52    /// labels. `rows` remains available for named lookup, while this carrier
53    /// preserves values that cannot be represented by a string-keyed map.
54    #[doc(hidden)]
55    pub positional_rows: Option<Vec<Vec<Value>>>,
56    /// Number of rows touched by an INSERT / UPDATE / DELETE.
57    pub affected_rows: u64,
58}
59
60impl SQLResult {
61    /// Attach the completion chosen by the executing SQL command.
62    pub fn with_command_tag(mut self, tag: impl Into<String>) -> Self {
63        self.command_tag = Some(tag.into());
64        self
65    }
66
67    pub fn empty() -> Self {
68        Self::default()
69    }
70
71    pub fn from_rows(columns: Vec<String>, rows: Vec<ResultRow>) -> Self {
72        let column_types = vec![None; columns.len()];
73        Self {
74            kind: SQLResultKind::Rows,
75            command_tag: None,
76            columns,
77            column_types,
78            row_schema: None,
79            rows,
80            positional_rows: None,
81            affected_rows: 0,
82        }
83    }
84
85    pub fn from_rows_with_positions(
86        columns: Vec<String>,
87        rows: Vec<ResultRow>,
88        positional_rows: Option<Vec<Vec<Value>>>,
89    ) -> Self {
90        let column_types = vec![None; columns.len()];
91        Self::from_typed_rows_with_positions(columns, column_types, rows, positional_rows)
92    }
93
94    pub fn from_typed_rows_with_positions(
95        columns: Vec<String>,
96        column_types: Vec<Option<ColumnType>>,
97        mut rows: Vec<ResultRow>,
98        positional_rows: Option<Vec<Vec<Value>>>,
99    ) -> Self {
100        debug_assert_eq!(columns.len(), column_types.len());
101        debug_assert!(positional_rows.as_ref().is_none_or(|values| {
102            values.len() == rows.len() && values.iter().all(|row| row.len() == columns.len())
103        }));
104        if let Some(positional_rows) = positional_rows.as_ref() {
105            let compatibility_labels = unique_compatibility_labels(&columns);
106            for (row, positional) in rows.iter_mut().zip(positional_rows) {
107                for (label, value) in compatibility_labels.iter().zip(positional) {
108                    row.insert(label.clone(), value.clone());
109                }
110            }
111        }
112        Self {
113            kind: SQLResultKind::Rows,
114            command_tag: None,
115            columns,
116            column_types,
117            row_schema: None,
118            rows,
119            positional_rows,
120            affected_rows: 0,
121        }
122    }
123
124    /// Retain a native output descriptor independently of whether execution returned any rows.
125    pub fn from_schema_rows_with_positions(
126        schema: RowSchema,
127        rows: Vec<ResultRow>,
128        positional_rows: Option<Vec<Vec<Value>>>,
129    ) -> Self {
130        let mut result = Self::from_typed_rows_with_positions(
131            schema.columns().to_vec(),
132            schema.column_types().to_vec(),
133            rows,
134            positional_rows,
135        );
136        result.row_schema = Some(schema);
137        result
138    }
139
140    /// Borrow the native descriptor, or construct the metadata supplied by a legacy or external result source. Runtime values never determine declared types or collations.
141    pub fn row_schema(&self) -> std::borrow::Cow<'_, RowSchema> {
142        self.row_schema.as_ref().map_or_else(
143            || {
144                std::borrow::Cow::Owned(RowSchema::with_types(
145                    self.columns.clone(),
146                    self.column_types.clone(),
147                ))
148            },
149            std::borrow::Cow::Borrowed,
150        )
151    }
152
153    /// Return a result value by row and output-column position.
154    ///
155    /// Positional access is the canonical way to distinguish repeated output
156    /// labels. Named rows remain available for compatibility with existing
157    /// callers.
158    pub fn value_at(&self, row: usize, column: usize) -> Option<&Value> {
159        self.positional_rows
160            .as_ref()
161            .and_then(|rows| rows.get(row))
162            .and_then(|row| row.get(column))
163            .or_else(|| {
164                self.columns
165                    .get(column)
166                    .and_then(|name| self.rows.get(row)?.get(name))
167            })
168    }
169
170    pub fn from_affected(affected: u64) -> Self {
171        Self {
172            affected_rows: affected,
173            ..Self::default()
174        }
175    }
176}
177
178fn unique_compatibility_labels(columns: &[String]) -> Vec<String> {
179    let mut labels = Vec::with_capacity(columns.len());
180    for base in columns {
181        let mut label = base.clone();
182        let mut suffix = 1usize;
183        while labels.contains(&label) {
184            label = format!("{base}_{suffix}");
185            suffix += 1;
186        }
187        labels.push(label);
188    }
189    labels
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn result_metadata_survives_empty_rows_and_shared_result_clones() {
198        let schema = RowSchema::with_column_metadata(
199            None,
200            [
201                ("value".into(), Some(ColumnType::Text), Some(950)),
202                ("value".into(), Some(ColumnType::Text), Some(951)),
203            ],
204        );
205        for rows in [Vec::new(), vec![vec![Value::Null, Value::Null]]] {
206            let result = SQLResult::from_schema_rows_with_positions(
207                schema.clone(),
208                vec![ResultRow::new(); rows.len()],
209                Some(rows),
210            );
211            let retained = result.row_schema();
212            assert!(std::ptr::eq(retained.columns(), schema.columns()));
213            assert!(matches!(retained, std::borrow::Cow::Borrowed(_)));
214            assert!(std::ptr::eq(
215                retained.as_ref(),
216                result.row_schema.as_ref().unwrap()
217            ));
218            assert_eq!(result.kind, SQLResultKind::Rows);
219            assert_eq!(retained.column_collation(0), Some(950));
220            assert_eq!(retained.column_collation(1), Some(951));
221            assert_eq!(*result.clone().row_schema(), schema);
222        }
223    }
224
225    #[test]
226    fn repeated_postgresql_labels_keep_unique_named_compatibility_keys() {
227        let result = SQLResult::from_rows_with_positions(
228            vec!["value".into(), "value".into()],
229            vec![ResultRow::from([("value".into(), Value::Int(6))])],
230            Some(vec![vec![Value::Int(5), Value::Int(6)]]),
231        );
232
233        assert_eq!(result.columns, ["value", "value"]);
234        assert_eq!(result.rows[0].get("value"), Some(&Value::Int(5)));
235        assert_eq!(result.rows[0].get("value_1"), Some(&Value::Int(6)));
236        assert_eq!(result.value_at(0, 0), Some(&Value::Int(5)));
237        assert_eq!(result.value_at(0, 1), Some(&Value::Int(6)));
238    }
239}
240
241pub mod completion;
242pub mod vector;
243
244/// Execution measurements supplied to EXPLAIN rendering.
245pub struct ExplainAnalysis {
246    pub vector_searches: vector::ExplainVectorSearches,
247    pub elapsed: std::time::Duration,
248    pub rows: u64,
249    pub affected_rows: u64,
250}
251
252/// Structured physical-plan output captured before execution. Method-specific properties are rendered by the owning planner; execution only carries this diagnostic result to the renderer.
253#[derive(Debug, Clone, Default)]
254pub struct ExplainPhysicalPlan {
255    pub nodes: Vec<serde_json::Value>,
256}