HyukjinKwon commented on code in PR #59933:
URL: https://github.com/apache/spark/pull/59933#discussion_r4417643695


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarWrite.scala:
##########
@@ -214,12 +227,119 @@ class ArrowBatchingDataWriter(delegate: 
DataWriter[ColumnarBatch], options: Arro
     }
   }
 
+  override def writeBatch(batch: ColumnarBatch): Unit = {
+    val numBatchRows = batch.numRows()
+    arrowVectors(batch) match {
+      case Some(vectors) =>
+        flush()
+        // The vectors of the batch, which this root does not own: their 
producer releases them.
+        val batchRoot =
+          new VectorSchemaRoot(vectors.map(_.getField).asJava, vectors.asJava, 
numBatchRows)
+        var start = 0
+        while (start < numBatchRows) {
+          // A slice shares the buffers of the batch. Arrow Java copies a 
vector of variable
+          // width, or a list, as if its offsets started at 0, which those of 
a slice do.
+          val slice = batchRoot.slice(
+            start, math.min(numBatchRows - start, options.maxRecordsPerBatch))
+          try {
+            val length = batchLength(slice)
+            if (length == slice.getRowCount) {
+              writeCopy(slice)
+            } else {
+              val head = slice.slice(0, length)
+              try writeCopy(head) finally head.close()
+            }
+            start += length
+          } finally {
+            slice.close()
+          }
+        }
+      case None =>
+        var i = 0
+        while (i < numBatchRows) {
+          write(batch.getRow(i))
+          i += 1
+        }
+    }
+  }
+
+  /**
+   * Returns the Arrow vectors of the columns of a batch, if its columns can 
be copied buffer by
+   * buffer: Arrow vectors with as many values as the batch has rows, whose 
fields are physically
+   * congruent with those of `arrowSchema`, though they may differ in the 
names, nullability and
+   * metadata of their fields, and in the time zones of timestamps, which the 
copy keeps.
+   */
+  private def arrowVectors(batch: ColumnarBatch): Option[Seq[FieldVector]] = {
+    val fields = arrowSchema.getFields.asScala
+    // Without columns, the number of rows of a batch is only kept by the 
conversion.
+    if (fields.isEmpty || batch.numCols() != fields.length) {
+      None
+    } else {
+      val vectors = fields.indices.map { i =>
+        batch.column(i) match {
+          case column: ArrowColumnVector => column.getValueVector match {
+            case vector: FieldVector if vector.getValueCount == 
batch.numRows() &&
+                ArrowUtils.isCompatibleWithDeclaredField(vector.getField, 
fields(i)) =>
+              Some(vector)
+            case _ => None
+          }
+          case _ => None
+        }
+      }
+      if (vectors.forall(_.isDefined)) Some(vectors.flatten) else None
+    }
+  }
+
+  /**
+   * Returns the number of rows of the batch that starts with a slice of the 
rows of a columnar
+   * batch: all of them, or as a batch of rows ends, up to the first row with 
which it holds
+   * `maxBytesPerBatch` bytes, by the same measure of its size.
+   */
+  private def batchLength(slice: VectorSchemaRoot): Int = {
+    val numSliceRows = slice.getRowCount
+    // The size is not computed when the limit is the largest possible one, as 
for rows.
+    if (options.maxBytesPerBatch >= Int.MaxValue) {
+      numSliceRows
+    } else {
+      def sizeInBytes(rows: Int): Long =
+        slice.getFieldVectors.asScala.map(_.getBufferSizeFor(rows).toLong).sum
+      // The size grows with the number of rows.
+      var lo = 1
+      var hi = numSliceRows
+      while (lo < hi) {
+        val mid = (lo + hi) >>> 1
+        if (sizeInBytes(mid) >= options.maxBytesPerBatch) hi = mid else lo = 
mid + 1
+      }
+      lo
+    }
+  }
+
+  /** Writes a slice of the Arrow vectors of a columnar batch, copied buffer 
by buffer. */
+  private def writeCopy(slice: VectorSchemaRoot): Unit = {
+    val length = slice.getRowCount
+    val copy = VectorSchemaRoot.create(slice.getSchema, allocator)
+    try {
+      copy.getFieldVectors.forEach(_.setInitialCapacity(length))

Review Comment:
   Done in 59c4691: the slice is unloaded with `VectorUnloader`, each buffer is 
copied once into a buffer of its size, and the copies are loaded with 
`VectorLoader`, instead of `VectorAppender`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarWrite.scala:
##########
@@ -214,12 +227,119 @@ class ArrowBatchingDataWriter(delegate: 
DataWriter[ColumnarBatch], options: Arro
     }
   }
 
+  override def writeBatch(batch: ColumnarBatch): Unit = {
+    val numBatchRows = batch.numRows()
+    arrowVectors(batch) match {
+      case Some(vectors) =>
+        flush()

Review Comment:
   Done in 59c4691: "write the pending rows of converted batches before a 
copied batch" passes an on-heap batch and then an Arrow batch to one 
`ArrowBatchingDataWriter`, and checks the order of the rows and the sizes of 
the batches. "write the columnar batches of a query" also checks that a write 
whose query has a shuffle is an adaptive plan with a `ColumnarToRow`.



##########
docs/sql-native-data-sources.md:
##########
@@ -37,7 +37,9 @@ package, and loads it when a query uses the data source. 
Spark adapts it to
 
 Data is exchanged only as columnar batches, through the
 [Arrow C data 
interface](https://arrow.apache.org/docs/format/CDataInterface.html), so Spark 
reads
-the data that a library returns without copying it.
+the data that a library returns without copying it. When the data to write is 
columnar too, such
+as the batches of a native scan, Spark writes them without converting them to 
rows: it copies
+their Arrow buffers, since a library may keep the data that it writes.

Review Comment:
   Done in 59c4691: the sentence now says that it applies when a write gets the 
batches of a scan as they are, as in a plain copy from one native data source 
to another, that columns of other Arrow types, such as `utf8_view` strings, are 
converted value by value, and that with adaptive query execution the data of a 
query with a shuffle or a subquery is still converted to rows.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarDataSource.scala:
##########
@@ -123,8 +135,10 @@ trait ColumnarStreamReader extends Serializable {
 /**
  * Writes data to a [[ColumnarDataSource]] for a batch write. Spark serializes 
the writer to the
  * executors, where `createWriter` is called for each task, and then commits 
or aborts the write
- * on the driver. The batches passed to the data writers are backed by Arrow, 
and are only valid
- * during the call.
+ * on the driver. The batches passed to the data writers are backed by new 
Arrow vectors, with the
+ * Arrow types of the schema, except for the time zones of timestamps, though 
not necessarily with
+ * its names and metadata. A batch is only valid during the call, but nothing 
else writes to the

Review Comment:
   Done in 59c4691: the contract now also mentions the `keysSorted` flag of 
maps, nullability, and the Spark types that `ArrowColumnVector` infers.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to