Skip to main content

wasm_dbms_api/dbms/query/
join_result.rs

1//! Result of a join query: column descriptions once, then rows of values.
2
3use serde::{Deserialize, Serialize};
4
5use crate::dbms::table::JoinColumnDef;
6use crate::dbms::value::Value;
7
8/// Result of [`Database::select_join`](crate::prelude::Database::select_join).
9///
10/// The column descriptions are stored once in `columns`; every entry of
11/// `rows` holds one [`Value`] per column, in the same order. Use
12/// [`Self::column_index`] to resolve a column name once and index each row,
13/// or [`Self::iter`] to walk the rows as [`JoinRow`] views.
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[cfg_attr(feature = "candid", derive(candid::CandidType))]
16pub struct JoinResultSet {
17    /// One description per output column, in row order. The `table` field
18    /// names the table each column comes from.
19    pub columns: Vec<JoinColumnDef>,
20    /// One entry per row; `rows[r][c]` belongs to `columns[c]`.
21    pub rows: Vec<Vec<Value>>,
22}
23
24impl JoinResultSet {
25    /// Returns the number of rows.
26    pub fn len(&self) -> usize {
27        self.rows.len()
28    }
29
30    /// Returns `true` when the result has no rows.
31    pub fn is_empty(&self) -> bool {
32        self.rows.is_empty()
33    }
34
35    /// Returns the index of `column` in [`Self::columns`].
36    ///
37    /// `"table.column"` matches the column of that table. A bare name matches
38    /// the first column with that name in row order.
39    pub fn column_index(&self, column: &str) -> Option<usize> {
40        column_index(&self.columns, column)
41    }
42
43    /// Returns a view over the row at `index`, if it exists.
44    pub fn row(&self, index: usize) -> Option<JoinRow<'_>> {
45        self.rows.get(index).map(|values| JoinRow {
46            columns: &self.columns,
47            values,
48        })
49    }
50
51    /// Iterates over the rows as [`JoinRow`] views.
52    pub fn iter(&self) -> impl Iterator<Item = JoinRow<'_>> {
53        self.rows.iter().map(|values| JoinRow {
54            columns: &self.columns,
55            values,
56        })
57    }
58}
59
60impl<'a> IntoIterator for &'a JoinResultSet {
61    type Item = JoinRow<'a>;
62    type IntoIter = JoinRows<'a>;
63
64    fn into_iter(self) -> Self::IntoIter {
65        JoinRows {
66            columns: &self.columns,
67            rows: self.rows.iter(),
68        }
69    }
70}
71
72/// Iterator over the rows of a [`JoinResultSet`], yielding [`JoinRow`] views.
73#[derive(Debug)]
74pub struct JoinRows<'a> {
75    columns: &'a [JoinColumnDef],
76    rows: std::slice::Iter<'a, Vec<Value>>,
77}
78
79impl<'a> Iterator for JoinRows<'a> {
80    type Item = JoinRow<'a>;
81
82    fn next(&mut self) -> Option<Self::Item> {
83        self.rows.next().map(|values| JoinRow {
84            columns: self.columns,
85            values,
86        })
87    }
88}
89
90/// A borrowed row of a [`JoinResultSet`], paired with its column descriptions.
91#[derive(Debug, Clone, Copy)]
92pub struct JoinRow<'a> {
93    columns: &'a [JoinColumnDef],
94    values: &'a [Value],
95}
96
97impl<'a> JoinRow<'a> {
98    /// Returns the column descriptions of the result set.
99    pub fn columns(&self) -> &'a [JoinColumnDef] {
100        self.columns
101    }
102
103    /// Returns the values of this row, in column order.
104    pub fn values(&self) -> &'a [Value] {
105        self.values
106    }
107
108    /// Returns the number of values in the row.
109    pub fn len(&self) -> usize {
110        self.values.len()
111    }
112
113    /// Returns `true` when the row has no values.
114    pub fn is_empty(&self) -> bool {
115        self.values.is_empty()
116    }
117
118    /// Returns the value of `column`, resolved like
119    /// [`JoinResultSet::column_index`].
120    pub fn get(&self, column: &str) -> Option<&'a Value> {
121        column_index(self.columns, column).and_then(|index| self.values.get(index))
122    }
123
124    /// Iterates over `(column, value)` pairs.
125    pub fn iter(&self) -> impl Iterator<Item = (&'a JoinColumnDef, &'a Value)> + use<'a> {
126        self.columns.iter().zip(self.values)
127    }
128}
129
130/// Resolves a qualified or bare column name against `columns`.
131fn column_index(columns: &[JoinColumnDef], column: &str) -> Option<usize> {
132    match column.split_once('.') {
133        Some((table, name)) => columns
134            .iter()
135            .position(|def| def.table.as_deref() == Some(table) && def.name == name),
136        None => columns.iter().position(|def| def.name == column),
137    }
138}
139
140#[cfg(test)]
141mod tests {
142
143    use super::*;
144    use crate::dbms::table::ColumnDef;
145    use crate::dbms::types::{DataTypeKind, Text};
146
147    fn column(table: &str, name: &'static str) -> JoinColumnDef {
148        let mut def = JoinColumnDef::from(ColumnDef {
149            name,
150            data_type: DataTypeKind::Text,
151            auto_increment: false,
152            nullable: false,
153            primary_key: false,
154            unique: false,
155            foreign_key: None,
156            default: None,
157            renamed_from: &[],
158        });
159        def.table = Some(table.to_string());
160        def
161    }
162
163    fn text(value: &str) -> Value {
164        Value::Text(Text(value.to_string()))
165    }
166
167    fn result_set() -> JoinResultSet {
168        JoinResultSet {
169            columns: vec![
170                column("users", "id"),
171                column("users", "name"),
172                column("posts", "id"),
173                column("posts", "title"),
174            ],
175            rows: vec![
176                vec![text("1"), text("alice"), text("10"), text("hello")],
177                vec![text("1"), text("alice"), text("11"), text("world")],
178            ],
179        }
180    }
181
182    #[test]
183    fn test_should_report_length_and_emptiness() {
184        let set = result_set();
185        assert_eq!(set.len(), 2);
186        assert!(!set.is_empty());
187        assert!(JoinResultSet::default().is_empty());
188    }
189
190    #[test]
191    fn test_should_resolve_qualified_and_unqualified_column_index() {
192        let set = result_set();
193        assert_eq!(set.column_index("posts.id"), Some(2));
194        assert_eq!(set.column_index("users.id"), Some(0));
195        assert_eq!(set.column_index("id"), Some(0));
196        assert_eq!(set.column_index("title"), Some(3));
197        assert_eq!(set.column_index("comments.id"), None);
198        assert_eq!(set.column_index("missing"), None);
199    }
200
201    #[test]
202    fn test_should_read_row_values_by_name() {
203        let set = result_set();
204        let row = set.row(1).expect("second row exists");
205        assert_eq!(row.len(), 4);
206        assert_eq!(row.get("posts.title"), Some(&text("world")));
207        assert_eq!(row.get("name"), Some(&text("alice")));
208        assert_eq!(row.get("posts.missing"), None);
209        assert!(set.row(2).is_none());
210    }
211
212    #[test]
213    fn test_should_iterate_rows_zipped_with_columns() {
214        let set = result_set();
215        let pairs: Vec<(Option<&str>, &str, &Value)> = set
216            .iter()
217            .flat_map(|row| row.iter())
218            .map(|(def, value)| (def.table.as_deref(), def.name.as_str(), value))
219            .collect();
220        assert_eq!(pairs.len(), 8);
221        assert_eq!(pairs[2], (Some("posts"), "id", &text("10")));
222        assert_eq!(pairs[7], (Some("posts"), "title", &text("world")));
223
224        let mut count = 0;
225        for row in &set {
226            assert_eq!(row.columns().len(), 4);
227            assert_eq!(row.values().len(), 4);
228            count += 1;
229        }
230        assert_eq!(count, 2);
231    }
232
233    #[test]
234    fn test_should_serialize_as_columns_and_rows() {
235        let set = result_set();
236        let json = serde_json::to_value(&set).expect("serialize");
237        assert_eq!(json["columns"].as_array().map(Vec::len), Some(4));
238        assert_eq!(json["rows"].as_array().map(Vec::len), Some(2));
239        let back: JoinResultSet = serde_json::from_value(json).expect("deserialize");
240        assert_eq!(back, set);
241    }
242}