Skip to main content

dataprof_db/
query_columns.rs

1//! Column-oriented query results that keep the query's column order.
2//!
3//! The connectors used to hand back `HashMap<String, Vec<String>>`, which has no
4//! order at all: `SELECT a, b FROM t` could profile as `["b", "a"]`, and hash
5//! iteration is not even stable between processes, so two runs of the same query
6//! could disagree. Every other input path reports columns in source order — CSV
7//! header order, Parquet schema order, JSON first-seen field order — so the
8//! database path was the one place where a format conversion reshuffled a
9//! report.
10//!
11//! [`QueryColumns`] is that map with the order kept: a vector of named columns,
12//! built in the order the driver reports them.
13
14use std::collections::HashMap;
15use std::ops::Index;
16
17/// A query result as columns, in the order the query selected them.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct QueryColumns {
20    columns: Vec<(String, Vec<String>)>,
21}
22
23impl QueryColumns {
24    /// An empty result with no columns.
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Build empty columns for `names`, in order, each sized for `row_capacity`
30    /// values.
31    ///
32    /// Callers then fill them positionally with [`push_value`](Self::push_value),
33    /// which is what ties a value to the column the driver read it from.
34    pub fn with_names<I, S>(names: I, row_capacity: usize) -> Self
35    where
36        I: IntoIterator<Item = S>,
37        S: Into<String>,
38    {
39        Self {
40            columns: names
41                .into_iter()
42                .map(|name| (name.into(), Vec::with_capacity(row_capacity)))
43                .collect(),
44        }
45    }
46
47    /// Append a value to the column at `index`.
48    ///
49    /// Out-of-range indices are ignored; callers iterate the same column list
50    /// they built the result from, so there is no in-range/out-of-range decision
51    /// for them to get wrong.
52    pub fn push_value(&mut self, index: usize, value: String) {
53        if let Some((_, data)) = self.columns.get_mut(index) {
54            data.push(value);
55        }
56    }
57
58    /// Number of columns.
59    pub fn len(&self) -> usize {
60        self.columns.len()
61    }
62
63    /// Whether the result has no columns.
64    pub fn is_empty(&self) -> bool {
65        self.columns.is_empty()
66    }
67
68    /// Rows in the result, read off the first column.
69    pub fn row_count(&self) -> usize {
70        self.columns.first().map_or(0, |(_, data)| data.len())
71    }
72
73    /// Column names, in query order.
74    pub fn names(&self) -> impl Iterator<Item = &str> {
75        self.columns.iter().map(|(name, _)| name.as_str())
76    }
77
78    /// Column data, in query order.
79    pub fn values(&self) -> impl Iterator<Item = &Vec<String>> {
80        self.columns.iter().map(|(_, data)| data)
81    }
82
83    /// Name/data pairs, in query order.
84    pub fn iter(&self) -> impl Iterator<Item = (&str, &Vec<String>)> {
85        self.columns
86            .iter()
87            .map(|(name, data)| (name.as_str(), data))
88    }
89
90    /// Mutable column data, in query order.
91    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Vec<String>> {
92        self.columns.iter_mut().map(|(_, data)| data)
93    }
94
95    /// The data for `name`, if the result has such a column.
96    pub fn get(&self, name: &str) -> Option<&Vec<String>> {
97        self.columns
98            .iter()
99            .find(|(column, _)| column == name)
100            .map(|(_, data)| data)
101    }
102
103    /// Drop the order and hand back a plain map.
104    ///
105    /// For consumers keyed purely by name — quality metrics look every column up
106    /// by name and never iterate for presentation.
107    pub fn into_map(self) -> HashMap<String, Vec<String>> {
108        self.columns.into_iter().collect()
109    }
110
111    /// Append a batch's values to the matching columns.
112    ///
113    /// Columns are matched by name so a driver that reorders between batches
114    /// cannot interleave data; a column seen for the first time in a later batch
115    /// is appended at the end, the same first-seen rule the JSON path uses for
116    /// fields that only appear in later records.
117    fn extend_from(&mut self, batch: QueryColumns) {
118        for (name, data) in batch.columns {
119            match self.columns.iter_mut().find(|(column, _)| *column == name) {
120                Some((_, existing)) => existing.extend(data),
121                None => self.columns.push((name, data)),
122            }
123        }
124    }
125}
126
127impl Index<&str> for QueryColumns {
128    type Output = Vec<String>;
129
130    fn index(&self, name: &str) -> &Self::Output {
131        self.get(name)
132            .unwrap_or_else(|| panic!("no column named {name} in query result"))
133    }
134}
135
136impl FromIterator<(String, Vec<String>)> for QueryColumns {
137    fn from_iter<I: IntoIterator<Item = (String, Vec<String>)>>(iter: I) -> Self {
138        Self {
139            columns: iter.into_iter().collect(),
140        }
141    }
142}
143
144impl IntoIterator for QueryColumns {
145    type Item = (String, Vec<String>);
146    type IntoIter = std::vec::IntoIter<(String, Vec<String>)>;
147
148    fn into_iter(self) -> Self::IntoIter {
149        self.columns.into_iter()
150    }
151}
152
153/// Merge batches of a streamed query into one result.
154///
155/// The first batch fixes the column order; later batches append to it.
156pub fn merge_column_batches(batches: Vec<QueryColumns>) -> QueryColumns {
157    let mut merged = QueryColumns::new();
158    for batch in batches {
159        merged.extend_from(batch);
160    }
161    merged
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn columns(pairs: &[(&str, &[&str])]) -> QueryColumns {
169        pairs
170            .iter()
171            .map(|(name, data)| {
172                (
173                    (*name).to_string(),
174                    data.iter().map(|v| (*v).to_string()).collect(),
175                )
176            })
177            .collect()
178    }
179
180    #[test]
181    fn names_come_back_in_the_order_they_went_in() {
182        // Deliberately non-alphabetical: sorting or hashing this yields a
183        // different order, so a regression is visible rather than accidental.
184        let result = QueryColumns::with_names(["id", "amount", "active"], 0);
185        assert_eq!(
186            result.names().collect::<Vec<_>>(),
187            ["id", "amount", "active"]
188        );
189    }
190
191    #[test]
192    fn values_land_in_the_column_they_were_pushed_to() {
193        let mut result = QueryColumns::with_names(["b", "a"], 2);
194        result.push_value(0, "b1".to_string());
195        result.push_value(1, "a1".to_string());
196        result.push_value(0, "b2".to_string());
197        result.push_value(1, "a2".to_string());
198
199        assert_eq!(result["b"], vec!["b1", "b2"]);
200        assert_eq!(result["a"], vec!["a1", "a2"]);
201        assert_eq!(result.row_count(), 2);
202    }
203
204    #[test]
205    fn merging_keeps_the_first_batch_order() {
206        let merged = merge_column_batches(vec![
207            columns(&[("id", &["1"]), ("cap", &["20121"])]),
208            columns(&[("id", &["2"]), ("cap", &["00184"])]),
209        ]);
210
211        assert_eq!(merged.names().collect::<Vec<_>>(), ["id", "cap"]);
212        assert_eq!(merged["id"], vec!["1", "2"]);
213        assert_eq!(merged["cap"], vec!["20121", "00184"]);
214    }
215
216    #[test]
217    fn merging_matches_columns_by_name_not_position() {
218        // A driver that hands back a later batch in another order must not have
219        // its values interleaved into the wrong column.
220        let merged = merge_column_batches(vec![
221            columns(&[("id", &["1"]), ("cap", &["20121"])]),
222            columns(&[("cap", &["00184"]), ("id", &["2"])]),
223        ]);
224
225        assert_eq!(merged.names().collect::<Vec<_>>(), ["id", "cap"]);
226        assert_eq!(merged["id"], vec!["1", "2"]);
227        assert_eq!(merged["cap"], vec!["20121", "00184"]);
228    }
229
230    #[test]
231    fn merging_nothing_yields_nothing() {
232        let merged = merge_column_batches(Vec::new());
233        assert!(merged.is_empty());
234        assert_eq!(merged.row_count(), 0);
235    }
236}