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;
14
15mod text;
16pub use text::format_postgres_text;
17
18pub type ResultRow = BTreeMap<String, Value>;
19
20/// Whether execution produced a row descriptor, independently of its column or row count.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub enum SQLResultKind {
23    #[default]
24    Command,
25    Rows,
26    /// An external result source did not provide descriptor information.
27    Unknown,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct SQLResult {
32    /// Descriptor presence, including zero-column and empty row results.
33    pub kind: SQLResultKind,
34    /// `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.
35    pub command_tag: Option<String>,
36    /// Column order as the SELECT clause specified.
37    pub columns: Vec<String>,
38    /// Statically bound SQL type for each output position. A missing entry
39    /// represents a type that has not yet been resolved, never a type inferred
40    /// from the first runtime value.
41    pub column_types: Vec<Option<ColumnType>>,
42    /// One row per result document, with the named columns in
43    /// `columns`. Extra columns from `_score` etc. are included here
44    /// too.
45    pub rows: Vec<ResultRow>,
46    /// Positional values for result sets whose output contains repeated column
47    /// labels. `rows` remains available for named lookup, while this carrier
48    /// preserves values that cannot be represented by a string-keyed map.
49    #[doc(hidden)]
50    pub positional_rows: Option<Vec<Vec<Value>>>,
51    /// Number of rows touched by an INSERT / UPDATE / DELETE.
52    pub affected_rows: u64,
53}
54
55impl SQLResult {
56    /// Attach the completion chosen by the executing SQL command.
57    pub fn with_command_tag(mut self, tag: impl Into<String>) -> Self {
58        self.command_tag = Some(tag.into());
59        self
60    }
61
62    pub fn empty() -> Self {
63        Self::default()
64    }
65
66    pub fn from_rows(columns: Vec<String>, rows: Vec<ResultRow>) -> Self {
67        let column_types = vec![None; columns.len()];
68        Self {
69            kind: SQLResultKind::Rows,
70            command_tag: None,
71            columns,
72            column_types,
73            rows,
74            positional_rows: None,
75            affected_rows: 0,
76        }
77    }
78
79    pub fn from_rows_with_positions(
80        columns: Vec<String>,
81        rows: Vec<ResultRow>,
82        positional_rows: Option<Vec<Vec<Value>>>,
83    ) -> Self {
84        let column_types = vec![None; columns.len()];
85        Self::from_typed_rows_with_positions(columns, column_types, rows, positional_rows)
86    }
87
88    pub fn from_typed_rows_with_positions(
89        columns: Vec<String>,
90        column_types: Vec<Option<ColumnType>>,
91        mut rows: Vec<ResultRow>,
92        positional_rows: Option<Vec<Vec<Value>>>,
93    ) -> Self {
94        debug_assert_eq!(columns.len(), column_types.len());
95        debug_assert!(positional_rows.as_ref().is_none_or(|values| {
96            values.len() == rows.len() && values.iter().all(|row| row.len() == columns.len())
97        }));
98        if let Some(positional_rows) = positional_rows.as_ref() {
99            let compatibility_labels = unique_compatibility_labels(&columns);
100            for (row, positional) in rows.iter_mut().zip(positional_rows) {
101                for (label, value) in compatibility_labels.iter().zip(positional) {
102                    row.insert(label.clone(), value.clone());
103                }
104            }
105        }
106        Self {
107            kind: SQLResultKind::Rows,
108            command_tag: None,
109            columns,
110            column_types,
111            rows,
112            positional_rows,
113            affected_rows: 0,
114        }
115    }
116
117    /// Return a result value by row and output-column position.
118    ///
119    /// Positional access is the canonical way to distinguish repeated output
120    /// labels. Named rows remain available for compatibility with existing
121    /// callers.
122    pub fn value_at(&self, row: usize, column: usize) -> Option<&Value> {
123        self.positional_rows
124            .as_ref()
125            .and_then(|rows| rows.get(row))
126            .and_then(|row| row.get(column))
127            .or_else(|| {
128                self.columns
129                    .get(column)
130                    .and_then(|name| self.rows.get(row)?.get(name))
131            })
132    }
133
134    pub fn from_affected(affected: u64) -> Self {
135        Self {
136            affected_rows: affected,
137            ..Self::default()
138        }
139    }
140}
141
142fn unique_compatibility_labels(columns: &[String]) -> Vec<String> {
143    let mut labels = Vec::with_capacity(columns.len());
144    for base in columns {
145        let mut label = base.clone();
146        let mut suffix = 1usize;
147        while labels.contains(&label) {
148            label = format!("{base}_{suffix}");
149            suffix += 1;
150        }
151        labels.push(label);
152    }
153    labels
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn repeated_postgresql_labels_keep_unique_named_compatibility_keys() {
162        let result = SQLResult::from_rows_with_positions(
163            vec!["value".into(), "value".into()],
164            vec![ResultRow::from([("value".into(), Value::Int(6))])],
165            Some(vec![vec![Value::Int(5), Value::Int(6)]]),
166        );
167
168        assert_eq!(result.columns, ["value", "value"]);
169        assert_eq!(result.rows[0].get("value"), Some(&Value::Int(5)));
170        assert_eq!(result.rows[0].get("value_1"), Some(&Value::Int(6)));
171        assert_eq!(result.value_at(0, 0), Some(&Value::Int(5)));
172        assert_eq!(result.value_at(0, 1), Some(&Value::Int(6)));
173    }
174}
175
176pub mod completion;
177
178/// Execution measurements supplied to EXPLAIN rendering.
179pub struct ExplainAnalysis {
180    pub elapsed: std::time::Duration,
181    pub rows: u64,
182    pub affected_rows: u64,
183}