You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fluss.apache.org by ab...@apache.org on 2026/09/06 22:45:48 UTC

(fluss) branch main updated: [gateway] Reject duplicate MAP keys in entry arrays (#4205)

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

fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 1e541870f5 [gateway] Reject duplicate MAP keys in entry arrays (#4205)
1e541870f5 is described below

commit 1e541870f5fd6e9f9e84546064d93f52be9d2a34
Author: Gezi-lzq <lz...@gmail.com>
AuthorDate: Mon Sep 7 06:45:42 2026 +0800

    [gateway] Reject duplicate MAP keys in entry arrays (#4205)
    
    Reject duplicate decoded keys before encoding canonical MAP entry arrays so later values cannot silently overwrite earlier entries.
---
 fluss-gateway/src/protocol/rest/codec/decoder.rs | 36 ++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/fluss-gateway/src/protocol/rest/codec/decoder.rs b/fluss-gateway/src/protocol/rest/codec/decoder.rs
index d4d6a43c57..9605936ba5 100644
--- a/fluss-gateway/src/protocol/rest/codec/decoder.rs
+++ b/fluss-gateway/src/protocol/rest/codec/decoder.rs
@@ -1272,6 +1272,15 @@ fn finish_map(
     value_type: &DataType,
     entries: Vec<(Datum<'static>, Datum<'static>)>,
 ) -> DecodeResult<Datum<'static>> {
+    let mut seen = HashSet::with_capacity(entries.len());
+    for (index, (key, _)) in entries.iter().enumerate() {
+        if !seen.insert(key) {
+            return Err(path
+                .map_part(index, "key")
+                .invalid("duplicates an earlier MAP key"));
+        }
+    }
+
     let mut writer = FlussMapWriter::new(entries.len(), key_type, value_type);
     for (key, value) in entries {
         if let Err(error) = writer.write_entry(key, value) {
@@ -1975,6 +1984,33 @@ mod tests {
         assert!(duplicate.message().contains("duplicate key `a`"));
     }
 
+    #[test]
+    fn rejects_duplicate_keys_in_map_entry_arrays() {
+        for (map_type, json, duplicate_path) in [
+            (
+                DataType::Map(MapType::new(
+                    DataType::String(StringType::new()),
+                    DataType::Int(IntType::new()),
+                )),
+                r#"[{"key":"role","value":1},{"key":"region","value":2},{"key":"role","value":3}]"#,
+                "v[2].key",
+            ),
+            (
+                DataType::Map(MapType::new(
+                    DataType::Int(IntType::new()),
+                    DataType::String(StringType::new()),
+                )),
+                r#"[{"key":7,"value":"first"},{"key":"7","value":"second"}]"#,
+                "v[1].key",
+            ),
+        ] {
+            let error = decode_one(map_type, json).unwrap_err();
+            assert!(!error.is_schema_mismatch(), "{json}");
+            assert!(error.message().contains(duplicate_path), "{json}");
+            assert!(error.message().contains("duplicate"), "{json}");
+        }
+    }
+
     #[test]
     fn keeps_object_shorthand_limited_to_string_map_keys() {
         let map_type = DataType::Map(MapType::new(