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

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


The following commit(s) were added to refs/heads/master by this push:
     new 21395ecb574 Decode MAP values by declared type instead of through 
Jackson (#19171)
21395ecb574 is described below

commit 21395ecb57417a9b079264593991ab9086f0c217
Author: Xiang Fu <[email protected]>
AuthorDate: Sat Aug 15 16:30:35 2026 -0700

    Decode MAP values by declared type instead of through Jackson (#19171)
---
 .../apache/pinot/perf/BenchmarkMapKeyAccess.java   |  41 +++++++-
 .../impl/forward/VarByteSVMutableForwardIndex.java |   6 ++
 .../local/segment/index/map/MapKeyIndexReader.java |  20 +++-
 .../segment/index/map/MapKeyIndexReaderTest.java   |  32 +++++-
 .../spi/index/reader/ForwardIndexReader.java       |  35 +++++--
 .../java/org/apache/pinot/spi/utils/MapUtils.java  | 109 +++++++++++++++++++--
 .../org/apache/pinot/spi/utils/MapUtilsTest.java   |  68 +++++++++++++
 7 files changed, 288 insertions(+), 23 deletions(-)

diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
index 2ba7aa04a20..cf26f711085 100644
--- a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
+++ b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
@@ -76,9 +76,11 @@ public class BenchmarkMapKeyAccess {
   @Param({"first", "last"})
   private String _targetPosition;
 
-  /// `flat` models scalar attribute maps (the common ingestion shape); 
`nested` models maps whose values are
-  /// themselves objects, where the full-map path pays to materialize a 
container per value.
-  @Param({"flat", "nested"})
+  /// `flat` models scalar string attribute maps (the common ingestion shape); 
`numeric` models a STRING-valued MAP
+  /// carrying integer entries, which 
`MapFieldTypeMixedValueIngestingIntegrationTest` shows is supported and
+  /// projected through the same accessor; `nested` models object-valued 
entries, the shape that still has to go
+  /// through Jackson.
+  @Param({"flat", "numeric", "nested"})
   private String _valueShape;
 
   private String _targetKey;
@@ -88,11 +90,24 @@ public class BenchmarkMapKeyAccess {
 
   @Setup(Level.Trial)
   public void setUp() {
-    boolean nested = "nested".equals(_valueShape);
     Map<String, Object> map = new LinkedHashMap<>();
     for (int i = 0; i < _numEntries; i++) {
       String value = "value-with-enough-bytes-to-exercise-json-parsing-" + i;
-      map.put(key(i), nested ? Map.of("value", value) : value);
+      Object stored;
+      switch (_valueShape) {
+        // A STRING-valued MAP carrying numeric entries, the shape
+        // MapFieldTypeMixedValueIngestingIntegrationTest ingests.
+        case "numeric":
+          stored = 9007199254740990L + i;
+          break;
+        case "nested":
+          stored = Map.of("value", value);
+          break;
+        default:
+          stored = value;
+          break;
+      }
+      map.put(key(i), stored);
     }
     _targetKey = key("first".equals(_targetPosition) ? 0 : _numEntries - 1);
     _targetMapKey = new PreparedMapKey(_targetKey);
@@ -129,4 +144,20 @@ public class BenchmarkMapKeyAccess {
   public Object selectiveMapValue() {
     return _forwardIndex.getMapEntryValue(0, null, _targetMapKey);
   }
+
+  /// The string baseline: what `MapKeyIndexReader#getString` did - 
deserialize to an object, then `toString()` it.
+  @Benchmark
+  public Object selectiveMapValueToString() {
+    Object value = _forwardIndex.getMapEntryValue(0, null, _targetMapKey);
+    return value == null ? null : value.toString();
+  }
+
+  /// Same scan, but decoding the value without handing it to Jackson - what a 
`STRING`-valued MAP column resolves
+  /// to when projected. Plain strings, canonical integers and booleans take 
that path; object and array values, and
+  /// non-integral numbers, still fall back, so `nested` should land on top of 
[#selectiveMapValueToString] rather
+  /// than beating it.
+  @Benchmark
+  public Object selectiveMapValueAsString() {
+    return _forwardIndex.getMapEntryValueAsString(0, null, _targetMapKey);
+  }
 }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/VarByteSVMutableForwardIndex.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/VarByteSVMutableForwardIndex.java
index cb2bc35f9da..36be816712a 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/VarByteSVMutableForwardIndex.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/VarByteSVMutableForwardIndex.java
@@ -113,6 +113,12 @@ public class VarByteSVMutableForwardIndex implements 
MutableForwardIndex {
     return MapUtils.frameToJsonString(getBytes(docId));
   }
 
+  @Override
+  @Nullable
+  public String getMapEntryValueAsString(int docId, ForwardIndexReaderContext 
context, PreparedMapKey key) {
+    return 
MapUtils.deserializeMapEntryValueAsString(_byteArrayStore.getByteBuffer(docId), 
key);
+  }
+
   @Override
   public void setBigDecimal(int docId, BigDecimal value) {
     setBytes(docId, BigDecimalUtils.serialize(value));
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReader.java
index dca7a8d2aed..c4f55493a35 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReader.java
@@ -58,14 +58,24 @@ public class MapKeyIndexReader implements 
ForwardIndexReader {
 
   @Override
   public int getInt(int docId, ForwardIndexReaderContext context) {
-    return Integer.parseInt(extractMapValue(docId, context).toString());
+    Object value = extractMapValue(docId, context);
+    return value instanceof Integer ? (Integer) value : 
Integer.parseInt(value.toString());
   }
 
   @Override
   public long getLong(int docId, ForwardIndexReaderContext context) {
-    return Long.parseLong(extractMapValue(docId, context).toString());
+    Object value = extractMapValue(docId, context);
+    if (value instanceof Long) {
+      return (Long) value;
+    }
+    return value instanceof Integer ? (Integer) value : 
Long.parseLong(value.toString());
   }
 
+  /// No fast path here: Jackson's untyped binding never yields a `Float` - a 
JSON decimal comes back as `Double` -
+  /// so a `Float` check would be dead code. Narrowing the `Double` instead is 
not equivalent: for a double sitting
+  /// near the midpoint between two floats the two conversions differ by an 
ulp, because one rounds the double
+  /// directly while the other rounds its shortest decimal. 
`-1.340092769725468E-17` narrows to `-1.3400928E-17`
+  /// but parses to `-1.3400927E-17`. This method has always produced the 
parsed value, so it keeps doing that.
   @Override
   public float getFloat(int docId, ForwardIndexReaderContext context) {
     return Float.parseFloat(extractMapValue(docId, context).toString());
@@ -73,12 +83,14 @@ public class MapKeyIndexReader implements 
ForwardIndexReader {
 
   @Override
   public double getDouble(int docId, ForwardIndexReaderContext context) {
-    return Double.parseDouble(extractMapValue(docId, context).toString());
+    Object value = extractMapValue(docId, context);
+    return value instanceof Double ? (Double) value : 
Double.parseDouble(value.toString());
   }
 
   @Override
   public String getString(int docId, ForwardIndexReaderContext context) {
-    return extractMapValue(docId, context).toString();
+    String value = _forwardIndexReader.getMapEntryValueAsString(docId, 
context, _mapKey);
+    return value != null ? value : _defaultNullValue.toString();
   }
 
   @Override
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReaderTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReaderTest.java
index f83f8c3457c..bd28d979531 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReaderTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReaderTest.java
@@ -29,6 +29,7 @@ import org.apache.pinot.spi.utils.MapUtils.PreparedMapKey;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
 
 
 /// Covers [MapKeyIndexReader] over both shapes of underlying reader: one that 
implements the selective
@@ -36,7 +37,8 @@ import static org.testng.Assert.assertEquals;
 /// [ForwardIndexReader#getMap] and therefore falls through to the default. 
Both must agree.
 public class MapKeyIndexReaderTest {
   private static final Map<String, Object> MAP =
-      Map.of("k8s.workload.name", "pinot-server", "k8s.workload.replicas", 3);
+      Map.of("k8s.workload.name", "pinot-server", "k8s.workload.replicas", 3,
+          "longValue", 9999999999L, "doubleValue", 1.5d);
 
   @Test
   public void testSelectiveReader() {
@@ -54,6 +56,31 @@ public class MapKeyIndexReaderTest {
     assertEquals(new SelectiveReader().getMapEntryValue(0, null, 
"k8s.workload.name"), "pinot-server");
   }
 
+  /// The numeric accessors fast-path only the exact boxed type Jackson yields 
for that JSON shape, and fall back to
+  /// the string round trip otherwise. Pinning both halves stops a future 
broadening to `Number#intValue()` from
+  /// silently truncating a decimal that currently throws.
+  @Test
+  public void testNumericAccessorsAcrossBoxedTypes() {
+    ForwardIndexReader<ForwardIndexReaderContext> reader = new 
SelectiveReader();
+    FieldSpec longSpec = new DimensionFieldSpec("value", DataType.LONG, true);
+    FieldSpec intSpec = new DimensionFieldSpec("value", DataType.INT, true);
+    FieldSpec doubleSpec = new DimensionFieldSpec("value", DataType.DOUBLE, 
true);
+    FieldSpec floatSpec = new DimensionFieldSpec("value", DataType.FLOAT, 
true);
+
+    // Long value through the Long branch, and an Integer widened through the 
Integer branch.
+    assertEquals(new MapKeyIndexReader(reader, "longValue", 
longSpec).getLong(0, null), 9999999999L);
+    assertEquals(new MapKeyIndexReader(reader, "k8s.workload.replicas", 
longSpec).getLong(0, null), 3L);
+    assertEquals(new MapKeyIndexReader(reader, "doubleValue", 
doubleSpec).getDouble(0, null), 1.5d);
+    // getFloat has no exact-type fast path; it still parses the rendered 
string.
+    assertEquals(new MapKeyIndexReader(reader, "doubleValue", 
floatSpec).getFloat(0, null), 1.5f);
+
+    // A decimal read as INT or LONG must keep failing rather than being 
silently truncated.
+    assertThrows(NumberFormatException.class,
+        () -> new MapKeyIndexReader(reader, "doubleValue", intSpec).getInt(0, 
null));
+    assertThrows(NumberFormatException.class,
+        () -> new MapKeyIndexReader(reader, "doubleValue", 
longSpec).getLong(0, null));
+  }
+
   private static void 
assertReaderBehavior(ForwardIndexReader<ForwardIndexReaderContext> reader) {
     FieldSpec stringSpec = new DimensionFieldSpec("value", DataType.STRING, 
true);
     assertEquals(new MapKeyIndexReader(reader, "k8s.workload.name", 
stringSpec).getString(0, null), "pinot-server");
@@ -64,6 +91,9 @@ public class MapKeyIndexReaderTest {
 
     FieldSpec intSpec = new DimensionFieldSpec("value", DataType.INT, true);
     assertEquals(new MapKeyIndexReader(reader, "k8s.workload.replicas", 
intSpec).getInt(0, null), 3);
+
+    // A STRING-valued MAP may still carry numeric entries; those must render 
as their canonical text.
+    assertEquals(new MapKeyIndexReader(reader, "longValue", 
stringSpec).getString(0, null), "9999999999");
   }
 
   /// Mirrors the mutable forward index: answers a single key without 
materializing the map.
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/ForwardIndexReader.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/ForwardIndexReader.java
index 1a9c37b0f7c..05652424438 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/ForwardIndexReader.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/ForwardIndexReader.java
@@ -467,6 +467,19 @@ public interface ForwardIndexReader<T extends 
ForwardIndexReaderContext> extends
         + "ForwardIndexReader is being created to read this column.");
   }
 
+  /// Reads the MAP type single-value at the given document id, rendered as a 
JSON object string.
+  ///
+  /// Readers that store the map as a serialized frame should override this to 
render straight from those bytes
+  /// (see [MapUtils#frameToJsonString(byte\[\])]); the default here 
materializes the map first and serializes it
+  /// again, which is what a reader holding the map columnar-decomposed has to 
do anyway.
+  ///
+  /// @param docId Document id
+  /// @param context Reader context
+  /// @return MAP type single-value at the given document id, as JSON
+  default String getMapAsJsonString(int docId, T context) {
+    return MapUtils.toString(getMap(docId, context));
+  }
+
   /// Reads a value for a key from a MAP type single-value column at the given 
document id.
   /// Implementations can override this method to avoid deserializing the 
entire map when only one key is needed.
   ///
@@ -486,17 +499,25 @@ public interface ForwardIndexReader<T extends 
ForwardIndexReaderContext> extends
     return getMap(docId, context).get(key.getKey());
   }
 
-  /// Reads the MAP type single-value at the given document id, rendered as a 
JSON object string.
+  /// Reads a value for a key from a MAP type single-value column as a string.
   ///
-  /// Readers that store the map as a serialized frame should override this to 
render straight from those bytes
-  /// (see [MapUtils#frameToJsonString(byte\[\])]); the default here 
materializes the map first and serializes it
-  /// again, which is what a reader holding the map columnar-decomposed has to 
do anyway.
+  /// Implementations that store the map as a serialized frame can override 
this to decode a stored string value
+  /// without routing it through a JSON parser.
   ///
   /// @param docId Document id
   /// @param context Reader context
-  /// @return MAP type single-value at the given document id, as JSON
-  default String getMapAsJsonString(int docId, T context) {
-    return MapUtils.toString(getMap(docId, context));
+  /// @param key Map key
+  /// @return Value for the key as a string, or `null` if the key is missing 
or its value is null
+  @Nullable
+  default String getMapEntryValueAsString(int docId, T context, String key) {
+    return getMapEntryValueAsString(docId, context, new PreparedMapKey(key));
+  }
+
+  /// Variant of [#getMapEntryValueAsString(int, ForwardIndexReaderContext, 
String)] that reuses a pre-encoded MAP key.
+  @Nullable
+  default String getMapEntryValueAsString(int docId, T context, PreparedMapKey 
key) {
+    Object value = getMapEntryValue(docId, context, key);
+    return value == null ? null : value.toString();
   }
 
   default int get32BitsMurmur3Hash(int docId, T context) {
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/MapUtils.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/MapUtils.java
index 26362b9f22e..9869611b2f2 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/MapUtils.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/MapUtils.java
@@ -239,6 +239,108 @@ public class MapUtils {
   /// @throws BufferUnderflowException if the MAP frame is malformed or 
truncated
   @Nullable
   public static Object deserializeMapEntryValue(ByteBuffer byteBuffer, 
PreparedMapKey key) {
+    byte[] valueBytes = findValueBytes(byteBuffer, key);
+    if (valueBytes == null) {
+      return null;
+    }
+    try {
+      return JsonUtils.bytesToObject(valueBytes, Object.class);
+    } catch (IOException e) {
+      LOGGER.error("Caught exception while deserializing value for key: {}", 
key.getKey(), e);
+      return null;
+    }
+  }
+
+  @Nullable
+  public static String deserializeMapEntryValueAsString(ByteBuffer byteBuffer, 
String key) {
+    return deserializeMapEntryValueAsString(byteBuffer, new 
PreparedMapKey(key));
+  }
+
+  /// Reads the value for a prepared key as a string, skipping Jackson for the 
value shapes whose stored bytes are
+  /// already exactly what `toString()` on the parsed value would produce.
+  ///
+  /// Consumes the buffer from its current position and forces 
[ByteOrder#BIG_ENDIAN] on it, exactly as
+  /// [#deserializeMapEntryValue(ByteBuffer, PreparedMapKey)] does, so a 
caller must not assume the buffer is
+  /// reusable afterwards. A truncated or corrupt frame raises 
[BufferUnderflowException].
+  @Nullable
+  public static String deserializeMapEntryValueAsString(ByteBuffer byteBuffer, 
PreparedMapKey key) {
+    byte[] valueBytes = findValueBytes(byteBuffer, key);
+    if (valueBytes == null) {
+      return null;
+    }
+    String decoded = decodeWithoutJackson(valueBytes);
+    if (decoded != null) {
+      return decoded;
+    }
+    try {
+      Object value = JsonUtils.bytesToObject(valueBytes, Object.class);
+      return value == null ? null : value.toString();
+    } catch (IOException e) {
+      LOGGER.error("Caught exception while deserializing value for key: {}", 
key.getKey(), e);
+      return null;
+    }
+  }
+
+  /// Renders a stored JSON value without Jackson when the bytes are already 
identical to `toString()` on whatever
+  /// Jackson would have parsed them into, otherwise returns `null` so the 
caller falls back.
+  ///
+  /// Covers plain strings, canonical integers and the two boolean literals. 
Non-integral numbers deliberately fall
+  /// back: Jackson binds them to `Double`, whose `toString()` re-normalizes, 
so `1.50` has to render as `1.5`.
+  @Nullable
+  private static String decodeWithoutJackson(byte[] valueBytes) {
+    int length = valueBytes.length;
+    if (length == 0) {
+      return null;
+    }
+    switch (valueBytes[0]) {
+      case '"':
+        return unquotePlainJsonString(valueBytes, length);
+      case 't':
+        return length == 4 && valueBytes[1] == 'r' && valueBytes[2] == 'u' && 
valueBytes[3] == 'e' ? "true" : null;
+      case 'f':
+        return length == 5 && valueBytes[1] == 'a' && valueBytes[2] == 'l' && 
valueBytes[3] == 's'
+            && valueBytes[4] == 'e' ? "false" : null;
+      default:
+        return isCanonicalInteger(valueBytes, length)
+            ? new String(valueBytes, 0, length, StandardCharsets.US_ASCII) : 
null;
+    }
+  }
+
+  /// True when the bytes are an integer in the exact form `Integer`, `Long` 
and `BigInteger` render - which is what
+  /// Jackson binds an integral JSON number to, so the stored bytes and 
`toString()` coincide. Leading zeros, `+`,
+  /// `-0`, decimal points and exponents are all rejected because they would 
re-render differently.
+  private static boolean isCanonicalInteger(byte[] valueBytes, int length) {
+    int start = valueBytes[0] == '-' ? 1 : 0;
+    if (length == start) {
+      return false;
+    }
+    if (valueBytes[start] == '0') {
+      // Bare "0" is canonical; "-0" renders as "0" and anything longer has a 
leading zero.
+      return length == 1;
+    }
+    for (int i = start; i < length; i++) {
+      if (valueBytes[i] < '0' || valueBytes[i] > '9') {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  private static String unquotePlainJsonString(byte[] valueBytes, int length) {
+    if (length < 2 || valueBytes[length - 1] != '"') {
+      return null;
+    }
+    for (int i = 1; i < length - 1; i++) {
+      if (valueBytes[i] == '\\') {
+        return null;
+      }
+    }
+    return new String(valueBytes, 1, length - 2, StandardCharsets.UTF_8);
+  }
+
+  @Nullable
+  private static byte[] findValueBytes(ByteBuffer byteBuffer, PreparedMapKey 
key) {
     byteBuffer.order(ByteOrder.BIG_ENDIAN);
     int size = byteBuffer.getInt();
     if (size < 0) {
@@ -278,12 +380,7 @@ public class MapUtils {
       // the remaining entries never need to be scanned.
       byte[] valueBytes = new byte[valueLength];
       byteBuffer.get(valueBytes);
-      try {
-        return JsonUtils.bytesToObject(valueBytes, Object.class);
-      } catch (IOException e) {
-        LOGGER.error("Caught exception while deserializing value for key: {}", 
key.getKey(), e);
-        return null;
-      }
+      return valueBytes;
     }
     return null;
   }
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/MapUtilsTest.java 
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/MapUtilsTest.java
index b67edb879b3..fcaa17f87dc 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/MapUtilsTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/MapUtilsTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.pinot.spi.utils;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -121,6 +123,72 @@ public class MapUtilsTest {
     assertNull(MapUtils.deserializeMapEntryValue(serialized, "命名"));
   }
 
+  /// The string accessor must agree with 
`deserializeMapValue(...).toString()` for every value shape, since the only
+  /// difference is meant to be whether Jackson was involved. Escaped and 
multi-byte strings take the Jackson
+  /// fallback and must come out identical to the plain ones.
+  @Test
+  void testDeserializeMapValueAsStringMatchesToString() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("plain", "pinot-server");
+    map.put("empty", "");
+    map.put("quoted", "has \"quotes\" inside");
+    map.put("backslash", "has \\ backslash");
+    map.put("newline", "has \n newline");
+    map.put("unicode", "çöğüşÇÖĞÜŞéÉ");
+    map.put("int", 42);
+    map.put("long", 9999999999L);
+    map.put("double", 1.5);
+    map.put("bool", true);
+    map.put("list", List.of(1, 2));
+    map.put("nested", Map.of("a", 1));
+    byte[] serialized = MapUtils.serializeMap(map, false);
+
+    for (String key : map.keySet()) {
+      Object asObject = MapUtils.deserializeMapEntryValue(serialized, key);
+      
assertEquals(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 key), asObject.toString(),
+          "String rendering should match toString() for key: " + key);
+    }
+  }
+
+  /// Mixed-type MAP columns are a supported shape - a STRING-valued MAP can 
carry numeric entries, as
+  /// `MapFieldTypeMixedValueIngestingIntegrationTest` ingests - so the 
non-string shapes have to render exactly as
+  /// `toString()` on the parsed value, whether or not they take a 
Jackson-free path.
+  @Test
+  void testDeserializeMapValueAsStringMatchesToStringForNonStringShapes() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("zero", 0);
+    map.put("negative", -42);
+    map.put("intMax", Integer.MAX_VALUE);
+    map.put("longMin", Long.MIN_VALUE);
+    map.put("bigInteger", new BigInteger("123456789012345678901234567890"));
+    map.put("boolTrue", true);
+    map.put("boolFalse", false);
+    // Renders as "1.50" in the frame but binds to a Double, so it must come 
back re-normalized as "1.5".
+    map.put("trailingZeroDecimal", new BigDecimal("1.50"));
+    map.put("exponent", 1.0E300);
+    map.put("negativeZeroDouble", -0.0d);
+    byte[] serialized = MapUtils.serializeMap(map, false);
+
+    for (String key : map.keySet()) {
+      Object asObject = MapUtils.deserializeMapEntryValue(serialized, key);
+      
assertEquals(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 key), asObject.toString(),
+          "String rendering should match toString() for key: " + key);
+    }
+    
assertEquals(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 "trailingZeroDecimal"), "1.5");
+  }
+
+  @Test
+  void testDeserializeMapValueAsStringHandlesMissingAndNull() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("present", "value");
+    map.put("nullValue", null);
+    byte[] serialized = MapUtils.serializeMap(map, false);
+
+    
assertNull(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 "missing"));
+    
assertNull(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 "nullValue"));
+    
assertEquals(MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(serialized),
 "present"), "value");
+  }
+
   /// An off-heap forward-index view inherits the platform's native byte 
order, while the frame is always written
   /// big-endian. The extractor has to force the order rather than trust the 
incoming buffer.
   @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to