From 33dad773f2c25db54ea019d8a96468931b71ca6a Mon Sep 17 00:00:00 2001 From: shekharrajak Date: Sat, 20 Dec 2025 20:42:35 +0530 Subject: [PATCH] feat: implement Base64 JSON deserialization for Fixed and Binary types --- crates/iceberg/src/spec/values/literal.rs | 37 +++++++++- crates/iceberg/src/spec/values/tests.rs | 90 +++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/crates/iceberg/src/spec/values/literal.rs b/crates/iceberg/src/spec/values/literal.rs index d6e502e8fd..010c3a7347 100644 --- a/crates/iceberg/src/spec/values/literal.rs +++ b/crates/iceberg/src/spec/values/literal.rs @@ -500,8 +500,41 @@ impl Literal { (PrimitiveType::Uuid, JsonValue::String(s)) => Ok(Some(Literal::Primitive( PrimitiveLiteral::UInt128(Uuid::parse_str(&s)?.as_u128()), ))), - (PrimitiveType::Fixed(_), JsonValue::String(_)) => todo!(), - (PrimitiveType::Binary, JsonValue::String(_)) => todo!(), + (PrimitiveType::Fixed(len), JsonValue::String(s)) => { + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD + .decode(&s) + .map_err(|e| { + Error::new( + ErrorKind::DataInvalid, + format!("Failed to decode base64 for Fixed type: {}", e), + ) + })?; + if bytes.len() != *len as usize { + return Err(Error::new( + ErrorKind::DataInvalid, + format!( + "Fixed({}) expects {} bytes, but got {} bytes after base64 decode", + len, + len, + bytes.len() + ), + )); + } + Ok(Some(Literal::Primitive(PrimitiveLiteral::Fixed(bytes)))) + } + (PrimitiveType::Binary, JsonValue::String(s)) => { + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD + .decode(&s) + .map_err(|e| { + Error::new( + ErrorKind::DataInvalid, + format!("Failed to decode base64 for Binary type: {}", e), + ) + })?; + Ok(Some(Literal::Primitive(PrimitiveLiteral::Binary(bytes)))) + } ( PrimitiveType::Decimal { precision: _, diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs index 73343a9a1a..87d4452009 100644 --- a/crates/iceberg/src/spec/values/tests.rs +++ b/crates/iceberg/src/spec/values/tests.rs @@ -1332,3 +1332,93 @@ fn test_date_from_json_as_number() { // Both formats should produce the same Literal value } + +/// Test Fixed and Binary deserialization from JSON (base64 encoded strings) +/// Per Iceberg spec, Fixed and Binary types are encoded as base64 strings in JSON. +#[test] +fn test_fixed_from_json_base64() { + use base64::Engine; + use serde_json::json; + + // Create test bytes and encode as base64 + let bytes = vec![0x01, 0x02, 0x03, 0x04]; + let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); + + let json_value = json!(base64_str); + let result = + Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Fixed(4))).unwrap(); + + assert_eq!(result, Some(Literal::Primitive(PrimitiveLiteral::Fixed(bytes)))); +} + +#[test] +fn test_fixed_from_json_wrong_length() { + use base64::Engine; + use serde_json::json; + + // Create 3 bytes but expect Fixed(4) + let bytes = vec![0x01, 0x02, 0x03]; + let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); + + let json_value = json!(base64_str); + let result = Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Fixed(4))); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::DataInvalid); + assert!(err.to_string().contains("expects 4 bytes")); +} + +#[test] +fn test_fixed_from_json_empty() { + use serde_json::json; + + // Empty base64 string for Fixed(0) + let json_value = json!(""); + let result = + Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Fixed(0))).unwrap(); + + assert_eq!(result, Some(Literal::Primitive(PrimitiveLiteral::Fixed(vec![])))); +} + +#[test] +fn test_binary_from_json_base64() { + use base64::Engine; + use serde_json::json; + + // Create test bytes and encode as base64 + let bytes = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]; + let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); + + let json_value = json!(base64_str); + let result = + Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Binary)).unwrap(); + + assert_eq!(result, Some(Literal::Primitive(PrimitiveLiteral::Binary(bytes)))); +} + +#[test] +fn test_binary_from_json_empty() { + use serde_json::json; + + // Empty base64 string + let json_value = json!(""); + let result = + Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Binary)).unwrap(); + + assert_eq!(result, Some(Literal::Primitive(PrimitiveLiteral::Binary(vec![])))); +} + +#[test] +fn test_binary_from_json_invalid_base64() { + use serde_json::json; + + // Invalid base64 string + let json_value = json!("not-valid-base64!!!"); + let result = Literal::try_from_json(json_value, &Type::Primitive(PrimitiveType::Binary)); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::DataInvalid); + assert!(err.to_string().contains("Failed to decode base64")); +}