gluesql_core/
row_conversion.rs

1use serde::Serialize;
2
3pub fn uuid_to_string(value: u128) -> String {
4    let hex = format!("{value:032x}");
5    format!(
6        "{}-{}-{}-{}-{}",
7        &hex[0..8],
8        &hex[8..12],
9        &hex[12..16],
10        &hex[16..20],
11        &hex[20..32]
12    )
13}
14
15#[derive(Debug, thiserror::Error, PartialEq, Serialize)]
16pub enum RowConversionError {
17    #[error("not a select payload")]
18    NotSelectPayload,
19    #[error("missing column for field '{field}': {column}")]
20    MissingColumn {
21        field: &'static str,
22        column: &'static str,
23    },
24    #[error("null not allowed for field '{field}' at column '{column}'")]
25    NullNotAllowed { field: &'static str, column: String },
26    #[error("type mismatch: expected {expected}, got {got}")]
27    TypeMismatch {
28        field: Option<&'static str>,
29        column: Option<String>,
30        expected: &'static str,
31        got: &'static str,
32    },
33    #[error("not found")]
34    NotFound,
35    #[error("more than one row: {got}")]
36    MoreThanOneRow { got: usize },
37}
38
39pub trait FromGlueRow: Sized {
40    fn from_glue_row(
41        labels: &[String],
42        row: &[crate::data::Value],
43    ) -> Result<Self, RowConversionError>;
44
45    // performance helpers implemented by derive
46    fn __glue_fields() -> &'static [(&'static str, &'static str)];
47    fn from_glue_row_with_idx(
48        idx: &[usize],
49        labels: &[String],
50        row: &[crate::data::Value],
51    ) -> Result<Self, RowConversionError>;
52}
53
54pub trait SelectExt {
55    fn rows_as<T: FromGlueRow>(self) -> Result<Vec<T>, RowConversionError>;
56    fn one_as<T: FromGlueRow>(self) -> Result<T, RowConversionError>;
57}
58
59impl SelectExt for crate::executor::Payload {
60    fn rows_as<T: FromGlueRow>(self) -> Result<Vec<T>, RowConversionError> {
61        match self {
62            crate::executor::Payload::Select { labels, rows } => {
63                if rows.is_empty() {
64                    return Ok(Vec::new());
65                }
66                // build label -> index mapping once per conversion
67                let fields = <T as FromGlueRow>::__glue_fields();
68                let mut idx = Vec::with_capacity(fields.len());
69                for (field, column) in fields.iter().copied() {
70                    let pos = labels
71                        .iter()
72                        .position(|l| l == column)
73                        .ok_or(RowConversionError::MissingColumn { field, column })?;
74                    idx.push(pos);
75                }
76
77                rows.iter()
78                    .map(|row| T::from_glue_row_with_idx(&idx, &labels, row))
79                    .collect()
80            }
81            _ => Err(RowConversionError::NotSelectPayload),
82        }
83    }
84
85    fn one_as<T: FromGlueRow>(self) -> Result<T, RowConversionError> {
86        let mut v = self.rows_as::<T>()?;
87        match v.len() {
88            0 => Err(RowConversionError::NotFound),
89            1 => Ok(v.remove(0)),
90            n => Err(RowConversionError::MoreThanOneRow { got: n }),
91        }
92    }
93}
94
95impl SelectExt for Vec<crate::executor::Payload> {
96    fn rows_as<T: FromGlueRow>(self) -> Result<Vec<T>, RowConversionError> {
97        let mut last_select = None;
98        for p in self.into_iter().rev() {
99            if matches!(p, crate::executor::Payload::Select { .. }) {
100                last_select = Some(p);
101                break;
102            }
103        }
104
105        match last_select {
106            Some(p) => SelectExt::rows_as::<T>(p),
107            None => Err(RowConversionError::NotSelectPayload),
108        }
109    }
110
111    fn one_as<T: FromGlueRow>(self) -> Result<T, RowConversionError> {
112        let mut last_select = None;
113        for p in self.into_iter().rev() {
114            if matches!(p, crate::executor::Payload::Select { .. }) {
115                last_select = Some(p);
116                break;
117            }
118        }
119
120        match last_select {
121            Some(p) => SelectExt::one_as::<T>(p),
122            None => Err(RowConversionError::NotSelectPayload),
123        }
124    }
125}
126
127// Convenience: allow chaining directly on execute() results (Result<Payloads, Error>)
128pub trait SelectResultExt {
129    fn rows_as<T: FromGlueRow>(self) -> crate::result::Result<Vec<T>>;
130    fn one_as<T: FromGlueRow>(self) -> crate::result::Result<T>;
131}
132
133impl SelectResultExt for crate::result::Result<Vec<crate::executor::Payload>> {
134    fn rows_as<T: FromGlueRow>(self) -> crate::result::Result<Vec<T>> {
135        self.and_then(|payloads| SelectExt::rows_as::<T>(payloads).map_err(Into::into))
136    }
137
138    fn one_as<T: FromGlueRow>(self) -> crate::result::Result<T> {
139        self.and_then(|payloads| SelectExt::one_as::<T>(payloads).map_err(Into::into))
140    }
141}
142
143impl SelectResultExt for crate::result::Result<crate::executor::Payload> {
144    fn rows_as<T: FromGlueRow>(self) -> crate::result::Result<Vec<T>> {
145        self.and_then(|payload| SelectExt::rows_as::<T>(payload).map_err(Into::into))
146    }
147
148    fn one_as<T: FromGlueRow>(self) -> crate::result::Result<T> {
149        self.and_then(|payload| SelectExt::one_as::<T>(payload).map_err(Into::into))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{FromGlueRow, RowConversionError, SelectExt, uuid_to_string};
156    use crate::{data::Value, executor::Payload};
157
158    #[test]
159    fn uuid_to_string_formats_hyphenated_lower() {
160        let value = 0x936D_A01F_9ABD_4D9D_80C7_02AF_85C8_22A8_u128;
161        assert_eq!(
162            uuid_to_string(value),
163            "936da01f-9abd-4d9d-80c7-02af85c822a8"
164        );
165    }
166
167    #[derive(Debug, PartialEq)]
168    struct Dummy {
169        id: i64,
170    }
171
172    impl FromGlueRow for Dummy {
173        fn from_glue_row(labels: &[String], row: &[Value]) -> Result<Self, RowConversionError> {
174            let pos =
175                labels
176                    .iter()
177                    .position(|l| l == "id")
178                    .ok_or(RowConversionError::MissingColumn {
179                        field: "id",
180                        column: "id",
181                    })?;
182            match &row[pos] {
183                Value::Null => Err(RowConversionError::NullNotAllowed {
184                    field: "id",
185                    column: labels[pos].clone(),
186                }),
187                Value::I64(n) => Ok(Dummy { id: *n }),
188                _ => Err(RowConversionError::TypeMismatch {
189                    field: Some("id"),
190                    column: Some(labels[pos].clone()),
191                    expected: "i64",
192                    got: "<other>",
193                }),
194            }
195        }
196
197        fn __glue_fields() -> &'static [(&'static str, &'static str)] {
198            static FIELDS: [(&str, &str); 1] = [("id", "id")];
199            &FIELDS
200        }
201
202        fn from_glue_row_with_idx(
203            idx: &[usize],
204            labels: &[String],
205            row: &[Value],
206        ) -> Result<Self, RowConversionError> {
207            let pos = idx[0];
208            match &row[pos] {
209                Value::Null => Err(RowConversionError::NullNotAllowed {
210                    field: "id",
211                    column: labels[pos].clone(),
212                }),
213                Value::I64(n) => Ok(Dummy { id: *n }),
214                _ => Err(RowConversionError::TypeMismatch {
215                    field: Some("id"),
216                    column: Some(labels[pos].clone()),
217                    expected: "i64",
218                    got: "<other>",
219                }),
220            }
221        }
222    }
223
224    #[test]
225    fn vec_one_as_not_select_payload() {
226        let payloads = vec![Payload::Insert(1)];
227        let err = payloads.one_as::<Dummy>().unwrap_err();
228        assert!(matches!(err, RowConversionError::NotSelectPayload));
229    }
230
231    #[test]
232    fn vec_one_as_picks_last_select() {
233        let rows = vec![
234            Payload::Select {
235                labels: vec!["id".into()],
236                rows: vec![vec![Value::I64(1)]],
237            },
238            Payload::Insert(0),
239            Payload::Select {
240                labels: vec!["id".into()],
241                rows: vec![vec![Value::I64(9)]],
242            },
243        ]
244        .one_as::<Dummy>()
245        .unwrap();
246
247        assert_eq!(rows, Dummy { id: 9 });
248    }
249
250    // Exercise from_glue_row_with_idx error branches via Payload::Select path
251    #[test]
252    fn rows_as_with_idx_null_not_allowed() {
253        let payload = Payload::Select {
254            labels: vec!["id".to_owned()],
255            rows: vec![vec![Value::Null]],
256        };
257        let err = payload.rows_as::<Dummy>().unwrap_err();
258        assert!(matches!(err, RowConversionError::NullNotAllowed { .. }));
259    }
260
261    #[test]
262    fn rows_as_with_idx_type_mismatch() {
263        let payload = Payload::Select {
264            labels: vec!["id".to_owned()],
265            rows: vec![vec![Value::Str("x".into())]],
266        };
267        let err = payload.rows_as::<Dummy>().unwrap_err();
268        assert!(matches!(err, RowConversionError::TypeMismatch { .. }));
269    }
270
271    // Directly exercise Dummy::from_glue_row to cover its branches
272    #[test]
273    fn dummy_from_glue_row_ok() {
274        let labels = vec!["id".to_owned()];
275        let row = vec![Value::I64(3)];
276        let got = Dummy::from_glue_row(&labels, &row).unwrap();
277        assert_eq!(got, Dummy { id: 3 });
278    }
279
280    #[test]
281    fn dummy_from_glue_row_missing_column() {
282        let labels = vec!["other".to_owned()];
283        let row = vec![Value::I64(1)];
284        let err = Dummy::from_glue_row(&labels, &row).unwrap_err();
285        assert!(matches!(err, RowConversionError::MissingColumn { .. }));
286    }
287
288    #[test]
289    fn dummy_from_glue_row_null_not_allowed() {
290        let labels = vec!["id".to_owned()];
291        let row = vec![Value::Null];
292        let err = Dummy::from_glue_row(&labels, &row).unwrap_err();
293        assert!(matches!(err, RowConversionError::NullNotAllowed { .. }));
294    }
295
296    #[test]
297    fn dummy_from_glue_row_type_mismatch() {
298        let labels = vec!["id".to_owned()];
299        let row = vec![Value::Str("x".into())];
300        let err = Dummy::from_glue_row(&labels, &row).unwrap_err();
301        assert!(matches!(err, RowConversionError::TypeMismatch { .. }));
302    }
303}