1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub enum SQLResultKind {
23 #[default]
24 Command,
25 Rows,
26 Unknown,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct SQLResult {
32 pub kind: SQLResultKind,
34 pub command_tag: Option<String>,
36 pub columns: Vec<String>,
38 pub column_types: Vec<Option<ColumnType>>,
42 pub rows: Vec<ResultRow>,
46 #[doc(hidden)]
50 pub positional_rows: Option<Vec<Vec<Value>>>,
51 pub affected_rows: u64,
53}
54
55impl SQLResult {
56 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 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
178pub struct ExplainAnalysis {
180 pub elapsed: std::time::Duration,
181 pub rows: u64,
182 pub affected_rows: u64,
183}