reifydb_engine/function/blob/
b64.rs1use reifydb_core::value::column::ColumnData;
5use reifydb_type::{OwnedFragment, value::Blob};
6
7use crate::function::{ScalarFunction, ScalarFunctionContext};
8
9pub struct BlobB64;
10
11impl BlobB64 {
12 pub fn new() -> Self {
13 Self
14 }
15}
16
17impl ScalarFunction for BlobB64 {
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 b64_str = &container[i];
33 let blob = Blob::from_b64(OwnedFragment::internal(b64_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!("BlobB64 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_b64_valid_input() {
60 let function = BlobB64::new();
61
62 let b64_data = vec!["SGVsbG8h".to_string()];
64 let bitvec = vec![true];
65 let input_column = Column {
66 name: Fragment::borrowed_internal("input"),
67 data: ColumnData::Utf8 {
68 container: Utf8Container::new(b64_data, bitvec.into()),
69 max_bytes: MaxBytes::MAX,
70 },
71 };
72
73 let columns = Columns::new(vec![input_column]);
74 let ctx = ScalarFunctionContext {
75 columns: &columns,
76 row_count: 1,
77 };
78 let result = function.scalar(ctx).unwrap();
79
80 let ColumnData::Blob {
81 container,
82 ..
83 } = result
84 else {
85 panic!("Expected BLOB column data");
86 };
87 assert_eq!(container.len(), 1);
88 assert!(container.is_defined(0));
89 assert_eq!(container[0].as_bytes(), "Hello!".as_bytes());
90 }
91
92 #[test]
93 fn test_blob_b64_empty_string() {
94 let function = BlobB64::new();
95
96 let b64_data = vec!["".to_string()];
97 let bitvec = vec![true];
98 let input_column = Column {
99 name: Fragment::borrowed_internal("input"),
100 data: ColumnData::Utf8 {
101 container: Utf8Container::new(b64_data, bitvec.into()),
102 max_bytes: MaxBytes::MAX,
103 },
104 };
105
106 let columns = Columns::new(vec![input_column]);
107 let ctx = ScalarFunctionContext {
108 columns: &columns,
109 row_count: 1,
110 };
111 let result = function.scalar(ctx).unwrap();
112
113 let ColumnData::Blob {
114 container,
115 ..
116 } = result
117 else {
118 panic!("Expected BLOB column data");
119 };
120 assert_eq!(container.len(), 1);
121 assert!(container.is_defined(0));
122 assert_eq!(container[0].as_bytes(), &[] as &[u8]);
123 }
124
125 #[test]
126 fn test_blob_b64_with_padding() {
127 let function = BlobB64::new();
128
129 let b64_data = vec!["SGVsbG8=".to_string()];
131 let bitvec = vec![true];
132 let input_column = Column {
133 name: Fragment::borrowed_internal("input"),
134 data: ColumnData::Utf8 {
135 container: Utf8Container::new(b64_data, bitvec.into()),
136 max_bytes: MaxBytes::MAX,
137 },
138 };
139
140 let columns = Columns::new(vec![input_column]);
141 let ctx = ScalarFunctionContext {
142 columns: &columns,
143 row_count: 1,
144 };
145 let result = function.scalar(ctx).unwrap();
146
147 let ColumnData::Blob {
148 container,
149 ..
150 } = result
151 else {
152 panic!("Expected BLOB column data");
153 };
154 assert_eq!(container.len(), 1);
155 assert!(container.is_defined(0));
156 assert_eq!(container[0].as_bytes(), "Hello".as_bytes());
157 }
158
159 #[test]
160 fn test_blob_b64_multiple_rows() {
161 let function = BlobB64::new();
162
163 let b64_data = vec!["QQ==".to_string(), "QkM=".to_string(), "REVG".to_string()];
165 let bitvec = vec![true, true, true];
166 let input_column = Column {
167 name: Fragment::borrowed_internal("input"),
168 data: ColumnData::Utf8 {
169 container: Utf8Container::new(b64_data, bitvec.into()),
170 max_bytes: MaxBytes::MAX,
171 },
172 };
173
174 let columns = Columns::new(vec![input_column]);
175 let ctx = ScalarFunctionContext {
176 columns: &columns,
177 row_count: 3,
178 };
179 let result = function.scalar(ctx).unwrap();
180
181 let ColumnData::Blob {
182 container,
183 ..
184 } = result
185 else {
186 panic!("Expected BLOB column data");
187 };
188 assert_eq!(container.len(), 3);
189 assert!(container.is_defined(0));
190 assert!(container.is_defined(1));
191 assert!(container.is_defined(2));
192
193 assert_eq!(container[0].as_bytes(), "A".as_bytes());
194 assert_eq!(container[1].as_bytes(), "BC".as_bytes());
195 assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
196 }
197
198 #[test]
199 fn test_blob_b64_with_null_data() {
200 let function = BlobB64::new();
201
202 let b64_data = vec!["QQ==".to_string(), "".to_string(), "REVG".to_string()];
203 let bitvec = vec![true, false, true];
204 let input_column = Column {
205 name: Fragment::borrowed_internal("input"),
206 data: ColumnData::Utf8 {
207 container: Utf8Container::new(b64_data, bitvec.into()),
208 max_bytes: MaxBytes::MAX,
209 },
210 };
211
212 let columns = Columns::new(vec![input_column]);
213 let ctx = ScalarFunctionContext {
214 columns: &columns,
215 row_count: 3,
216 };
217 let result = function.scalar(ctx).unwrap();
218
219 let ColumnData::Blob {
220 container,
221 ..
222 } = result
223 else {
224 panic!("Expected BLOB column data");
225 };
226 assert_eq!(container.len(), 3);
227 assert!(container.is_defined(0));
228 assert!(!container.is_defined(1));
229 assert!(container.is_defined(2));
230
231 assert_eq!(container[0].as_bytes(), "A".as_bytes());
232 assert_eq!(container[1].as_bytes(), [].as_slice() as &[u8]);
233 assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
234 }
235
236 #[test]
237 fn test_blob_b64_binary_data() {
238 let function = BlobB64::new();
239
240 let b64_data = vec!["3q2+7w==".to_string()];
242 let bitvec = vec![true];
243 let input_column = Column {
244 name: Fragment::borrowed_internal("input"),
245 data: ColumnData::Utf8 {
246 container: Utf8Container::new(b64_data, bitvec.into()),
247 max_bytes: MaxBytes::MAX,
248 },
249 };
250
251 let columns = Columns::new(vec![input_column]);
252 let ctx = ScalarFunctionContext {
253 columns: &columns,
254 row_count: 1,
255 };
256 let result = function.scalar(ctx).unwrap();
257
258 let ColumnData::Blob {
259 container,
260 ..
261 } = result
262 else {
263 panic!("Expected BLOB column data");
264 };
265 assert_eq!(container.len(), 1);
266 assert!(container.is_defined(0));
267 assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
268 }
269
270 #[test]
271 fn test_blob_b64_invalid_input_should_error() {
272 let function = BlobB64::new();
273
274 let b64_data = vec!["invalid@base64!".to_string()];
275 let bitvec = vec![true];
276 let input_column = Column {
277 name: Fragment::borrowed_internal("input"),
278 data: ColumnData::Utf8 {
279 container: Utf8Container::new(b64_data, bitvec.into()),
280 max_bytes: MaxBytes::MAX,
281 },
282 };
283
284 let columns = Columns::new(vec![input_column]);
285 let ctx = ScalarFunctionContext {
286 columns: &columns,
287 row_count: 1,
288 };
289 let result = function.scalar(ctx);
290 assert!(result.is_err(), "Expected error for invalid base64 input");
291 }
292
293 #[test]
294 fn test_blob_b64_malformed_padding_should_error() {
295 let function = BlobB64::new();
296
297 let b64_data = vec!["SGVsbG8===".to_string()]; let bitvec = vec![true];
299 let input_column = Column {
300 name: Fragment::borrowed_internal("input"),
301 data: ColumnData::Utf8 {
302 container: Utf8Container::new(b64_data, bitvec.into()),
303 max_bytes: MaxBytes::MAX,
304 },
305 };
306
307 let columns = Columns::new(vec![input_column]);
308 let ctx = ScalarFunctionContext {
309 columns: &columns,
310 row_count: 1,
311 };
312 let result = function.scalar(ctx);
313 assert!(result.is_err(), "Expected error for malformed base64 padding");
314 }
315}