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

cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 544704c87b86 [SPARK-57929][CORE][SQL] Support Binary/List view types 
in ArrowColumnVector
544704c87b86 is described below

commit 544704c87b86d16bade9b1f0acaa55b3578f8fcb
Author: Robert Kruszewski <[email protected]>
AuthorDate: Thu Jul 16 22:15:25 2026 +0800

    [SPARK-57929][CORE][SQL] Support Binary/List view types in ArrowColumnVector
    
    ### What changes were proposed in this pull request?
    Add support for constructing ArrowColumnVector from String/BinaryView and 
ListView
    
    ### Why are the changes needed?
    Arrow in spec 1.4 has added these as more efficient way to represent list 
and string types in memory
    
    ### Does this PR introduce _any_ user-facing change?
    No, this is internal developer api
    
    ### How was this patch tested?
    Added Tests
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes, this pr was authored with use of Fable 5
    
    Closes #57210 from robert3005/arrow-view-column-vectors.
    
    Authored-by: Robert Kruszewski <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../org/apache/spark/sql/util/ArrowUtils.scala     |   4 +-
 .../spark/sql/vectorized/ArrowColumnVector.java    |  63 +++++++--
 .../spark/sql/execution/arrow/ArrowWriter.scala    |   6 +
 .../connect/client/arrow/ArrowEncoderSuite.scala   |  54 +++++++-
 .../connect/client/arrow/ArrowVectorReader.scala   |  13 ++
 .../sql/execution/arrow/ArrowFileReadWrite.scala   |  30 ++++-
 .../execution/arrow/ArrowFileReadWriteSuite.scala  |  40 +++++-
 .../sql/execution/arrow/ArrowWriterSuite.scala     |  28 +++-
 .../sql/vectorized/ArrowColumnVectorSuite.scala    | 145 +++++++++++++++++++++
 9 files changed, 363 insertions(+), 20 deletions(-)

diff --git a/sql/api/src/main/scala/org/apache/spark/sql/util/ArrowUtils.scala 
b/sql/api/src/main/scala/org/apache/spark/sql/util/ArrowUtils.scala
index e69a0fa7f415..b8dab164cf78 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/util/ArrowUtils.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/util/ArrowUtils.scala
@@ -94,6 +94,8 @@ private[sql] object ArrowUtils {
     case ArrowType.Binary.INSTANCE => BinaryType
     case ArrowType.LargeUtf8.INSTANCE => StringType
     case ArrowType.LargeBinary.INSTANCE => BinaryType
+    case ArrowType.Utf8View.INSTANCE => StringType
+    case ArrowType.BinaryView.INSTANCE => BinaryType
     case d: ArrowType.Decimal => DecimalType(d.getPrecision, d.getScale)
     case date: ArrowType.Date if date.getUnit == DateUnit.DAY => DateType
     case ts: ArrowType.Timestamp
@@ -516,7 +518,7 @@ private[sql] object ArrowUtils {
         val keyType = fromArrowField(elementField.getChildren.get(0))
         val valueType = fromArrowField(elementField.getChildren.get(1))
         MapType(keyType, valueType, elementField.getChildren.get(1).isNullable)
-      case ArrowType.List.INSTANCE =>
+      case ArrowType.List.INSTANCE | ArrowType.ListView.INSTANCE =>
         val elementField = field.getChildren().get(0)
         val elementType = fromArrowField(elementField)
         ArrayType(elementType, containsNull = elementField.isNullable)
diff --git 
a/sql/catalyst/src/main/java/org/apache/spark/sql/vectorized/ArrowColumnVector.java
 
b/sql/catalyst/src/main/java/org/apache/spark/sql/vectorized/ArrowColumnVector.java
index f44de5ffa9df..d6e2f3f2c12e 100644
--- 
a/sql/catalyst/src/main/java/org/apache/spark/sql/vectorized/ArrowColumnVector.java
+++ 
b/sql/catalyst/src/main/java/org/apache/spark/sql/vectorized/ArrowColumnVector.java
@@ -17,6 +17,7 @@
 
 package org.apache.spark.sql.vectorized;
 
+import org.apache.arrow.memory.ArrowBuf;
 import org.apache.arrow.vector.*;
 import org.apache.arrow.vector.complex.*;
 import org.apache.arrow.vector.holders.NullableIntervalMonthDayNanoHolder;
@@ -209,7 +210,11 @@ public class ArrowColumnVector extends ColumnVector {
     } else if (vector instanceof VarBinaryVector varBinaryVector) {
       accessor = new BinaryAccessor(varBinaryVector);
     } else if (vector instanceof LargeVarBinaryVector largeVarBinaryVector) {
-      accessor = new LargeBinaryAccessor(largeVarBinaryVector);
+      accessor = new BinaryAccessor(largeVarBinaryVector);
+    } else if (vector instanceof ViewVarCharVector viewVarCharVector) {
+      accessor = new StringViewAccessor(viewVarCharVector);
+    } else if (vector instanceof ViewVarBinaryVector viewVarBinaryVector) {
+      accessor = new BinaryAccessor(viewVarBinaryVector);
     } else if (vector instanceof DateDayVector dateDayVector) {
       accessor = new DateAccessor(dateDayVector);
     } else if (vector instanceof TimeStampMicroTZVector 
timeStampMicroTZVector) {
@@ -225,7 +230,9 @@ public class ArrowColumnVector extends ColumnVector {
     } else if (vector instanceof MapVector mapVector) {
       accessor = new MapAccessor(mapVector);
     } else if (vector instanceof ListVector listVector) {
-      accessor = new ArrayAccessor(listVector);
+      accessor = new ArrayAccessor<>(listVector);
+    } else if (vector instanceof ListViewVector listViewVector) {
+      accessor = new ArrayAccessor<>(listViewVector);
     } else if (vector instanceof StructVector structVector) {
       if (ArrowUtils.isTimestampNanosStructField(structVector.getField())) {
         // Lossless struct representation of a nanosecond timestamp 
(ArrowUtils.toArrowField with
@@ -500,33 +507,59 @@ public class ArrowColumnVector extends ColumnVector {
     }
   }
 
+  // Covers VarBinaryVector, LargeVarBinaryVector and ViewVarBinaryVector: all 
of them return
+  // the value as a byte array from getObject, with a null check.
   static class BinaryAccessor extends ArrowVectorAccessor {
 
-    private final VarBinaryVector accessor;
+    private final VariableWidthFieldVector accessor;
 
-    BinaryAccessor(VarBinaryVector vector) {
+    BinaryAccessor(VariableWidthFieldVector vector) {
       super(vector);
       this.accessor = vector;
     }
 
     @Override
     final byte[] getBinary(int rowId) {
-      return accessor.getObject(rowId);
+      return (byte[]) accessor.getObject(rowId);
     }
   }
 
-  static class LargeBinaryAccessor extends ArrowVectorAccessor {
+  static class StringViewAccessor extends ArrowVectorAccessor {
 
-    private final LargeVarBinaryVector accessor;
+    private final ViewVarCharVector accessor;
 
-    LargeBinaryAccessor(LargeVarBinaryVector vector) {
+    StringViewAccessor(ViewVarCharVector vector) {
       super(vector);
       this.accessor = vector;
     }
 
     @Override
-    final byte[] getBinary(int rowId) {
-      return accessor.getObject(rowId);
+    final UTF8String getUTF8String(int rowId) {
+      if (accessor.isNull(rowId)) {
+        return null;
+      }
+      // Decode the 16-byte view struct directly rather than through Arrow's
+      // NullableViewVarCharHolder: the holder's int start/end fields truncate 
the inline-value
+      // offset (rowId * 16 + 4) once the views buffer grows past 
Integer.MAX_VALUE bytes, which
+      // would silently read the wrong memory. Both branches read the value 
with zero copy, like
+      // the non-view string accessors.
+      ArrowBuf views = accessor.getDataBuffer();
+      long viewOffset = (long) rowId * 
BaseVariableWidthViewVector.ELEMENT_SIZE;
+      int length = views.getInt(viewOffset);
+      if (length <= BaseVariableWidthViewVector.INLINE_SIZE) {
+        // Short values are stored inline in the views buffer, right after the 
length.
+        long start = viewOffset + BaseVariableWidthViewVector.LENGTH_WIDTH;
+        return UTF8String.fromAddress(null, views.memoryAddress() + start, 
length);
+      } else {
+        // Long values live in one of the variadic data buffers; the view 
struct holds the buffer
+        // index and the offset within that buffer, after the length and a 
4-byte prefix.
+        long bufferIndexOffset = viewOffset + 
BaseVariableWidthViewVector.LENGTH_WIDTH
+          + BaseVariableWidthViewVector.PREFIX_WIDTH;
+        int bufferIndex = views.getInt(bufferIndexOffset);
+        int start = views.getInt(bufferIndexOffset + 
BaseVariableWidthViewVector.BUF_INDEX_WIDTH);
+        ArrowBuf dataBuffer = accessor.getDataBuffers().get(bufferIndex);
+        return UTF8String.fromAddress(null, dataBuffer.memoryAddress() + 
start, length);
+      }
     }
   }
 
@@ -679,12 +712,16 @@ public class ArrowColumnVector extends ColumnVector {
     }
   }
 
-  static class ArrayAccessor extends ArrowVectorAccessor {
+  // Covers ListVector and ListViewVector. ListView stores an explicit 
per-value offset and size
+  // (rather than ListVector's contiguous offsets), but both expose a value's 
element range in the
+  // data vector as [getElementStartIndex, getElementEndIndex).
+  static class ArrayAccessor<T extends BaseListVector & RepeatedValueVector>
+      extends ArrowVectorAccessor {
 
-    private final ListVector accessor;
+    private final T accessor;
     private final ArrowColumnVector arrayData;
 
-    ArrayAccessor(ListVector vector) {
+    ArrayAccessor(T vector) {
       super(vector);
       this.accessor = vector;
       this.arrayData = new ArrowColumnVector(vector.getDataVector());
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowWriter.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowWriter.scala
index b96e57ce49af..f4c3bc08f403 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowWriter.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowWriter.scala
@@ -131,6 +131,12 @@ object ArrowWriter {
           createFieldWriter(vector.getChildByOrdinal(ordinal))
         }
         new GeographyWriter(dt, vector, children.toArray)
+      // The Arrow view types are readable through ArrowColumnVector but have 
no field writers.
+      // Their Spark types resolve to the non-view vector cases above, so 
without this case they
+      // would fall through to UNSUPPORTED_DATATYPE naming a fully supported 
Spark type; report
+      // the offending Arrow type instead.
+      case (_, vector @ (_: ViewVarCharVector | _: ViewVarBinaryVector | _: 
ListViewVector)) =>
+        throw 
ExecutionErrors.unsupportedArrowTypeError(vector.getField.getType)
       case (dt, _) =>
         throw ExecutionErrors.unsupportedDataTypeError(dt)
     }
diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
index 9f171cda2ea2..efb6a7a2304f 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
@@ -16,7 +16,7 @@
  */
 package org.apache.spark.sql.connect.client.arrow
 
-import java.io.File
+import java.io.{ByteArrayOutputStream, File}
 import java.math.BigInteger
 import java.net.URLClassLoader
 import java.time.{Duration, Period, ZoneOffset}
@@ -32,7 +32,8 @@ import scala.reflect.classTag
 import scala.reflect.runtime.{universe => ru}
 
 import org.apache.arrow.memory.{BufferAllocator, RootAllocator}
-import org.apache.arrow.vector.VarBinaryVector
+import org.apache.arrow.vector.{BaseVariableWidthViewVector, FieldVector, 
VarBinaryVector, VectorSchemaRoot, ViewVarBinaryVector, ViewVarCharVector}
+import org.apache.arrow.vector.ipc.ArrowStreamWriter
 
 import org.apache.spark.{SparkRuntimeException, 
SparkUnsupportedOperationException}
 import org.apache.spark.sql.{Encoders, Row}
@@ -259,6 +260,55 @@ class ArrowEncoderSuite extends ConnectFunSuite {
     }
   }
 
+  test("deserializing string and binary view vectors") {
+    // The client never produces view-encoded batches itself, but it can 
receive them, so the
+    // readers must handle them. Mix short (inline, <= 12 bytes) and long 
(stored in a data
+    // buffer) values to exercise both view-storage paths.
+    val values = Seq("a", "a-string-longer-than-twelve-bytes", null)
+
+    def serializeViewVector(vector: BaseVariableWidthViewVector): Array[Byte] 
= {
+      vector.allocateNew()
+      values.zipWithIndex.foreach {
+        case (null, i) => vector.setNull(i)
+        case (s, i) =>
+          val bytes = s.getBytes("utf8")
+          vector.setSafe(i, bytes, 0, bytes.length)
+      }
+      vector.setValueCount(values.size)
+      val root = new 
VectorSchemaRoot(Collections.singletonList[FieldVector](vector))
+      try {
+        val out = new ByteArrayOutputStream()
+        val writer = new ArrowStreamWriter(root, null, out)
+        writer.start()
+        writer.writeBatch()
+        writer.end()
+        out.toByteArray
+      } finally {
+        root.close()
+      }
+    }
+
+    withAllocator { allocator =>
+      val strings = ArrowDeserializers.deserializeFromArrow(
+        Iterator.single(serializeViewVector(new ViewVarCharVector("s", 
allocator))),
+        StringEncoder,
+        allocator,
+        timeZoneId = "UTC")
+      compareIterators(values.iterator, strings)
+      strings.close()
+
+      val binaries = ArrowDeserializers.deserializeFromArrow(
+        Iterator.single(serializeViewVector(new ViewVarBinaryVector("b", 
allocator))),
+        BinaryEncoder,
+        allocator,
+        timeZoneId = "UTC")
+      compareIterators(
+        values.iterator.map(Option(_).map(_.getBytes("utf8").toSeq)),
+        binaries.map(Option(_).map(_.toSeq)))
+      binaries.close()
+    }
+  }
+
   test("single batch") {
     val inspector = new CountingBatchInspector
     roundTripAndCheckIdentical(singleIntEncoder, inspectBatch = inspector) { 
() =>
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
index 54311cecc162..1f71ed8f8432 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
@@ -95,8 +95,10 @@ object ArrowVectorReader {
       case v: DecimalVector => new DecimalVectorReader(v)
       case v: VarCharVector => new VarCharVectorReader(v)
       case v: LargeVarCharVector => new LargeVarCharVectorReader(v)
+      case v: ViewVarCharVector => new ViewVarCharVectorReader(v)
       case v: VarBinaryVector => new VarBinaryVectorReader(v)
       case v: LargeVarBinaryVector => new LargeVarBinaryVectorReader(v)
+      case v: ViewVarBinaryVector => new ViewVarBinaryVectorReader(v)
       case v: DurationVector => new DurationVectorReader(v)
       case v: IntervalYearVector => new IntervalYearVectorReader(v)
       case v: DateDayVector => new DateDayVectorReader(v, timeZoneId)
@@ -215,6 +217,11 @@ private[arrow] class LargeVarCharVectorReader(v: 
LargeVarCharVector)
   override def getString(i: Int): String = Text.decode(vector.get(i))
 }
 
+private[arrow] class ViewVarCharVectorReader(v: ViewVarCharVector)
+    extends TypedArrowVectorReader[ViewVarCharVector](v) {
+  override def getString(i: Int): String = Text.decode(vector.get(i))
+}
+
 private[arrow] class VarBinaryVectorReader(v: VarBinaryVector)
     extends TypedArrowVectorReader[VarBinaryVector](v) {
   override def getBytes(i: Int): Array[Byte] = vector.get(i)
@@ -227,6 +234,12 @@ private[arrow] class LargeVarBinaryVectorReader(v: 
LargeVarBinaryVector)
   override def getString(i: Int): String = 
SparkStringUtils.getHexString(getBytes(i))
 }
 
+private[arrow] class ViewVarBinaryVectorReader(v: ViewVarBinaryVector)
+    extends TypedArrowVectorReader[ViewVarBinaryVector](v) {
+  override def getBytes(i: Int): Array[Byte] = vector.get(i)
+  override def getString(i: Int): String = 
SparkStringUtils.getHexString(getBytes(i))
+}
+
 private[arrow] class DurationVectorReader(v: DurationVector)
     extends TypedArrowVectorReader[DurationVector](v) {
   override def getDuration(i: Int): Duration = vector.getObject(i)
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWrite.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWrite.scala
index e7ec2d2b7984..516b01b25fdc 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWrite.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWrite.scala
@@ -24,9 +24,10 @@ import scala.jdk.CollectionConverters._
 
 import org.apache.arrow.vector._
 import org.apache.arrow.vector.ipc.{ArrowFileReader, ArrowFileWriter}
-import org.apache.arrow.vector.types.pojo.Schema
+import org.apache.arrow.vector.types.pojo.{ArrowType, Field, Schema}
 
 import org.apache.spark.sql.classic.{DataFrame, SparkSession}
+import org.apache.spark.sql.errors.ExecutionErrors
 import org.apache.spark.sql.util.ArrowUtils
 
 private[sql] class SparkArrowFileWriter(schema: Schema, path: Path) extends 
AutoCloseable {
@@ -97,6 +98,33 @@ private[spark] object ArrowFileReadWrite {
   def load(spark: SparkSession, path: Path): DataFrame = {
     val reader = new SparkArrowFileReader(path)
     val schema = ArrowUtils.fromArrowSchema(reader.schema)
+    // `toDataFrame` rebuilds vectors from `schema` via `toArrowSchema` and 
loads the file's raw
+    // record batches into them, so the file's physical layout must match 
Spark's canonical Arrow
+    // encoding of that schema. An Arrow type that converts to a Spark type 
but is encoded
+    // differently (e.g. Utf8View or LargeUtf8 vs Utf8) would have its buffers 
reinterpreted as
+    // the wrong layout and read back as corrupt values, so reject it up front.
+    val canonicalSchema = ArrowUtils.toArrowSchema(
+      schema, "UTC", errorOnDuplicatedFieldNames = true, largeVarTypes = false)
+    
reader.schema.getFields.asScala.zip(canonicalSchema.getFields.asScala).foreach {
+      case (actual, canonical) => checkLayoutMatch(actual, canonical)
+    }
     ArrowConverters.toDataFrame(reader.read(), schema, spark, "UTC", true, 
false)
   }
+
+  private def checkLayoutMatch(actual: Field, canonical: Field): Unit = {
+    val compatible = (actual.getType, canonical.getType) match {
+      // The timezone label does not affect the physical encoding (values are 
epoch-based), and
+      // `fromArrowSchema` already rejects the units Spark cannot read.
+      case (a: ArrowType.Timestamp, c: ArrowType.Timestamp) => a.getUnit == 
c.getUnit
+      // Map equality includes the keysSorted flag, which has no layout impact.
+      case (_: ArrowType.Map, _: ArrowType.Map) => true
+      case (a, c) => a == c
+    }
+    if (!compatible) {
+      throw ExecutionErrors.unsupportedArrowTypeError(actual.getType)
+    }
+    actual.getChildren.asScala.zip(canonical.getChildren.asScala).foreach {
+      case (a, c) => checkLayoutMatch(a, c)
+    }
+  }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWriteSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWriteSuite.scala
index 8accda825403..dda9827e80a6 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWriteSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowFileReadWriteSuite.scala
@@ -17,9 +17,19 @@
 package org.apache.spark.sql.execution.arrow
 
 import java.io.File
+import java.nio.channels.Channels
+import java.nio.file.Files
 
+import scala.jdk.CollectionConverters._
+
+import org.apache.arrow.vector.{FieldVector, VectorSchemaRoot, 
ViewVarCharVector}
+import org.apache.arrow.vector.ipc.ArrowFileWriter
+import org.apache.arrow.vector.types.pojo.ArrowType
+
+import org.apache.spark.SparkUnsupportedOperationException
 import org.apache.spark.sql.functions._
 import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.util.ArrowUtils
 import org.apache.spark.util.Utils
 
 class ArrowFileReadWriteSuite extends SharedSparkSession {
@@ -38,7 +48,8 @@ class ArrowFileReadWriteSuite extends SharedSparkSession {
       lit(2L).alias("long"),
       lit(3.0).alias("double"),
       lit("a string").alias("str"),
-      lit(Array(1.0, 2.0, Double.NaN, Double.NegativeInfinity)).alias("arr"))
+      lit(Array(1.0, 2.0, Double.NaN, Double.NegativeInfinity)).alias("arr"),
+      col("id").cast("timestamp").alias("ts"))
 
     val path = new File(tempDataPath, "simple.arrowfile").toPath
     ArrowFileReadWrite.save(df, path)
@@ -57,4 +68,31 @@ class ArrowFileReadWriteSuite extends SharedSparkSession {
     val df2 = ArrowFileReadWrite.load(spark, path)
     checkAnswer(df, df2)
   }
+
+  test("loading a file whose layout differs from the canonical Arrow encoding 
fails fast") {
+    // `load` rebuilds vectors from the Spark schema, so an Arrow type that 
converts to a Spark
+    // type but is encoded differently (here Utf8View vs Utf8) cannot be 
loaded; it must be
+    // rejected at the schema check instead of having its buffers misread as 
garbage values.
+    val allocator = 
ArrowUtils.rootAllocator.newChildAllocator("stringViewFile", 0, Long.MaxValue)
+    val vector = new ViewVarCharVector("v", allocator)
+    vector.allocateNew()
+    val bytes = "a-string-longer-than-twelve-bytes".getBytes("utf8")
+    vector.setSafe(0, bytes, 0, bytes.length)
+    vector.setValueCount(1)
+    val root = new VectorSchemaRoot(Seq[FieldVector](vector).asJava)
+    val path = new File(tempDataPath, "stringview.arrowfile").toPath
+    val writer = new ArrowFileWriter(root, null, 
Channels.newChannel(Files.newOutputStream(path)))
+    writer.start()
+    writer.writeBatch()
+    writer.close()
+    root.close()
+    allocator.close()
+
+    checkError(
+      exception = intercept[SparkUnsupportedOperationException] {
+        ArrowFileReadWrite.load(spark, path)
+      },
+      condition = "UNSUPPORTED_ARROWTYPE",
+      parameters = Map("typeName" -> ArrowType.Utf8View.INSTANCE.toString))
+  }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowWriterSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowWriterSuite.scala
index 2584193008ff..6e8e2deeba67 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowWriterSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/arrow/ArrowWriterSuite.scala
@@ -19,9 +19,11 @@ package org.apache.spark.sql.execution.arrow
 
 import scala.jdk.CollectionConverters._
 
-import org.apache.arrow.vector.VectorSchemaRoot
+import org.apache.arrow.vector.{VectorSchemaRoot, ViewVarBinaryVector, 
ViewVarCharVector}
+import org.apache.arrow.vector.complex.ListViewVector
+import org.apache.arrow.vector.types.pojo.{ArrowType, FieldType}
 
-import org.apache.spark.{SparkArithmeticException, SparkFunSuite}
+import org.apache.spark.{SparkArithmeticException, SparkFunSuite, 
SparkUnsupportedOperationException}
 import org.apache.spark.sql.Row
 import org.apache.spark.sql.YearUDT
 import org.apache.spark.sql.catalyst.InternalRow
@@ -153,6 +155,28 @@ class ArrowWriterSuite extends SparkFunSuite {
     check(new YearUDT, Seq(2020, 2021, null, 2022))
   }
 
+  test("view vectors are rejected with UNSUPPORTED_ARROWTYPE") {
+    // The view types are readable through ArrowColumnVector but have no field 
writers; the error
+    // must name the Arrow type, not the Spark type its schema maps to.
+    val allocator = ArrowUtils.rootAllocator.newChildAllocator("view", 0, 
Long.MaxValue)
+    val listView = ListViewVector.empty("arr", allocator)
+    listView.addOrGetVector(FieldType.nullable(new ArrowType.Int(8 * 4, true)))
+    val vectors = Seq(
+      new ViewVarCharVector("str", allocator),
+      new ViewVarBinaryVector("bin", allocator),
+      listView)
+    vectors.foreach { vector =>
+      checkError(
+        exception = intercept[SparkUnsupportedOperationException] {
+          ArrowWriter.createFieldWriter(vector)
+        },
+        condition = "UNSUPPORTED_ARROWTYPE",
+        parameters = Map("typeName" -> vector.getField.getType.toString))
+      vector.close()
+    }
+    allocator.close()
+  }
+
   test("timestamp nanos round-trip") {
     // Decompose an int64 epoch-nanoseconds value into the (epochMicros, 
nanosWithinMicro) pair,
     // matching how the Arrow reader reconstructs it.
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/vectorized/ArrowColumnVectorSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/vectorized/ArrowColumnVectorSuite.scala
index 9180ce1aee19..ea6bc4a78f02 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/vectorized/ArrowColumnVectorSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/vectorized/ArrowColumnVectorSuite.scala
@@ -19,6 +19,7 @@ package org.apache.spark.sql.vectorized
 
 import org.apache.arrow.vector._
 import org.apache.arrow.vector.complex._
+import org.apache.arrow.vector.types.pojo.{ArrowType, FieldType}
 
 import org.apache.spark.SparkFunSuite
 import org.apache.spark.sql.types._
@@ -331,6 +332,96 @@ class ArrowColumnVectorSuite extends SparkFunSuite {
     allocator.close()
   }
 
+  test("string_view") {
+    val allocator = ArrowUtils.rootAllocator.newChildAllocator("string_view", 
0, Long.MaxValue)
+    val vector = new ViewVarCharVector("stringView", allocator)
+    vector.allocateNew()
+
+    // Mix short (inline, <= 12 bytes) and long (stored in a data buffer, > 12 
bytes) values to
+    // exercise both view-storage paths.
+    val values = (0 until 10).map { i =>
+      if (i % 2 == 0) s"str$i" else s"a-long-string-value-$i"
+    }
+    values.zipWithIndex.foreach { case (s, i) =>
+      val utf8 = s.getBytes("utf8")
+      vector.setSafe(i, utf8, 0, utf8.length)
+    }
+    vector.setNull(10)
+    vector.setValueCount(11)
+
+    val columnVector = new ArrowColumnVector(vector)
+    assert(columnVector.dataType === StringType)
+    assert(columnVector.hasNull)
+    assert(columnVector.numNulls === 1)
+
+    values.zipWithIndex.foreach { case (s, i) =>
+      assert(columnVector.getUTF8String(i) === UTF8String.fromString(s))
+    }
+    assert(columnVector.isNullAt(10))
+
+    columnVector.close()
+    allocator.close()
+  }
+
+  test("binary_view") {
+    val allocator = ArrowUtils.rootAllocator.newChildAllocator("binary_view", 
0, Long.MaxValue)
+    val vector = new ViewVarBinaryVector("binaryView", allocator)
+    vector.allocateNew()
+
+    // Mix short (inline, <= 12 bytes) and long (stored in a data buffer, > 12 
bytes) values to
+    // exercise both view-storage paths.
+    val values = (0 until 10).map { i =>
+      if (i % 2 == 0) s"str$i" else s"a-long-binary-value-$i"
+    }
+    values.zipWithIndex.foreach { case (s, i) =>
+      val utf8 = s.getBytes("utf8")
+      vector.setSafe(i, utf8, 0, utf8.length)
+    }
+    vector.setNull(10)
+    vector.setValueCount(11)
+
+    val columnVector = new ArrowColumnVector(vector)
+    assert(columnVector.dataType === BinaryType)
+    assert(columnVector.hasNull)
+    assert(columnVector.numNulls === 1)
+
+    values.zipWithIndex.foreach { case (s, i) =>
+      assert(columnVector.getBinary(i) === s.getBytes("utf8"))
+    }
+    assert(columnVector.isNullAt(10))
+
+    columnVector.close()
+    allocator.close()
+  }
+
+  test("string_view with multiple data buffers") {
+    val allocator = ArrowUtils.rootAllocator.newChildAllocator("string_view", 
0, Long.MaxValue)
+    val vector = new ViewVarCharVector("stringView", allocator)
+    // Keep the variadic data buffers small (16 * 8 = 128 bytes each) so the 
long values below
+    // spill into multiple buffers, exercising the non-zero buffer-index 
branch of the accessor.
+    vector.setInitialCapacity(16, 8)
+    vector.allocateNew()
+
+    val values = (0 until 16).map(i => s"a-long-string-value-spilling-over-$i")
+    values.zipWithIndex.foreach { case (s, i) =>
+      val utf8 = s.getBytes("utf8")
+      vector.setSafe(i, utf8, 0, utf8.length)
+    }
+    vector.setValueCount(16)
+    // The values must not fit in a single data buffer, otherwise this test 
exercises nothing
+    // beyond the plain string_view test.
+    assert(vector.getDataBuffers.size() > 1)
+
+    val columnVector = new ArrowColumnVector(vector)
+    assert(columnVector.dataType === StringType)
+    values.zipWithIndex.foreach { case (s, i) =>
+      assert(columnVector.getUTF8String(i) === UTF8String.fromString(s))
+    }
+
+    columnVector.close()
+    allocator.close()
+  }
+
   test("array") {
     val allocator = ArrowUtils.rootAllocator.newChildAllocator("array", 0, 
Long.MaxValue)
     val vector = ArrowUtils.toArrowField("array", ArrayType(IntegerType), 
nullable = true, null)
@@ -385,6 +476,60 @@ class ArrowColumnVectorSuite extends SparkFunSuite {
     allocator.close()
   }
 
+  test("array_view") {
+    val allocator = ArrowUtils.rootAllocator.newChildAllocator("array_view", 
0, Long.MaxValue)
+    val vector = ListViewVector.empty("arrayView", allocator)
+    vector.addOrGetVector(FieldType.nullable(new ArrowType.Int(8 * 4, true)))
+    vector.allocateNew()
+    val elementVector = vector.getDataVector().asInstanceOf[IntVector]
+
+    // [1, 2]
+    vector.startNewValue(0)
+    elementVector.setSafe(0, 1)
+    elementVector.setSafe(1, 2)
+    vector.endValue(0, 2)
+
+    // [3, null, 5]
+    vector.startNewValue(1)
+    elementVector.setSafe(2, 3)
+    elementVector.setNull(3)
+    elementVector.setSafe(4, 5)
+    vector.endValue(1, 3)
+
+    // null
+
+    // []
+    vector.startNewValue(3)
+    vector.endValue(3, 0)
+
+    elementVector.setValueCount(5)
+    vector.setValueCount(4)
+
+    val columnVector = new ArrowColumnVector(vector)
+    assert(columnVector.dataType === ArrayType(IntegerType))
+    assert(columnVector.hasNull)
+    assert(columnVector.numNulls === 1)
+
+    val array0 = columnVector.getArray(0)
+    assert(array0.numElements() === 2)
+    assert(array0.getInt(0) === 1)
+    assert(array0.getInt(1) === 2)
+
+    val array1 = columnVector.getArray(1)
+    assert(array1.numElements() === 3)
+    assert(array1.getInt(0) === 3)
+    assert(array1.isNullAt(1))
+    assert(array1.getInt(2) === 5)
+
+    assert(columnVector.isNullAt(2))
+
+    val array3 = columnVector.getArray(3)
+    assert(array3.numElements() === 0)
+
+    columnVector.close()
+    allocator.close()
+  }
+
   test("non nullable struct") {
     val allocator = ArrowUtils.rootAllocator.newChildAllocator("struct", 0, 
Long.MaxValue)
     val schema = new StructType().add("int", IntegerType).add("long", LongType)


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

Reply via email to