Skip to main content

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