spark-test commented on code in PR #59933:
URL: https://github.com/apache/spark/pull/59933#discussion_r4468509210
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/WriteToDataSourceV2Exec.scala:
##########
@@ -873,6 +942,34 @@ object DataWritingSparkTask extends
WritingSparkTask[DataWriter[InternalRow]] {
writer: DataWriter[InternalRow], iter: java.util.Iterator[InternalRow]):
Unit = {
writer.writeAll(iter)
}
+
+ override def runColumnar(
+ writerFactory: DataWriterFactory,
+ context: TaskContext,
+ iter: Iterator[ColumnarBatch],
+ useCommitCoordinator: Boolean,
+ customMetrics: Map[String, SQLMetric]): DataWritingSparkTaskResult = {
+ val dataWriter = writerFactory.createWriter(context.partitionId(),
context.taskAttemptId())
+ writeAndCommit(dataWriter, context, useCommitCoordinator, customMetrics) {
+ val columnarWriter = dataWriter match {
+ case writer: ColumnarInputDataWriter => writer
+ case writer => throw SparkException.internalError(
+ s"Expected a data writer of columnar batches, but got
${writer.getClass.getName}.")
+ }
+ var numRows = 0L
+ iter.foreach { batch =>
+ // Releases the batch once it is written, as ColumnarToRowExec does
once it returned its
+ // rows: a producer of new column vectors for each batch leaves them
to the consumer.
+ Utils.tryWithSafeFinally {
+ columnarWriter.writeBatch(batch)
+ numRows += batch.numRows()
+ }(batch.closeIfFreeable())
Review Comment:
**Nit (P3):** Nothing in the tests can observe this release. Every scan that
feeds a columnar write reuses its vectors, so `closeIfFreeable()` is a no-op
for them: `ReusedArrowColumnVector` in the native reader and in
`TestArrowBatchReader`, and `OnHeapColumnVector` (a `WritableColumnVector`) in
`TestBatchReader`. The plain `ArrowColumnVector`s of `ColumnarDataSourceSuite`
go directly to `ArrowBatchingDataWriter.writeBatch`, which does not release
them. So deleting this call, skipping it when `writeBatch` throws, or moving it
before `writeBatch` keeps `ColumnarDataSourceSuite` and `NativeDataSourceSuite`
green. For a producer whose batches own their vectors, such as a connector that
returns plain `ArrowColumnVector`s, that regression would leak the vectors of
every batch, at least until the task ends, or free a batch before it is copied.
**Verification:**
- **Behavior:** A columnar write whose scan allocates new freeable vectors
for every batch releases each batch exactly once, after the data writer has
taken its data, and also when the data writer fails on that batch, so that no
memory of the scan stays allocated once the task ends.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/connector/SupportsColumnarInput.scala:
##########
@@ -0,0 +1,40 @@
+/*
+ * 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.spark.sql.internal.connector
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.connector.write.DataWriter
+import org.apache.spark.sql.vectorized.ColumnarBatch
+
+/**
+ * An internal mixin for a `Write`, or for the `StreamingWrite` of a
micro-batch write, whose data
+ * writers are [[ColumnarInputDataWriter]]s. When the query of an append or an
overwrite, or of a
+ * micro-batch, produces columnar batches, Spark passes them to the data
writers instead of
Review Comment:
**Nit (P3):** This sentence states the behavior without its conditions, and
so does the scaladoc of `ColumnarWrite` (`ColumnarWrite.scala:61-62`). Spark
passes the batches only when `V2TableWriteExec.writesColumnar` holds, that is
when the query supports columnar output but not rows, and only when the session
has no columnar rules (`Columnar.scala:597`). The scan of a cached DataFrame, a
query that adaptive query execution wraps, and any query in a session whose
extensions have columnar rules are written as rows, as
`ColumnarDataSourceSuite` asserts. A reader of these two docs would instead
expect every columnar query to reach the data writers as batches. The scaladoc
of `writesColumnar` and the user guide already state the conditions.
**Verification:**
- **Inspection:** The documentation of the internal mixin for columnar input
and of the columnar write says that the data writers get the batches of a query
only when the query supports columnar output and not rows and the session has
no columnar rules, names or refers to the queries that are written as rows,
such as the scan of a cached DataFrame, a query that adaptive query execution
wraps and a query in a session whose extensions have columnar rules, and agrees
with the documentation of the write node and with the user guide.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/WriteToDataSourceV2Exec.scala:
##########
@@ -873,6 +942,34 @@ object DataWritingSparkTask extends
WritingSparkTask[DataWriter[InternalRow]] {
writer: DataWriter[InternalRow], iter: java.util.Iterator[InternalRow]):
Unit = {
writer.writeAll(iter)
}
+
+ override def runColumnar(
+ writerFactory: DataWriterFactory,
+ context: TaskContext,
+ iter: Iterator[ColumnarBatch],
+ useCommitCoordinator: Boolean,
+ customMetrics: Map[String, SQLMetric]): DataWritingSparkTaskResult = {
+ val dataWriter = writerFactory.createWriter(context.partitionId(),
context.taskAttemptId())
+ writeAndCommit(dataWriter, context, useCommitCoordinator, customMetrics) {
+ val columnarWriter = dataWriter match {
+ case writer: ColumnarInputDataWriter => writer
+ case writer => throw SparkException.internalError(
+ s"Expected a data writer of columnar batches, but got
${writer.getClass.getName}.")
+ }
+ var numRows = 0L
+ iter.foreach { batch =>
+ // Releases the batch once it is written, as ColumnarToRowExec does
once it returned its
+ // rows: a producer of new column vectors for each batch leaves them
to the consumer.
+ Utils.tryWithSafeFinally {
+ columnarWriter.writeBatch(batch)
+ numRows += batch.numRows()
Review Comment:
**Nit (P3):** This count is new code for columnar writes, and no test checks
it. `writeWithV2` adds it to the `numOutputRows` metric of the write node, and
a micro-batch reports it as the sink's `numOutputRows` through
`commitProgress`, but no test in `ColumnarDataSourceSuite` or
`NativeDataSourceSuite` asserts either one for a columnar write. Counting
batches instead of rows, or never adding to the count, keeps both suites green,
while the SQL metric and the streaming progress of every columnar write would
be wrong.
**Verification:**
- **Behavior:** A batch write and a micro-batch write that take the columnar
batches of a scan, in several batches per task, report exactly the number of
rows that they wrote: as the number of output rows in the SQL metrics of the
batch write, and as the output rows of the sink in the progress of the
micro-batch.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarDataSourceSuite.scala:
##########
@@ -380,6 +970,57 @@ class TestBatchReader(lo: Long, hi: Long, schema:
StructType)
override def close(): Unit = if (consumed) batch.close()
}
+/**
+ * Returns the ids [lo, hi) and their names in Arrow batches of at most
`batchSize` rows. It writes
+ * each batch to the same Arrow vectors, over the previous one.
+ */
+/** @param largeVarTypes whether the strings are `large_utf8`, which the
writers convert */
Review Comment:
**Nit (P3):** This `@param` line opens a second doc comment, so Scaladoc
attaches only it to `TestArrowBatchReader`, and the description above it (lines
973-976) is left as a detached doc comment.
**Verification:**
- **Inspection:** The Arrow batch test reader is preceded by a single doc
comment that holds its description followed by the tag of its parameter for
large strings, with no detached doc comment before it.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -75,12 +75,17 @@ static boolean isSupported() {
static final long MICRO_BATCH_READ = 2;
static final long BATCH_WRITE = 4;
static final long STREAMING_WRITE = 8;
- static final long TRUNCATE = 16;
+ static final long BATCH_TRUNCATE = 16;
+ static final long STREAMING_TRUNCATE = 32;
Review Comment:
**Nit (P3):** `BATCH_TRUNCATE` and `STREAMING_TRUNCATE` must equal
`SPARK_DS_BATCH_TRUNCATE` and `SPARK_DS_STREAMING_TRUNCATE` in
`spark_datasource.h`, but no test compares them with the header. The test "the
bindings match the layout of spark_datasource.h" compares only the sizes and
offsets of `NativeLibrary.layout()`, and the test "operations that a data
source does not support" builds its capabilities option from these same Java
constants, which the test library returns unchanged; the only complete-mode
writes pass such options. So swapping 16 and 32, or giving `STREAMING_TRUNCATE`
another value, keeps `NativeDataSourceSuite` green, while a library that
declares only `SPARK_DS_BATCH_TRUNCATE` could have its batch overwrites
rejected and its complete-mode writes accepted, or the reverse.
**Verification:**
- **Compatibility:** The capability values that Spark checks, including the
batch and the streaming truncate bits, and the write modes equal the macros of
the spark_datasource.h that libraries compile against, so that swapping the two
truncate bits or changing either value fails a check that needs only a C++
compiler, not Java 22.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarWrite.scala:
##########
@@ -214,15 +239,240 @@ class ArrowBatchingDataWriter(delegate:
DataWriter[ColumnarBatch], options: Arro
}
}
- private def flush(): Unit = {
- if (root != null) {
- arrowWriter.finish()
- val columns = root.getFieldVectors.asScala.map(new ArrowColumnVector(_):
ColumnVector)
+ override def writeBatch(batch: ColumnarBatch): Unit = {
+ val numBatchRows = batch.numRows()
+ val copied = copiedVectors(batch)
+ if (copied.forall(_.isEmpty)) {
+ // Without columns to copy, the values are converted as rows are.
+ var i = 0
+ while (i < numBatchRows) {
+ write(batch.getRow(i))
+ i += 1
+ }
+ } else if (numBatchRows > 0) {
+ // A batch without rows leaves the pending batch of rows as it is.
+ flush()
+ // Each batch starts where the last one ended, so that the batches end
as those of rows do.
+ var start = 0
+ while (start < numBatchRows) {
+ start += writeRows(
+ batch, copied, start, start + math.min(numBatchRows - start,
options.maxRecordsPerBatch))
+ }
+ }
+ }
+
+ /**
+ * Returns, for each column of a batch, its Arrow vector if it can be copied
buffer by buffer, see
+ * `ArrowBatchingDataWriter.canCopy`. The other columns are converted value
by value.
+ */
+ private def copiedVectors(batch: ColumnarBatch): Seq[Option[FieldVector]] = {
+ val fields = arrowSchema.getFields.asScala
+ if (batch.numCols() != fields.length) {
+ Seq.fill(fields.length)(None)
+ } else {
+ fields.indices.map { i =>
+ batch.column(i) match {
+ case column: ArrowColumnVector => column.getValueVector match {
+ case vector: FieldVector
+ if ArrowBatchingDataWriter.canCopy(vector, fields(i),
batch.numRows()) =>
+ Some(vector)
+ case _ => None
+ }
+ case _ => None
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes the batch of the rows of a columnar batch that starts at the row
`start`, and returns
+ * its number of rows: up to `end`, or as a batch of rows ends, up to the
first row with which it
+ * holds `maxBytesPerBatch` bytes, by the measure of `getBufferSizeFor`,
which `rangeSize`
+ * computes for the copied columns without slicing them.
+ *
+ * @param copied the vector of each column of the batch that is copied, see
`copiedVectors`
+ */
+ private def writeRows(
+ batch: ColumnarBatch,
+ copied: Seq[Option[FieldVector]],
+ start: Int,
+ end: Int): Int = {
+ // The columns that are not copied, and the vectors that they are
converted into.
+ val ordinals = copied.indices.filter(copied(_).isEmpty)
+ val converted = ArrayBuffer.empty[FieldVector]
+ Utils.tryWithSafeFinally {
+ val writers = ordinals.map { ordinal =>
+ val vector = arrowSchema.getFields.get(ordinal).createVector(allocator)
+ converted += vector
+ vector.setInitialCapacity(end - start)
+ vector.allocateNew()
+ ArrowWriter.createFieldWriter(vector)
+ }
+ val length = if (writers.isEmpty) {
+ batchLength(copied.flatten, start, end)
+ } else {
+ convertRows(batch, copied.flatten, ordinals.zip(writers), start, end)
+ }
+ writers.foreach(_.finish())
+ writeSlices(copied, converted.toSeq, start, length)
+ length
+ } {
+ AutoCloseables.close(converted.asJava)
+ }
+ }
+
+ /**
+ * Returns the number of rows of the batch of copied columns that starts at
the row `start`, see
+ * `writeRows`.
+ */
+ private def batchLength(copied: Seq[FieldVector], start: Int, end: Int): Int
= {
+ def sizeInBytes(rows: Int): Long =
+ copied.map(ArrowBatchingDataWriter.rangeSize(_, start, start + rows)).sum
+ // The size is not computed when the limit is the largest possible one, as
for rows.
+ if (options.maxBytesPerBatch >= Int.MaxValue ||
+ sizeInBytes(end - start) < options.maxBytesPerBatch) {
+ end - start
+ } else {
+ // The size grows with the number of rows.
+ var lo = 1
+ var hi = end - start
+ while (lo < hi) {
+ val mid = (lo + hi) >>> 1
+ if (sizeInBytes(mid) >= options.maxBytesPerBatch) hi = mid else lo =
mid + 1
+ }
+ lo
+ }
+ }
+
+ /**
+ * Converts the columns of a columnar batch that are not copied with their
writers, by ordinal,
+ * row by row from the row `start` up to the end of the batch, and returns
its number of rows, see
+ * `writeRows`. As for rows, the size of the batch is computed after each
row, so that the
+ * conversion stops at the row with which the batch holds `maxBytesPerBatch`
bytes.
+ */
+ private def convertRows(
+ batch: ColumnarBatch,
+ copied: Seq[FieldVector],
+ writers: Seq[(Int, ArrowFieldWriter)],
+ start: Int,
+ end: Int): Int = {
+ val copiedVectors = copied.toArray
+ val ordinals = writers.map(_._1).toArray
+ val fieldWriters = writers.map(_._2).toArray
+ // The size of the batch once it holds the rows [start, rowEnd).
+ def sizeInBytes(rowEnd: Int): Long = {
+ var size = 0L
+ var i = 0
+ while (i < copiedVectors.length) {
+ size += ArrowBatchingDataWriter.rangeSize(copiedVectors(i), start,
rowEnd)
+ i += 1
+ }
+ i = 0
+ while (i < fieldWriters.length) {
+ size += fieldWriters(i).getSizeInBytes()
+ i += 1
+ }
+ size
+ }
+ // The size is not computed when the limit is the largest possible one, as
for rows.
+ val sized = options.maxBytesPerBatch < Int.MaxValue
+ var next = start
+ var full = false
+ while (next < end && !full) {
+ val row = batch.getRow(next)
+ var i = 0
+ while (i < fieldWriters.length) {
+ fieldWriters(i).write(row, ordinals(i))
+ i += 1
+ }
+ next += 1
+ full = sized && sizeInBytes(next) >= options.maxBytesPerBatch
+ }
+ next - start
+ }
+
+ /**
+ * Writes the rows [start, start + length) of a columnar batch, with the
vectors that its columns
+ * that are not copied were converted into from these rows, in their order.
The copied columns are
+ * sliced, which shares most buffers of the batch, though it allocates new
offsets for lists, and
+ * for strings and binaries unless the offset of the row `start` is 0, and
new validity buffers,
+ * and value buffers for booleans, unless `start` is a multiple of 8. Then
each buffer of the
+ * slice is copied at its size. Arrow Java reads the offsets of a vector of
variable width, or of
+ * a list, as if they started at 0, which those of a slice do. The converted
vectors, which
+ * nothing else writes to, are written as they are.
+ */
+ private def writeSlices(
+ copied: Seq[Option[FieldVector]],
+ converted: Seq[FieldVector],
+ start: Int,
+ length: Int): Unit = {
+ // The slices of the copied columns share the buffers of the batch, which
their producer
+ // releases, so they are closed once they are copied.
+ val slices = ArrayBuffer.empty[FieldVector]
+ val copy = Utils.tryWithSafeFinally {
+ copied.flatten.foreach { vector =>
+ // The slice is closed also when the buffers that it allocates fail to
allocate.
+ val pair = vector.getTransferPair(vector.getAllocator)
+ slices += pair.getTo.asInstanceOf[FieldVector]
+ pair.splitAndTransfer(start, length)
Review Comment:
**Nit (P3):** In the tests, only vectors allocated in the JVM reach this
call with a `start` other than 0. `NativeDataSourceSuite` never lowers
`maxRecordsPerBatch` or `maxBytesPerBatch`, and the scans of its test library
return batches of at most 1000 rows (the default `batch_size`), below the
default `maxRecordsPerBatch` of 10000, so vectors imported from a library
through the C data interface are only ever copied whole, from row 0. At a later
start, `splitAndTransfer` allocates new offset or validity buffers and
transfers slices of the imported buffers, which is what every native copy does
when its scan returns batches larger than the write's limits, for example
65536-row batches. A regression there, such as releasing the imported buffers
when a slice is closed or misreading their offsets, would keep both suites
green while such a copy fails or writes corrupt data.
**Verification:**
- **Behavior:** Copying columns imported through the Arrow C data interface,
in batches larger than the record limit and under byte limits of a few rows so
that slices start at non-zero and unaligned rows, writes the same rows and
batch sizes as the row path for fixed-width, boolean, variable-width and nested
columns, leaves the imported batch readable until its producer releases it, and
leaves no memory allocated, in a test that needs no native data source library
and so runs in every build.
--
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]