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 8e814b0dbc6 Reuse UTF-8 scratch for DataTable dictionary decoding 
(#19535)
8e814b0dbc6 is described below

commit 8e814b0dbc6e44ba37d84d2bcd8a5373bf923a63
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Sep 21 17:56:59 2026 -0700

    Reuse UTF-8 scratch for DataTable dictionary decoding (#19535)
    
    * Reuse UTF-8 scratch for DataTable dictionary decoding
    
    Decoding a string dictionary allocates temporary UTF-8 storage for every 
entry. Reuse method-local scratch while preserving each returned String's 
ownership and existing buffer and error behavior.
    
    * Extract DataTableUtils.decodeStringArray and reuse it for DataBlock 
dictionaries
    
    * Remove unused BaseDataBlock dictionary ser/de; reuse scratch bytes in 
ZeroCopyDataBlockSerde dictionary decoding
---
 .../pinot/common/datablock/BaseDataBlock.java      |  35 -----
 .../common/datablock/ZeroCopyDataBlockSerde.java   |  22 ++-
 .../pinot/common/datatable/DataTableImplV4.java    |   7 +-
 .../pinot/common/datatable/DataTableUtils.java     |  29 ++++
 .../datablock/ZeroCopyDataBlockSerdeTest.java      |  27 ++++
 .../DataTableUtilsDecodeStringArrayTest.java       | 174 +++++++++++++++++++++
 .../datatable/DataTableDictionarySerDeTest.java    |  95 +++++++++++
 7 files changed, 347 insertions(+), 42 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datablock/BaseDataBlock.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/datablock/BaseDataBlock.java
index d3d88b3fd45..bdc3aefc015 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datablock/BaseDataBlock.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datablock/BaseDataBlock.java
@@ -19,7 +19,6 @@
 package org.apache.pinot.common.datablock;
 
 import com.google.common.base.Preconditions;
-import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.math.BigDecimal;
@@ -29,9 +28,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
-import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
 import org.apache.pinot.common.CustomObject;
-import org.apache.pinot.common.datatable.DataTableUtils;
 import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.segment.spi.memory.DataBuffer;
 import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
@@ -41,8 +38,6 @@ import org.apache.pinot.spi.utils.ByteArray;
 import org.apache.pinot.spi.utils.MapUtils;
 import org.roaringbitmap.RoaringBitmap;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 
 /// Base data block mostly replicating implementation of 
[org.apache.pinot.common.datatable.DataTableImplV4].
 ///
@@ -387,36 +382,6 @@ public abstract class BaseDataBlock implements DataBlock {
   // Ser/De and exception handling
   // --------------------------------------------------------------------------
 
-  /// Helper method to serialize dictionary map.
-  protected byte[] serializeStringDictionary()
-      throws IOException {
-    if (_stringDictionary.length == 0) {
-      return new byte[4];
-    }
-    UnsynchronizedByteArrayOutputStream byteArrayOutputStream = new 
UnsynchronizedByteArrayOutputStream(1024);
-    DataOutputStream dataOutputStream = new 
DataOutputStream(byteArrayOutputStream);
-
-    dataOutputStream.writeInt(_stringDictionary.length);
-    for (String entry : _stringDictionary) {
-      byte[] valueBytes = entry.getBytes(UTF_8);
-      dataOutputStream.writeInt(valueBytes.length);
-      dataOutputStream.write(valueBytes);
-    }
-
-    return byteArrayOutputStream.toByteArray();
-  }
-
-  /// Helper method to deserialize dictionary map.
-  protected String[] deserializeStringDictionary(ByteBuffer buffer)
-      throws IOException {
-    int dictionarySize = buffer.getInt();
-    String[] stringDictionary = new String[dictionarySize];
-    for (int i = 0; i < dictionarySize; i++) {
-      stringDictionary[i] = DataTableUtils.decodeString(buffer);
-    }
-    return stringDictionary;
-  }
-
   @Override
   public void addException(int errCode, String errMsg) {
     _errCodeToExceptionMap.put(errCode, errMsg);
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerde.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerde.java
index d83a537aafe..0b51eef6d04 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerde.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerde.java
@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.function.LongConsumer;
 import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.common.utils.HashUtil;
 import org.apache.pinot.segment.spi.memory.CompoundDataBuffer;
@@ -39,6 +40,8 @@ import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
 import org.apache.pinot.segment.spi.memory.PinotInputStream;
 import org.apache.pinot.segment.spi.memory.PinotOutputStream;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 
 /// An efficient serde that implements [DataBlockSerde.Version#V1_V2] using 
trying to make as fewer copies as
 /// possible.
@@ -310,8 +313,25 @@ public class ZeroCopyDataBlockSerde implements 
DataBlockSerde {
 
     int dictionarySize = stream.readInt();
     String[] stringDictionary = new String[dictionarySize];
+    // Reuse one scratch array across entries instead of allocating a 
temporary byte array per entry as
+    // PinotInputStream.readInt4UTF() does.
+    byte[] bytes = null;
     for (int i = 0; i < dictionarySize; i++) {
-      stringDictionary[i] = stream.readInt4UTF();
+      int length = stream.readInt();
+      if (length == 0) {
+        stringDictionary[i] = StringUtils.EMPTY;
+        continue;
+      }
+      if (length < 0) {
+        // Preserve the exception raised by allocating the entry buffer in 
readInt4UTF().
+        throw new NegativeArraySizeException(Integer.toString(length));
+      }
+      if (bytes == null || bytes.length < length) {
+        bytes = new byte[length];
+      }
+      stream.readFully(bytes, 0, length);
+      // String copies the decoded contents, so the next entry can reuse the 
scratch bytes.
+      stringDictionary[i] = new String(bytes, 0, length, UTF_8);
     }
     return stringDictionary;
   }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
index fc947e78121..d2ab8c43b23 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableImplV4.java
@@ -415,12 +415,7 @@ public class DataTableImplV4 implements DataTable {
   /// Helper method to deserialize dictionary map.
   protected String[] deserializeStringDictionary(ByteBuffer buffer)
       throws IOException {
-    int dictionarySize = buffer.getInt();
-    String[] stringDictionary = new String[dictionarySize];
-    for (int i = 0; i < dictionarySize; i++) {
-      stringDictionary[i] = DataTableUtils.decodeString(buffer);
-    }
-    return stringDictionary;
+    return DataTableUtils.decodeStringArray(buffer);
   }
 
   @Override
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableUtils.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableUtils.java
index 05b47f80e0a..929e52d2220 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableUtils.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/datatable/DataTableUtils.java
@@ -85,4 +85,33 @@ public class DataTableUtils {
       return new String(bytes, UTF_8);
     }
   }
+
+  /// Decodes a string array serialized as an `int` array size followed by 
entries in the [#decodeString] format.
+  ///
+  /// A single scratch byte array is reused across entries and grown only when 
a longer entry is encountered, so
+  /// decoding does not allocate a temporary byte array per entry. Leaves the 
buffer positioned after the last entry.
+  public static String[] decodeStringArray(ByteBuffer buffer)
+      throws IOException {
+    int size = buffer.getInt();
+    String[] strings = new String[size];
+    byte[] bytes = null;
+    for (int i = 0; i < size; i++) {
+      int length = buffer.getInt();
+      if (length == 0) {
+        strings[i] = StringUtils.EMPTY;
+        continue;
+      }
+      if (length < 0) {
+        // Preserve the exception raised by allocating the entry buffer in 
decodeString().
+        throw new NegativeArraySizeException(Integer.toString(length));
+      }
+      if (bytes == null || bytes.length < length) {
+        bytes = new byte[length];
+      }
+      buffer.get(bytes, 0, length);
+      // String copies the decoded contents, so the next entry can reuse the 
scratch bytes.
+      strings[i] = new String(bytes, 0, length, UTF_8);
+    }
+    return strings;
+  }
 }
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerdeTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerdeTest.java
index 604e3e9b246..7169e8865cc 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerdeTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/datablock/ZeroCopyDataBlockSerdeTest.java
@@ -19,9 +19,12 @@
 package org.apache.pinot.common.datablock;
 
 import com.google.common.collect.Lists;
+import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
 import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
 import org.testng.annotations.AfterSuite;
@@ -30,6 +33,7 @@ import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertSame;
 import static org.testng.Assert.fail;
 
 
@@ -100,4 +104,27 @@ public class ZeroCopyDataBlockSerdeTest {
     assertEquals(deserialized.getExceptions(), block.getExceptions(), 
"Unexpected exceptions");
     DataBlockEquals.checkSameContent(deserialized, block, "Unexpected data");
   }
+
+  @Test
+  void testStringDictionary()
+      throws Exception {
+    // Longer, shorter and empty entries share the scratch bytes used while 
decoding the dictionary.
+    String[] dictionary = {"", "long-" + "x".repeat(4097), "tiny", "東京", "π„žπŸ˜€", 
"", "\u0000tail", "final"};
+    ByteBuffer fixedSizeData = ByteBuffer.allocate(dictionary.length * 
Integer.BYTES);
+    for (int dictId = dictionary.length - 1; dictId >= 0; dictId--) {
+      fixedSizeData.putInt(dictId);
+    }
+    DataSchema dataSchema = new DataSchema(new String[]{"value"}, new 
ColumnDataType[]{ColumnDataType.STRING});
+    RowDataBlock block =
+        new RowDataBlock(dictionary.length, dataSchema, dictionary, 
fixedSizeData.array(), new byte[0]);
+
+    DataBlock deserialized = 
DataBlockUtils.deserialize(DataBlockUtils.serialize(block));
+    String[] actual = deserialized.getStringDictionary();
+    assertEquals(actual, dictionary);
+    assertSame(actual[0], "");
+    assertSame(actual[5], "");
+    for (int row = 0; row < dictionary.length; row++) {
+      assertEquals(deserialized.getString(row, 0), 
dictionary[dictionary.length - 1 - row]);
+    }
+  }
 }
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/datatable/DataTableUtilsDecodeStringArrayTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/datatable/DataTableUtilsDecodeStringArrayTest.java
new file mode 100644
index 00000000000..6b1293d53a3
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/datatable/DataTableUtilsDecodeStringArrayTest.java
@@ -0,0 +1,174 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.datatable;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.expectThrows;
+
+
+/// Tests [DataTableUtils#decodeStringArray] decoding and buffer consumption. 
Each invocation owns its buffers.
+public class DataTableUtilsDecodeStringArrayTest {
+  private static final int BUFFER_PREFIX_SIZE = 11;
+  private static final int TRAILING_SENTINEL = 0x12345678;
+
+  @DataProvider
+  public Object[][] bufferKinds() {
+    return new Object[][]{
+        {false, false, false}, {false, false, true}, {false, true, false}, 
{false, true, true},
+        {true, false, false}, {true, false, true}, {true, true, false}, {true, 
true, true}
+    };
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testMixedStringsAndBufferPosition(boolean direct, boolean 
readOnly, boolean sliced)
+      throws IOException {
+    String[] expected = {
+        "", "first", "Γ©", "東京", "π„žπŸ˜€", "", "long-" + "x".repeat(4097), "tiny", 
"\u0000tail", "final"
+    };
+    byte[][] entries = new byte[expected.length][];
+    for (int i = 0; i < expected.length; i++) {
+      entries[i] = expected[i].getBytes(UTF_8);
+    }
+    byte[] payload = dictionaryPayload(entries);
+    ByteBuffer buffer = inputBuffer(payload, direct, readOnly, sliced);
+    int initialLimit = buffer.limit();
+    buffer.mark();
+
+    String[] actual = DataTableUtils.decodeStringArray(buffer);
+
+    // Earlier entries must remain unchanged after longer, shorter and empty 
entries reuse the scratch bytes.
+    assertEquals(actual, expected);
+    assertSame(actual[0], "");
+    assertSame(actual[5], "");
+    assertEquals(buffer.position(), BUFFER_PREFIX_SIZE + payload.length - 
Integer.BYTES);
+    assertEquals(buffer.limit(), initialLimit);
+    assertEquals(buffer.getInt(), TRAILING_SENTINEL);
+    assertEquals(buffer.remaining(), 0);
+    buffer.reset();
+    assertEquals(buffer.position(), BUFFER_PREFIX_SIZE);
+    assertEquals(DataTableUtils.decodeStringArray(buffer), expected);
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testEmptyDictionary(boolean direct, boolean readOnly, boolean 
sliced)
+      throws IOException {
+    ByteBuffer buffer = inputBuffer(dictionaryPayload(), direct, readOnly, 
sliced);
+    assertEquals(DataTableUtils.decodeStringArray(buffer), new String[0]);
+    assertEquals(buffer.position(), BUFFER_PREFIX_SIZE + Integer.BYTES);
+    assertEquals(buffer.getInt(), TRAILING_SENTINEL);
+    assertEquals(buffer.remaining(), 0);
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testMalformedUtf8MatchesStandaloneDecoder(boolean direct, 
boolean readOnly, boolean sliced)
+      throws IOException {
+    byte[][] entries = {
+        {(byte) 0xc3, 0x28}, {(byte) 0xf0, (byte) 0x9f}, {(byte) 0xed, (byte) 
0xa0, (byte) 0x80},
+        {(byte) 0x80, 0x41}, {(byte) 0xef, (byte) 0xbf, (byte) 0xbd}, {}, 
"last".getBytes(UTF_8)
+    };
+    byte[] payload = dictionaryPayload(entries);
+    ByteBuffer buffer = inputBuffer(payload, direct, readOnly, sliced);
+    ByteBuffer reference = inputBuffer(payload, direct, readOnly, sliced);
+    String[] expected = new String[reference.getInt()];
+    for (int i = 0; i < expected.length; i++) {
+      expected[i] = DataTableUtils.decodeString(reference);
+    }
+
+    String[] actual = DataTableUtils.decodeStringArray(buffer);
+    assertEquals(actual, expected);
+    assertEquals(actual, new String[]{"\ufffd(", "\ufffd", "\ufffd", 
"\ufffdA", "\ufffd", "", "last"});
+    assertEquals(buffer.position(), reference.position());
+    assertEquals(buffer.getInt(), TRAILING_SENTINEL);
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testNegativeDictionaryAndEntryLengths(boolean direct, boolean 
readOnly, boolean sliced) {
+    assertDecodeFailure(ByteBuffer.allocate(4).putInt(-1).array(), direct, 
readOnly, sliced,
+        NegativeArraySizeException.class, 4);
+    assertDecodeFailure(ByteBuffer.allocate(8).putInt(1).putInt(-1).array(), 
direct, readOnly, sliced,
+        NegativeArraySizeException.class, 8);
+    // A negative length must retain its original failure even after a 
previous entry allocated the scratch bytes.
+    assertDecodeFailure(ByteBuffer.allocate(13).putInt(2).putInt(1).put((byte) 
'a').putInt(-1).array(), direct,
+        readOnly, sliced, NegativeArraySizeException.class, 13);
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testTruncatedDictionaryAndEntryLengths(boolean direct, boolean 
readOnly, boolean sliced) {
+    for (int availableBytes = 0; availableBytes < Integer.BYTES; 
availableBytes++) {
+      assertDecodeFailure(new byte[availableBytes], direct, readOnly, sliced, 
BufferUnderflowException.class, 0);
+      assertDecodeFailure(ByteBuffer.allocate(4 + 
availableBytes).putInt(1).array(), direct, readOnly, sliced,
+          BufferUnderflowException.class, 4);
+    }
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testTruncatedStringBytes(boolean direct, boolean readOnly, 
boolean sliced) {
+    assertDecodeFailure(ByteBuffer.allocate(10).putInt(1).putInt(5).put((byte) 
'a').put((byte) 'b').array(), direct,
+        readOnly, sliced, BufferUnderflowException.class, 8);
+    // The second entry fits the existing scratch array but exceeds the 
remaining bytes in the source buffer.
+    assertDecodeFailure(ByteBuffer.allocate(17).putInt(2).putInt(3).put(new 
byte[]{'a', 'b', 'c'}).putInt(3)
+        .put(new byte[]{'d', 'e'}).array(), direct, readOnly, sliced, 
BufferUnderflowException.class, 15);
+  }
+
+  private static void assertDecodeFailure(byte[] payload, boolean direct, 
boolean readOnly, boolean sliced,
+      Class<? extends Throwable> exceptionType, int consumedBytes) {
+    ByteBuffer buffer = inputBuffer(payload, direct, readOnly, sliced);
+    int initialLimit = buffer.limit();
+    Throwable exception = expectThrows(exceptionType,
+        () -> DataTableUtils.decodeStringArray(buffer));
+    assertSame(exception.getClass(), exceptionType);
+    assertEquals(buffer.position(), BUFFER_PREFIX_SIZE + consumedBytes);
+    assertEquals(buffer.limit(), initialLimit);
+  }
+
+  private static byte[] dictionaryPayload(byte[]... entries) {
+    int payloadSize = Integer.BYTES * 2;
+    for (byte[] entry : entries) {
+      payloadSize += Integer.BYTES + entry.length;
+    }
+    ByteBuffer payload = 
ByteBuffer.allocate(payloadSize).putInt(entries.length);
+    for (byte[] entry : entries) {
+      payload.putInt(entry.length).put(entry);
+    }
+    return payload.putInt(TRAILING_SENTINEL).array();
+  }
+
+  private static ByteBuffer inputBuffer(byte[] payload, boolean direct, 
boolean readOnly, boolean sliced) {
+    int sliceOffset = sliced ? 7 : 0;
+    int capacity = sliceOffset + BUFFER_PREFIX_SIZE + payload.length + 5;
+    ByteBuffer buffer = direct ? ByteBuffer.allocateDirect(capacity) : 
ByteBuffer.allocate(capacity);
+    buffer.position(sliceOffset + BUFFER_PREFIX_SIZE);
+    buffer.put(payload);
+    buffer.limit(buffer.position());
+    buffer.position(sliceOffset);
+    if (sliced) {
+      buffer = buffer.slice();
+    }
+    buffer.position(BUFFER_PREFIX_SIZE);
+    return readOnly ? buffer.asReadOnlyBuffer() : buffer;
+  }
+}
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableDictionarySerDeTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableDictionarySerDeTest.java
new file mode 100644
index 00000000000..84939773982
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableDictionarySerDeTest.java
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.pinot.core.common.datatable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.pinot.common.datatable.DataTable;
+import org.apache.pinot.common.datatable.DataTable.MetadataKey;
+import org.apache.pinot.common.datatable.DataTableFactory;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.exception.QueryErrorCode;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// Exercises complete V4 messages with shared scalar/MV dictionaries across 
transport buffer types.
+public class DataTableDictionarySerDeTest {
+  @DataProvider
+  public Object[][] bufferKinds() {
+    return new Object[][]{
+        {false, false, false}, {false, false, true}, {false, true, false}, 
{false, true, true},
+        {true, false, false}, {true, false, true}, {true, true, false}, {true, 
true, true}
+    };
+  }
+
+  @Test(dataProvider = "bufferKinds")
+  public void testCompleteMessage(boolean direct, boolean readOnly, boolean 
sliced)
+      throws IOException {
+    String[] values = {"", "first", "東京", "πŸ˜€", "long-" + "x".repeat(2049), 
"tiny"};
+    DataSchema schema = new DataSchema(new String[]{"id", "value", "values"},
+        new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.STRING, 
ColumnDataType.STRING_ARRAY});
+    DataTableBuilder builder = new DataTableBuilderV4(schema);
+    for (int row = 0; row < values.length; row++) {
+      builder.startRow();
+      builder.setColumn(0, row);
+      if (row == 0) {
+        builder.setNull(1);
+      } else {
+        builder.setColumn(1, values[row]);
+      }
+      builder.setColumn(2, new String[]{values[(row + 1) % values.length], 
values[row]});
+      builder.finishRow();
+    }
+    DataTable original = builder.build();
+    original.getMetadata().put(MetadataKey.NUM_DOCS_SCANNED.getName(), 
Integer.toString(values.length));
+    original.addException(QueryErrorCode.QUERY_EXECUTION, "test-梈息");
+    byte[] wire = original.toBytes();
+    int prefix = sliced ? 13 : 0;
+    ByteBuffer storage = direct
+        ? ByteBuffer.allocateDirect(prefix + wire.length)
+        : ByteBuffer.allocate(prefix + wire.length);
+    storage.position(prefix);
+    storage.put(wire).flip();
+    storage.position(prefix);
+    ByteBuffer input = sliced ? storage.slice() : storage;
+    if (readOnly) {
+      input = input.asReadOnlyBuffer();
+    }
+
+    DataTable decoded = DataTableFactory.getDataTable(input);
+    assertEquals(input.position(), wire.length);
+    assertEquals(decoded.getVersion(), DataTableFactory.VERSION_4);
+    assertEquals(decoded.getDataSchema(), schema);
+    assertEquals(decoded.getNumberOfRows(), values.length);
+    
assertEquals(decoded.getMetadata().get(MetadataKey.NUM_DOCS_SCANNED.getName()), 
Integer.toString(values.length));
+    assertEquals(decoded.getExceptions(), original.getExceptions());
+    assertTrue(decoded.getNullRowIds(1).contains(0));
+    for (int row = 0; row < values.length; row++) {
+      assertEquals(decoded.getInt(row, 0), row);
+      assertEquals(decoded.getString(row, 1), row == 0 ? 
ColumnDataType.STRING.getNullPlaceholder() : values[row]);
+      assertEquals(decoded.getStringArray(row, 2), new String[]{values[(row + 
1) % values.length], values[row]});
+    }
+  }
+}


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

Reply via email to