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 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 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
127pub 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 #[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 #[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}