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]