andygrove opened a new issue, #6087:
URL: https://github.com/apache/datafusion-comet/issues/6087

   ### Describe the bug
   
   Comet's cache serializer fails with `ArrayIndexOutOfBoundsException` when 
the columnar batch it is
   handed has more columns than the cached relation's output schema.
   
   ```
   java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
     at 
org.apache.spark.sql.comet.execution.arrow.ArrowWriter.writeColumns(ArrowWriters.scala:171)
     at 
org.apache.spark.sql.comet.execution.arrow.CometArrowConverters$.columnarBatchToArrowBatch(CometArrowConverters.scala:102)
     at 
org.apache.spark.sql.comet.execution.arrow.ArrowCachedBatchSerializer.$anonfun$encodeBatches$1(ArrowCachedBatchSerializer.scala:231)
     at 
org.apache.spark.sql.execution.columnar.CachedRDDBuilder$$anon$2.next(InMemoryRelation.scala:342)
     at 
org.apache.spark.storage.memory.MemoryStore.putIterator(MemoryStore.scala:232)
   ```
   
   ### Root cause
   
   `ArrowWriter.writeColumns` drives its loop from the *batch* width while 
indexing the writer's
   fields, which are built from the cached relation's attributes:
   
   ```scala
   def writeColumns(input: ColumnarBatch, startRow: Int, numRows: Int): Unit = {
     var columnIndex = 0
     while (columnIndex < input.numCols()) {
       fields(columnIndex).writeColumnSlice(input.column(columnIndex), 
startRow, numRows)
       columnIndex += 1
     }
   ```
   
   `InMemoryRelation.buildBuffers` passes `cachedPlan.output` as the cache 
schema and
   `cachedPlan.executeColumnar()` as the input. Because `supportsColumnarInput` 
returns true, Spark
   strips the `ColumnarToRow` above the cached plan, so the serializer receives 
the scan's raw
   batches, whatever shape the connector produces them in.
   
   Iceberg's vectorized reader emits batches wider than the scan's output 
schema.
   `BaseBatchReader.BatchDeleteFilter.filterBatch` reads with 
`deletes.requiredSchema()`, which
   carries `_pos` when a data file has position deletes, and it only trims the 
extras back with
   `ColumnarBatchUtil.removeExtraColumns` when `deletes.hasEqDeletes()` is 
true. A merge-on-read
   UPDATE writes position deletes and no equality deletes, so the extra column 
survives: caching
   `SELECT name FROM t` over `id INT, name STRING` hands the writer a `[name, 
_pos]` batch (2 columns)
   while it holds 1 field, which is the out-of-bounds index above. Iceberg 
removes extras from the end
   of the vector array, so the projected columns are always the leading ones.
   
   The direct-write path is unaffected: `CachedBatchIpc.matchesReaderLayout` 
requires
   `batch.numCols() == readerFields.length`, so a wider batch already declines 
that path and falls
   into the conversion path, which is where it fails.
   
   For comparison, Spark's own `ArrowCachedBatchSerializer` (SPARK-57268) 
converts a non-Arrow batch
   through `batch.rowIterator()` and `ArrowWriter.write(row)`, and that loop is 
driven by
   `fields.length`, so it tolerates a wider batch. Comet's 
`ArrowWriter.write(row)` is driven by
   `fields.length` too. `writeColumns` is the only path in Comet that trusts 
the batch width.
   
   ### Steps to reproduce
   
   With `spark.comet.exec.inMemoryCache.enabled=true`, against an Iceberg 
merge-on-read table
   (`format-version` 2 or 3, Parquet, vectorized reads):
   
   ```sql
   CREATE TABLE t (id INT, name STRING);
   INSERT INTO t VALUES (1, 'n1');
   
   CACHE TABLE tmp AS SELECT name FROM t;
   MERGE INTO t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET 
t.name = s.name;
   SELECT * FROM tmp;
   ```
   
   In CI this reproduces as 
`TestMergeOnReadMerge.testMergeRefreshesRelationCache`
   (`catalogName = testhadoop`, `format = PARQUET`, `vectorized = true`, 
`formatVersion = 3`).
   
   ### Expected behavior
   
   Writing a cached batch should be driven by the cache schema rather than the 
batch width: write the
   first `fields.length` columns and ignore trailing ones, the same columns 
Iceberg itself keeps when
   it trims. A batch with fewer columns than the schema is a genuine contract 
violation and should
   still fail loudly.
   
   ### Additional context
   
   Found by #5634, which enables `spark.comet.exec.inMemoryCache.enabled` by 
default so that the whole
   CI matrix exercises the cache format rather than only the two suites that 
opt in.
   
   Failing job:
   
https://github.com/apache/datafusion-comet/actions/runs/34270155816/job/102213466325
   


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