reifydb_engine/function/blob/
hex.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4use reifydb_core::value::column::ColumnData;
5use reifydb_type::{OwnedFragment, value::Blob};
6
7use crate::function::{ScalarFunction, ScalarFunctionContext};
8
9pub struct BlobHex;
10
11impl BlobHex {
12	pub fn new() -> Self {
13		Self
14	}
15}
16
17impl ScalarFunction for BlobHex {
18	fn scalar(&self, ctx: ScalarFunctionContext) -> crate::Result<ColumnData> {
19		let columns = ctx.columns;
20		let row_count = ctx.row_count;
21		let column = columns.get(0).unwrap();
22
23		match &column.data() {
24			ColumnData::Utf8 {
25				container,
26				..
27			} => {
28				let mut result_data = Vec::with_capacity(container.data().len());
29
30				for i in 0..row_count {
31					if container.is_defined(i) {
32						let hex_str = &container[i];
33						let blob = Blob::from_hex(OwnedFragment::internal(hex_str))?;
34						result_data.push(blob);
35					} else {
36						result_data.push(Blob::empty())
37					}
38				}
39
40				Ok(ColumnData::blob_with_bitvec(result_data, container.bitvec().clone()))
41			}
42			_ => unimplemented!("BlobHex only supports text input"),
43		}
44	}
45}
46
47#[cfg(test)]
48mod tests {
49	use reifydb_core::value::{
50		column::{Column, Columns},
51		container::Utf8Container,
52	};
53	use reifydb_type::{Fragment, value::constraint::bytes::MaxBytes};
54
55	use super::*;
56	use crate::function::ScalarFunctionContext;
57
58	#[test]
59	fn test_blob_hex_valid_input() {
60		let function = BlobHex::new();
61
62		let hex_data = vec!["deadbeef".to_string()];
63		let bitvec = vec![true];
64		let input_column = Column {
65			name: Fragment::borrowed_internal("input"),
66			data: ColumnData::Utf8 {
67				container: Utf8Container::new(hex_data, bitvec.into()),
68				max_bytes: MaxBytes::MAX,
69			},
70		};
71
72		let columns = Columns::new(vec![input_column]);
73		let ctx = ScalarFunctionContext {
74			columns: &columns,
75			row_count: 1,
76		};
77		let result = function.scalar(ctx).unwrap();
78
79		let ColumnData::Blob {
80			container,
81			..
82		} = result
83		else {
84			panic!("Expected BLOB column data");
85		};
86		assert_eq!(container.len(), 1);
87		assert!(container.is_defined(0));
88		assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
89	}
90
91	#[test]
92	fn test_blob_hex_empty_string() {
93		let function = BlobHex::new();
94
95		let hex_data = vec!["".to_string()];
96		let bitvec = vec![true];
97		let input_column = Column {
98			name: Fragment::borrowed_internal("input"),
99			data: ColumnData::Utf8 {
100				container: Utf8Container::new(hex_data, bitvec.into()),
101				max_bytes: MaxBytes::MAX,
102			},
103		};
104
105		let columns = Columns::new(vec![input_column]);
106		let ctx = ScalarFunctionContext {
107			columns: &columns,
108			row_count: 1,
109		};
110		let result = function.scalar(ctx).unwrap();
111
112		let ColumnData::Blob {
113			container,
114			..
115		} = result
116		else {
117			panic!("Expected BLOB column data");
118		};
119		assert_eq!(container.len(), 1);
120		assert!(container.is_defined(0));
121		assert_eq!(container[0].as_bytes(), &[] as &[u8]);
122	}
123
124	#[test]
125	fn test_blob_hex_uppercase() {
126		let function = BlobHex::new();
127
128		let hex_data = vec!["DEADBEEF".to_string()];
129		let bitvec = vec![true];
130		let input_column = Column {
131			name: Fragment::borrowed_internal("input"),
132			data: ColumnData::Utf8 {
133				container: Utf8Container::new(hex_data, bitvec.into()),
134				max_bytes: MaxBytes::MAX,
135			},
136		};
137
138		let columns = Columns::new(vec![input_column]);
139		let ctx = ScalarFunctionContext {
140			columns: &columns,
141			row_count: 1,
142		};
143		let result = function.scalar(ctx).unwrap();
144
145		let ColumnData::Blob {
146			container,
147			..
148		} = result
149		else {
150			panic!("Expected BLOB column data");
151		};
152		assert_eq!(container.len(), 1);
153		assert!(container.is_defined(0));
154		assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
155	}
156
157	#[test]
158	fn test_blob_hex_mixed_case() {
159		let function = BlobHex::new();
160
161		let hex_data = vec!["DeAdBeEf".to_string()];
162		let bitvec = vec![true];
163		let input_column = Column {
164			name: Fragment::borrowed_internal("input"),
165			data: ColumnData::Utf8 {
166				container: Utf8Container::new(hex_data, bitvec.into()),
167				max_bytes: MaxBytes::MAX,
168			},
169		};
170
171		let columns = Columns::new(vec![input_column]);
172		let ctx = ScalarFunctionContext {
173			columns: &columns,
174			row_count: 1,
175		};
176		let result = function.scalar(ctx).unwrap();
177
178		let ColumnData::Blob {
179			container,
180			..
181		} = result
182		else {
183			panic!("Expected BLOB column data");
184		};
185		assert_eq!(container.len(), 1);
186		assert!(container.is_defined(0));
187		assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
188	}
189
190	#[test]
191	fn test_blob_hex_multiple_rows() {
192		let function = BlobHex::new();
193
194		let hex_data = vec!["ff".to_string(), "00".to_string(), "deadbeef".to_string()];
195		let bitvec = vec![true, true, true];
196		let input_column = Column {
197			name: Fragment::borrowed_internal("input"),
198			data: ColumnData::Utf8 {
199				container: Utf8Container::new(hex_data, bitvec.into()),
200				max_bytes: MaxBytes::MAX,
201			},
202		};
203
204		let columns = Columns::new(vec![input_column]);
205		let ctx = ScalarFunctionContext {
206			columns: &columns,
207			row_count: 3,
208		};
209		let result = function.scalar(ctx).unwrap();
210
211		let ColumnData::Blob {
212			container,
213			..
214		} = result
215		else {
216			panic!("Expected BLOB column data");
217		};
218		assert_eq!(container.len(), 3);
219		assert!(container.is_defined(0));
220		assert!(container.is_defined(1));
221		assert!(container.is_defined(2));
222
223		assert_eq!(container[0].as_bytes(), &[0xff]);
224		assert_eq!(container[1].as_bytes(), &[0x00]);
225		assert_eq!(container[2].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
226	}
227
228	#[test]
229	fn test_blob_hex_with_null_data() {
230		let function = BlobHex::new();
231
232		let hex_data = vec!["ff".to_string(), "".to_string(), "deadbeef".to_string()];
233		let bitvec = vec![true, false, true];
234		let input_column = Column {
235			name: Fragment::borrowed_internal("input"),
236			data: ColumnData::Utf8 {
237				container: Utf8Container::new(hex_data, bitvec.into()),
238				max_bytes: MaxBytes::MAX,
239			},
240		};
241
242		let columns = Columns::new(vec![input_column]);
243		let ctx = ScalarFunctionContext {
244			columns: &columns,
245			row_count: 3,
246		};
247		let result = function.scalar(ctx).unwrap();
248
249		let ColumnData::Blob {
250			container,
251			..
252		} = result
253		else {
254			panic!("Expected BLOB column data");
255		};
256		assert_eq!(container.len(), 3);
257		assert!(container.is_defined(0));
258		assert!(!container.is_defined(1));
259		assert!(container.is_defined(2));
260
261		assert_eq!(container[0].as_bytes(), &[0xff]);
262		assert_eq!(container[1].as_bytes(), [].as_slice() as &[u8]);
263		assert_eq!(container[2].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
264	}
265
266	#[test]
267	fn test_blob_hex_invalid_input_should_error() {
268		let function = BlobHex::new();
269
270		let hex_data = vec!["invalid_hex".to_string()];
271		let bitvec = vec![true];
272		let input_column = Column {
273			name: Fragment::borrowed_internal("input"),
274			data: ColumnData::Utf8 {
275				container: Utf8Container::new(hex_data, bitvec.into()),
276				max_bytes: MaxBytes::MAX,
277			},
278		};
279
280		let columns = Columns::new(vec![input_column]);
281		let ctx = ScalarFunctionContext {
282			columns: &columns,
283			row_count: 1,
284		};
285		let result = function.scalar(ctx);
286		assert!(result.is_err(), "Expected error for invalid hex input");
287	}
288
289	#[test]
290	fn test_blob_hex_odd_length_should_error() {
291		let function = BlobHex::new();
292
293		let hex_data = vec!["abc".to_string()];
294		let bitvec = vec![true];
295		let input_column = Column {
296			name: Fragment::borrowed_internal("input"),
297			data: ColumnData::Utf8 {
298				container: Utf8Container::new(hex_data, bitvec.into()),
299				max_bytes: MaxBytes::MAX,
300			},
301		};
302
303		let columns = Columns::new(vec![input_column]);
304		let ctx = ScalarFunctionContext {
305			columns: &columns,
306			row_count: 1,
307		};
308		let result = function.scalar(ctx);
309		assert!(result.is_err(), "Expected error for odd length hex string");
310	}
311}