1use crate::dialect::{PlaceholderGen, SqlDialect};
2
3use super::DbError;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum DbValue {
8 Null,
10 Integer(i64),
12 Text(String),
14 Blob(Vec<u8>),
16 Uuid([u8; 16]),
18}
19
20impl DbValue {
21 pub fn from_u64(value: u64) -> Result<Self, DbError> {
30 i64::try_from(value)
31 .map(DbValue::Integer)
32 .map_err(|_| DbError::IntegerOutOfRange(value as i128))
33 }
34}
35
36#[derive(Debug)]
38pub struct DbRow {
39 pub values: Vec<DbValue>,
41}
42
43impl DbRow {
44 pub fn get_i64(&self, idx: usize) -> Result<i64, DbError> {
47 match self.values.get(idx) {
48 Some(DbValue::Integer(v)) => Ok(*v),
49 Some(DbValue::Null) => Err(DbError::UnexpectedNull(idx)),
50 Some(_) => Err(DbError::TypeMismatch {
51 col: idx,
52 expected: "integer",
53 }),
54 None => Err(DbError::ColumnOutOfBounds(idx)),
55 }
56 }
57
58 pub fn get_text(&self, idx: usize) -> Result<&str, DbError> {
61 match self.values.get(idx) {
62 Some(DbValue::Text(v)) => Ok(v),
63 Some(DbValue::Null) => Err(DbError::UnexpectedNull(idx)),
64 Some(_) => Err(DbError::TypeMismatch {
65 col: idx,
66 expected: "text",
67 }),
68 None => Err(DbError::ColumnOutOfBounds(idx)),
69 }
70 }
71
72 pub fn get_blob(&self, idx: usize) -> Result<&[u8], DbError> {
75 match self.values.get(idx) {
76 Some(DbValue::Blob(v)) => Ok(v),
77 Some(DbValue::Null) => Err(DbError::UnexpectedNull(idx)),
78 Some(_) => Err(DbError::TypeMismatch {
79 col: idx,
80 expected: "blob",
81 }),
82 None => Err(DbError::ColumnOutOfBounds(idx)),
83 }
84 }
85
86 pub fn get_u64(&self, idx: usize) -> Result<u64, DbError> {
92 let v = self.get_i64(idx)?;
93 u64::try_from(v).map_err(|_| DbError::IntegerOutOfRange(v as i128))
94 }
95
96 pub fn get_optional_u64(&self, idx: usize) -> Result<Option<u64>, DbError> {
102 if let Some(v) = self.get_optional_i64(idx)? {
103 Ok(Some(
104 u64::try_from(v).map_err(|_| DbError::IntegerOutOfRange(v as i128))?,
105 ))
106 } else {
107 Ok(None)
108 }
109 }
110
111 pub fn get_bool(&self, idx: usize) -> Result<bool, DbError> {
114 match self.values.get(idx) {
115 Some(DbValue::Integer(v)) => Ok(*v != 0),
116 Some(DbValue::Null) => Err(DbError::UnexpectedNull(idx)),
117 Some(_) => Err(DbError::TypeMismatch {
118 col: idx,
119 expected: "bool/integer",
120 }),
121 None => Err(DbError::ColumnOutOfBounds(idx)),
122 }
123 }
124
125 pub fn get_optional_i64(&self, idx: usize) -> Result<Option<i64>, DbError> {
128 match self.values.get(idx) {
129 Some(DbValue::Integer(v)) => Ok(Some(*v)),
130 Some(DbValue::Null) => Ok(None),
131 Some(_) => Err(DbError::TypeMismatch {
132 col: idx,
133 expected: "integer",
134 }),
135 None => Err(DbError::ColumnOutOfBounds(idx)),
136 }
137 }
138
139 pub fn get_optional_text(&self, idx: usize) -> Result<Option<&str>, DbError> {
142 match self.values.get(idx) {
143 Some(DbValue::Text(v)) => Ok(Some(v)),
144 Some(DbValue::Null) => Ok(None),
145 Some(_) => Err(DbError::TypeMismatch {
146 col: idx,
147 expected: "text",
148 }),
149 None => Err(DbError::ColumnOutOfBounds(idx)),
150 }
151 }
152
153 pub fn get_uuid(&self, idx: usize) -> Result<[u8; 16], DbError> {
157 match self.values.get(idx) {
158 Some(DbValue::Blob(v)) => v.as_slice().try_into().map_err(|_| DbError::TypeMismatch {
159 col: idx,
160 expected: "16-byte UUID blob",
161 }),
162 Some(DbValue::Uuid(v)) => Ok(*v),
163 Some(DbValue::Null) => Err(DbError::UnexpectedNull(idx)),
164 Some(_) => Err(DbError::TypeMismatch {
165 col: idx,
166 expected: "uuid or 16-byte blob",
167 }),
168 None => Err(DbError::ColumnOutOfBounds(idx)),
169 }
170 }
171
172 pub fn get_optional_uuid(&self, idx: usize) -> Result<Option<[u8; 16]>, DbError> {
176 match self.values.get(idx) {
177 Some(DbValue::Blob(v)) => {
178 let arr = v.as_slice().try_into().map_err(|_| DbError::TypeMismatch {
179 col: idx,
180 expected: "16-byte UUID blob",
181 })?;
182 Ok(Some(arr))
183 }
184 Some(DbValue::Uuid(v)) => Ok(Some(*v)),
185 Some(DbValue::Null) => Ok(None),
186 Some(_) => Err(DbError::TypeMismatch {
187 col: idx,
188 expected: "uuid or 16-byte blob",
189 }),
190 None => Err(DbError::ColumnOutOfBounds(idx)),
191 }
192 }
193
194 pub fn get_optional_blob(&self, idx: usize) -> Result<Option<&[u8]>, DbError> {
197 match self.values.get(idx) {
198 Some(DbValue::Blob(v)) => Ok(Some(v)),
199 Some(DbValue::Null) => Ok(None),
200 Some(_) => Err(DbError::TypeMismatch {
201 col: idx,
202 expected: "blob",
203 }),
204 None => Err(DbError::ColumnOutOfBounds(idx)),
205 }
206 }
207}
208
209pub struct ValueBinder {
214 placeholder_gen: PlaceholderGen,
215 values: Vec<DbValue>,
216}
217
218impl ValueBinder {
219 pub fn new(dialect: SqlDialect) -> Self {
221 Self {
222 placeholder_gen: PlaceholderGen::new(dialect),
223 values: vec![],
224 }
225 }
226
227 pub fn bind_next(&mut self, value: DbValue) -> String {
229 self.values.push(value);
230 self.placeholder_gen.next_placeholder()
231 }
232
233 pub fn values(self) -> Vec<DbValue> {
235 self.values
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn from_u64_round_trips_through_get_u64() {
245 for v in [0u64, 1, 1_700_000_000_000 << 16, i64::MAX as u64] {
248 let DbValue::Integer(stored) = DbValue::from_u64(v).unwrap() else {
249 panic!("from_u64 must produce an Integer");
250 };
251 let row = DbRow {
252 values: vec![DbValue::Integer(stored)],
253 };
254 assert_eq!(row.get_u64(0).unwrap(), v);
255 }
256 }
257
258 #[test]
259 fn from_u64_rejects_values_past_i64_max() {
260 assert!(matches!(
263 DbValue::from_u64(i64::MAX as u64 + 1),
264 Err(DbError::IntegerOutOfRange(_))
265 ));
266 assert!(matches!(
267 DbValue::from_u64(u64::MAX),
268 Err(DbError::IntegerOutOfRange(_))
269 ));
270 }
271
272 #[test]
273 fn get_u64_rejects_negative_stored_value() {
274 let row = DbRow {
277 values: vec![DbValue::Integer(-1)],
278 };
279 assert!(matches!(row.get_u64(0), Err(DbError::IntegerOutOfRange(_))));
280 }
281
282 #[test]
283 fn get_optional_u64_passes_null_through_and_still_guards_range() {
284 let null_row = DbRow {
286 values: vec![DbValue::Null],
287 };
288 assert_eq!(null_row.get_optional_u64(0).unwrap(), None);
289
290 let value_row = DbRow {
291 values: vec![DbValue::Integer(42)],
292 };
293 assert_eq!(value_row.get_optional_u64(0).unwrap(), Some(42));
294
295 let negative_row = DbRow {
296 values: vec![DbValue::Integer(-1)],
297 };
298 assert!(matches!(
299 negative_row.get_optional_u64(0),
300 Err(DbError::IntegerOutOfRange(_))
301 ));
302 }
303}