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