This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new c5f21c0c2 perf(connectors): drain field_mappings instead of 
clone-then-clear (#4362)
c5f21c0c2 is described below

commit c5f21c0c2d6931b957cbdf7211ed3f5cc82326c9
Author: Matthew Patton <[email protected]>
AuthorDate: Sat Oct 10 16:09:08 2026 -0400

    perf(connectors): drain field_mappings instead of clone-then-clear (#4362)
---
 core/connectors/sdk/src/convert.rs                 | 287 ++++++++++++++++++++-
 core/connectors/sdk/src/decoders/avro.rs           |  35 +--
 core/connectors/sdk/src/decoders/flatbuffer.rs     |  35 +--
 core/connectors/sdk/src/decoders/proto.rs          |  64 +----
 core/connectors/sdk/src/encoders/avro.rs           |  54 ++--
 core/connectors/sdk/src/encoders/flatbuffer.rs     |  48 ++--
 core/connectors/sdk/src/encoders/proto.rs          |  91 ++++---
 core/connectors/sdk/src/transforms/avro_convert.rs |  36 +--
 .../connectors/sdk/src/transforms/proto_convert.rs |  31 +--
 .../connectors/sdk/tests/flatbuffer_integration.rs |  16 ++
 10 files changed, 435 insertions(+), 262 deletions(-)

diff --git a/core/connectors/sdk/src/convert.rs 
b/core/connectors/sdk/src/convert.rs
index 6fe4dd1bd..993e052b4 100644
--- a/core/connectors/sdk/src/convert.rs
+++ b/core/connectors/sdk/src/convert.rs
@@ -15,10 +15,12 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! Value conversion utilities for connector sinks.
+//! Value conversion utilities for connectors.
 //!
-//! Provides shared conversion functions between serialization formats used by
-//! the connector ecosystem (e.g., `simd_json` ↔ `serde_json`).
+//! Shared conversion and payload-transform functions used by the connector
+//! ecosystem: `simd_json`/`serde_json` bridging, JSON key renaming.
+
+use crate::Payload;
 
 /// Convert `simd_json::OwnedValue` to `serde_json::Value` via direct 
structural mapping.
 ///
@@ -49,11 +51,38 @@ pub fn owned_value_to_serde_json(value: 
&simd_json::OwnedValue) -> serde_json::V
     }
 }
 
+/// Renames each key of a `Payload::Json` object via `rename`; other payloads 
pass through. Collisions keep the later entry while the map is Vec-backed, 
unspecified once hash-backed.
+pub(crate) fn apply_field_mappings(
+    payload: Payload,
+    mut rename: impl FnMut(String) -> String,
+) -> Payload {
+    match payload {
+        Payload::Json(json_value) => {
+            if let simd_json::OwnedValue::Object(mut map) = json_value {
+                let renamed: Vec<(String, simd_json::OwnedValue)> = map
+                    .drain()
+                    .map(|(key, value)| (rename(key), value))
+                    .collect();
+                for (key, value) in renamed {
+                    map.insert(key, value);
+                }
+                Payload::Json(simd_json::OwnedValue::Object(map))
+            } else {
+                Payload::Json(json_value)
+            }
+        }
+        other => other,
+    }
+}
+
 #[cfg(test)]
 mod tests {
-    use super::*;
+    use std::collections::HashMap;
+
     use simd_json::{OwnedValue, StaticNode};
 
+    use super::*;
+
     #[test]
     fn test_null() {
         assert_eq!(
@@ -161,4 +190,254 @@ mod tests {
             serde_json::json!({"a": {"b": "v"}})
         );
     }
+
+    #[test]
+    fn apply_field_mappings_should_rename_mapped_key_and_keep_unmapped_key() {
+        let mappings = HashMap::from([("old_field".to_string(), 
"new_field".to_string())]);
+
+        let payload = Payload::Json(simd_json::json!({
+            "old_field": "renamed",
+            "unchanged_field": "stays_same"
+        }));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.get("new_field").unwrap(), 
&OwnedValue::from("renamed"));
+            assert!(!map.contains_key("old_field"));
+            assert_eq!(
+                map.get("unchanged_field").unwrap(),
+                &OwnedValue::from("stays_same")
+            );
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn 
apply_field_mappings_should_swap_two_fields_without_losing_either_value() {
+        let mappings = HashMap::from([
+            ("a".to_string(), "b".to_string()),
+            ("b".to_string(), "a".to_string()),
+        ]);
+
+        let payload = Payload::Json(simd_json::json!({
+            "a": 1,
+            "b": 2
+        }));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.get("a").unwrap(), &OwnedValue::from(2));
+            assert_eq!(map.get("b").unwrap(), &OwnedValue::from(1));
+            assert_eq!(map.len(), 2);
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn 
apply_field_mappings_should_rename_chained_fields_without_a_pre_existing_target()
 {
+        let mappings = HashMap::from([
+            ("a".to_string(), "b".to_string()),
+            ("b".to_string(), "c".to_string()),
+        ]);
+        let payload = Payload::Json(simd_json::json!({"a": 1, "b": 2}));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.get("b").unwrap(), &OwnedValue::from(1));
+            assert_eq!(map.get("c").unwrap(), &OwnedValue::from(2));
+            assert_eq!(map.len(), 2);
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_only_rename_top_level_keys() {
+        let mappings = HashMap::from([("a".to_string(), 
"renamed".to_string())]);
+        let payload = Payload::Json(simd_json::json!({
+            "a": {"a": "nested_value"}
+        }));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert!(!map.contains_key("a"));
+            if let OwnedValue::Object(nested) = map.get("renamed").unwrap() {
+                assert_eq!(nested.get("a").unwrap(), 
&OwnedValue::from("nested_value"));
+            } else {
+                panic!("Expected nested JSON object");
+            }
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_allow_self_mapping_as_a_no_op() {
+        let mappings = HashMap::from([("a".to_string(), "a".to_string())]);
+        let payload = Payload::Json(simd_json::json!({"a": 1}));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.get("a").unwrap(), &OwnedValue::from(1));
+            assert_eq!(map.len(), 1);
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_keep_last_value_when_rename_collides() {
+        let mappings = HashMap::from([("a".to_string(), "c".to_string())]);
+        let payload = Payload::Json(simd_json::json!({"a": 1, "c": 2}));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.len(), 1);
+            assert_eq!(map.get("c").unwrap(), &OwnedValue::from(2));
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn 
apply_field_mappings_should_collapse_a_pre_existing_duplicate_key_to_its_last_value()
 {
+        // Raw bytes, not `json!` (which dedupes at macro-build time), so the 
real parser's duplicate-key handling is exercised.
+        let mut bytes = br#"{"a":1,"a":2,"b":3}"#.to_vec();
+        let value: OwnedValue = simd_json::to_owned_value(&mut bytes).unwrap();
+        if let OwnedValue::Object(map) = &value {
+            assert_eq!(
+                map.len(),
+                3,
+                "expected both raw \"a\" entries to survive parsing"
+            );
+        } else {
+            panic!("Expected JSON object");
+        }
+        let payload = Payload::Json(value);
+
+        let result = apply_field_mappings(payload, |key| key);
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.len(), 2);
+            assert_eq!(map.get("a").unwrap(), &OwnedValue::from(2));
+            assert_eq!(map.get("b").unwrap(), &OwnedValue::from(3));
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn 
apply_field_mappings_should_rename_and_preserve_every_value_above_the_32_key_vec_threshold()
+    {
+        // 40 keys forces simd_json's backing map past the 32-key Vec 
threshold.
+        let mut json = String::from("{");
+        for i in 0..39 {
+            json.push_str(&format!(r#""field_{i}":{i},"#));
+        }
+        json.push_str(r#""field_39":39}"#);
+        let mut bytes = json.into_bytes();
+        let value: OwnedValue = simd_json::to_owned_value(&mut bytes).unwrap();
+        if let OwnedValue::Object(map) = &value {
+            assert!(map.is_map(), "expected the hashbrown backend above 32 
keys");
+        } else {
+            panic!("Expected JSON object");
+        }
+        let payload = Payload::Json(value);
+
+        let mappings: HashMap<String, String> = (0..5)
+            .map(|i| (format!("field_{i}"), format!("renamed_{i}")))
+            .collect();
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.len(), 40);
+            for i in 0..40 {
+                let key = if i < 5 {
+                    format!("renamed_{i}")
+                } else {
+                    format!("field_{i}")
+                };
+                assert_eq!(
+                    map.get(key.as_str())
+                        .unwrap_or_else(|| panic!("missing {key}")),
+                    &OwnedValue::from(i as i64),
+                    "wrong value for {key}"
+                );
+            }
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_ignore_rename_when_old_key_absent() {
+        let mappings = HashMap::from([("missing".to_string(), 
"new_name".to_string())]);
+        let payload = Payload::Json(simd_json::json!({"a": 1}));
+
+        let result =
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key));
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert!(!map.contains_key("new_name"));
+            assert_eq!(map.get("a").unwrap(), &OwnedValue::from(1));
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn 
apply_field_mappings_should_leave_populated_object_unchanged_when_renames_empty()
 {
+        let payload = Payload::Json(simd_json::json!({"a": 1, "b": 2}));
+
+        let result = apply_field_mappings(payload, |key| key);
+
+        if let Payload::Json(OwnedValue::Object(map)) = result {
+            assert_eq!(map.get("a").unwrap(), &OwnedValue::from(1));
+            assert_eq!(map.get("b").unwrap(), &OwnedValue::from(2));
+            assert_eq!(map.len(), 2);
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_leave_non_object_json_unchanged() {
+        let payload = Payload::Json(simd_json::json!([1, 2, 3]));
+
+        let result = apply_field_mappings(payload, |key| key);
+
+        if let Payload::Json(json_value) = result {
+            assert_eq!(json_value, simd_json::json!([1, 2, 3]));
+        } else {
+            panic!("Expected JSON payload");
+        }
+    }
+
+    #[test]
+    fn apply_field_mappings_should_leave_non_json_payload_unchanged() {
+        let payload = Payload::Raw(vec![1, 2, 3]);
+
+        let result = apply_field_mappings(payload, |key| key);
+
+        if let Payload::Raw(bytes) = result {
+            assert_eq!(bytes, vec![1, 2, 3]);
+        } else {
+            panic!("Expected Raw payload");
+        }
+    }
 }
diff --git a/core/connectors/sdk/src/decoders/avro.rs 
b/core/connectors/sdk/src/decoders/avro.rs
index 7a0e519c8..039c2b609 100644
--- a/core/connectors/sdk/src/decoders/avro.rs
+++ b/core/connectors/sdk/src/decoders/avro.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamDecoder};
+use crate::{Error, Payload, Schema, StreamDecoder, 
convert::apply_field_mappings};
 use apache_avro::Schema as AvroSchema;
 use apache_avro::reader::datum::GenericDatumReader;
 use serde::{Deserialize, Serialize};
@@ -166,43 +166,18 @@ impl AvroStreamDecoder {
             Error::CannotDecode(Schema::Avro)
         })?;
 
-        let transformed = 
self.apply_field_transformations(Payload::Json(json_value))?;
-        Ok(transformed)
+        Ok(self.apply_field_transformations(Payload::Json(json_value)))
     }
 
     fn decode_as_raw(&self, payload: Vec<u8>) -> Result<Payload, Error> {
         Ok(Payload::Avro(payload))
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            if let Some(new_key) = mappings.get(key) {
-                                new_entries.push((new_key.clone(), 
value.clone()));
-                            } else {
-                                new_entries.push((key.clone(), value.clone()));
-                            }
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 }
diff --git a/core/connectors/sdk/src/decoders/flatbuffer.rs 
b/core/connectors/sdk/src/decoders/flatbuffer.rs
index 54f70509c..ec2f13ff4 100644
--- a/core/connectors/sdk/src/decoders/flatbuffer.rs
+++ b/core/connectors/sdk/src/decoders/flatbuffer.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamDecoder};
+use crate::{Error, Payload, Schema, StreamDecoder, 
convert::apply_field_mappings};
 use base64::Engine;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
@@ -106,8 +106,7 @@ impl FlatBufferStreamDecoder {
         );
 
         let json_payload = simd_json::OwnedValue::Object(json_map);
-        let transformed = 
self.apply_field_transformations(Payload::Json(json_payload))?;
-        Ok(transformed)
+        Ok(self.apply_field_transformations(Payload::Json(json_payload)))
     }
 
     fn decode_as_raw(&self, data: Vec<u8>) -> Result<Payload, Error> {
@@ -117,35 +116,11 @@ impl FlatBufferStreamDecoder {
         Ok(Payload::FlatBuffer(data))
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            if let Some(new_key) = mappings.get(key) {
-                                new_entries.push((new_key.clone(), 
value.clone()));
-                            } else {
-                                new_entries.push((key.clone(), value.clone()));
-                            }
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 }
diff --git a/core/connectors/sdk/src/decoders/proto.rs 
b/core/connectors/sdk/src/decoders/proto.rs
index a4d774a6e..faed2ac8c 100644
--- a/core/connectors/sdk/src/decoders/proto.rs
+++ b/core/connectors/sdk/src/decoders/proto.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamDecoder};
+use crate::{Error, Payload, Schema, StreamDecoder, 
convert::apply_field_mappings};
 use base64::Engine;
 use prost::Message;
 use prost_types::Any;
@@ -521,35 +521,11 @@ impl ProtoStreamDecoder {
             .ok_or(Error::InvalidProtobufPayload)
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            if let Some(new_key) = mappings.get(key) {
-                                new_entries.push((new_key.clone(), 
value.clone()));
-                            } else {
-                                new_entries.push((key.clone(), value.clone()));
-                            }
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 
@@ -586,7 +562,7 @@ impl StreamDecoder for ProtoStreamDecoder {
 
         let decoded_payload = self.decode_with_schema(payload)?;
 
-        self.apply_field_transformations(decoded_payload)
+        Ok(self.apply_field_transformations(decoded_payload))
     }
 }
 
@@ -655,40 +631,28 @@ mod tests {
 
     #[test]
     fn decode_should_apply_field_mappings_when_configured() {
+        // use_any_wrapper alone always emits literal type_url/value keys, 
making a mapping unobservable, so decode via a real schema instead.
         let mut field_mappings = HashMap::new();
-        field_mappings.insert("old_field".to_string(), 
"new_field".to_string());
-        field_mappings.insert("user_id".to_string(), "id".to_string());
+        field_mappings.insert("id".to_string(), "user_id".to_string());
 
         let config = ProtoConfig {
+            schema_path: Some(PathBuf::from("examples/user.proto")),
+            message_type: Some("com.example.User".to_string()),
+            use_any_wrapper: false,
             field_mappings: Some(field_mappings),
-            use_any_wrapper: true,
             ..ProtoConfig::default()
         };
         let decoder = ProtoStreamDecoder::new(config);
 
-        let json_content = simd_json::json!({
-            "old_field": "should_be_renamed",
-            "user_id": 123,
-            "unchanged_field": "stays_same"
-        });
-        let json_string = simd_json::to_string(&json_content).unwrap();
-
-        let any = Any {
-            type_url: "type.googleapis.com/custom.Message".to_string(),
-            value: json_string.into_bytes(),
-        };
+        // Hand-encode a second, unmapped field (name) alongside id so the 
rename isn't the message's only field.
+        let mut encoded = 42i32.encode_to_vec();
+        encoded.extend_from_slice(&[0x12, 3, b'B', b'o', b'b']);
 
-        let encoded = any.encode_to_vec();
         let result = decoder.decode(encoded);
 
         assert!(result.is_ok());
         if let Ok(Payload::Json(json_value)) = result {
-            if let simd_json::OwnedValue::Object(map) = &json_value {
-                assert!(map.contains_key("type_url"));
-                assert!(map.contains_key("value"));
-            } else {
-                panic!("Expected JSON object");
-            }
+            assert_eq!(json_value, simd_json::json!({"user_id": 42, "name": 
"Bob"}));
         } else {
             panic!("Expected JSON payload");
         }
diff --git a/core/connectors/sdk/src/encoders/avro.rs 
b/core/connectors/sdk/src/encoders/avro.rs
index 1d6701390..588207fc9 100644
--- a/core/connectors/sdk/src/encoders/avro.rs
+++ b/core/connectors/sdk/src/encoders/avro.rs
@@ -15,7 +15,10 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamEncoder, 
convert::owned_value_to_serde_json};
+use crate::{
+    Error, Payload, Schema, StreamEncoder,
+    convert::{apply_field_mappings, owned_value_to_serde_json},
+};
 use apache_avro::Schema as AvroSchema;
 use apache_avro::writer::datum::GenericDatumWriter;
 use base64::Engine;
@@ -118,35 +121,11 @@ impl AvroStreamEncoder {
         Ok(())
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            if let Some(new_key) = mappings.get(key) {
-                                new_entries.push((new_key.clone(), 
value.clone()));
-                            } else {
-                                new_entries.push((key.clone(), value.clone()));
-                            }
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 
@@ -352,7 +331,7 @@ impl StreamEncoder for AvroStreamEncoder {
     }
 
     fn encode(&self, payload: Payload) -> Result<Vec<u8>, Error> {
-        let transformed_payload = self.apply_field_transformations(payload)?;
+        let transformed_payload = self.apply_field_transformations(payload);
 
         match transformed_payload {
             Payload::Json(json_value) => self.encode_json_to_avro(json_value),
@@ -373,6 +352,8 @@ impl Default for AvroStreamEncoder {
 
 #[cfg(test)]
 mod tests {
+    use apache_avro::reader::datum::GenericDatumReader;
+
     use super::*;
 
     fn create_test_schema_json() -> String {
@@ -428,7 +409,7 @@ mod tests {
         field_mappings.insert("full_name".to_string(), "name".to_string());
 
         let config = AvroEncoderConfig {
-            schema_json: Some(schema_json),
+            schema_json: Some(schema_json.clone()),
             field_mappings: Some(field_mappings),
             ..AvroEncoderConfig::default()
         };
@@ -444,6 +425,19 @@ mod tests {
         assert!(result.is_ok());
         let encoded_data = result.unwrap();
         assert!(!encoded_data.is_empty());
+
+        // Decode the real bytes and check the value; schema strictness alone 
isn't proof the rename worked.
+        let schema = AvroSchema::parse_str(&schema_json).unwrap();
+        let mut reader: &[u8] = &encoded_data;
+        let decoded = GenericDatumReader::builder(&schema)
+            .build()
+            .unwrap()
+            .read_value(&mut reader)
+            .unwrap();
+        let decoded_json = serde_json::Value::try_from(decoded).unwrap();
+
+        assert_eq!(decoded_json["name"], serde_json::json!("Alice"));
+        assert_eq!(decoded_json["age"], serde_json::json!(30));
     }
 
     #[test]
diff --git a/core/connectors/sdk/src/encoders/flatbuffer.rs 
b/core/connectors/sdk/src/encoders/flatbuffer.rs
index 32c60daaf..07187c358 100644
--- a/core/connectors/sdk/src/encoders/flatbuffer.rs
+++ b/core/connectors/sdk/src/encoders/flatbuffer.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamEncoder};
+use crate::{Error, Payload, Schema, StreamEncoder, 
convert::apply_field_mappings};
 use base64::Engine;
 use flatbuffers::FlatBufferBuilder;
 use serde::{Deserialize, Serialize};
@@ -63,35 +63,11 @@ impl FlatBufferStreamEncoder {
         Ok(())
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            if let Some(new_key) = mappings.get(key) {
-                                new_entries.push((new_key.clone(), 
value.clone()));
-                            } else {
-                                new_entries.push((key.clone(), value.clone()));
-                            }
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 
@@ -229,7 +205,7 @@ impl StreamEncoder for FlatBufferStreamEncoder {
     }
 
     fn encode(&self, payload: Payload) -> Result<Vec<u8>, Error> {
-        let transformed_payload = self.apply_field_transformations(payload)?;
+        let transformed_payload = self.apply_field_transformations(payload);
 
         match transformed_payload {
             Payload::Json(json_value) => 
self.encode_json_to_flatbuffer(json_value),
@@ -327,6 +303,20 @@ mod tests {
         assert!(result.is_ok());
         let encoded_data = result.unwrap();
         assert!(!encoded_data.is_empty());
+
+        // No `.fbs` schema to decode typed fields, but keys are plain UTF-8 
strings inline, so the rename is checkable byte-wise.
+        assert!(contains_bytes(&encoded_data, b"new_field"));
+        assert!(!contains_bytes(&encoded_data, b"old_field"));
+        assert!(contains_bytes(&encoded_data, b"unchanged_field"));
+        // Confirms the renamed key kept its value rather than being dropped.
+        assert!(contains_bytes(&encoded_data, b"should_be_renamed"));
+        assert!(contains_bytes(&encoded_data, b"stays_same"));
+    }
+
+    fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
+        haystack
+            .windows(needle.len())
+            .any(|window| window == needle)
     }
 
     #[test]
diff --git a/core/connectors/sdk/src/encoders/proto.rs 
b/core/connectors/sdk/src/encoders/proto.rs
index 44dca94f0..acac89b82 100644
--- a/core/connectors/sdk/src/encoders/proto.rs
+++ b/core/connectors/sdk/src/encoders/proto.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{Error, Payload, Schema, StreamEncoder};
+use crate::{Error, Payload, Schema, StreamEncoder, 
convert::apply_field_mappings};
 use base64::{Engine as Base64Engine, engine::general_purpose};
 use iggy_common::IggyTimestamp;
 use prost::Message;
@@ -279,37 +279,17 @@ impl ProtoStreamEncoder {
         Ok(())
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            let proto_key = mappings
-                                .iter()
-                                .find(|(_, json_name)| *json_name == key)
-                                .map(|(proto_name, _)| proto_name.clone())
-                                .unwrap_or_else(|| key.clone());
-
-                            new_entries.push((proto_key, value.clone()));
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| {
+                mappings
+                    .iter()
+                    .find(|(_, json_name)| json_name.as_str() == key.as_str())
+                    .map(|(proto_name, _)| proto_name.clone())
+                    .unwrap_or(key)
+            })
         } else {
-            Ok(payload)
+            payload
         }
     }
 
@@ -730,7 +710,7 @@ impl StreamEncoder for ProtoStreamEncoder {
     }
 
     fn encode(&self, payload: Payload) -> Result<Vec<u8>, Error> {
-        let transformed_payload = self.apply_field_transformations(payload)?;
+        let transformed_payload = self.apply_field_transformations(payload);
 
         self.encode_with_schema(transformed_payload)
     }
@@ -909,9 +889,18 @@ mod tests {
         let decoded_json = simd_json::to_owned_value(&mut 
encoded_bytes_mut).unwrap();
 
         if let simd_json::OwnedValue::Object(map) = decoded_json {
-            assert!(map.contains_key("user_id"));
-            assert!(map.contains_key("full_name"));
-            assert!(map.contains_key("email"));
+            assert_eq!(
+                map.get("user_id").unwrap(),
+                &simd_json::OwnedValue::from(123)
+            );
+            assert_eq!(
+                map.get("full_name").unwrap(),
+                &simd_json::OwnedValue::from("John Doe")
+            );
+            assert_eq!(
+                map.get("email").unwrap(),
+                &simd_json::OwnedValue::from("[email protected]")
+            );
             assert!(!map.contains_key("id"));
             assert!(!map.contains_key("name"));
         } else {
@@ -919,6 +908,40 @@ mod tests {
         }
     }
 
+    #[test]
+    fn 
encode_should_resolve_ambiguous_reverse_lookup_to_one_of_the_colliding_keys() {
+        // Reverse-lookup collision resolves via HashMap iteration order (not 
deterministic), so only pin that one of the two survives.
+        let mut field_mappings = HashMap::new();
+        field_mappings.insert("proto_a".to_string(), "shared".to_string());
+        field_mappings.insert("proto_b".to_string(), "shared".to_string());
+
+        let config = ProtoEncoderConfig {
+            field_mappings: Some(field_mappings),
+            use_any_wrapper: false,
+            ..ProtoEncoderConfig::default()
+        };
+        let encoder = ProtoStreamEncoder::new_with_config(config);
+
+        let json_value = simd_json::json!({"shared": "value"});
+        let result = encoder.encode(Payload::Json(json_value));
+
+        assert!(result.is_ok());
+        let mut encoded_bytes = result.unwrap();
+        let decoded_json = simd_json::to_owned_value(&mut 
encoded_bytes).unwrap();
+
+        if let simd_json::OwnedValue::Object(map) = decoded_json {
+            assert_eq!(map.len(), 1);
+            let renamed_to_a = map.get("proto_a") == 
Some(&simd_json::OwnedValue::from("value"));
+            let renamed_to_b = map.get("proto_b") == 
Some(&simd_json::OwnedValue::from("value"));
+            assert!(
+                renamed_to_a || renamed_to_b,
+                "expected the value to land on proto_a or proto_b, got {map:?}"
+            );
+        } else {
+            panic!("Expected JSON object");
+        }
+    }
+
     #[test]
     fn encode_should_use_custom_type_url_prefix_when_configured() {
         let config = ProtoEncoderConfig {
diff --git a/core/connectors/sdk/src/transforms/avro_convert.rs 
b/core/connectors/sdk/src/transforms/avro_convert.rs
index 5a14dd000..02062715c 100644
--- a/core/connectors/sdk/src/transforms/avro_convert.rs
+++ b/core/connectors/sdk/src/transforms/avro_convert.rs
@@ -22,7 +22,7 @@ use std::path::PathBuf;
 use super::{Transform, TransformType};
 use crate::decoders::avro::{AvroConfig, AvroStreamDecoder};
 use crate::encoders::avro::{AvroEncoderConfig, AvroStreamEncoder};
-use crate::{DecodedMessage, Error, Payload, Schema, TopicMetadata};
+use crate::{DecodedMessage, Error, Payload, Schema, TopicMetadata, 
convert::apply_field_mappings};
 use crate::{StreamDecoder, StreamEncoder};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -68,36 +68,14 @@ impl AvroConvert {
         Self::new(AvroConvertConfig::default())
     }
 
-    fn apply_field_mappings(
+    fn apply_field_transformations(
         &self,
         payload: Payload,
         field_mappings: &HashMap<String, String>,
-    ) -> Result<Payload, Error> {
-        match payload {
-            Payload::Json(json_value) => {
-                if let simd_json::OwnedValue::Object(mut map) = json_value {
-                    let mut new_entries = Vec::new();
-
-                    for (key, value) in map.iter() {
-                        if let Some(new_key) = field_mappings.get(key) {
-                            new_entries.push((new_key.clone(), value.clone()));
-                        } else {
-                            new_entries.push((key.clone(), value.clone()));
-                        }
-                    }
-
-                    map.clear();
-                    for (key, value) in new_entries {
-                        map.insert(key, value);
-                    }
-
-                    Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                } else {
-                    Ok(Payload::Json(json_value))
-                }
-            }
-            other => Ok(other),
-        }
+    ) -> Payload {
+        apply_field_mappings(payload, |key| {
+            field_mappings.get(&key).cloned().unwrap_or(key)
+        })
     }
 }
 
@@ -112,7 +90,7 @@ impl Transform for AvroConvert {
         mut message: DecodedMessage,
     ) -> Result<Option<DecodedMessage>, Error> {
         if let Some(field_mappings) = &self.config.field_mappings {
-            message.payload = self.apply_field_mappings(message.payload, 
field_mappings)?;
+            message.payload = 
self.apply_field_transformations(message.payload, field_mappings);
         }
 
         message.payload = match (&self.config.source_format, 
&self.config.target_format) {
diff --git a/core/connectors/sdk/src/transforms/proto_convert.rs 
b/core/connectors/sdk/src/transforms/proto_convert.rs
index 4f239641f..da32a9b40 100644
--- a/core/connectors/sdk/src/transforms/proto_convert.rs
+++ b/core/connectors/sdk/src/transforms/proto_convert.rs
@@ -26,7 +26,7 @@ use tracing::{error, info};
 
 use super::{Transform, TransformType};
 use crate::encoders::proto::ProtoStreamEncoder;
-use crate::{DecodedMessage, Error, Payload, Schema, TopicMetadata};
+use crate::{DecodedMessage, Error, Payload, Schema, TopicMetadata, 
convert::apply_field_mappings};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ProtoConvertConfig {
@@ -484,32 +484,11 @@ impl ProtoConvert {
         }
     }
 
-    fn apply_field_transformations(&self, payload: Payload) -> Result<Payload, 
Error> {
+    fn apply_field_transformations(&self, payload: Payload) -> Payload {
         if let Some(mappings) = &self.config.field_mappings {
-            match payload {
-                Payload::Json(json_value) => {
-                    if let simd_json::OwnedValue::Object(mut map) = json_value 
{
-                        let mut new_entries = Vec::new();
-
-                        for (key, value) in map.iter() {
-                            let new_key = 
mappings.get(key).cloned().unwrap_or_else(|| key.clone());
-                            new_entries.push((new_key, value.clone()));
-                        }
-
-                        map.clear();
-                        for (key, value) in new_entries {
-                            map.insert(key, value);
-                        }
-
-                        Ok(Payload::Json(simd_json::OwnedValue::Object(map)))
-                    } else {
-                        Ok(Payload::Json(json_value))
-                    }
-                }
-                other => Ok(other),
-            }
+            apply_field_mappings(payload, |key| 
mappings.get(&key).cloned().unwrap_or(key))
         } else {
-            Ok(payload)
+            payload
         }
     }
 
@@ -732,7 +711,7 @@ impl Transform for ProtoConvert {
         _metadata: &TopicMetadata,
         mut message: DecodedMessage,
     ) -> Result<Option<DecodedMessage>, Error> {
-        let transformed_payload = 
self.apply_field_transformations(message.payload)?;
+        let transformed_payload = 
self.apply_field_transformations(message.payload);
 
         let converted_payload = match (self.config.source_format, 
self.config.target_format) {
             (Schema::Proto, _) => 
self.convert_from_protobuf(transformed_payload)?,
diff --git a/core/connectors/sdk/tests/flatbuffer_integration.rs 
b/core/connectors/sdk/tests/flatbuffer_integration.rs
index ab14669e1..b360744b5 100644
--- a/core/connectors/sdk/tests/flatbuffer_integration.rs
+++ b/core/connectors/sdk/tests/flatbuffer_integration.rs
@@ -78,12 +78,24 @@ async fn 
should_transform_with_real_schema_and_field_mapping() {
                     bytes.len() >= 4,
                     "FlatBuffer should have minimum header size"
                 );
+
+                // "id"/"name" are substrings of "user_id"/"full_name", so 
absence of the old keys is the reliable signal, not presence of the new ones.
+                assert!(!contains_bytes(&bytes, b"user_id"));
+                assert!(!contains_bytes(&bytes, b"full_name"));
+                assert!(contains_bytes(&bytes, b"email"));
+                assert!(contains_bytes(&bytes, b"Jane Smith"));
             }
             other => panic!("Expected FlatBuffer payload, got: {other:?}"),
         }
     }
 }
 
+fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
+    haystack
+        .windows(needle.len())
+        .any(|window| window == needle)
+}
+
 #[tokio::test]
 async fn should_encode_decode_with_schema_configuration() {
     let encoder_config = FlatBufferEncoderConfig {
@@ -225,6 +237,10 @@ async fn 
should_transform_json_to_flatbuffer_with_field_mappings() {
             Payload::FlatBuffer(bytes) => {
                 println!("Transformed FlatBuffer: {} bytes", bytes.len());
                 assert!(!bytes.is_empty(), "FlatBuffer should not be empty");
+                assert!(!contains_bytes(&bytes, b"user_id"));
+                assert!(!contains_bytes(&bytes, b"full_name"));
+                assert!(contains_bytes(&bytes, b"email"));
+                assert!(contains_bytes(&bytes, b"Jane Smith"));
             }
             other => panic!("Expected FlatBuffer payload, got: {other:?}"),
         }

Reply via email to