[ 
https://issues.apache.org/jira/browse/SPARK-60262?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated SPARK-60262:
-----------------------------------
    Labels: pull-request-available  (was: )

> Parquet field-id reads return zeros or uninitialized memory when an id match 
> and a name match select the same column
> --------------------------------------------------------------------------------------------------------------------
>
>                 Key: SPARK-60262
>                 URL: https://issues.apache.org/jira/browse/SPARK-60262
>             Project: Spark
>          Issue Type: Bug
>          Components: SQL
>    Affects Versions: 5.0.0
>            Reporter: Andrew Grove
>            Priority: Major
>              Labels: pull-request-available
>
> _Drafted with LLM assistance (Claude Code); verified by running the 
> reproduction below on Spark 4.1.3, and by reading the same code in Spark 
> 4.2.0._
> h3. Problem
> With {{spark.sql.parquet.fieldId.read.enabled=true}}, 
> {{ParquetReadSupport.clipParquetGroupFields}} resolves each requested field 
> on its own. A field that carries a field id resolves by id 
> ({{matchIdField}}), and any other field resolves by name. Two requested 
> fields can therefore select the same Parquet column. Take a file column {{A}} 
> with field id 1: a requested {{x}} with id 1 selects it by id, and a 
> requested {{a}} without an id selects it by name. Primitive types are not 
> clipped, so the requested Parquet schema lists {{A}} twice.
> Neither reader returns {{A}}'s values for both fields, and the vectorized 
> reader returns wrong values without an error:
> * *Vectorized reader.* {{VectorizedParquetRecordReader}} builds one 
> {{VectorizedColumnReader}} per requested leaf. Both leaves have the same 
> {{ColumnDescriptor}}. parquet-java's 
> {{ColumnChunkPageReadStore.getPageReader}} looks up one 
> {{ColumnChunkPageReader}} per descriptor, and its {{readPage}} polls a queue. 
> The first reader drains the pages. For the second, {{readPage()}} gets no 
> page and returns {{-1}}, and {{readBatch}} stops as if it had read every 
> page. Its vector is never written, and the null mask isn't either, so the 
> rows read as non-null:
> ** With on-heap vectors, the default, they read {{0}}.
> ** With off-heap vectors they read uninitialized memory. Off-heap vectors are 
> controlled by {{spark.sql.columnVector.offheap.enabled}}, which follows 
> {{spark.memory.offHeap.enabled}} from Spark 4.0 on.
> ** Whichever field comes first in the requested schema gets the values.
> * *parquet-mr reader* ({{spark.sql.parquet.enableVectorizedReader=false}}). 
> {{ParquetRowConverter}} maps the clipped Parquet fields back to requested 
> fields by id first. Both copies of {{A}} carry id 1, so both map to {{x}}, 
> and {{a}} reads null.
> h3. Reproduce
> {code:scala}
> import org.apache.spark.sql.Row
> import org.apache.spark.sql.types._
> val id1 = new MetadataBuilder().putLong("parquet.field.id", 1L).build()
> spark
>   .createDataFrame(
>     spark.sparkContext.parallelize(Seq(Row(1), Row(2), Row(3)), 1),
>     StructType(Seq(StructField("A", IntegerType, nullable = true, id1))))
>   .write
>   .parquet("/tmp/field-id-twice")
> spark.conf.set("spark.sql.parquet.fieldId.read.enabled", "true")
> val x = StructField("x", IntegerType, nullable = true, id1)
> val a = StructField("a", IntegerType, nullable = true)
> spark.read.schema(StructType(Seq(x, a))).parquet("/tmp/field-id-twice").show()
> {code}
> ||Requested fields||Vectorized, on-heap vectors (default)||Vectorized, 
> off-heap vectors||parquet-mr||
> |{{x}} (id 1), {{a}}|{{x}} = 1, 2, 3; {{a}} = 0, 0, 0|{{x}} = 1, 2, 3; {{a}} 
> = 0, 0, 1634492735|{{x}} = 1, 2, 3; {{a}} = null|
> |{{a}}, {{x}} (id 1)|{{a}} = 1, 2, 3; {{x}} = 0, 0, 0|{{a}} = 1, 2, 3; {{x}} 
> = stale memory|{{a}} = null; {{x}} = 1, 2, 3|
> |struct {{s}} holding {{x}} (id 1), {{a}}|{{a}} = 0, 0, 0|{{a}} = stale 
> memory|{{a}} = null|
> For the struct case, write {{A}} inside a struct {{s}} and request {{s}} with 
> the fields {{x}} and {{a}}. The off-heap values change between runs 
> ({{1634492735}}, {{489223743}}, {{1403037696}}, ...). These results are from 
> Spark 4.1.3, where nested column vectorization is on by default.
> h3. Expected behavior
> Both fields read {{1, 2, 3}}, because {{clipParquetGroupFields}} resolves 
> each of them to {{A}}. Failing the read would also be reasonable. Returning 
> zeros or uninitialized memory without an error is not.
> h3. Possible fix
> * *Guard.* Make {{VectorizedColumnReader.readBatch}} fail, instead of 
> returning, when it runs out of pages with rows still to read in a column that 
> isn't repeated. That would turn the wrong values into an error, whatever else 
> changes.
> * *Vectorized reader.* In {{VectorizedParquetRecordReader}}, read a column 
> once when several requested leaves resolve to it, and fill each of their 
> vectors from that read.
> * *parquet-mr reader.* {{clipParquetGroupFields}} returns one clipped field 
> per requested field, in requested order. So {{ParquetRowConverter}} could 
> pair them by position instead of by id or name. Someone who knows why it 
> matches by id would need to confirm that is safe.
> h3. Context
> I found this while comparing DataFusion Comet's native Parquet scan with 
> Spark for this shape ([apache/datafusion-comet issue 
> 6994|https://github.com/apache/datafusion-comet/issues/6994]). Field-id reads 
> arrived in Spark 3.3 ([apache/spark PR 
> 35385|https://github.com/apache/spark/pull/35385], SPARK-38094). The shape 
> needs a requested struct that mixes fields with and without field ids, so it 
> is uncommon: Iceberg and Delta give every column an id.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to