reifydb_engine/function/blob/
b64url.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 BlobB64url;
10
11impl BlobB64url {
12	pub fn new() -> Self {
13		Self
14	}
15}
16
17impl ScalarFunction for BlobB64url {
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 b64url_str = &container[i];
33						let blob = Blob::from_b64url(OwnedFragment::internal(b64url_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!("BlobB64url 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_b64url_valid_input() {
60		let function = BlobB64url::new();
61
62		// "Hello!" in base64url is "SGVsbG8h" (no padding needed)
63		let b64url_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(b64url_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_b64url_empty_string() {
94		let function = BlobB64url::new();
95
96		let b64url_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(b64url_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_b64url_url_safe_characters() {
127		let function = BlobB64url::new();
128
129		// Base64url uses - and _ instead of + and /
130		// This string contains URL-safe characters
131		let b64url_data = vec!["SGVsbG9fV29ybGQtSGVsbG8".to_string()];
132		let bitvec = vec![true];
133		let input_column = Column {
134			name: Fragment::borrowed_internal("input"),
135			data: ColumnData::Utf8 {
136				container: Utf8Container::new(b64url_data, bitvec.into()),
137				max_bytes: MaxBytes::MAX,
138			},
139		};
140
141		let columns = Columns::new(vec![input_column]);
142		let ctx = ScalarFunctionContext {
143			columns: &columns,
144			row_count: 1,
145		};
146		let result = function.scalar(ctx).unwrap();
147
148		let ColumnData::Blob {
149			container,
150			..
151		} = result
152		else {
153			panic!("Expected BLOB column data");
154		};
155		assert_eq!(container.len(), 1);
156		assert!(container.is_defined(0));
157		assert_eq!(container[0].as_bytes(), "Hello_World-Hello".as_bytes());
158	}
159
160	#[test]
161	fn test_blob_b64url_no_padding() {
162		let function = BlobB64url::new();
163
164		// Base64url typically omits padding characters
165		// "Hello" in base64url without padding is "SGVsbG8"
166		let b64url_data = vec!["SGVsbG8".to_string()];
167		let bitvec = vec![true];
168		let input_column = Column {
169			name: Fragment::borrowed_internal("input"),
170			data: ColumnData::Utf8 {
171				container: Utf8Container::new(b64url_data, bitvec.into()),
172				max_bytes: MaxBytes::MAX,
173			},
174		};
175
176		let columns = Columns::new(vec![input_column]);
177		let ctx = ScalarFunctionContext {
178			columns: &columns,
179			row_count: 1,
180		};
181		let result = function.scalar(ctx).unwrap();
182
183		let ColumnData::Blob {
184			container,
185			..
186		} = result
187		else {
188			panic!("Expected BLOB column data");
189		};
190		assert_eq!(container.len(), 1);
191		assert!(container.is_defined(0));
192		assert_eq!(container[0].as_bytes(), "Hello".as_bytes());
193	}
194
195	#[test]
196	fn test_blob_b64url_multiple_rows() {
197		let function = BlobB64url::new();
198
199		// "A" = "QQ", "BC" = "QkM", "DEF" = "REVG" (no padding in
200		// base64url)
201		let b64url_data = vec!["QQ".to_string(), "QkM".to_string(), "REVG".to_string()];
202		let bitvec = vec![true, true, true];
203		let input_column = Column {
204			name: Fragment::borrowed_internal("input"),
205			data: ColumnData::Utf8 {
206				container: Utf8Container::new(b64url_data, bitvec.into()),
207				max_bytes: MaxBytes::MAX,
208			},
209		};
210
211		let columns = Columns::new(vec![input_column]);
212		let ctx = ScalarFunctionContext {
213			columns: &columns,
214			row_count: 3,
215		};
216		let result = function.scalar(ctx).unwrap();
217
218		let ColumnData::Blob {
219			container,
220			..
221		} = result
222		else {
223			panic!("Expected BLOB column data");
224		};
225		assert_eq!(container.len(), 3);
226		assert!(container.is_defined(0));
227		assert!(container.is_defined(1));
228		assert!(container.is_defined(2));
229
230		assert_eq!(container[0].as_bytes(), "A".as_bytes());
231		assert_eq!(container[1].as_bytes(), "BC".as_bytes());
232		assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
233	}
234
235	#[test]
236	fn test_blob_b64url_with_null_data() {
237		let function = BlobB64url::new();
238
239		let b64url_data = vec!["QQ".to_string(), "".to_string(), "REVG".to_string()];
240		let bitvec = vec![true, false, true];
241		let input_column = Column {
242			name: Fragment::borrowed_internal("input"),
243			data: ColumnData::Utf8 {
244				container: Utf8Container::new(b64url_data, bitvec.into()),
245				max_bytes: MaxBytes::MAX,
246			},
247		};
248
249		let columns = Columns::new(vec![input_column]);
250		let ctx = ScalarFunctionContext {
251			columns: &columns,
252			row_count: 3,
253		};
254		let result = function.scalar(ctx).unwrap();
255
256		let ColumnData::Blob {
257			container,
258			..
259		} = result
260		else {
261			panic!("Expected BLOB column data");
262		};
263		assert_eq!(container.len(), 3);
264		assert!(container.is_defined(0));
265		assert!(!container.is_defined(1));
266		assert!(container.is_defined(2));
267
268		assert_eq!(container[0].as_bytes(), "A".as_bytes());
269		assert_eq!(container[1].as_bytes(), [].as_slice() as &[u8]);
270		assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
271	}
272
273	#[test]
274	fn test_blob_b64url_binary_data() {
275		let function = BlobB64url::new();
276
277		// Binary data: [0xde, 0xad, 0xbe, 0xef] in base64url is
278		// "3q2-7w" (no padding)
279		let b64url_data = vec!["3q2-7w".to_string()];
280		let bitvec = vec![true];
281		let input_column = Column {
282			name: Fragment::borrowed_internal("input"),
283			data: ColumnData::Utf8 {
284				container: Utf8Container::new(b64url_data, bitvec.into()),
285				max_bytes: MaxBytes::MAX,
286			},
287		};
288
289		let columns = Columns::new(vec![input_column]);
290		let ctx = ScalarFunctionContext {
291			columns: &columns,
292			row_count: 1,
293		};
294		let result = function.scalar(ctx).unwrap();
295
296		let ColumnData::Blob {
297			container,
298			..
299		} = result
300		else {
301			panic!("Expected BLOB column data");
302		};
303		assert_eq!(container.len(), 1);
304		assert!(container.is_defined(0));
305		assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
306	}
307
308	#[test]
309	fn test_blob_b64url_invalid_input_should_error() {
310		let function = BlobB64url::new();
311
312		// Using standard base64 characters that are invalid in
313		// base64url
314		let b64url_data = vec!["invalid+base64/chars".to_string()];
315		let bitvec = vec![true];
316		let input_column = Column {
317			name: Fragment::borrowed_internal("input"),
318			data: ColumnData::Utf8 {
319				container: Utf8Container::new(b64url_data, bitvec.into()),
320				max_bytes: MaxBytes::MAX,
321			},
322		};
323
324		let columns = Columns::new(vec![input_column]);
325		let ctx = ScalarFunctionContext {
326			columns: &columns,
327			row_count: 1,
328		};
329		let result = function.scalar(ctx);
330		assert!(result.is_err(), "Expected error for invalid base64url input");
331	}
332
333	#[test]
334	fn test_blob_b64url_with_standard_base64_padding_should_error() {
335		let function = BlobB64url::new();
336
337		// Base64url typically doesn't use padding, so this should error
338		let b64url_data = vec!["SGVsbG8=".to_string()];
339		let bitvec = vec![true];
340		let input_column = Column {
341			name: Fragment::borrowed_internal("input"),
342			data: ColumnData::Utf8 {
343				container: Utf8Container::new(b64url_data, bitvec.into()),
344				max_bytes: MaxBytes::MAX,
345			},
346		};
347
348		let columns = Columns::new(vec![input_column]);
349		let ctx = ScalarFunctionContext {
350			columns: &columns,
351			row_count: 1,
352		};
353		let result = function.scalar(ctx);
354		assert!(result.is_err(), "Expected error for base64url with padding characters");
355	}
356}