reifydb_engine/function/blob/
b58.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 BlobB58;
10
11impl BlobB58 {
12	pub fn new() -> Self {
13		Self
14	}
15}
16
17impl ScalarFunction for BlobB58 {
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 b58_str = &container[i];
33						let blob = Blob::from_b58(OwnedFragment::internal(b58_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!("BlobB58 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_b58_valid_input() {
60		let function = BlobB58::new();
61
62		// "Hello" in base58 is "9Ajdvzr"
63		let b58_data = vec!["9Ajdvzr".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(b58_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_b58_empty_string() {
94		let function = BlobB58::new();
95
96		let b58_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(b58_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_b58_multiple_rows() {
127		let function = BlobB58::new();
128
129		// "A" = "28", "BC" = "63U", "DEF" = "Pw25"
130		let b58_data = vec!["28".to_string(), "63U".to_string(), "Pw25".to_string()];
131		let bitvec = vec![true, true, true];
132		let input_column = Column {
133			name: Fragment::borrowed_internal("input"),
134			data: ColumnData::Utf8 {
135				container: Utf8Container::new(b58_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: 3,
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(), 3);
155		assert!(container.is_defined(0));
156		assert!(container.is_defined(1));
157		assert!(container.is_defined(2));
158
159		assert_eq!(container[0].as_bytes(), "A".as_bytes());
160		assert_eq!(container[1].as_bytes(), "BC".as_bytes());
161		assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
162	}
163
164	#[test]
165	fn test_blob_b58_with_null_data() {
166		let function = BlobB58::new();
167
168		let b58_data = vec!["28".to_string(), "".to_string(), "Pw25".to_string()];
169		let bitvec = vec![true, false, true];
170		let input_column = Column {
171			name: Fragment::borrowed_internal("input"),
172			data: ColumnData::Utf8 {
173				container: Utf8Container::new(b58_data, bitvec.into()),
174				max_bytes: MaxBytes::MAX,
175			},
176		};
177
178		let columns = Columns::new(vec![input_column]);
179		let ctx = ScalarFunctionContext {
180			columns: &columns,
181			row_count: 3,
182		};
183		let result = function.scalar(ctx).unwrap();
184
185		let ColumnData::Blob {
186			container,
187			..
188		} = result
189		else {
190			panic!("Expected BLOB column data");
191		};
192		assert_eq!(container.len(), 3);
193		assert!(container.is_defined(0));
194		assert!(!container.is_defined(1));
195		assert!(container.is_defined(2));
196
197		assert_eq!(container[0].as_bytes(), "A".as_bytes());
198		assert_eq!(container[1].as_bytes(), [].as_slice() as &[u8]);
199		assert_eq!(container[2].as_bytes(), "DEF".as_bytes());
200	}
201
202	#[test]
203	fn test_blob_b58_binary_data() {
204		let function = BlobB58::new();
205
206		// Binary data: [0xde, 0xad, 0xbe, 0xef] in base58 is "6h8cQN"
207		let b58_data = vec!["6h8cQN".to_string()];
208		let bitvec = vec![true];
209		let input_column = Column {
210			name: Fragment::borrowed_internal("input"),
211			data: ColumnData::Utf8 {
212				container: Utf8Container::new(b58_data, bitvec.into()),
213				max_bytes: MaxBytes::MAX,
214			},
215		};
216
217		let columns = Columns::new(vec![input_column]);
218		let ctx = ScalarFunctionContext {
219			columns: &columns,
220			row_count: 1,
221		};
222		let result = function.scalar(ctx).unwrap();
223
224		let ColumnData::Blob {
225			container,
226			..
227		} = result
228		else {
229			panic!("Expected BLOB column data");
230		};
231		assert_eq!(container.len(), 1);
232		assert!(container.is_defined(0));
233		assert_eq!(container[0].as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
234	}
235
236	#[test]
237	fn test_blob_b58_invalid_input_should_error() {
238		let function = BlobB58::new();
239
240		// Characters not in base58 alphabet: 0, O, I, l
241		let b58_data = vec!["invalid0!".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(b58_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);
257		assert!(result.is_err(), "Expected error for invalid base58 input");
258	}
259
260	#[test]
261	fn test_blob_b58_invalid_zero_char() {
262		let function = BlobB58::new();
263
264		// '0' is not in base58 alphabet
265		let b58_data = vec!["abc0def".to_string()];
266		let bitvec = vec![true];
267		let input_column = Column {
268			name: Fragment::borrowed_internal("input"),
269			data: ColumnData::Utf8 {
270				container: Utf8Container::new(b58_data, bitvec.into()),
271				max_bytes: MaxBytes::MAX,
272			},
273		};
274
275		let columns = Columns::new(vec![input_column]);
276		let ctx = ScalarFunctionContext {
277			columns: &columns,
278			row_count: 1,
279		};
280		let result = function.scalar(ctx);
281		assert!(result.is_err(), "Expected error for '0' in base58 input");
282	}
283}