This is an automated email from the ASF dual-hosted git repository.
abstractdog pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 9019223a868 HIVE-29866: Optimize LLAP IO cache encoding path for text
tables with MultiDelimitSerDe (#6747)
9019223a868 is described below
commit 9019223a868a730e97e2694a99445511d57cbfda
Author: Bodor Laszlo <[email protected]>
AuthorDate: Tue Sep 8 15:52:51 2026 +0200
HIVE-29866: Optimize LLAP IO cache encoding path for text tables with
MultiDelimitSerDe (#6747)
---
.../io/encoded/VectorDeserializeOrcWriter.java | 70 ++-
.../io/encoded/TestVectorDeserializeOrcWriter.java | 136 ++++--
.../hive/serde2/lazy/LazySerDeParameters.java | 34 +-
.../lazy/fast/LazySimpleDeserializeRead.java | 312 +++++++++---
.../lazy/fast/TestLazySimpleDeserializeRead.java | 535 ++++++++++++++++++++-
5 files changed, 973 insertions(+), 114 deletions(-)
diff --git
a/llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/VectorDeserializeOrcWriter.java
b/llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/VectorDeserializeOrcWriter.java
index a55b64cdcbf..46e8a12bdb5 100644
---
a/llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/VectorDeserializeOrcWriter.java
+++
b/llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/VectorDeserializeOrcWriter.java
@@ -29,6 +29,7 @@
import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@@ -55,6 +56,7 @@
import org.apache.hadoop.hive.ql.plan.PartitionDesc;
import org.apache.hadoop.hive.serde.serdeConstants;
import org.apache.hadoop.hive.serde2.Deserializer;
+import org.apache.hadoop.hive.serde2.MultiDelimitSerDe;
import org.apache.hadoop.hive.serde2.SerDeException;
import org.apache.hadoop.hive.serde2.io.HiveDecimalWritable;
import org.apache.hadoop.hive.serde2.lazy.LazySerDeParameters;
@@ -102,33 +104,56 @@ public static EncodingWriter create(InputFormat<?, ?>
sourceIf, Deserializer ser
Configuration daemonConf, Configuration jobConf, Path splitPath,
StructObjectInspector sourceOi,
List<Integer> sourceIncludes, boolean[] cacheIncludes, int allocSize,
ExecutorService encodeExecutor)
throws IOException {
- // Vector SerDe can be disabled both on client and server side.
- if (!HiveConf.getBoolVar(daemonConf,
ConfVars.LLAP_IO_ENCODE_VECTOR_SERDE_ENABLED)
- || !HiveConf.getBoolVar(jobConf,
ConfVars.LLAP_IO_ENCODE_VECTOR_SERDE_ENABLED)
- || !(sourceIf instanceof TextInputFormat) || !(serDe instanceof
LazySimpleSerDe)) {
+ /*
+ * Vector SerDe can be disabled both on client and server side.
+ * MultiDelimitSerDe is accepted alongside LazySimpleSerDe: it's the same
+ * text-row shape, only its top-level FIELD_DELIM can span multiple bytes.
+ * The reader (LazySimpleDeserializeRead) picks the multi-byte scanner
+ * when LazySerDeParameters.setFieldDelimMulti has been called below —
+ * otherwise the specialized single-byte hot loop runs unchanged.
+ */
+ final boolean vectorSerdeEnabled =
+ HiveConf.getBoolVar(daemonConf,
ConfVars.LLAP_IO_ENCODE_VECTOR_SERDE_ENABLED)
+ && HiveConf.getBoolVar(jobConf,
ConfVars.LLAP_IO_ENCODE_VECTOR_SERDE_ENABLED);
+ final boolean isMultiDelim = serDe instanceof MultiDelimitSerDe;
+ if (!vectorSerdeEnabled
+ || !(sourceIf instanceof TextInputFormat)
+ || (!(serDe instanceof LazySimpleSerDe) && !isMultiDelim)) {
return new DeserializerOrcWriter(serDe, sourceOi, allocSize);
}
Path path = splitPath.getFileSystem(jobConf).makeQualified(splitPath);
PartitionDesc partDesc = HiveFileFormatUtils.getFromPathRecursively(parts,
path, null);
if (partDesc == null) {
- LlapIoImpl.LOG.info("Not using VertorDeserializeOrcWriter: no partition
desc for " + path);
+ LlapIoImpl.LOG.info("Not using VectorDeserializeOrcWriter: no partition
desc for {}", path);
return new DeserializerOrcWriter(serDe, sourceOi, allocSize);
}
Properties tblProps = partDesc.getTableDesc().getProperties();
if ("true".equalsIgnoreCase(tblProps.getProperty(
serdeConstants.SERIALIZATION_LAST_COLUMN_TAKES_REST))) {
- LlapIoImpl.LOG.info("Not using VertorDeserializeOrcWriter due to "
- + serdeConstants.SERIALIZATION_LAST_COLUMN_TAKES_REST);
+ LlapIoImpl.LOG.info("Not using VectorDeserializeOrcWriter due to {}",
+ serdeConstants.SERIALIZATION_LAST_COLUMN_TAKES_REST);
+ return new DeserializerOrcWriter(serDe, sourceOi, allocSize);
+ }
+ /*
+ * MultiDelimitSerDe's own row parser (LazyStruct.parseMultiDelimit) does
+ * NOT honour escape.delim for the top-level delimiter. Combining a
+ * multi-byte field delim with an escape char in the fast path would let
+ * it silently diverge from the slow path, so bail to DeserializerOrcWriter
+ * whenever both are configured on a MultiDelimit table.
+ */
+ if (isMultiDelim && tblProps.getProperty(serdeConstants.ESCAPE_CHAR) !=
null) {
+ LlapIoImpl.LOG.info("Not using VectorDeserializeOrcWriter:
MultiDelimitSerDe with {} "
+ + "is not supported by the vectorized fast path",
serdeConstants.ESCAPE_CHAR);
return new DeserializerOrcWriter(serDe, sourceOi, allocSize);
}
for (StructField sf : sourceOi.getAllStructFieldRefs()) {
Category c = sf.getFieldObjectInspector().getCategory();
if (c != Category.PRIMITIVE) {
- LlapIoImpl.LOG.info("Not using VertorDeserializeOrcWriter: " + c + "
is not supported");
+ LlapIoImpl.LOG.info("Not using VectorDeserializeOrcWriter: {} is not
supported", c);
return new DeserializerOrcWriter(serDe, sourceOi, allocSize);
}
}
- LlapIoImpl.LOG.info("Creating VertorDeserializeOrcWriter for " + path);
+ LlapIoImpl.LOG.info("Creating VectorDeserializeOrcWriter for {}", path);
return new VectorDeserializeOrcWriter(
jobConf, tblProps, sourceOi, sourceIncludes, cacheIncludes, allocSize,
encodeExecutor);
}
@@ -143,7 +168,8 @@ private VectorDeserializeOrcWriter(Configuration conf,
Properties tblProps, Stru
this.cacheIncludes = cacheIncludes;
this.sourceBatch = vrbCtx.createVectorizedRowBatch();
deserializeRead = new
LazySimpleDeserializeRead(vrbCtx.getRowColumnTypeInfos(),
- vrbCtx.getRowdataTypePhysicalVariations(),/* useExternalBuffer */ true,
createSerdeParams(conf, tblProps));
+ vrbCtx.getRowdataTypePhysicalVariations(), /* useExternalBuffer */ true,
+ createSerdeParams(conf, tblProps));
vectorDeserializeRow = new
VectorDeserializeRow<LazySimpleDeserializeRead>(deserializeRead);
int colCount = vrbCtx.getRowColumnTypeInfos().length;
boolean[] includes = null;
@@ -212,8 +238,9 @@ private static VectorizedRowBatchCtx
createVrbCtx(StructObjectInspector oi, fina
.HIVE_VECTORIZED_INPUT_FORMAT_SUPPORTS_ENABLED).equalsIgnoreCase("decimal_64");
final String serde =
tblProps.getProperty(serdeConstants.SERIALIZATION_LIB);
final String inputFormat =
tblProps.getProperty(hive_metastoreConstants.FILE_INPUT_FORMAT);
- final boolean isTextFormat = inputFormat != null &&
inputFormat.equals(TextInputFormat.class.getName()) &&
- serde != null && serde.equals(LazySimpleSerDe.class.getName());
+ final boolean isTextFormat =
TextInputFormat.class.getName().equals(inputFormat)
+ && (LazySimpleSerDe.class.getName().equals(serde)
+ || MultiDelimitSerDe.class.getName().equals(serde));
List<DataTypePhysicalVariation> dataTypePhysicalVariations = new
ArrayList<>();
if (isTextFormat) {
StructTypeInfo structTypeInfo = (StructTypeInfo)
TypeInfoUtils.getTypeInfoFromObjectInspector(oi);
@@ -247,7 +274,24 @@ private static VectorizedRowBatchCtx
createVrbCtx(StructObjectInspector oi, fina
private static LazySerDeParameters createSerdeParams(
Configuration conf, Properties tblProps) throws IOException {
try {
- return new LazySerDeParameters(conf, tblProps,
LazySimpleSerDe.class.getName());
+ LazySerDeParameters params =
+ new LazySerDeParameters(conf, tblProps,
LazySimpleSerDe.class.getName());
+ /*
+ * For MultiDelimitSerDe tables, if the field delimiter is > 1 byte, opt
+ * the reader into its multi-byte scan branch. LazySerDeParameters
+ * silently ignores single-byte values here, so misconfigured MultiDelim
+ * tables (e.g. FIELD_DELIM=",") still take the specialized single-byte
+ * fast path via separators[0]. The SerDe class is read from tblProps
+ * (same source of truth used by createVrbCtx above).
+ */
+ String serdeClass =
tblProps.getProperty(serdeConstants.SERIALIZATION_LIB);
+ if (MultiDelimitSerDe.class.getName().equals(serdeClass)) {
+ String rawDelim = tblProps.getProperty(serdeConstants.FIELD_DELIM);
+ if (rawDelim != null && rawDelim.length() > 1) {
+ params.setFieldDelimMulti(rawDelim.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ return params;
} catch (SerDeException e) {
throw new IOException(e);
}
diff --git
a/llap-server/src/test/org/apache/hadoop/hive/llap/io/encoded/TestVectorDeserializeOrcWriter.java
b/llap-server/src/test/org/apache/hadoop/hive/llap/io/encoded/TestVectorDeserializeOrcWriter.java
index beca9fa7ed8..45baff6db33 100644
---
a/llap-server/src/test/org/apache/hadoop/hive/llap/io/encoded/TestVectorDeserializeOrcWriter.java
+++
b/llap-server/src/test/org/apache/hadoop/hive/llap/io/encoded/TestVectorDeserializeOrcWriter.java
@@ -19,10 +19,14 @@
package org.apache.hadoop.hive.llap.io.encoded;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
+import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.common.io.encoded.EncodedColumnBatch;
import org.apache.hadoop.hive.llap.io.api.impl.ColumnVectorBatch;
import org.apache.hadoop.hive.llap.io.decode.EncodedDataConsumer;
@@ -30,13 +34,20 @@
import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch;
import org.apache.hadoop.hive.ql.io.orc.WriterImpl;
import org.apache.hadoop.hive.ql.io.orc.encoded.Consumer;
+import org.apache.hadoop.hive.serde.serdeConstants;
+import org.apache.hadoop.hive.serde2.Deserializer;
+import org.apache.hadoop.hive.serde2.MultiDelimitSerDe;
+import org.apache.hadoop.hive.serde2.lazy.LazySerDeParameters;
+import org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe;
import org.apache.hive.common.util.FixedSizedObjectPool;
import org.apache.orc.impl.SchemaEvolution;
import org.junit.Test;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
@@ -49,34 +60,6 @@ public class TestVectorDeserializeOrcWriter {
private static final int TEST_NUM_COLS = 2;
- private static Field reflectField(Class<?> classToReflect, String
fieldNameValueToFetch) {
- try {
- Field reflectField = null;
- Class<?> classForReflect = classToReflect;
- do {
- try {
- reflectField =
classForReflect.getDeclaredField(fieldNameValueToFetch);
- } catch (NoSuchFieldException e) {
- classForReflect = classForReflect.getSuperclass();
- }
- } while (reflectField == null || classForReflect == null);
- reflectField.setAccessible(true);
- return reflectField;
- } catch (Exception e) {
- fail("Failed to reflect " + fieldNameValueToFetch + " from " +
classToReflect);
- }
- return null;
- }
-
- private static void reflectSetValue(Object objToReflect, String
fieldNameToSet, Object valueToSet) {
- try {
- Field reflectField = reflectField(objToReflect.getClass(),
fieldNameToSet);
- reflectField.set(objToReflect, valueToSet);
- } catch (Exception e) {
- fail("Failed to reflectively set " + fieldNameToSet + "=" + valueToSet);
- }
- }
-
@Test
public void testConcurrencyIssueWhileWriting() throws Exception {
@@ -123,6 +106,90 @@ public void testConcurrencyIssueWhileWriting() throws
Exception {
assertEquals(newCvb, cvb);
}
+ // --- createSerdeParams: MultiDelimit routing → fieldDelimMulti wiring -----
+
+ /**
+ * When routing sees a MultiDelimitSerDe with a multi-byte field.delim, the
+ * LazySerDeParameters we hand to LazySimpleDeserializeRead must carry the
+ * raw delimiter bytes — that's the signal the reader uses to switch to its
+ * multi-byte scan branch (and hence keep the row on the vectorized LLAP
+ * fast path instead of falling back to DeserializerOrcWriter).
+ */
+ @Test
+ public void testCreateSerdeParamsMultiDelimSetsFieldDelimMulti() throws
Exception {
+ Properties tblProps = new Properties();
+ tblProps.setProperty(serdeConstants.FIELD_DELIM, "~|");
+ tblProps.setProperty(serdeConstants.SERIALIZATION_FORMAT, "~|");
+ tblProps.setProperty(serdeConstants.LIST_COLUMNS, "a,b");
+ tblProps.setProperty(serdeConstants.LIST_COLUMN_TYPES, "string:string");
+
+ LazySerDeParameters params = invokeCreateSerdeParams(tblProps, new
MultiDelimitSerDe());
+ assertArrayEquals("~|".getBytes(StandardCharsets.UTF_8),
params.getFieldDelimMulti());
+ }
+
+ /**
+ * A LazySimpleSerDe table must NEVER opt into the multi-byte scan branch,
+ * even if someone shoved a >1-byte value into field.delim — the slow path
+ * silently truncates to a single byte, and diverging here would produce
+ * different results between fast and slow paths for the same table.
+ */
+ @Test
+ public void testCreateSerdeParamsLazySimpleNeverSetsFieldDelimMulti() throws
Exception {
+ Properties tblProps = new Properties();
+ tblProps.setProperty(serdeConstants.FIELD_DELIM, "~|");
+ tblProps.setProperty(serdeConstants.SERIALIZATION_FORMAT, "~|");
+ tblProps.setProperty(serdeConstants.LIST_COLUMNS, "a,b");
+ tblProps.setProperty(serdeConstants.LIST_COLUMN_TYPES, "string:string");
+
+ LazySerDeParameters params = invokeCreateSerdeParams(tblProps, new
LazySimpleSerDe());
+ assertNull(params.getFieldDelimMulti());
+ }
+
+ /**
+ * A MultiDelimit table whose field.delim is only one byte still takes the
+ * specialized single-byte fast path — setFieldDelimMulti drops sub-2-byte
+ * values, so separators[0] remains the source of truth.
+ */
+ @Test
+ public void testCreateSerdeParamsMultiDelimWithSingleByteFallsThrough()
throws Exception {
+ Properties tblProps = new Properties();
+ tblProps.setProperty(serdeConstants.FIELD_DELIM, "|");
+ tblProps.setProperty(serdeConstants.SERIALIZATION_FORMAT, "|");
+ tblProps.setProperty(serdeConstants.LIST_COLUMNS, "a,b");
+ tblProps.setProperty(serdeConstants.LIST_COLUMN_TYPES, "string:string");
+
+ LazySerDeParameters params = invokeCreateSerdeParams(tblProps, new
MultiDelimitSerDe());
+ assertNull(params.getFieldDelimMulti());
+ }
+
+ private static Field reflectField(Class<?> classToReflect, String
fieldNameValueToFetch) {
+ try {
+ Field reflectField = null;
+ Class<?> classForReflect = classToReflect;
+ do {
+ try {
+ reflectField =
classForReflect.getDeclaredField(fieldNameValueToFetch);
+ } catch (NoSuchFieldException e) {
+ classForReflect = classForReflect.getSuperclass();
+ }
+ } while (reflectField == null || classForReflect == null);
+ reflectField.setAccessible(true);
+ return reflectField;
+ } catch (Exception e) {
+ fail("Failed to reflect " + fieldNameValueToFetch + " from " +
classToReflect);
+ }
+ return null;
+ }
+
+ private static void reflectSetValue(Object objToReflect, String
fieldNameToSet, Object valueToSet) {
+ try {
+ Field reflectField = reflectField(objToReflect.getClass(),
fieldNameToSet);
+ reflectField.set(objToReflect, valueToSet);
+ } catch (Exception e) {
+ fail("Failed to reflectively set " + fieldNameToSet + "=" + valueToSet);
+ }
+ }
+
private static void createTestVectors(ColumnVectorBatch cvb,
VectorizedRowBatch vrb) {
for (int i = 0; i < TEST_NUM_COLS; ++i) {
LongColumnVector cv = new LongColumnVector();
@@ -145,6 +212,19 @@ private static VectorDeserializeOrcWriter createOrcWriter(
return orcWriter;
}
+ private static LazySerDeParameters invokeCreateSerdeParams(Properties
tblProps,
+ Deserializer serDe) throws Exception {
+ // createSerdeParams no longer takes a Deserializer — it reads the SerDe
class
+ // from tblProps' SERIALIZATION_LIB (matches createVrbCtx's own routing).
We
+ // set that key here so the test keeps its "give me a MultiDelimit vs a
+ // LazySimple table" API.
+ tblProps.setProperty(serdeConstants.SERIALIZATION_LIB,
serDe.getClass().getName());
+ Method m = VectorDeserializeOrcWriter.class.getDeclaredMethod(
+ "createSerdeParams", Configuration.class, Properties.class);
+ m.setAccessible(true);
+ return (LazySerDeParameters) m.invoke(null, new Configuration(false),
tblProps);
+ }
+
private static EncodedDataConsumer createBlankEncodedDataConsumer() {
return new EncodedDataConsumer(null, 1, null, null) {
@Override
diff --git
a/serde/src/java/org/apache/hadoop/hive/serde2/lazy/LazySerDeParameters.java
b/serde/src/java/org/apache/hadoop/hive/serde2/lazy/LazySerDeParameters.java
index 9da6bf5e3c3..1db95d3b086 100644
--- a/serde/src/java/org/apache/hadoop/hive/serde2/lazy/LazySerDeParameters.java
+++ b/serde/src/java/org/apache/hadoop/hive/serde2/lazy/LazySerDeParameters.java
@@ -60,11 +60,18 @@ public class LazySerDeParameters implements
LazyObjectInspectorParameters {
private Properties tableProperties;
private String serdeName;
- // The list of bytes used for the separators in the column (a nested struct
+ // The list of bytes used for the separators in the column (a nested struct
// such as Array<Array<int>> will use multiple separators).
// The list of separators + escapeChar are the bytes required to be escaped.
private byte[] separators;
+ // Populated only when the top-level field delimiter is longer than one byte
+ // (i.e. by MultiDelimitSerDe). Never populated for LazySimpleSerDe callers,
+ // so existing single-byte behaviour is preserved. The single-byte
`separators[0]`
+ // slot is still set to the first byte of the multi-byte delim so that
+ // needsEscape[] and any legacy consumers of getSeparators() keep working.
+ private byte[] fieldDelimMulti;
+
private Text nullSequence;
private TypeInfo rowTypeInfo;
@@ -196,6 +203,31 @@ public byte[] getSeparators() {
return separators;
}
+ /**
+ * @return the raw (potentially multi-byte) top-level field delimiter, or
+ * {@code null} when the effective delimiter is a single byte (the
+ * value in {@code separators[0]} is authoritative in that case).
+ *
+ * Set by callers that know they're wiring up a multi-byte delimiter
+ * (e.g. LLAP's {@code VectorDeserializeOrcWriter} when the SerDe is
+ * {@code MultiDelimitSerDe}). Left {@code null} for plain LazySimpleSerDe so
+ * its single-byte fast path is preserved verbatim.
+ */
+ public byte[] getFieldDelimMulti() {
+ return fieldDelimMulti;
+ }
+
+ /**
+ * Opt-in switch for the multi-byte top-level delimiter. Only meaningful when
+ * {@code delim.length > 1}; shorter values are ignored (the {@code
separators[0]}
+ * byte remains the source of truth). Never set this for a
+ * LazySimpleSerDe-backed table — its slow path still truncates to a single
+ * byte, so opting the fast path into multi-byte would silently diverge.
+ */
+ public void setFieldDelimMulti(byte[] delim) {
+ this.fieldDelimMulti = (delim != null && delim.length > 1) ? delim : null;
+ }
+
public Text getNullSequence() {
return nullSequence;
}
diff --git
a/serde/src/java/org/apache/hadoop/hive/serde2/lazy/fast/LazySimpleDeserializeRead.java
b/serde/src/java/org/apache/hadoop/hive/serde2/lazy/fast/LazySimpleDeserializeRead.java
index 747d389db56..1a5a390e709 100644
---
a/serde/src/java/org/apache/hadoop/hive/serde2/lazy/fast/LazySimpleDeserializeRead.java
+++
b/serde/src/java/org/apache/hadoop/hive/serde2/lazy/fast/LazySimpleDeserializeRead.java
@@ -57,7 +57,7 @@
import com.google.common.base.Preconditions;
-/*
+/**
* Directly deserialize with the caller reading field-by-field the LazySimple
(text)
* serialization format.
*
@@ -71,6 +71,29 @@
* Some type values are by reference to either bytes in the deserialization
buffer or to
* other type specific buffers. So, those references are only valid until the
next time set is
* called.
+ *
+ * <h3>Multi-byte top-level field delimiter</h3>
+ * By default this reader assumes a single-byte top-level FIELD_DELIM and runs
a specialized
+ * byte-at-a-time hot loop against {@code separators[0]} — the LazySimpleSerDe
fast path.
+ *
+ * A multi-byte top-level delimiter (e.g. MultiDelimitSerDe's {@code ~|}) is
supported when, and
+ * only when, the caller opts in via
+ * {@link
org.apache.hadoop.hive.serde2.lazy.LazySerDeParameters#setFieldDelimMulti(byte[])}
on
+ * the {@code LazySerDeParameters} passed to the constructor. In that mode
{@code topLevelParse()}
+ * takes a separate multi-byte scan branch and the specialized single-byte
loop is not entered.
+ *
+ * Restrictions when opting into the multi-byte branch:
+ * <ul>
+ * <li>only the <em>top-level</em> field delimiter is multi-byte — nested
COLLECTION_DELIM /
+ * MAPKEY_DELIM remain single-byte;</li>
+ * <li>ESCAPE_CHAR is rejected at construction (see {@code
MultiDelimitSerDe.parseMultiDelimit}
+ * which itself ignores escape at the top level — enabling both here
would silently
+ * diverge from the slow path);</li>
+ * <li>LAST_COLUMN_TAKES_REST is not honoured on this path (the LLAP router
bails to
+ * DeserializerOrcWriter before we get here).</li>
+ * </ul>
+ * When {@code setFieldDelimMulti} is not called (or the value is shorter than
2 bytes), the
+ * single-byte fast path is unchanged.
*/
public final class LazySimpleDeserializeRead extends DeserializeRead {
public static final Logger LOG =
LoggerFactory.getLogger(LazySimpleDeserializeRead.class.getName());
@@ -198,6 +221,22 @@ public UnionComplexTypeHelper(Field complexField, Field[]
fields) {
private int[] startPositions;
private final byte[] separators;
+ /*
+ * Multi-byte top-level field delimiter (e.g. MultiDelimitSerDe's `~|`).
+ * null in the common single-byte case, in which case `separators[0]` is used
+ * and the row-scan hot loop keeps its per-byte specialization. Non-null only
+ * when the caller (LLAP's VectorDeserializeOrcWriter for MultiDelimitSerDe)
+ * explicitly opted in via LazySerDeParameters.setFieldDelimMulti.
+ */
+ private final byte[] fieldDelimMulti;
+ /*
+ * Byte length "charged" to a top-level separator: 1 for the single-byte fast
+ * path, delim.length for multi-byte. Baked into (a) the Arrays.fill sentinel
+ * for missing/trailing fields and (b) the "length = next.start - this.start
+ * - sepLen" arithmetic in readField and getDetailedReadPositionString, so
+ * both paths share the same downstream code.
+ */
+ private final int topLevelSeparatorLen;
private final boolean isEscaped;
private final byte escapeChar;
private final int[] escapeCounts;
@@ -336,11 +375,23 @@ public LazySimpleDeserializeRead(TypeInfo[] typeInfos,
startPositions = new int[count + 1];
this.separators = lazyParams.getSeparators();
+ this.fieldDelimMulti = lazyParams.getFieldDelimMulti();
+ this.topLevelSeparatorLen = (fieldDelimMulti == null) ? 1 :
fieldDelimMulti.length;
isEscaped = lazyParams.isEscaped();
if (isEscaped) {
escapeChar = lazyParams.getEscapeChar();
escapeCounts = new int[count];
+ // Multi-byte field delimiters with escape sequences are not implemented
+ // in this fast path — the escape/separator interaction would diverge
+ // from MultiDelimitSerDe's own row-based parser, which ignores escapes
+ // for the top-level delim. Callers (VectorDeserializeOrcWriter) route
+ // this combination to the DeserializerOrcWriter slow path instead.
+ if (fieldDelimMulti != null) {
+ throw new RuntimeException(
+ "Multi-byte field delimiter combined with escape.delim is not "
+ + "supported in the vectorized fast path.");
+ }
} else {
escapeChar = (byte) 0;
escapeCounts = null;
@@ -402,7 +453,7 @@ public String getDetailedReadPositionString() {
sb.append(" at field start position ");
sb.append(startPositions[currentTopLevelFieldIndex]);
int currentFieldLength = startPositions[currentTopLevelFieldIndex + 1] -
- startPositions[currentTopLevelFieldIndex] - 1;
+ startPositions[currentTopLevelFieldIndex] - topLevelSeparatorLen;
sb.append(" for field length ");
sb.append(currentFieldLength);
}
@@ -410,97 +461,216 @@ public String getDetailedReadPositionString() {
return sb.toString();
}
+ /**
+ * True if {@code bytes[off..off+delim.length)} equals the top-level field
+ * delimiter. Callers have already verified the first byte matches, so this
+ * only compares the tail — the mismatching-byte case in the multi-byte hot
+ * loop still costs a single load+compare.
+ */
+ private static boolean delimiterTailMatchesAt(byte[] bytes, int off, byte[]
delim, int dlen) {
+ for (int i = 1; i < dlen; i++) {
+ if (bytes[off + i] != delim[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Parse the byte[] and fill each field.
*
* This is an adapted version of the parse method in the LazyStruct class.
* They should parse things the same way.
+ *
+ * <p>Structure: pick one of three parsing algorithms based on the
(isEscaped,
+ * fieldDelimMulti) pair, run it to find each field's start position, then
+ * fill the trailing sentinel and record EOF. Each algorithm is a single-
+ * callsite private method — splitting them out is a readability move; the
+ * combined loops used to hide three flow-controls inside one big if/else.
+ *
+ * <p>The helpers write into {@link #startPositions} and (for the escape
+ * path) {@link #escapeCounts} directly, and stash the parse-stop cursor —
+ * how many fields they consumed, and where in the byte[] they stopped — on
+ * two instance fields {@link #parsedFieldCount} / {@link
#parsedFieldByteEnd}.
+ * Bundling those into a return value would cost a heap allocation on every
+ * row, which we can't afford here.
*/
private void topLevelParse() {
+ if (isEscaped) {
+ parseTopLevelSingleByteWithEscape();
+ } else if (fieldDelimMulti != null) {
+ parseTopLevelMultiByteNoEscape();
+ } else {
+ parseTopLevelSingleByteNoEscape();
+ }
+
+ final int fieldId = parsedFieldCount;
+ final int fieldByteEnd = parsedFieldByteEnd;
+ /*
+ * All fields have been parsed, or the row has been consumed. Fill
+ * startPositions[fieldId..end] with a sentinel so that the shared
+ * "length = next.start - this.start - sepLen" arithmetic used by
+ * readField yields a negative length for missing fields —
+ * uncheckedGetField turns those into SQL NULLs. Charge the actual
+ * separator width (1 byte for the single-byte fast path, delim.length
+ * for the multi-byte MultiDelimit path) so both paths share the same
+ * downstream code.
+ */
+ if (fieldId == fieldCount || fieldByteEnd == end) {
+ Arrays.fill(startPositions, fieldId, startPositions.length,
+ fieldByteEnd + topLevelSeparatorLen);
+ }
+
+ isEndOfInputReached = (fieldByteEnd == end);
+ }
+
+ /*
+ * Parse-stop cursor written by the three parseTopLevelXxx helpers and
consumed
+ * by topLevelParse() for its sentinel/EOF handling. Using fields instead of
a
+ * multi-value return avoids per-row allocation on the hot path.
+ */
+ private int parsedFieldCount;
+ private int parsedFieldByteEnd;
+
+ /**
+ * Single-byte top-level FIELD_DELIM, no escape — the hot path on every row
+ * of a LazySimple table.
+ */
+ private void parseTopLevelSingleByteNoEscape() {
+ final byte separator = separators[0];
int fieldId = 0;
int fieldByteBegin = start;
int fieldByteEnd = start;
-
- final byte separator = this.separators[0];
- final int fieldCount = this.fieldCount;
- final int[] startPositions = this.startPositions;
- final byte[] bytes = this.bytes;
- final int end = this.end;
-
- /*
- * Optimize the loops by pulling special end cases and global decisions
like isEscaped out!
- */
- if (!isEscaped) {
- while (fieldByteEnd < end) {
- if (bytes[fieldByteEnd] == separator) {
- startPositions[fieldId++] = fieldByteBegin;
- if (fieldId == fieldCount) {
- break;
- }
- fieldByteBegin = ++fieldByteEnd;
- } else {
- fieldByteEnd++;
+ while (fieldByteEnd < end) {
+ if (bytes[fieldByteEnd] == separator) {
+ startPositions[fieldId++] = fieldByteBegin;
+ if (fieldId == fieldCount) {
+ break;
}
+ fieldByteBegin = ++fieldByteEnd;
+ } else {
+ fieldByteEnd++;
}
- // End serves as final separator.
- if (fieldByteEnd == end && fieldId < fieldCount) {
+ }
+ // End serves as final separator.
+ if (fieldByteEnd == end && fieldId < fieldCount) {
+ startPositions[fieldId++] = fieldByteBegin;
+ }
+ parsedFieldCount = fieldId;
+ parsedFieldByteEnd = fieldByteEnd;
+ }
+
+ /**
+ * Multi-byte top-level FIELD_DELIM, no escape — MultiDelimitSerDe's fast
+ * path. We keep the "test the first byte, then verify the tail" idiom so
+ * the mismatching-byte case still costs a single load+compare — the tail
+ * memcmp only fires on a first-byte hit.
+ */
+ private void parseTopLevelMultiByteNoEscape() {
+ final byte[] delim = fieldDelimMulti;
+ final int dlen = delim.length;
+ final byte first = delim[0];
+ final int scanEnd = end - dlen; // last index at which a full delim can
start
+
+ int fieldId = 0;
+ int fieldByteBegin = start;
+ int fieldByteEnd = start;
+ while (fieldByteEnd <= scanEnd) {
+ if (bytes[fieldByteEnd] == first && delimiterTailMatchesAt(bytes,
fieldByteEnd, delim, dlen)) {
startPositions[fieldId++] = fieldByteBegin;
- }
- } else {
- final byte escapeChar = this.escapeChar;
- final int endLessOne = end - 1;
- final int[] escapeCounts = this.escapeCounts;
- int escapeCount = 0;
- // Process the bytes that can be escaped (the last one can't be).
- while (fieldByteEnd < endLessOne) {
- if (bytes[fieldByteEnd] == separator) {
- escapeCounts[fieldId] = escapeCount;
- escapeCount = 0;
- startPositions[fieldId++] = fieldByteBegin;
- if (fieldId == fieldCount) {
- break;
- }
- fieldByteBegin = ++fieldByteEnd;
- } else if (bytes[fieldByteEnd] == escapeChar) {
- // Ignore the char after escape_char
- fieldByteEnd += 2;
- escapeCount++;
- } else {
- fieldByteEnd++;
+ if (fieldId == fieldCount) {
+ // Malformed row with more delims than expected: leave fieldByteEnd
+ // where it is (matching single-byte-path semantics) and stop.
+ parsedFieldCount = fieldId;
+ parsedFieldByteEnd = fieldByteEnd;
+ return;
}
+ fieldByteEnd += dlen;
+ fieldByteBegin = fieldByteEnd;
+ } else {
+ fieldByteEnd++;
}
- // Process the last byte if necessary.
- if (fieldByteEnd == endLessOne && fieldId < fieldCount) {
- if (bytes[fieldByteEnd] == separator) {
- escapeCounts[fieldId] = escapeCount;
- escapeCount = 0;
- startPositions[fieldId++] = fieldByteBegin;
- if (fieldId <= fieldCount) {
- fieldByteBegin = ++fieldByteEnd;
- }
- } else {
- fieldByteEnd++;
+ }
+ /*
+ * No trailing delim (single-byte parses "end as final separator" here).
+ * If we still owe a field, the remainder from fieldByteBegin..end is the
+ * last field; fast-forward fieldByteEnd so the Arrays.fill sentinel and
+ * isEndOfInputReached both see the row as fully consumed.
+ */
+ if (fieldId < fieldCount) {
+ fieldByteEnd = end;
+ startPositions[fieldId++] = fieldByteBegin;
+ }
+ parsedFieldCount = fieldId;
+ parsedFieldByteEnd = fieldByteEnd;
+ }
+
+ /**
+ * Single-byte top-level FIELD_DELIM with escape.delim — the escape byte
+ * "swallows" whatever byte follows it (including a delim byte), so we
+ * track how many escapes each field contains for the copy-out path.
+ */
+ private void parseTopLevelSingleByteWithEscape() {
+ final byte separator = separators[0];
+ final int endLessOne = end - 1;
+
+ int fieldId = 0;
+ int fieldByteBegin = start;
+ int fieldByteEnd = start;
+ int escapeCount = 0;
+
+ // Process the bytes that can be escaped (the last one can't be).
+ while (fieldByteEnd < endLessOne) {
+ if (bytes[fieldByteEnd] == separator) {
+ escapeCounts[fieldId] = escapeCount;
+ escapeCount = 0;
+ startPositions[fieldId++] = fieldByteBegin;
+ if (fieldId == fieldCount) {
+ parsedFieldCount = fieldId;
+ parsedFieldByteEnd = fieldByteEnd;
+ return;
}
+ fieldByteBegin = ++fieldByteEnd;
+ } else if (bytes[fieldByteEnd] == escapeChar) {
+ // Ignore the char after escape_char
+ fieldByteEnd += 2;
+ escapeCount++;
+ } else {
+ fieldByteEnd++;
}
- // End serves as final separator.
- if (fieldByteEnd == end && fieldId < fieldCount) {
+ }
+ finalizeTopLevelSingleByteWithEscape(separator, endLessOne, fieldId,
fieldByteBegin,
+ fieldByteEnd, escapeCount);
+ }
+
+ /**
+ * Tail of {@link #parseTopLevelSingleByteWithEscape()} — handles the last
byte
+ * (which can't be escaped) plus the "end-of-row acts as a final separator"
+ * case, and commits the parsed cursor state.
+ */
+ private void finalizeTopLevelSingleByteWithEscape(byte separator, int
endLessOne, int fieldId,
+ int fieldByteBegin, int fieldByteEnd, int escapeCount) {
+ // Process the last byte if necessary.
+ if (fieldByteEnd == endLessOne && fieldId < fieldCount) {
+ if (bytes[fieldByteEnd] == separator) {
escapeCounts[fieldId] = escapeCount;
+ escapeCount = 0;
startPositions[fieldId++] = fieldByteBegin;
+ if (fieldId <= fieldCount) {
+ fieldByteBegin = ++fieldByteEnd;
+ }
+ } else {
+ fieldByteEnd++;
}
}
-
- if (fieldId == fieldCount || fieldByteEnd == end) {
- // All fields have been parsed, or bytes have been parsed.
- // We need to set the startPositions of fields.length to ensure we
- // can use the same formula to calculate the length of each field.
- // For missing fields, their starting positions will all be the same,
- // which will make their lengths to be -1 and uncheckedGetField will
- // return these fields as NULLs.
- Arrays.fill(startPositions, fieldId, startPositions.length, fieldByteEnd
+ 1);
+ // End serves as final separator.
+ if (fieldByteEnd == end && fieldId < fieldCount) {
+ escapeCounts[fieldId] = escapeCount;
+ startPositions[fieldId++] = fieldByteBegin;
}
-
- isEndOfInputReached = (fieldByteEnd == end);
+ parsedFieldCount = fieldId;
+ parsedFieldByteEnd = fieldByteEnd;
}
private int parseComplexField(int start, int end, int level) {
@@ -639,7 +809,7 @@ public boolean readField(int fieldIndex) throws IOException
{
currentTopLevelFieldIndex = fieldIndex;
currentFieldStart = startPositions[fieldIndex];
- currentFieldLength = startPositions[fieldIndex + 1] -
startPositions[fieldIndex] - 1;
+ currentFieldLength = startPositions[fieldIndex + 1] -
startPositions[fieldIndex] - topLevelSeparatorLen;
currentEscapeCount = (isEscaped ? escapeCounts[fieldIndex] : 0);
return doReadField(fields[fieldIndex]);
diff --git
a/serde/src/test/org/apache/hadoop/hive/serde2/lazy/fast/TestLazySimpleDeserializeRead.java
b/serde/src/test/org/apache/hadoop/hive/serde2/lazy/fast/TestLazySimpleDeserializeRead.java
index 465a616fa93..f705f0baeb6 100644
---
a/serde/src/test/org/apache/hadoop/hive/serde2/lazy/fast/TestLazySimpleDeserializeRead.java
+++
b/serde/src/test/org/apache/hadoop/hive/serde2/lazy/fast/TestLazySimpleDeserializeRead.java
@@ -20,6 +20,8 @@
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
import java.util.Properties;
import org.apache.hadoop.hive.conf.HiveConf;
@@ -29,8 +31,12 @@
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory;
import org.apache.hadoop.io.Text;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.junit.Test;
/**
@@ -86,4 +92,531 @@ public void testEscaping() throws Exception {
assertTrue("The escaped result is incorrect", field.compareTo(escaped) ==
0);
}
+
+ // --- single-byte field delimiter, no escape (LazySimple fast path) --------
+
+ /**
+ * Mixed INT / STRING columns on the single-byte hot loop — the numeric
+ * parsers depend on the length arithmetic between {@code startPositions[i]}
+ * and {@code startPositions[i+1]} charging exactly 1 byte for the delim.
+ */
+ @Test
+ public void testSingleByteNoEscapeMixedTypes() throws Exception {
+ LazySerDeParameters params = singleDelimParams("|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.intTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.longTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "42|hello|9876543210".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertTrue(r.readNextField());
+ assertEquals(42, r.currentInt);
+
+ assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+
+ assertTrue(r.readNextField());
+ assertEquals(9876543210L, r.currentLong);
+ }
+
+ /**
+ * A row ending exactly on a delim: the last field is empty. Exercises the
+ * "separator hit, then loop terminates because fieldByteEnd == end" arc in
+ * the hot loop and the trailing "end serves as final separator" branch.
+ */
+ @Test
+ public void testSingleByteNoEscapeEmptyLastField() throws Exception {
+ LazySerDeParameters params = singleDelimParams("|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "one|".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("one".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue("empty last field should be non-null (zero-length)",
r.readNextField());
+ assertEquals(0, r.currentBytesLength);
+ }
+
+ /**
+ * A middle field that's empty (two consecutive delims). Ensures the
+ * "fieldByteBegin = ++fieldByteEnd" step correctly records a zero-length
+ * span in {@code startPositions}, not a spurious NULL.
+ */
+ @Test
+ public void testSingleByteNoEscapeEmptyMiddleField() throws Exception {
+ LazySerDeParameters params = singleDelimParams("|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "a||c".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("a".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue("empty middle field must be non-null", r.readNextField());
+ assertEquals(0, r.currentBytesLength);
+ assertArrayEquals("c".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ }
+
+ /**
+ * Row has fewer fields than the schema expects. All missing fields must
+ * come back as NULL — this is the sentinel-fill branch of
+ * {@link
org.apache.hadoop.hive.serde2.lazy.fast.LazySimpleDeserializeRead#topLevelParse()}.
+ */
+ @Test
+ public void testSingleByteNoEscapeMissingTrailingFields() throws Exception {
+ LazySerDeParameters params = singleDelimParams("|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "only-one".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("only-one".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertFalse(r.readNextField());
+ assertFalse(r.readNextField());
+ }
+
+ /**
+ * Row supplies more delims than the schema has columns: the hot loop must
+ * break out on {@code fieldId == fieldCount} without consuming past the
+ * last expected field.
+ */
+ @Test
+ public void testSingleByteNoEscapeExtraDelimsIgnored() throws Exception {
+ LazySerDeParameters params = singleDelimParams("|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "a|b|extra|ignored".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("a".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("b".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertFalse("schema has exactly 2 fields — the rest must not surface",
r.readNextField());
+ }
+
+ // --- single-byte field delimiter, with escape (escape hot loop) ----------
+
+ /**
+ * The point of escape: a delim byte preceded by escape.delim must NOT split
+ * fields. Also verifies the {@code escapeCounts} bookkeeping — reading the
+ * field back out requires the external-buffer copy path.
+ */
+ @Test
+ public void testSingleByteEscapeEscapedDelimInsideField() throws Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ // "foo\|bar|baz" — the escaped '|' stays inside field 0.
+ byte[] row = "foo\\|bar|baz".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertTrue(r.readNextField());
+ assertTrue("escaped delim inside field forces the copy-out path",
+ r.currentExternalBufferNeeded);
+ byte[] buf = new byte[r.currentExternalBufferNeededLen];
+ r.copyToExternalBuffer(buf, 0);
+ assertArrayEquals("foo|bar".getBytes(StandardCharsets.UTF_8), buf);
+
+ assertArrayEquals("baz".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ }
+
+ /**
+ * Escape path, empty last field. The endLessOne / trailing-tail branch has
+ * to notice that we're at EOL and record a zero-length last field.
+ */
+ @Test
+ public void testSingleByteEscapeEmptyLastField() throws Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "one|".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("one".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue("empty last field on escape path must be non-null
(zero-length)",
+ r.readNextField());
+ assertEquals(0, r.currentBytesLength);
+ }
+
+ /**
+ * Escape path, missing trailing fields → NULL. Symmetric to the no-escape
+ * missing-trailing test; makes sure the sentinel-fill epilogue is common
+ * to both parse helpers.
+ */
+ @Test
+ public void testSingleByteEscapeMissingTrailingFields() throws Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "solo".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("solo".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertFalse(r.readNextField());
+ assertFalse(r.readNextField());
+ }
+
+ /**
+ * Escape path with mixed INT / STRING columns and NO escape characters in
+ * the payload. Exercises the "no escape, no delim" plain-byte inner branch
+ * many times in a row and confirms numeric parsing still sees a
+ * zero-escape field boundary.
+ */
+ @Test
+ public void testSingleByteEscapeMixedTypesNoEscapesInPayload() throws
Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.intTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.longTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "42|hello|9876543210".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertTrue(r.readNextField());
+ assertEquals(42, r.currentInt);
+ assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue(r.readNextField());
+ assertEquals(9876543210L, r.currentLong);
+ }
+
+ /**
+ * The last byte of the row is a delim: {@code endLessOne} loop stops one
+ * short, then the trailing single-byte branch fires. Confirms both the
+ * "last byte is separator" mini-branch and the following empty-last-field
+ * epilogue produce the right startPositions.
+ */
+ @Test
+ public void testSingleByteEscapeLastByteIsDelim() throws Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "a|b|".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("a".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("b".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue("empty tail after final delim must be non-null (zero-length)",
+ r.readNextField());
+ assertEquals(0, r.currentBytesLength);
+ }
+
+ /**
+ * Escape char at the very last position of the row has nothing to swallow.
+ * The {@code endLessOne} guard is what prevents the "escape swallows next
+ * byte" step from reading past {@code end}; this test pins that behavior.
+ */
+ @Test
+ public void testSingleByteEscapeCharAtEndOfInput() throws Exception {
+ LazySerDeParameters params = escapeParams("|", "\\");
+ TypeInfo[] typeInfos = new TypeInfo[] {TypeInfoFactory.stringTypeInfo };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ // Single field whose last byte is the escape char with no follower.
+ byte[] row = "abc\\".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ // Must not throw and must not read past end. Whatever surface value the
+ // reader chooses is fine as long as the length is bounded.
+ assertTrue(r.readNextField());
+ if (r.currentExternalBufferNeeded) {
+ byte[] buf = new byte[r.currentExternalBufferNeededLen];
+ r.copyToExternalBuffer(buf, 0);
+ assertTrue("copy-out length must not exceed the row length",
+ buf.length <= row.length);
+ } else {
+ assertTrue("in-place length must not exceed the row length",
+ r.currentBytesLength <= row.length);
+ }
+ }
+
+ // --- multi-byte field delimiter (MultiDelimitSerDe → LLAP fast path) ------
+
+ /**
+ * Three STRING columns separated by "~|" — the delimiter used by the BofA
+ * MultiDelimitSerDe workload that motivated this path.
+ */
+ @Test
+ public void testMultiByteDelimThreeStringColumns() throws Exception {
+ LazySerDeParameters params = multiDelimParams("~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "alpha~|beta~|gamma".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("alpha".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("beta".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("gamma".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertFalse("row should have exactly 3 fields", r.readNextField());
+ }
+
+ /**
+ * Mixed INT / STRING columns. Verifies that the length arithmetic charges
+ * the full delim.length (not 1) between fields so numeric parsing sees the
+ * correct field boundaries.
+ */
+ @Test
+ public void testMultiByteDelimMixedTypes() throws Exception {
+ LazySerDeParameters params = multiDelimParams("~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.intTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.longTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "42~|hello~|9876543210".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertTrue(r.readNextField());
+ assertEquals(42, r.currentInt);
+
+ assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+
+ assertTrue(r.readNextField());
+ assertEquals(9876543210L, r.currentLong);
+ }
+
+ /**
+ * The first byte of the delimiter ("~") appearing standalone inside a field
+ * must NOT trigger a split — the tail-match on delim[1] rescues it.
+ */
+ @Test
+ public void testMultiByteDelimFirstByteInsideField() throws Exception {
+ LazySerDeParameters params = multiDelimParams("~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ // A bare '~' followed by non-'|' must stay inside the first field.
+ byte[] row = "foo~bar~|baz".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("foo~bar".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("baz".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ }
+
+ /**
+ * The empty-tail case: last field extends to EOL and has zero length.
+ * startPositions must still tabulate length 0 (not -1), so NULL isn't
+ * spuriously returned.
+ */
+ @Test
+ public void testMultiByteDelimEmptyLastField() throws Exception {
+ LazySerDeParameters params = multiDelimParams("~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "one~|".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("one".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertTrue("empty last field should be non-null (zero-length)",
r.readNextField());
+ assertEquals(0, r.currentBytesLength);
+ }
+
+ /**
+ * Missing trailing fields must land in the "startPositions filled with
+ * sentinel" branch and come back as NULL — same behavior as single-byte.
+ */
+ @Test
+ public void testMultiByteDelimMissingTrailingFields() throws Exception {
+ LazySerDeParameters params = multiDelimParams("~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "only-one".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("only-one".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ // Missing fields → NULL.
+ assertFalse(r.readNextField());
+ assertFalse(r.readNextField());
+ }
+
+ /**
+ * A three-byte delimiter exercises the "delim length > 2" arithmetic.
+ */
+ @Test
+ public void testMultiByteDelimThreeByteDelimiter() throws Exception {
+ LazySerDeParameters params = multiDelimParams("|~|");
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "a|~|bb|~|ccc".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+
+ assertArrayEquals("a".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("bb".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("ccc".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ }
+
+ /**
+ * Multi-byte delim combined with escape.delim is not supported in the fast
+ * path — LazySimpleDeserializeRead must reject the combination at
+ * construction time so we never silently diverge from
+ * MultiDelimitSerDe.parseMultiDelimit (which itself ignores escape at the
+ * top level).
+ */
+ @Test
+ public void testMultiByteDelimRejectsEscape() throws Exception {
+ Properties props = new Properties();
+ props.setProperty(serdeConstants.FIELD_DELIM, "~|");
+ props.setProperty(serdeConstants.SERIALIZATION_FORMAT, "~|");
+ props.setProperty(serdeConstants.ESCAPE_CHAR, "\\");
+ LazySerDeParameters params = new LazySerDeParameters(new HiveConf(), props,
+ LazySimpleSerDe.class.getName());
+ params.setFieldDelimMulti("~|".getBytes(StandardCharsets.UTF_8));
+
+ TypeInfo[] typeInfos = new TypeInfo[]{TypeInfoFactory.stringTypeInfo};
+ try {
+ new LazySimpleDeserializeRead(typeInfos, null, true, params);
+ fail("expected RuntimeException for multi-byte delim + escape.delim");
+ } catch (RuntimeException expected) {
+ assertTrue("unexpected message: " + expected.getMessage(),
+ expected.getMessage().contains("Multi-byte field delimiter"));
+ }
+ }
+
+ /**
+ * When the table is a plain LazySimpleSerDe (setFieldDelimMulti never called
+ * on the params), LazySimpleDeserializeRead must select the specialized
+ * single-byte hot loop in {@code topLevelParse()}. The branch is gated on
+ * {@code fieldDelimMulti == null}, so we prove path selection by reflecting
+ * on the reader's internal state after a successful parse:
+ * <ul>
+ * <li>{@code fieldDelimMulti} is {@code null} — the {@code if} in
+ * {@code topLevelParse()} evaluated to the single-byte branch;</li>
+ * <li>{@code topLevelSeparatorLen == 1} — the length arithmetic
+ * (both in the sentinel fill and in {@code readField}) uses the
+ * single-byte constant, not the multi-byte {@code dlen}.</li>
+ * </ul>
+ * A correct parse of the row confirms the branch actually ran.
+ */
+ @Test
+ public void testSingleByteDelimHitsSpecializedHotPath() throws Exception {
+ Properties props = new Properties();
+ props.setProperty(serdeConstants.FIELD_DELIM, "|");
+ props.setProperty(serdeConstants.SERIALIZATION_FORMAT, "|");
+ LazySerDeParameters params = new LazySerDeParameters(new HiveConf(), props,
+ LazySimpleSerDe.class.getName());
+ // Intentionally do NOT call setFieldDelimMulti — this is a LazySimple
table.
+
+ TypeInfo[] typeInfos = new TypeInfo[] {
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo,
+ TypeInfoFactory.stringTypeInfo
+ };
+ LazySimpleDeserializeRead r = new LazySimpleDeserializeRead(typeInfos,
null, true, params);
+
+ byte[] row = "alpha|beta|gamma".getBytes(StandardCharsets.UTF_8);
+ r.set(row, 0, row.length);
+ assertArrayEquals("alpha".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("beta".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+ assertArrayEquals("gamma".getBytes(StandardCharsets.UTF_8),
readStringField(r));
+
+ // Path-selection proof: the reader's fieldDelimMulti stayed null and its
+ // topLevelSeparatorLen is 1 — the only code path in topLevelParse() that
+ // could have produced the correct parse above.
+ assertNull("fieldDelimMulti must be null on the LazySimple fast path",
+ readPrivateField(r, "fieldDelimMulti"));
+ assertEquals("topLevelSeparatorLen must be 1 on the LazySimple fast path",
+ 1, ((Integer) readPrivateField(r, "topLevelSeparatorLen")).intValue());
+ }
+
+ private static LazySerDeParameters singleDelimParams(String delim) throws
Exception {
+ Properties props = new Properties();
+ props.setProperty(serdeConstants.FIELD_DELIM, delim);
+ props.setProperty(serdeConstants.SERIALIZATION_FORMAT, delim);
+ return new LazySerDeParameters(new HiveConf(), props,
LazySimpleSerDe.class.getName());
+ }
+
+ private static LazySerDeParameters escapeParams(String delim, String escape)
throws Exception {
+ Properties props = new Properties();
+ props.setProperty(serdeConstants.FIELD_DELIM, delim);
+ props.setProperty(serdeConstants.SERIALIZATION_FORMAT, delim);
+ props.setProperty(serdeConstants.ESCAPE_CHAR, escape);
+ return new LazySerDeParameters(new HiveConf(), props,
LazySimpleSerDe.class.getName());
+ }
+
+ private static LazySerDeParameters multiDelimParams(String delim) throws
Exception {
+ Properties props = new Properties();
+ props.setProperty(serdeConstants.FIELD_DELIM, delim);
+ props.setProperty(serdeConstants.SERIALIZATION_FORMAT, delim);
+ LazySerDeParameters p = new LazySerDeParameters(new HiveConf(), props,
+ LazySimpleSerDe.class.getName());
+ p.setFieldDelimMulti(delim.getBytes(StandardCharsets.UTF_8));
+ return p;
+ }
+
+ private static byte[] readStringField(LazySimpleDeserializeRead r) throws
Exception {
+ assertTrue("expected non-null field", r.readNextField());
+ int len = r.currentBytesLength;
+ byte[] out = new byte[len];
+ System.arraycopy(r.currentBytes, r.currentBytesStart, out, 0, len);
+ return out;
+ }
+
+ private static Object readPrivateField(Object target, String name) throws
Exception {
+ Field f = LazySimpleDeserializeRead.class.getDeclaredField(name);
+ f.setAccessible(true);
+ return f.get(target);
+ }
}