reifydb_engine/function/blob/
b64.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 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		// "Hello!" in base64 is "SGVsbG8h"
63		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		// "Hello" in base64 is "SGVsbG8="
130		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		// "A" = "QQ==", "BC" = "QkM=", "DEF" = "REVG"
164		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		// Binary data: [0xde, 0xad, 0xbe, 0xef] in base64 is "3q2+7w=="
241		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()]; // Too many padding characters
298		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}