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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarTable.scala:
##########
@@ -123,39 +131,60 @@ class ColumnarScanBuilder(
 
   override def pruneColumns(requiredSchema: StructType): Unit = {
     // Only top-level columns are pruned by the data source. Spark prunes the 
nested fields of the
-    // columns it reads.
+    // columns it reads. The required columns are only recorded here: a batch 
scan asks its planner
+    // to prune the others, and a streaming scan, which has no planner, passes 
the required
+    // columns to its stream reader.
     val required =
       StructType(requiredSchema.fieldNames.flatMap(name => schema.find(_.name 
== name)))
-    if (required.length < schema.length && 
batchPlanner.pruneColumns(required)) {
-      readSchema = required
+    if (required.length < schema.length) {
+      this.requiredSchema = Some(required)
     }
   }
 
   // The scan takes over the planner, so it is built once.
   private lazy val scan = new ColumnarScan(
-    shortName, schema, readSchema, dataSource, Option(planner), pushedLimit)
+    shortName, schema, requiredSchema, dataSource, Option(planner), 
pushedLimit)
 
   override def build(): Scan = scan
 }
 
 /**
+ * @param requiredSchema the top-level columns that the query needs, if fewer 
than those of the
+ *                       full schema. A batch scan asks its planner to prune 
the other columns,
+ *                       while a streaming scan reads only these columns: they 
are the schema that
+ *                       its stream reader gets.
  * @param pushedDownPlanner the planner that the operations were pushed down 
to, if any. Otherwise,
- *                          the scan creates one when its batch is planned.
+ *                          the scan creates one when its batch is planned or 
pruned.
  */
 class ColumnarScan(
     shortName: String,
     fullSchema: StructType,
-    prunedSchema: StructType,
+    requiredSchema: Option[StructType],
     dataSource: ColumnarDataSource,
     pushedDownPlanner: Option[ColumnarBatchPlanner],
     pushedLimit: Option[Int])
   extends Scan with SupportsMetadata {
 
-  // Spark may call `toBatch` more than once for the same scan; the partitions 
are planned once.
-  private lazy val batch = new ColumnarBatchScan(
-    pushedDownPlanner.getOrElse(dataSource.batchPlanner(fullSchema)))
+  private lazy val planner = 
pushedDownPlanner.getOrElse(dataSource.batchPlanner(fullSchema))
+
+  // The columns that the batch reads: the required ones if the planner prunes 
the others.
+  private lazy val batchSchema = 
requiredSchema.filter(planner.pruneColumns).getOrElse(fullSchema)
 
-  override def readSchema(): StructType = prunedSchema
+  // The columns that the stream reads.
+  private def streamSchema: StructType = requiredSchema.getOrElse(fullSchema)

Review Comment:
   **[P2] Stream column pruning never happens in a real streaming query**
   
   MicroBatchExecution (and ContinuousExecution) always call 
`pruneColumns(output.toStructType)` with the full relation output, so 
`streamSchema` is always the full schema in production. The stream reader is 
still created with every column, although the header, the docs and 
`ColumnarDataSource.streamReader` promise that only the needed columns are 
passed. The new `streaming` flag and the requiredSchema plumbing for streams 
are only exercised by unit tests that call pruneColumns by hand.
   
   **Failure scenario:** readStream.format(<native>).load().select("mod") calls 
create_stream_reader with every column of the source rather than only `mod`. 
That contradicts the documented contract, and the unit tests pass anyway 
because they call pruneColumns themselves.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/Columnar.scala:
##########
@@ -589,6 +590,9 @@ case class ApplyColumnarRulesAndInsertTransitions(
         case write: DataWritingCommandExec
             if write.cmd.isInstanceOf[V1WriteCommand] && 
conf.plannedWriteEnabled =>
           write.child.supportsColumnar
+        // A V2 write whose data writers take columnar batches writes those of 
a query that only
+        // supports columnar output.
+        case write: V2TableWriteExec if write.writesColumnar => true

Review Comment:
   **[P2] Columnar V2 write skips the ColumnarToRowExec that ColumnarRule 
extensions replace**
   
   When `writesColumnar` is true, `ensureOutputsRowBased` no longer puts a 
ColumnarToRowExec above a query that only outputs columnar batches. 
ColumnarRule extensions (Gluten/Velox, Comet) usually swap that transition for 
their own in postColumnarTransitions. With no transition in the plan, their 
batches go straight to `ArrowBatchingDataWriter.writeBatch`, which only copies 
`ArrowColumnVector` columns and otherwise falls back to `batch.getRow(i)`.
   
   **Failure scenario:** With Gluten or Comet installed, run INSERT INTO 
native_ds SELECT * FROM t, where t is read by the extension's columnar scan. 
The child is columnar-only, so no C2R is inserted and the extension has none to 
replace. Its opaque batches (e.g. IndicatorVector) reach writeBatch, and the 
row-by-row fallback through their ColumnVector getters either throws or is very 
slow. Before this change, the same query was written through the extension's 
C2R.



-- 
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