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