reifydb_engine/function/blob/
b64url.rs1use 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 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 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 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 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 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 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 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}