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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4aefe4358 KUDU-1261 [Java] Add write support for Array Type
4aefe4358 is described below

commit 4aefe435870b8a99f624de81173c449c692e1c1e
Author: Abhishek Chennaka <[email protected]>
AuthorDate: Wed Oct 1 14:22:12 2025 -0700

    KUDU-1261 [Java] Add write support for Array Type
    
    Added ArrayCellView to expose efficient typed getters and semantic helpers.
    
    Extended PartialRow with addArray* APIs for all supported array types,
    with both column-index and column-name overloads along with some basic
    testing.
    
    Change-Id: Icbe5e243eafe12a8977d40204dacab99624451eb
    Reviewed-on: http://gerrit.cloudera.org:8080/23482
    Reviewed-by: Alexey Serbin <[email protected]>
    Tested-by: Abhishek Chennaka <[email protected]>
---
 java/config/spotbugs/excludeFilter.xml             |  11 +
 .../src/main/java/org/apache/kudu/Type.java        |   2 +-
 .../java/org/apache/kudu/client/Array1dSerdes.java |  57 +-
 .../java/org/apache/kudu/client/ArrayCellView.java | 412 +++++++++++++
 .../java/org/apache/kudu/client/PartialRow.java    | 676 ++++++++++++++++++++-
 .../org/apache/kudu/client/TestArraySerdes.java    | 105 ++++
 .../org/apache/kudu/client/TestPartialRow.java     | 343 +++++++++++
 7 files changed, 1600 insertions(+), 6 deletions(-)

diff --git a/java/config/spotbugs/excludeFilter.xml 
b/java/config/spotbugs/excludeFilter.xml
index e713eaec2..453965d55 100644
--- a/java/config/spotbugs/excludeFilter.xml
+++ b/java/config/spotbugs/excludeFilter.xml
@@ -174,6 +174,17 @@
     </Match>
 
     <!-- kudu-client exclusions -->
+    <Match>
+        <!-- For invalid elements we return null. -->
+        <Class name="org.apache.kudu.client.ArrayCellView"/>
+        <Method name="getBinary" />
+        <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS" />
+    </Match>
+    <Match>
+        <Class name="org.apache.kudu.client.PartialRow"/>
+        <Method name="normalizeValidity" />
+        <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS" />
+    </Match>
     <Match>
         <!-- Reference equality is intended here. -->
         <Class name="org.apache.kudu.client.AsyncKuduClient"/>
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/Type.java 
b/java/kudu-client/src/main/java/org/apache/kudu/Type.java
index 8c91685d0..065469337 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/Type.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/Type.java
@@ -235,6 +235,6 @@ public enum Type {
    * @return true if this type has a pre-determined fixed size, false otherwise
    */
   public boolean isFixedSize() {
-    return this != BINARY && this != STRING && this != VARCHAR;
+    return this != BINARY && this != STRING && this != VARCHAR && this != 
NESTED;
   }
 }
\ No newline at end of file
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/Array1dSerdes.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/Array1dSerdes.java
index e9c2f31c6..ee32dc82d 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/Array1dSerdes.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Array1dSerdes.java
@@ -17,13 +17,13 @@
 
 package org.apache.kudu.client;
 
+import java.lang.reflect.Array;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.Arrays;
 import java.util.Objects;
 
 import com.google.flatbuffers.FlatBufferBuilder;
-import com.google.flatbuffers.Table;
 
 // FlatBuffers generated classes
 import org.apache.kudu.serdes.BinaryArray;
@@ -84,6 +84,20 @@ public class Array1dSerdes {
     if (validity == null || validity.length == 0) {
       return 0;
     }
+
+    // If all entries are true, omit encoding
+    boolean allTrue = true;
+    for (boolean v : validity) {
+      if (!v) {
+        allTrue = false;
+        break;
+      }
+    }
+    if (allTrue) {
+      return 0;
+    }
+
+    // Only encode if at least one false
     return Content.createValidityVector(b, validity);
   }
 
@@ -121,6 +135,18 @@ public class Array1dSerdes {
     return validity;
   }
 
+  private static boolean hasNulls(Object[] values) {
+    if (values == null) {
+      return false;
+    }
+    for (Object v : values) {
+      if (v == null) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   // ---------------- Int8 ----------------
   public static byte[] serializeInt8(byte[] values, boolean[] validity) {
     return serializePrimitive(
@@ -354,6 +380,14 @@ public class Array1dSerdes {
 
   // ---------------- String ----------------
   public static byte[] serializeString(String[] values, boolean[] validity) {
+    // If validity is omitted, data must not contain nulls. If not,
+    // the serializer can't differentiate null from an empty value.
+    if (validity == null || validity.length == 0) {
+      if (hasNulls(values)) {
+        throw new IllegalArgumentException(
+            "Empty validity vector not allowed when values contain nulls");
+      }
+    }
     FlatBufferBuilder b = new FlatBufferBuilder();
     int[] offs = new int[values.length];
     for (int i = 0; i < values.length; i++) {
@@ -391,6 +425,14 @@ public class Array1dSerdes {
 
   // ---------------- Binary ----------------
   public static byte[] serializeBinary(byte[][] values, boolean[] validity) {
+    // If validity is omitted, data must not contain nulls. If not,
+    // the serializer can't differentiate null from an empty value.
+    if (validity == null || validity.length == 0) {
+      if (hasNulls(values)) {
+        throw new IllegalArgumentException(
+            "Empty validity vector not allowed when values contain nulls");
+      }
+    }
     FlatBufferBuilder b = new FlatBufferBuilder();
     int[] elemOffs = new int[values.length];
     for (int i = 0; i < values.length; i++) {
@@ -432,12 +474,21 @@ public class Array1dSerdes {
       CreateVec<V> createVec, Start start, AddValues addValues, End end) {
 
     if (values != null && validity != null) {
-      int valueLen = java.lang.reflect.Array.getLength(values);
-      if (validity.length != valueLen) {
+      int valueLen = Array.getLength(values);
+      if (validity.length != 0 && validity.length != valueLen) {
         throw new IllegalArgumentException(
             String.format("Validity length %d does not match values length %d",
                 validity.length, valueLen));
       }
+      if (validity.length == 0) {
+        for (int i = 0; i < valueLen; i++) {
+          Object elem = Array.get(values, i);
+          if (elem == null) {
+            throw new IllegalArgumentException(
+                String.format("Empty validity vector provided, but values[%d] 
is null", i));
+          }
+        }
+      }
     }
 
     FlatBufferBuilder b = new FlatBufferBuilder();
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/ArrayCellView.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/ArrayCellView.java
new file mode 100644
index 000000000..d16798efb
--- /dev/null
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/ArrayCellView.java
@@ -0,0 +1,412 @@
+// 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.kudu.client;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+
+import org.apache.kudu.serdes.BinaryArray;
+import org.apache.kudu.serdes.Content;
+import org.apache.kudu.serdes.DoubleArray;
+import org.apache.kudu.serdes.FloatArray;
+import org.apache.kudu.serdes.Int16Array;
+import org.apache.kudu.serdes.Int32Array;
+import org.apache.kudu.serdes.Int64Array;
+import org.apache.kudu.serdes.Int8Array;
+import org.apache.kudu.serdes.ScalarArray;
+import org.apache.kudu.serdes.StringArray;
+import org.apache.kudu.serdes.UInt16Array;
+import org.apache.kudu.serdes.UInt32Array;
+import org.apache.kudu.serdes.UInt64Array;
+import org.apache.kudu.serdes.UInt8Array;
+
+/**
+ * Lightweight view over a FlatBuffers Content blob representing a single 
array cell.
+ */
+public final class ArrayCellView {
+
+  private final byte[] rawBytes;
+  private final Content content;
+  private final byte typeTag;
+
+  private static final String NULL_ELEMENT_MSG = "Element %d is NULL";
+
+  public ArrayCellView(byte[] buf) {
+    this.rawBytes = buf;
+    ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
+    this.content = Content.getRootAsContent(bb);
+    this.typeTag = content.dataType();
+  }
+
+  /** Return the underlying FlatBuffer bytes exactly as passed in. */
+  public byte[] toBytes() {
+    return rawBytes;
+  }
+
+  /** Number of logical elements (driven by the data vector). */
+  public int length() {
+    switch (typeTag) {
+      case ScalarArray.Int8Array: {
+        Int8Array arr = new Int8Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.UInt8Array: {
+        UInt8Array arr = new UInt8Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.Int16Array: {
+        Int16Array arr = new Int16Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.UInt16Array: {
+        UInt16Array arr = new UInt16Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.Int32Array: {
+        Int32Array arr = new Int32Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.UInt32Array: {
+        UInt32Array arr = new UInt32Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.Int64Array: {
+        Int64Array arr = new Int64Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.UInt64Array: {
+        UInt64Array arr = new UInt64Array();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.FloatArray: {
+        FloatArray arr = new FloatArray();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.DoubleArray: {
+        DoubleArray arr = new DoubleArray();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.StringArray: {
+        StringArray arr = new StringArray();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      case ScalarArray.BinaryArray: {
+        BinaryArray arr = new BinaryArray();
+        content.data(arr);
+        return arr.valuesLength();
+      }
+      default:
+        throw new UnsupportedOperationException(
+            "Unsupported array type tag: " + typeTag);
+    }
+  }
+
+  /** Returns true iff element i is valid (non-null). */
+  public boolean isValid(int i) {
+    int n = length();
+    if (i < 0 || i >= n) {
+      throw new IndexOutOfBoundsException(
+          String.format("Index %d out of bounds for array length %d", i, n));
+    }
+
+    int vlen = content.validityLength();
+    if (vlen == 0) {
+      // No validity vector present => all elements are valid
+      return true;
+    }
+
+    if (i >= vlen) {
+      throw new IllegalStateException(
+          String.format("Validity vector shorter than array: i=%d, vlen=%d", 
i, vlen));
+    }
+
+    return content.validity(i);
+  }
+
+  // ----------------------------------------------------------------------
+  // Types single element accessors
+  // ----------------------------------------------------------------------
+
+  /** BOOL is encoded as UInt8 (0/1). */
+  public boolean getBoolean(int i) {
+    ensureTag(ScalarArray.UInt8Array);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    UInt8Array arr = new UInt8Array();
+    content.data(arr);
+    return arr.values(i) != 0;
+  }
+
+  public byte getInt8(int i) {
+    ensureTag(ScalarArray.Int8Array);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    Int8Array arr = new Int8Array();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  public short getInt16(int i) {
+    ensureTag(ScalarArray.Int16Array);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    Int16Array arr = new Int16Array();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  /** INT32 (also used by DATE, DECIMAL32 on the wire). */
+  public int getInt32(int i) {
+    ensureTag(ScalarArray.Int32Array);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    Int32Array arr = new Int32Array();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  /** INT64 (also used by DECIMAL64, UNIXTIME_MICROS on the wire). */
+  public long getInt64(int i) {
+    ensureTag(ScalarArray.Int64Array);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    Int64Array arr = new Int64Array();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  public float getFloat(int i) {
+    ensureTag(ScalarArray.FloatArray);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    FloatArray arr = new FloatArray();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  public double getDouble(int i) {
+    ensureTag(ScalarArray.DoubleArray);
+    if (!isValid(i)) {
+      throw new IllegalStateException(String.format(NULL_ELEMENT_MSG, i));
+    }
+    DoubleArray arr = new DoubleArray();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  public String getString(int i) {
+    ensureTag(ScalarArray.StringArray);
+    if (!isValid(i)) {
+      return null;
+    }
+    StringArray arr = new StringArray();
+    content.data(arr);
+    return arr.values(i);
+  }
+
+  public byte[] getBinary(int i) {
+    ensureTag(ScalarArray.BinaryArray);
+    if (!isValid(i)) {
+      return null;
+    }
+    BinaryArray arr = new BinaryArray();
+    content.data(arr);
+    UInt8Array elem = arr.values(new UInt8Array(), i);
+    if (elem == null) {
+      throw new IllegalStateException(
+          String.format("Corrupt BinaryArray: missing element at index %d", 
i));
+    }
+    ByteBuffer bb = elem.valuesAsByteBuffer();
+    byte[] out = new byte[bb.remaining()];
+    bb.get(out);
+    return out;
+  }
+
+
+  // ----------------------------------------------------------------------
+  // Bulk conversion for callers that want plain Java arrays
+  // ----------------------------------------------------------------------
+
+  public Object toJavaArray() {
+    switch (typeTag) {
+      case ScalarArray.Int32Array:   return 
Array1dSerdes.parseInt32(rawBytes).getValues();
+      case ScalarArray.Int64Array:   return 
Array1dSerdes.parseInt64(rawBytes).getValues();
+      case ScalarArray.Int16Array:   return 
Array1dSerdes.parseInt16(rawBytes).getValues();
+      case ScalarArray.Int8Array:    return 
Array1dSerdes.parseInt8(rawBytes).getValues();
+      case ScalarArray.UInt8Array:   return 
Array1dSerdes.parseUInt8(rawBytes).getValues();
+      case ScalarArray.UInt16Array:  return 
Array1dSerdes.parseUInt16(rawBytes).getValues();
+      case ScalarArray.UInt32Array:  return 
Array1dSerdes.parseUInt32(rawBytes).getValues();
+      case ScalarArray.UInt64Array:  return 
Array1dSerdes.parseUInt64(rawBytes).getValues();
+      case ScalarArray.FloatArray:   return 
Array1dSerdes.parseFloat(rawBytes).getValues();
+      case ScalarArray.DoubleArray:  return 
Array1dSerdes.parseDouble(rawBytes).getValues();
+      case ScalarArray.StringArray:  return 
Array1dSerdes.parseString(rawBytes).getValues();
+      case ScalarArray.BinaryArray:  return 
Array1dSerdes.parseBinary(rawBytes).getValues();
+      default:
+        throw new UnsupportedOperationException("Unsupported array type tag: " 
+ typeTag);
+    }
+  }
+
+  // ----------------------------------------------------------------------
+  // Pretty print with NULLs preserved
+  // ----------------------------------------------------------------------
+
+  @Override
+  public String toString() {
+    final int n = length();
+    StringBuilder sb = new StringBuilder();
+    sb.append('[');
+
+    switch (typeTag) {
+      case ScalarArray.Int8Array: {
+        Int8Array arr = new Int8Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.UInt8Array: {
+        UInt8Array arr = new UInt8Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.Int16Array: {
+        Int16Array arr = new Int16Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.UInt16Array: {
+        UInt16Array arr = new UInt16Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.Int32Array: {
+        Int32Array arr = new Int32Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.UInt32Array: {
+        UInt32Array arr = new UInt32Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.Int64Array: {
+        Int64Array arr = new Int64Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.UInt64Array: {
+        UInt64Array arr = new UInt64Array();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.FloatArray: {
+        FloatArray arr = new FloatArray();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.DoubleArray: {
+        DoubleArray arr = new DoubleArray();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.StringArray: {
+        StringArray arr = new StringArray();
+        content.data(arr);
+        appendArrayValuesToString(sb, n, arr::values);
+      } break;
+
+      case ScalarArray.BinaryArray: {
+        BinaryArray bin = new BinaryArray();
+        content.data(bin);
+        appendArrayValuesToString(sb, n, i -> {
+          UInt8Array elem = bin.values(new UInt8Array(), i);
+          if (elem == null) {
+            throw new IllegalStateException(
+                String.format("Corrupt BinaryArray: missing element at index 
%d", i));
+          }
+          int len = elem.valuesLength();
+          byte[] out = new byte[len];
+          for (int j = 0; j < len; j++) {
+            out[j] = (byte) elem.values(j);
+          }
+          return Arrays.toString(out);
+        });
+      } break;
+      default:
+        sb.append("<unsupported array type tag ").append(typeTag).append('>');
+        break;
+    }
+
+    sb.append(']');
+    return sb.toString();
+  }
+
+  private void ensureTag(byte expectedTag) {
+    if (typeTag != expectedTag) {
+      String expectedName = ScalarArray.name(expectedTag);
+      String actualName   = ScalarArray.name(typeTag);
+      throw new IllegalStateException(
+          String.format("Type mismatch: expected %s (tag=%d) but found %s 
(tag=%d)",
+              expectedName, expectedTag,
+              actualName, typeTag));
+    }
+  }
+
+  private void appendArrayValuesToString(
+      StringBuilder sb, int n,
+      IntFunction<Object> valueFn) {
+    for (int i = 0; i < n; i++) {
+      if (i > 0) {
+        sb.append(", ");
+      }
+      if (!isValid(i)) {
+        sb.append("NULL");
+        continue;
+      }
+      sb.append(valueFn.apply(i));
+    }
+  }
+
+}
\ No newline at end of file
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
index bade6df50..559bf607b 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
@@ -18,6 +18,7 @@
 package org.apache.kudu.client;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.sql.Date;
@@ -71,6 +72,16 @@ public class PartialRow {
 
   private boolean frozen = false;
 
+  private static final Type ARRAY_TYPE = Type.NESTED;
+
+  private byte[] getVarLenBytes(int columnIndex) {
+    ByteBuffer dup = getVarLengthData(columnIndex);
+    dup.reset();
+    byte[] out = new byte[dup.remaining()];
+    dup.get(out);
+    return out;
+  }
+
   /**
    * This is not a stable API, prefer using {@link Schema#newPartialRow()}
    * to create a new partial row.
@@ -931,6 +942,504 @@ public class PartialRow {
     return getVarLengthData(columnIndex);
   }
 
+  /**
+   * Normalize a validity array for object arrays where elements may be null.
+   * For primitive arrays (e.g. boolean[]), callers can pass validity directly.
+   *
+   * Behavior:
+   * - If {@code validity == null}, infer validity by marking each non-null 
element {@code true}.
+   * - If {@code validity.length == values.length}, validate length and mask 
out any null elements
+   *   (i.e., null values are always marked invalid regardless of the validity 
bit).
+   * - If {@code validity.length == 0}, treat it as an optimization meaning 
"all elements valid",
+   *   but only if all {@code values[]} are non-null; otherwise throw
+   *   {@link IllegalArgumentException}.
+   *
+   * This ensures consistency with serialization rules, where an empty 
validity vector
+   * is accepted only when all elements are valid (non-null).
+   */
+
+  private static <T> boolean[] normalizeValidity(T[] values, boolean[] 
validity) {
+    if (values == null) {
+      return null;
+    }
+
+    // explicit non-empty validity vector
+    if (validity != null && validity.length != 0) {
+      if (validity.length != values.length) {
+        throw new IllegalArgumentException(
+            "validity length mismatch: " + validity.length + " vs " + 
values.length);
+      }
+      boolean[] corrected = new boolean[values.length];
+      for (int i = 0; i < values.length; i++) {
+        corrected[i] = (values[i] != null) && validity[i];
+      }
+      return corrected;
+    }
+
+    // empty validity vector => all valid, must have no nulls
+    if (validity != null && validity.length == 0) {
+      for (int i = 0; i < values.length; i++) {
+        if (values[i] == null) {
+          throw new IllegalArgumentException(
+              String.format("Empty validity vector provided, but values[%d] is 
null", i));
+        }
+      }
+      boolean[] allValid = new boolean[values.length];
+      java.util.Arrays.fill(allValid, true);
+      return allValid;
+    }
+
+    // validity == null => infer validity from non-nulls
+    boolean[] inferred = new boolean[values.length];
+    for (int i = 0; i < values.length; i++) {
+      inferred[i] = (values[i] != null);
+    }
+    return inferred;
+  }
+
+  private void checkValidityLength(int valuesLen, boolean[] validity) {
+    if (validity != null && validity.length != 0 && validity.length != 
valuesLen) {
+      throw new IllegalArgumentException(
+          "validity length mismatch: " + validity.length + " vs " + valuesLen);
+    }
+  }
+
+
+  /**
+   * Adds array-typed column values to this row and defines how validity 
(nullability)
+   * is interpreted for array elements.
+   *
+   * <p>Each {@code addArray*()} method writes a complete array cell into the 
row.
+   * The caller may optionally provide a {@code validity} vector to control 
which
+   * elements are null or valid:
+   * <ul>
+   *   <li>If {@code validity == null}, validity is inferred from whether each
+   *       array element is {@code null}.</li>
+   *   <li>If {@code validity.length == 0}, it signals that all elements are 
valid;
+   *       this is only allowed when the array contains no nulls.</li>
+   *   <li>If {@code validity.length > 0}, it must match the array length; each
+   *       element is valid only if both the value is non-null and the 
corresponding
+   *       validity bit is {@code true}.</li>
+   * </ul>
+   *
+   * <p>For primitive arrays (which cannot contain nulls), the validity vector
+   * is ignored, and the entire array is treated as valid. Empty or all-true
+   * validity masks are omitted from serialization for efficiency.
+   */
+
+  public void addArrayInt8(int columnIndex, byte[] values, boolean[] validity) 
{
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeInt8(values, 
validity));
+  }
+
+  public void addArrayInt8(String columnName, byte[] values, boolean[] 
validity) {
+    addArrayInt8(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayInt8(int columnIndex, byte[] values) {
+    addArrayInt8(columnIndex, values, null);
+  }
+
+  public void addArrayInt8(String columnName, byte[] values) {
+    addArrayInt8(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayInt16(int columnIndex, short[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeInt16(values, 
validity));
+  }
+
+  public void addArrayInt16(String columnName, short[] values, boolean[] 
validity) {
+    addArrayInt16(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayInt16(int columnIndex, short[] values) {
+    addArrayInt16(columnIndex, values, null);
+  }
+
+  public void addArrayInt16(String columnName, short[] values) {
+    addArrayInt16(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayInt32(int columnIndex, int[] values, boolean[] validity) 
{
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeInt32(values, 
validity));
+  }
+
+  public void addArrayInt32(String columnName, int[] values, boolean[] 
validity) {
+    addArrayInt32(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayInt32(int columnIndex, int[] values) {
+    addArrayInt32(columnIndex, values, null);
+  }
+
+  public void addArrayInt32(String columnName, int[] values) {
+    addArrayInt32(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayInt64(int columnIndex, long[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeInt64(values, 
validity));
+  }
+
+  public void addArrayInt64(String columnName, long[] values, boolean[] 
validity) {
+    addArrayInt64(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayInt64(int columnIndex, long[] values) {
+    addArrayInt64(columnIndex, values, null);
+  }
+
+  public void addArrayInt64(String columnName, long[] values) {
+    addArrayInt64(schema.getColumnIndex(columnName), values, null);
+  }
+
+
+  public void addArrayFloat(int columnIndex, float[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeFloat(values, 
validity));
+  }
+
+  public void addArrayFloat(String columnName, float[] values, boolean[] 
validity) {
+    addArrayFloat(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayFloat(int columnIndex, float[] values) {
+    addArrayFloat(columnIndex, values, null);
+  }
+
+  public void addArrayFloat(String columnName, float[] values) {
+    addArrayFloat(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayDouble(int columnIndex, double[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeDouble(values, 
validity));
+  }
+
+  public void addArrayDouble(String columnName, double[] values, boolean[] 
validity) {
+    addArrayDouble(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayDouble(int columnIndex, double[] values) {
+    addArrayDouble(columnIndex, values, null);
+  }
+
+  public void addArrayDouble(String columnName, double[] values) {
+    addArrayDouble(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayString(int columnIndex, String[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeString(values, 
valids));
+  }
+
+
+  public void addArrayString(String columnName, String[] values, boolean[] 
validity) {
+    addArrayString(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayString(int columnIndex, String[] values) {
+    addArrayString(columnIndex, values, null);
+  }
+
+  public void addArrayString(String columnName, String[] values) {
+    addArrayString(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayBinary(int columnIndex, byte[][] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+    addVarLengthData(columnIndex, Array1dSerdes.serializeBinary(values, 
valids));
+  }
+
+  public void addArrayBinary(String columnName, byte[][] values, boolean[] 
validity) {
+    addArrayBinary(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayBinary(int columnIndex, byte[][] values) {
+    addArrayBinary(columnIndex, values, null);
+  }
+
+  public void addArrayBinary(String columnName, byte[][] values) {
+    addArrayBinary(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayBool(int columnIndex, boolean[] values, boolean[] 
validity) {
+    checkNotFrozen();
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+    checkValidityLength(values.length, validity);
+    byte[] asBytes = new byte[values.length];
+    for (int i = 0; i < values.length; i++) {
+      asBytes[i] = (byte)(values[i] ? 1 : 0);
+    }
+
+    addVarLengthData(columnIndex, Array1dSerdes.serializeUInt8(asBytes, 
validity));
+  }
+
+  public void addArrayBool(String columnName, boolean[] values, boolean[] 
validity) {
+    addArrayBool(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayBool(int columnIndex, boolean[] values) {
+    addArrayBool(columnIndex, values, null);
+  }
+
+  public void addArrayBool(String columnName, boolean[] values) {
+    addArrayBool(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayTimestamp(int columnIndex, Timestamp[] values) {
+    addArrayTimestampInternal(columnIndex, values, null);
+  }
+
+  public void addArrayTimestamp(String columnName, Timestamp[] values) {
+    addArrayTimestamp(schema.getColumnIndex(columnName), values);
+  }
+
+  public void addArrayTimestamp(int columnIndex,
+                                Timestamp[] values,
+                                boolean[] validity) {
+    addArrayTimestampInternal(columnIndex, values, validity);
+  }
+
+  public void addArrayTimestamp(String columnName,
+                                Timestamp[] values,
+                                boolean[] validity) {
+    addArrayTimestamp(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayDate(int columnIndex, Date[] values) {
+    addArrayDateInternal(columnIndex, values, null);
+  }
+
+  public void addArrayDate(String columnName, Date[] values) {
+    addArrayDate(schema.getColumnIndex(columnName), values);
+  }
+
+  public void addArrayDate(int columnIndex, Date[] values, boolean[] validity) 
{
+    addArrayDateInternal(columnIndex, values, validity);
+  }
+
+  public void addArrayDate(String columnName, Date[] values, boolean[] 
validity) {
+    addArrayDate(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayVarchar(int columnIndex, String[] values) {
+    addArrayVarcharInternal(columnIndex, values, null);
+  }
+
+  public void addArrayVarchar(String columnName, String[] values) {
+    addArrayVarchar(schema.getColumnIndex(columnName), values, null);
+  }
+
+  public void addArrayVarchar(int columnIndex, String[] values, boolean[] 
validity) {
+    addArrayVarcharInternal(columnIndex, values, validity);
+  }
+
+  public void addArrayVarchar(String columnName, String[] values, boolean[] 
validity) {
+    addArrayVarchar(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  public void addArrayDecimal(int columnIndex, BigDecimal[] values) {
+    addArrayDecimalInternal(columnIndex, values, null);
+  }
+
+  public void addArrayDecimal(String columnName, BigDecimal[] values) {
+    addArrayDecimal(schema.getColumnIndex(columnName), values);
+  }
+
+  public void addArrayDecimal(int columnIndex,
+                              BigDecimal[] values,
+                              boolean[] validity) {
+    addArrayDecimalInternal(columnIndex, values, validity);
+  }
+
+  public void addArrayDecimal(String columnName,
+                              BigDecimal[] values,
+                              boolean[] validity) {
+    addArrayDecimal(schema.getColumnIndex(columnName), values, validity);
+  }
+
+  private ArrayCellView getArray(int columnIndex) {
+    checkColumn(schema.getColumnByIndex(columnIndex), ARRAY_TYPE);
+    checkValue(columnIndex);
+    return new ArrayCellView(getVarLenBytes(columnIndex));
+  }
+
+  private void addArrayTimestampInternal(int columnIndex,
+                                         Timestamp[] values,
+                                         boolean[] validity) {
+    checkNotFrozen();
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+    long[] micros = new long[values.length];
+
+    for (int i = 0; i < values.length; i++) {
+      if (valids[i]) {
+        micros[i] = TimestampUtil.timestampToMicros(values[i]);
+      }
+    }
+
+    addArrayInt64(columnIndex, micros, valids);
+  }
+
+  private void addArrayVarcharInternal(int columnIndex,
+                                       String[] values,
+                                       boolean[] validity) {
+    checkNotFrozen();
+    ColumnSchema col = schema.getColumnByIndex(columnIndex);
+    int maxLen = col.getTypeAttributes().getLength();
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+    String[] truncated = new String[values.length];
+
+    for (int i = 0; i < values.length; i++) {
+      if (valids[i]) {
+        String s = values[i];
+        truncated[i] = (s.length() > maxLen) ? s.substring(0, maxLen) : s;
+      } else {
+        truncated[i] = ""; // content unused when validity[i] is false
+      }
+    }
+
+    addVarLengthData(columnIndex, Array1dSerdes.serializeString(truncated, 
valids));
+  }
+
+  private void addArrayDateInternal(int columnIndex, Date[] values, boolean[] 
validity) {
+    checkNotFrozen();
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+    int[] days = new int[values.length];
+
+    for (int i = 0; i < values.length; i++) {
+      if (valids[i]) {
+        days[i] = DateUtil.sqlDateToEpochDays(values[i]);
+      }
+    }
+
+    addArrayInt32(columnIndex, days, valids);
+  }
+
+  private void addArrayDecimalInternal(int columnIndex,
+                                       BigDecimal[] values,
+                                       boolean[] validity) {
+    checkNotFrozen();
+    ColumnSchema col = schema.getColumnByIndex(columnIndex);
+    ColumnTypeAttributes attrs = col.getTypeAttributes();
+    int precision = attrs.getPrecision();
+    int scale = attrs.getScale();
+
+    if (values == null) {
+      setNull(columnIndex);
+      return;
+    }
+
+    boolean[] valids = normalizeValidity(values, validity);
+
+    if (precision <= 9) {
+      int[] unscaled = new int[values.length];
+      for (int i = 0; i < values.length; i++) {
+        if (valids[i]) {
+          BigDecimal bd = values[i].setScale(scale, RoundingMode.UNNECESSARY);
+          unscaled[i] = bd.unscaledValue().intValueExact();
+        }
+      }
+      addVarLengthData(columnIndex, Array1dSerdes.serializeInt32(unscaled, 
valids));
+
+    } else if (precision <= 18) {
+      long[] unscaled = new long[values.length];
+      for (int i = 0; i < values.length; i++) {
+        if (valids[i]) {
+          BigDecimal bd = values[i].setScale(scale, RoundingMode.UNNECESSARY);
+          unscaled[i] = bd.unscaledValue().longValueExact();
+        }
+      }
+      addVarLengthData(columnIndex, Array1dSerdes.serializeInt64(unscaled, 
valids));
+
+    } else {
+      throw new IllegalStateException("DECIMAL128 arrays not supported yet");
+    }
+  }
+
   private void addVarLengthData(int columnIndex, byte[] val) {
     addVarLengthData(columnIndex, ByteBuffer.wrap(val));
   }
@@ -1063,11 +1572,27 @@ public class PartialRow {
    *  Type.BINARY -> byte[] or java.lang.ByteBuffer
    *  Type.DECIMAL -> java.math.BigDecimal
    *  Type.DATE -> java.sql.Date
+   *  Type.NESTED (array)    -> one of:
+   *     - Primitive arrays: byte[], short[], int[], long[], float[], 
double[], boolean[]
+   *     - Object arrays: java.lang.String[], java.sql.Date[], 
java.sql.Timestamp[],
+   *                      java.math.BigDecimal[], byte[][]
+   *     - Already-wrapped: {@link ArrayCellView} (serialized form)
+   *
+   * For arrays, {@code validity} is always passed as {@code null} from here:
+   *  - Primitive arrays cannot contain null elements, so all entries are 
valid.
+   *  - Object arrays may contain nulls; the corresponding addArray* methods
+   *    internally reconstruct validity masks via {@code normalizeValidity()}.
+   * This keeps {@code addObject()} simple while ensuring correct null 
handling.
+   *
+   * Array serializers also treat {@code validity.length == 0}
+   * the same as {@code null}, meaning "all elements valid". This allows 
callers
+   * to omit the validity vector for fully non-null data.
    *
    * @param columnIndex column index in the schema
    * @param val the value to add as an Object
    * @throws IllegalStateException if the row was already applied
    * @throws IndexOutOfBoundsException if the column doesn't exist
+   * @throws IllegalArgumentException if the value type is incompatible
    */
   public void addObject(int columnIndex, Object val) {
     checkNotFrozen();
@@ -1126,6 +1651,77 @@ public class PartialRow {
         case DECIMAL:
           addDecimal(columnIndex, (BigDecimal) val);
           break;
+        case /*ARRAY*/ NESTED: {
+          // Note: we always pass `validity = null` here.
+          //   - For primitive arrays: elements cannot be null. Hence, all 
entries valid.
+          //   - For object arrays: the addArray* methods call 
normalizeValidity()
+          //     to infer nulls from the values[]. So validity is 
reconstructed.
+          // This keeps addObject() simple while ensuring nulls are handled 
correctly.
+          if (val instanceof byte[]) {
+            addArrayInt8(columnIndex, (byte[]) val, null);
+            break;
+          }
+          if (val instanceof short[]) {
+            addArrayInt16(columnIndex, (short[]) val, null);
+            break;
+          }
+          if (val instanceof int[]) {
+            addArrayInt32(columnIndex, (int[]) val, null);
+            break;
+          }
+          if (val instanceof long[]) {
+            addArrayInt64(columnIndex, (long[]) val, null);
+            break;
+          }
+          if (val instanceof boolean[]) {
+            addArrayBool(columnIndex, (boolean[]) val, null);
+            break;
+          }
+          if (val instanceof float[]) {
+            addArrayFloat(columnIndex, (float[]) val, null);
+            break;
+          }
+          if (val instanceof double[]) {
+            addArrayDouble(columnIndex, (double[]) val, null);
+            break;
+          }
+          if (val instanceof Timestamp[]) {
+            addArrayTimestamp(columnIndex, (Timestamp[]) val);
+            break;
+          }
+          if (val instanceof String[]) {
+            if 
(col.getNestedTypeDescriptor().getArrayDescriptor().getElemType() == 
Type.VARCHAR) {
+              addArrayVarchar(columnIndex, (String[]) val, null);
+            } else {
+              addArrayString(columnIndex, (String[]) val, null);
+            }
+            break;
+          }
+          if (val instanceof Date[]) {
+            addArrayDate(columnIndex, (Date[]) val);
+            break;
+          }
+          if (val instanceof byte[][]) {
+            addArrayBinary(columnIndex, (byte[][]) val, null);
+            break;
+          }
+          if (val instanceof BigDecimal[]) {
+            addArrayDecimal(columnIndex, (BigDecimal[]) val, null);
+            break;
+          }
+          // Allow pre-built serialized FlatBuffer payloads.
+          if (val instanceof ByteBuffer) {
+            addVarLengthData(columnIndex, (ByteBuffer) val);
+            break;
+          }
+          if (val instanceof ArrayCellView) {
+            addVarLengthData(columnIndex, ((ArrayCellView) val).toBytes());
+            break;
+          }
+
+          throw new IllegalArgumentException(
+              "Unsupported object type for array column " + col.getName());
+        }
         default:
           throw new IllegalArgumentException("Unsupported column type: " + 
col.getType());
       }
@@ -1209,6 +1805,7 @@ public class PartialRow {
       case STRING: return getString(columnIndex);
       case BINARY: return getBinaryCopy(columnIndex);
       case DECIMAL: return getDecimal(columnIndex);
+      case NESTED: return getArray(columnIndex);
       default: throw new UnsupportedOperationException("Unsupported type: " + 
type);
     }
   }
@@ -1420,11 +2017,46 @@ public class PartialRow {
     ColumnSchema col = schema.getColumnByIndex(idx);
     Preconditions.checkState(columnsBitSet.get(idx), "Column %s is not set", 
col.getName());
 
+    // Handle nulls
     if (nullsBitSet != null && nullsBitSet.get(idx)) {
-      sb.append("NULL");
+      if (col.isArray()) {
+        sb.append(col.getType().getName())
+                .append("[] ")
+                .append(col.getName())
+                .append("=NULL");
+      } else {
+        sb.append("NULL");
+      }
       return;
     }
 
+    // Handle arrays
+    if (col.isArray()) {
+      ByteBuffer buf = getVarLengthData(idx);
+      if (buf == null || !buf.hasRemaining()) {
+        sb.append(col.getType().getName())
+                .append("[] ")
+                .append(col.getName())
+                .append("=NULL");
+        return;
+      }
+
+      ByteBuffer dup = buf.duplicate();
+      byte[] raw = new byte[dup.remaining()];
+      dup.get(raw);
+
+      try {
+        ArrayCellView view = new ArrayCellView(raw);
+        sb.append(col.getType().getName())
+                .append("[] ")
+                .append(col.getName())
+                .append("=")
+                .append(view);
+      } catch (RuntimeException e) {
+        sb.append("<invalid array data>");
+      }
+      return;
+    }
     switch (col.getType()) {
       case BOOL:
         sb.append(Bytes.getBoolean(rowAlloc, schema.getColumnOffset(idx)));
@@ -1475,6 +2107,45 @@ public class PartialRow {
           sb.append(Bytes.pretty(data));
         }
         return;
+      case /*ARRAY*/ NESTED: {
+        ArrayCellView view = getArray(idx);
+        sb.append("ARRAY[len=").append(view.length()).append("]");
+        try {
+          Object arr = view.toJavaArray();
+          String s;
+          if (arr instanceof Object[]) {
+            Object[] objs = (Object[]) arr;
+            if (objs instanceof byte[][]) {
+              // Pretty-print nested binary arrays
+              s = java.util.Arrays.deepToString(objs);
+            } else {
+              // Strings, Dates, Timestamps, BigDecimals, etc.
+              s = java.util.Arrays.toString(objs);
+            }
+          } else if (arr instanceof int[]) {
+            s = java.util.Arrays.toString((int[]) arr);
+          } else if (arr instanceof long[]) {
+            s = java.util.Arrays.toString((long[]) arr);
+          } else if (arr instanceof short[]) {
+            s = java.util.Arrays.toString((short[]) arr);
+          } else if (arr instanceof byte[]) {
+            s = java.util.Arrays.toString((byte[]) arr);
+          } else if (arr instanceof float[]) {
+            s = java.util.Arrays.toString((float[]) arr);
+          } else if (arr instanceof double[]) {
+            s = java.util.Arrays.toString((double[]) arr);
+          } else if (arr instanceof boolean[]) {
+            s = java.util.Arrays.toString((boolean[]) arr);
+          } else {
+            // Fallback for unexpected array types
+            s = arr.toString();
+          }
+          sb.append(s);
+        } catch (RuntimeException ignore) {
+          sb.append("<invalid array>");
+        }
+        return;
+      }
       default:
         throw new RuntimeException("unreachable");
     }
@@ -1558,7 +2229,8 @@ public class PartialRow {
       }
       case VARCHAR:
       case STRING:
-      case BINARY: {
+      case BINARY:
+      case /*ARRAY*/ NESTED: {
         addVarLengthData(index, value);
         break;
       }
diff --git 
a/java/kudu-client/src/test/java/org/apache/kudu/client/TestArraySerdes.java 
b/java/kudu-client/src/test/java/org/apache/kudu/client/TestArraySerdes.java
index 2cbaa285c..da31c3b47 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestArraySerdes.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestArraySerdes.java
@@ -306,4 +306,109 @@ public class TestArraySerdes {
     assertNotNull(res.getValidity());
     assertEquals(0, res.getValidity().length);
   }
+
+  // ----------------------------
+  // Empty validity vector tests
+  // ----------------------------
+  @Test
+  public void testEmptyValidityInt32AllValid() {
+    int[] vals = {10, 20, 30};
+    boolean[] validity = new boolean[0];
+
+    byte[] buf = Array1dSerdes.serializeInt32(vals, validity);
+    Array1dSerdes.ArrayResult res = Array1dSerdes.parseInt32(buf);
+
+    assertArrayEquals(vals, (int[]) res.getValues());
+    assertArrayEquals(new boolean[]{true, true, true}, res.getValidity());
+  }
+
+  @Test
+  public void testEmptyValidityStringAllValid() {
+    String[] vals = {"a", "b", "c"};
+    boolean[] validity = new boolean[0];
+
+    byte[] buf = Array1dSerdes.serializeString(vals, validity);
+    Array1dSerdes.ArrayResult res = Array1dSerdes.parseString(buf);
+
+    assertArrayEquals(vals, (String[]) res.getValues());
+    assertArrayEquals(new boolean[]{true, true, true}, res.getValidity());
+  }
+
+  @Test
+  public void testEmptyValidityBinaryAllValid() {
+    byte[][] vals = {
+        new byte[]{1, 2},
+        new byte[]{3, 4}
+    };
+    boolean[] validity = new boolean[0];
+
+    byte[] buf = Array1dSerdes.serializeBinary(vals, validity);
+    Array1dSerdes.ArrayResult res = Array1dSerdes.parseBinary(buf);
+
+    byte[][] decoded = (byte[][]) res.getValues();
+    assertEquals(vals.length, decoded.length);
+    for (int i = 0; i < vals.length; i++) {
+      assertArrayEquals(vals[i], decoded[i]);
+    }
+    assertArrayEquals(new boolean[]{true, true}, res.getValidity());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEmptyValidityStringWithNullsRejected() {
+    String[] vals = {"a", null, "b"};
+    boolean[] validity = new boolean[0];
+    Array1dSerdes.serializeString(vals, validity);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testEmptyValidityBinaryWithNullsRejected() {
+    byte[][] vals = {
+        new byte[]{1, 2},
+        null,
+        new byte[]{3}
+    };
+    boolean[] validity = new boolean[0];
+    Array1dSerdes.serializeBinary(vals, validity);
+  }
+
+  // All true validity vector test
+  @Test
+  public void testAllTrueValiditySkipped_Int32() {
+    byte[] buf1 = Array1dSerdes.serializeInt32(new int[]{1, 2, 3}, null);
+    byte[] buf2 = Array1dSerdes.serializeInt32(new int[]{1, 2, 3},
+        new boolean[]{true, true, true});
+    byte[] buf3 = Array1dSerdes.serializeInt32(new int[]{1, 2, 3}, new 
boolean[0]);
+
+    assertArrayEquals(buf1, buf2);
+    assertArrayEquals(buf1, buf3);
+  }
+
+  @Test
+  public void testAllTrueValiditySkipped_String() {
+    String[] vals = {"a", "b", "c"};
+    byte[] buf1 = Array1dSerdes.serializeString(vals, null);
+    byte[] buf2 = Array1dSerdes.serializeString(vals,
+        new boolean[]{true, true, true});
+    byte[] buf3 = Array1dSerdes.serializeString(vals, new boolean[0]);
+
+    assertArrayEquals(buf1, buf2);
+    assertArrayEquals(buf1, buf3);
+  }
+
+  @Test
+  public void testAllTrueValiditySkipped_Binary() {
+    byte[][] vals = {
+        new byte[]{1, 2},
+        new byte[]{3, 4}
+    };
+    byte[] buf1 = Array1dSerdes.serializeBinary(vals, null);
+    byte[] buf2 = Array1dSerdes.serializeBinary(vals,
+        new boolean[]{true, true});
+    byte[] buf3 = Array1dSerdes.serializeBinary(vals, new boolean[0]);
+
+    assertArrayEquals(buf1, buf2);
+    assertArrayEquals(buf1, buf3);
+  }
+
+
 }
diff --git 
a/java/kudu-client/src/test/java/org/apache/kudu/client/TestPartialRow.java 
b/java/kudu-client/src/test/java/org/apache/kudu/client/TestPartialRow.java
index df8dd4e70..48ccae9d9 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestPartialRow.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestPartialRow.java
@@ -27,6 +27,7 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.sql.Date;
 import java.sql.Timestamp;
@@ -37,6 +38,7 @@ import org.junit.Rule;
 import org.junit.Test;
 
 import org.apache.kudu.ColumnSchema;
+import org.apache.kudu.ColumnTypeAttributes;
 import org.apache.kudu.Schema;
 import org.apache.kudu.Type;
 import org.apache.kudu.test.junit.RetryRule;
@@ -142,6 +144,347 @@ public class TestPartialRow {
     }
   }
 
+  @Test
+  public void testAddAndGetInt8Array() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("ints", 
Type.INT8).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    byte[] vals = {42, -5, 100};
+    boolean[] validity = {true, true, true};
+    row.addArrayInt8("ints", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("ints");
+    assertEquals(3, view.length());
+    assertEquals(42, view.getInt8(0));
+    assertEquals(-5, view.getInt8(1));
+    assertEquals(100, view.getInt8(2));
+  }
+
+  @Test
+  public void testAddAndGetInt16Array() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("ints16", 
Type.INT16).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    short[] vals = {123, -456, 789};
+    boolean[] validity = {true, true, true};
+    row.addArrayInt16("ints16", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("ints16");
+    assertEquals(3, view.length());
+    assertEquals((short) 123, view.getInt16(0));
+    assertEquals((short) -456, view.getInt16(1));
+    assertEquals((short) 789, view.getInt16(2));
+  }
+
+  @Test
+  public void testAddAndGetInt32Array() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("ints32", 
Type.INT32).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    int[] vals = {1, -2, 3};
+    boolean[] validity = {true, true, true};
+    row.addArrayInt32("ints32", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("ints32");
+    assertEquals(3, view.length());
+    assertEquals(1, view.getInt32(0));
+    assertEquals(-2, view.getInt32(1));
+    assertEquals(3, view.getInt32(2));
+  }
+
+  @Test
+  public void testAddAndGetInt64Array() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("ints64", 
Type.INT64).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    long[] vals = {1L, -2L, 3L};
+    boolean[] validity = {true, true, true};
+    row.addArrayInt64("ints64", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("ints64");
+    assertEquals(3, view.length());
+    assertEquals(1L, view.getInt64(0));
+    assertEquals(-2L, view.getInt64(1));
+    assertEquals(3L, view.getInt64(2));
+  }
+
+  @Test
+  public void testAddAndGetFloatArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("floats", 
Type.FLOAT).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    float[] vals = {1.5f, 2.5f};
+    boolean[] validity = {true, true};
+    row.addArrayFloat("floats", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("floats");
+    assertEquals(2, view.length());
+    assertEquals(1.5f, view.getFloat(0), 0.0f);
+    assertEquals(2.5f, view.getFloat(1), 0.0f);
+  }
+
+  @Test
+  public void testAddAndGetDoubleArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("doubles", 
Type.DOUBLE).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    double[] vals = {1.1, 2.2};
+    boolean[] validity = {true, true};
+    row.addArrayDouble("doubles", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("doubles");
+    assertEquals(2, view.length());
+    assertEquals(1.1, view.getDouble(0), 0.0);
+    assertEquals(2.2, view.getDouble(1), 0.0);
+  }
+
+  @Test
+  public void testAddAndGetStringArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("strings", 
Type.STRING).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    String[] vals = {"foo", null, "bar"};
+    boolean[] validity = {true, false, true};
+    row.addArrayString("strings", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("strings");
+    assertEquals(3, view.length());
+    assertEquals("foo", view.getString(0));
+
+    assertFalse(view.isValid(1));
+    assertNull(view.getString(1));
+    assertEquals("bar", view.getString(2));
+  }
+
+  @Test
+  public void testAddAndGetBinaryArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("binaries", 
Type.BINARY).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    byte[][] vals = { {1,2}, {3,4,5} };
+    boolean[] validity = {true, true};
+    row.addArrayBinary("binaries", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("binaries");
+    assertEquals(2, view.length());
+    assertArrayEquals(new byte[]{1,2}, view.getBinary(0));
+    assertArrayEquals(new byte[]{3,4,5}, view.getBinary(1));
+  }
+
+  @Test
+  public void testAddAndGetBoolArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("bools", 
Type.BOOL).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    boolean[] vals = {true, false, true};
+    boolean[] validity = {true, true, true};
+    row.addArrayBool("bools", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("bools");
+    assertEquals(3, view.length());
+    assertTrue(view.getBoolean(0));
+    assertFalse(view.getBoolean(1));
+    assertTrue(view.getBoolean(2));
+  }
+
+  @Test
+  public void testAddAndGetDateArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("dates", 
Type.DATE).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    Date[] vals = { Date.valueOf("2020-01-01"), null, 
Date.valueOf("2020-01-03") };
+    row.addArrayDate("dates", vals);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("dates");
+    assertEquals(3, view.length());
+
+    int days0 = view.getInt32(0);
+    assertEquals(vals[0], DateUtil.epochDaysToSqlDate(days0));
+
+    assertFalse(view.isValid(1));
+
+    int days2 = view.getInt32(2);
+    assertEquals(vals[2], (DateUtil.epochDaysToSqlDate(days2)));
+  }
+
+  @Test
+  public void testAddAndGetTimestampArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("times", 
Type.UNIXTIME_MICROS).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    Timestamp[] vals = {
+        Timestamp.valueOf("2021-01-01 00:00:00"),
+        null,
+        Timestamp.valueOf("2021-01-01 12:34:56")
+    };
+    row.addArrayTimestamp("times", vals);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("times");
+    assertEquals(3, view.length());
+
+    long micros0 = view.getInt64(0);
+    long millis0 = micros0 / 1000;
+    int nanos0 = (int) (micros0 % 1_000_000) * 1000;
+    Timestamp ts0 = new Timestamp(millis0);
+    ts0.setNanos(ts0.getNanos() + nanos0);
+    assertEquals(vals[0], ts0);
+
+    assertFalse(view.isValid(1));
+
+    long micros2 = view.getInt64(2);
+    long millis2 = micros2 / 1000;
+    int nanos2 = (int) (micros2 % 1_000_000) * 1000;
+    Timestamp ts2 = new Timestamp(millis2);
+    ts2.setNanos(ts2.getNanos() + nanos2);
+    assertEquals(vals[2], ts2);
+  }
+
+  @Test
+  public void testAddAndGetVarcharArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("varchars", Type.VARCHAR)
+            .typeAttributes(new 
ColumnTypeAttributes.ColumnTypeAttributesBuilder()
+                .length(5).build())
+            .array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    String[] vals = {"abcdef", "xy", null};
+    row.addArrayVarchar("varchars", vals);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("varchars");
+    assertEquals(3, view.length());
+    assertEquals("abcde", view.getString(0));
+    assertEquals("xy", view.getString(1));
+    assertFalse(view.isValid(2));
+  }
+
+  @Test
+  public void testAddAndGetDecimalArray() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("decimals", Type.DECIMAL)
+            .typeAttributes(new 
ColumnTypeAttributes.ColumnTypeAttributesBuilder()
+                .precision(10).scale(2).build())
+            .array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    BigDecimal[] vals = { new BigDecimal("123.45"), null, new 
BigDecimal("67.89") };
+    boolean[] validity = {true, false, true};
+    row.addArrayDecimal("decimals", vals, validity);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("decimals");
+    assertEquals(3, view.length());
+
+    int scale = schema.getColumnByIndex(0).getTypeAttributes().getScale();
+
+    long unscaled0 = view.getInt64(0);
+    assertEquals(vals[0], new BigDecimal(BigInteger.valueOf(unscaled0), 
scale));
+    assertFalse(view.isValid(1));
+    long unscaled2 = view.getInt64(2);
+    assertEquals(vals[2], new BigDecimal(BigInteger.valueOf(unscaled2), 
scale));
+  }
+
+  @Test
+  public void testAddArrayInt32WithEmptyValidity() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("ints32", 
Type.INT32).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    int[] vals = {10, 20, 30};
+    boolean[] validity = new boolean[0];
+
+    row.addArrayInt32("ints32", vals, validity);
+
+    ArrayCellView view =  (ArrayCellView) row.getObject("ints32");
+    assertEquals(3, view.length());
+    for (int i = 0; i < view.length(); i++) {
+      assertTrue(view.isValid(i));
+      assertEquals(vals[i], view.getInt32(i));
+    }
+  }
+
+  @Test
+  public void testAddArrayStringWithEmptyValidityAllValid() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("strings", 
Type.STRING).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    String[] vals = {"a", "b", "c"};
+    row.addArrayString("strings", vals, new boolean[0]);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("strings");
+    assertEquals(3, view.length());
+    for (int i = 0; i < 3; i++) {
+      assertTrue(view.isValid(i));
+      assertEquals(vals[i], view.getString(i));
+    }
+  }
+
+
+  @Test
+  public void testAddArrayBinaryWithEmptyValidityAllValid() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("binaries", 
Type.BINARY).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    byte[][] vals = { {1,2}, {3,4} };
+    row.addArrayBinary("binaries", vals, new boolean[0]);
+
+    ArrayCellView view = (ArrayCellView) row.getObject("binaries");
+    assertEquals(2, view.length());
+    assertArrayEquals(vals[0], view.getBinary(0));
+    assertArrayEquals(vals[1], view.getBinary(1));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testAddArrayStringEmptyValidityWithNullsRejected() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("strings", 
Type.STRING).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    String[] vals = {"a", null, "b"};
+    row.addArrayString("strings", vals, new boolean[0]); // should throw
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testAddArrayBinaryEmptyValidityWithNullsRejected() {
+    Schema schema = new Schema(Arrays.asList(
+        new ColumnSchema.ColumnSchemaBuilder("binaries", 
Type.BINARY).array(true).build()
+    ));
+    PartialRow row = schema.newPartialRow();
+
+    byte[][] vals = { {1,2}, null, {3} };
+    row.addArrayBinary("binaries", vals, new boolean[0]); // should throw
+  }
+
+
   @Test(expected = IllegalArgumentException.class)
   public void testGetNullColumn() {
     PartialRow partialRow = getPartialRowWithAllTypes();

Reply via email to