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();