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
15pub type ResultRow = BTreeMap<String, Value>;
16
17#[derive(Debug, Clone, Default)]
18pub struct SQLResult {
19    /// Column order as the SELECT clause specified.
20    pub columns: Vec<String>,
21    /// Statically bound SQL type for each output position. A missing entry
22    /// represents a type that has not yet been resolved, never a type inferred
23    /// from the first runtime value.
24    pub column_types: Vec<Option<ColumnType>>,
25    /// One row per result document, with the named columns in
26    /// `columns`. Extra columns from `_score` etc. are included here
27    /// too.
28    pub rows: Vec<ResultRow>,
29    /// Positional values for result sets whose output contains repeated column
30    /// labels. `rows` remains available for named lookup, while this carrier
31    /// preserves values that cannot be represented by a string-keyed map.
32    #[doc(hidden)]
33    pub positional_rows: Option<Vec<Vec<Value>>>,
34    /// Number of rows touched by an INSERT / UPDATE / DELETE.
35    pub affected_rows: u64,
36}
37
38impl SQLResult {
39    pub fn empty() -> Self {
40        Self::default()
41    }
42
43    pub fn from_rows(columns: Vec<String>, rows: Vec<ResultRow>) -> Self {
44        let column_types = vec![None; columns.len()];
45        Self {
46            columns,
47            column_types,
48            rows,
49            positional_rows: None,
50            affected_rows: 0,
51        }
52    }
53
54    pub fn from_rows_with_positions(
55        columns: Vec<String>,
56        rows: Vec<ResultRow>,
57        positional_rows: Option<Vec<Vec<Value>>>,
58    ) -> Self {
59        let column_types = vec![None; columns.len()];
60        Self::from_typed_rows_with_positions(columns, column_types, rows, positional_rows)
61    }
62
63    pub fn from_typed_rows_with_positions(
64        columns: Vec<String>,
65        column_types: Vec<Option<ColumnType>>,
66        mut rows: Vec<ResultRow>,
67        positional_rows: Option<Vec<Vec<Value>>>,
68    ) -> Self {
69        debug_assert_eq!(columns.len(), column_types.len());
70        debug_assert!(positional_rows.as_ref().is_none_or(|values| {
71            values.len() == rows.len() && values.iter().all(|row| row.len() == columns.len())
72        }));
73        if let Some(positional_rows) = positional_rows.as_ref() {
74            let compatibility_labels = unique_compatibility_labels(&columns);
75            for (row, positional) in rows.iter_mut().zip(positional_rows) {
76                for (label, value) in compatibility_labels.iter().zip(positional) {
77                    row.insert(label.clone(), value.clone());
78                }
79            }
80        }
81        Self {
82            columns,
83            column_types,
84            rows,
85            positional_rows,
86            affected_rows: 0,
87        }
88    }
89
90    /// Return a result value by row and output-column position.
91    ///
92    /// Positional access is the canonical way to distinguish repeated output
93    /// labels. Named rows remain available for compatibility with existing
94    /// callers.
95    pub fn value_at(&self, row: usize, column: usize) -> Option<&Value> {
96        self.positional_rows
97            .as_ref()
98            .and_then(|rows| rows.get(row))
99            .and_then(|row| row.get(column))
100            .or_else(|| {
101                self.columns
102                    .get(column)
103                    .and_then(|name| self.rows.get(row)?.get(name))
104            })
105    }
106
107    pub fn from_affected(affected: u64) -> Self {
108        Self {
109            affected_rows: affected,
110            ..Self::default()
111        }
112    }
113}
114
115fn unique_compatibility_labels(columns: &[String]) -> Vec<String> {
116    let mut labels = Vec::with_capacity(columns.len());
117    for base in columns {
118        let mut label = base.clone();
119        let mut suffix = 1usize;
120        while labels.contains(&label) {
121            label = format!("{base}_{suffix}");
122            suffix += 1;
123        }
124        labels.push(label);
125    }
126    labels
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn repeated_postgresql_labels_keep_unique_named_compatibility_keys() {
135        let result = SQLResult::from_rows_with_positions(
136            vec!["value".into(), "value".into()],
137            vec![ResultRow::from([("value".into(), Value::Int(6))])],
138            Some(vec![vec![Value::Int(5), Value::Int(6)]]),
139        );
140
141        assert_eq!(result.columns, ["value", "value"]);
142        assert_eq!(result.rows[0].get("value"), Some(&Value::Int(5)));
143        assert_eq!(result.rows[0].get("value_1"), Some(&Value::Int(6)));
144        assert_eq!(result.value_at(0, 0), Some(&Value::Int(5)));
145        assert_eq!(result.value_at(0, 1), Some(&Value::Int(6)));
146    }
147}