andygrove commented on code in PR #5654:
URL: https://github.com/apache/datafusion-comet/pull/5654#discussion_r4095051479


##########
native/core/src/parquet/schema_adapter.rs:
##########
@@ -4170,4 +4686,88 @@ mod test {
         let target = struct_type(vec![("id", DataType::Int64)]);
         assert!(!is_pure_structural_narrowing(&physical, &target, 
&opts).unwrap());
     }
+
+    /// A requested column named like the shield's placeholder must still 
receive its
+    /// configured default. File: `k` (id 2). Required: `k` (id 1) and an 
id-less
+    /// `__COMET_UNMATCHED_FIELD_ID_1` with default 7, case-insensitive, 
field-id reading on.
+    /// The file's `k` is not the id match for requested `k`, so it is hidden 
behind a
+    /// placeholder name; that placeholder must not fold onto the requested 
column, or the
+    /// missing-column check treats it as present and the default is lost.
+    #[tokio::test]
+    async fn parquet_shield_placeholder_never_folds_onto_requested_column() {
+        let file_schema = Arc::new(Schema::new(vec![field_with_id("k", 2)]));
+        let col = Arc::new(Int64Array::from(vec![1])) as Arc<dyn 
arrow::array::Array>;
+        let required_schema = Arc::new(Schema::new(vec![
+            field_with_id("k", 1),
+            Field::new("__COMET_UNMATCHED_FIELD_ID_1", DataType::Int64, true),
+        ]));
+        let defaults = HashMap::from([(
+            Column::new("__COMET_UNMATCHED_FIELD_ID_1", 1),
+            ScalarValue::Int64(Some(7)),
+        )]);
+
+        let mut opts = SparkParquetOptions::new(EvalMode::Legacy, "UTC", 
false);
+        opts.case_sensitive = false;
+        opts.use_field_id = true;
+
+        let batch = scan_with_defaults(
+            file_schema,
+            vec![col],
+            required_schema,
+            opts,
+            Some(defaults),
+        )
+        .await
+        .unwrap();
+        assert_eq!(batch.num_rows(), 1);
+        let k = batch
+            .column(0)
+            .as_any()
+            .downcast_ref::<Int64Array>()
+            .unwrap();
+        assert!(
+            k.is_null(0),
+            "requested k (id 1) has no id match in the file"
+        );
+        let defaulted = batch
+            .column(1)
+            .as_any()
+            .downcast_ref::<Int64Array>()
+            .unwrap();
+        assert!(!defaulted.is_null(0), "configured default must apply");
+        assert_eq!(defaulted.value(0), 7);
+    }
+
+    /// File and requested schema are identical: `s` holding `x` and `y` that 
both carry
+    /// field id 1. No column needs conversion, so no cast is ever emitted, 
yet Spark's
+    /// `clipParquetSchema` rejects the read because requested id 1 resolves 
to two file
+    /// fields. The validation must therefore run when the file schema is 
mapped, not
+    /// only inside a cast.
+    #[tokio::test]
+    async fn parquet_duplicate_struct_field_id_rejected_without_cast() {

Review Comment:
   #6004 now declines a requested schema that repeats an id at planning, so 
this test covers a read Spark can no longer hand to the native scan, and the 
reasoning in its doc comment no longer applies. The reachable version is 
@sunchao's shape from the first round, `s<x (id 1), y (id 1), z (id 2)>` read 
as `s<x (id 1), y (id 3), z (id 2)>`. `main` returns all three values there, 
Spark raises, and this branch raises too. Its test 
(`test_field_id_read_rejects_duplicate_ids_despite_matching_names`) was dropped 
in `b4549c9d2`. Could this one use that shape instead?
   



##########
native/core/src/parquet/parquet_support.rs:
##########
@@ -162,43 +169,327 @@ impl SparkParquetOptions {
 
 /// Spark-compatible cast implementation. Defers to DataFusion's cast where 
that is known
 /// to be compatible, and returns an error when a not supported and not 
DF-compatible cast
-/// is requested.
+/// is requested. Resolves the nested field mapping for this one value; a 
per-file caller
+/// resolves once and uses [`spark_parquet_convert_with_mapping`] for every 
batch.
 pub fn spark_parquet_convert(
     arg: ColumnarValue,
     data_type: &DataType,
     parquet_options: &SparkParquetOptions,
+) -> DataFusionResult<ColumnarValue> {
+    let mapping =
+        resolve_field_mapping(&arg.data_type(), data_type, 
parquet_options).map_err(spark_error)?;
+    spark_parquet_convert_with_mapping(arg, data_type, &mapping, 
parquet_options)
+}
+
+/// [`spark_parquet_convert`] with a mapping already resolved for the value's 
type.
+pub(crate) fn spark_parquet_convert_with_mapping(
+    arg: ColumnarValue,
+    data_type: &DataType,
+    mapping: &FieldMapping,
+    parquet_options: &SparkParquetOptions,
 ) -> DataFusionResult<ColumnarValue> {
     match arg {
-        ColumnarValue::Array(array) => 
Ok(ColumnarValue::Array(parquet_convert_array(
+        ColumnarValue::Array(array) => Ok(ColumnarValue::Array(convert_array(
             array,
             data_type,
+            mapping,
             parquet_options,
+            None,
         )?)),
         ColumnarValue::Scalar(scalar) => {
             // Note that normally CAST(scalar) should be fold in Spark JVM 
side. However, for
             // some cases e.g., scalar subquery, Spark will not fold it, so we 
need to handle it
             // here.
             let array = scalar.to_array()?;
             let scalar = ScalarValue::try_from_array(
-                &parquet_convert_array(array, data_type, parquet_options)?,
+                &convert_array(array, data_type, mapping, parquet_options, 
None)?,
                 0,
             )?;
             Ok(ColumnarValue::Scalar(scalar))
         }
     }
 }
 
-fn parquet_convert_array(
-    array: ArrayRef,
+/// Wrap a [`SparkError`] the way every native operator surfaces it to the JVM.
+pub(crate) fn spark_error(error: SparkError) -> DataFusionError {
+    DataFusionError::External(Box::new(error))
+}
+
+/// Outcome of matching one requested id or name against a struct's file 
fields: the last
+/// file field that matched and whether more than one did. A plain `Copy` 
value, so resolving
+/// a wide struct allocates nothing per id or per name; the matched names are 
only gathered
+/// when an ambiguity is reported.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct FieldMatch {
+    pub(crate) index: usize,
+    pub(crate) ambiguous: bool,
+}
+
+impl FieldMatch {
+    pub(crate) fn new(index: usize, ambiguous: bool) -> Self {
+        Self { index, ambiguous }
+    }
+
+    /// The first file field carrying this id or name.
+    pub(crate) fn first(index: usize) -> Self {
+        Self::new(index, false)
+    }
+
+    /// A further file field carrying the same id or name: the later index 
wins, as Spark's
+    /// `toMap` does for exact names, and the entry turns ambiguous.
+    pub(crate) fn also(self, index: usize) -> Self {
+        Self::new(index, true)
+    }
+}
+
+/// Record file field `index` under `key`, keeping the entry `Copy`-sized 
however many fields
+/// share the key.
+pub(crate) fn record_field_match<K: Hash + Eq>(
+    matches: &mut HashMap<K, FieldMatch>,
+    key: K,
+    index: usize,
+) {
+    matches
+        .entry(key)
+        .and_modify(|m| *m = m.also(index))
+        .or_insert_with(|| FieldMatch::first(index));
+}
+
+/// Comma-joined names of the fields carrying `id`, for the duplicate-id error 
message.
+pub(crate) fn field_names_with_id(fields: &Fields, id: i32) -> String {

Review Comment:
   Spark brackets this list in `matchIdField` with `mkString("[", ", ", "]")`, 
so its message reads `Found duplicate field(s) "1": [x, y] in id mapping mode` 
and ours reads `"1": x, y`. Now that this helper formats the list for root and 
nested fields alike, could it add the brackets? The Scala test could then 
compare the whole message with Spark's instead of just the prefix.
   



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