1use 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 pub columns: Vec<String>,
21 pub column_types: Vec<Option<ColumnType>>,
25 pub rows: Vec<ResultRow>,
29 #[doc(hidden)]
33 pub positional_rows: Option<Vec<Vec<Value>>>,
34 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 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}