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


##########
native/jni-bridge/src/errors.rs:
##########
@@ -573,7 +573,12 @@ fn throw_exception(env: &mut Env, error: &CometError, 
backtrace: Option<String>)
             // FAILED_READ_FILE / FileNotFound via the structured SparkError 
channel. Anything else
             // falls back to generic handling.
             CometError::DataFusion { msg: _, source } => {
-                if let Some(spark_error) = 
try_classify_file_read_error(source) {
+                if let Some(json_message) = spark_error_json_in_chain(source) {
+                    env.throw_new(
+                        
jni::jni_str!("org/apache/comet/exceptions/CometQueryExecutionException"),

Review Comment:
   This makes four copies of 
`throw_new("org/apache/comet/exceptions/CometQueryExecutionException", 
json_message)` in this file, at 543, 549, 578 and 667. Would you pull out a 
`throw_spark_error_json(env, json_message)` and have 
`throw_spark_error_as_json` call it too? The `External` arm just above also 
hand-rolls the same two downcasts `spark_error_json_in_chain` does, only 
without the chain walk, so pointing it at the new function would remove that 
duplication and stop the two arms disagreeing about how deep they look.



##########
native/core/src/parquet/eager_page_index_reader_factory.rs:
##########
@@ -57,27 +64,99 @@ use 
datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
 use datafusion_datasource::PartitionedFile;
 use futures::future::BoxFuture;
 use futures::{FutureExt, TryFutureExt};
+use object_store::path::Path;
 use object_store::{ObjectStore, ObjectStoreExt};
 use parquet::arrow::arrow_reader::ArrowReaderOptions;
 use parquet::arrow::async_reader::AsyncFileReader;
+use parquet::arrow::parquet_to_arrow_schema;
 use parquet::errors::ParquetError;
 use parquet::file::metadata::{PageIndexPolicy, ParquetMetaData};
+use std::collections::HashMap;
 use std::fmt::Debug;
 use std::ops::Range;
-use std::sync::Arc;
+use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
 
 #[derive(Debug)]
 pub struct EagerPageIndexReaderFactory {
     store: Arc<dyn ObjectStore>,
     metadata_cache: Arc<FileMetadataCache>,
+    field_id_check: Option<Arc<FieldIdCheck>>,
 }
 
 impl EagerPageIndexReaderFactory {
     pub fn new(store: Arc<dyn ObjectStore>, metadata_cache: 
Arc<FileMetadataCache>) -> Self {
         Self {
             store,
             metadata_cache,
+            field_id_check: None,
+        }
+    }
+
+    /// Validate the ids `requested_schema` carries against each file's schema 
as its footer
+    /// loads. Installs nothing when field id matching is off or the schema 
carries no id, so
+    /// ordinary reads pay nothing.
+    pub fn with_field_id_check(
+        mut self,
+        requested_schema: SchemaRef,
+        parquet_options: &SparkParquetOptions,
+    ) -> Self {
+        if parquet_options.use_field_id && 
schema_holds_field_ids(&requested_schema) {
+            self.field_id_check = Some(Arc::new(FieldIdCheck {
+                requested_schema,
+                parquet_options: parquet_options.clone(),
+                validated: Mutex::new(HashMap::new()),
+            }));
         }
+        self
+    }
+}
+
+/// Validates requested field ids for files the expression adapter never sees: 
DataFusion's
+/// opener creates the adapter only when a predicate is pushed or the file 
schema differs from
+/// the requested one, so a metadata-free file whose schema equals it is read 
positionally
+/// (comet#5801). Resolves the mapping the adapter resolves, so both raise the 
same error.
+#[derive(Debug)]
+struct FieldIdCheck {
+    requested_schema: SchemaRef,
+    parquet_options: SparkParquetOptions,
+    /// Files already validated, keyed by path to the metadata they were 
checked against, so a
+    /// footer served from `FileMetadataCache` is not rechecked on every open.
+    validated: Mutex<HashMap<Path, Weak<ParquetMetaData>>>,

Review Comment:
   `parking_lot::Mutex` is already a dependency of `native/core` and is what 
every other mutex in the crate uses, in `fair_pool.rs`, `task_shared.rs` and 
`jni_api.rs`. Switching `validated` to it would let the `lock()` helper and the 
`MutexGuard` and `PoisonError` imports go away. `PoisonError::into_inner` is 
currently the only occurrence in the whole native tree.



##########
native/core/src/parquet/parquet_support.rs:
##########
@@ -148,43 +150,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 {
+    fields
+        .iter()
+        .filter(|f| field_id(f) == Some(id))
+        .map(|f| f.name().as_str())
+        .collect::<Vec<_>>()
+        .join(", ")
+}
+
+/// Which file field supplies each requested field, resolved once per file and 
reused for
+/// every batch. Follows the requested type as Spark's `clipParquetSchema` 
does: a struct
+/// lists one source per requested field, a list (large or not) or map carries 
the mapping
+/// of its element or key and value types, and anything else is a leaf 
converted by type.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub(crate) enum FieldMapping {
+    Struct(Vec<StructFieldSource>),
+    List(Box<FieldMapping>),
+    Map(Box<FieldMapping>, Box<FieldMapping>),
+    Leaf,
+}
+
+/// The file field behind one requested struct field.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub(crate) struct StructFieldSource {
+    /// Index of the file field supplying the requested field; `None` 
null-fills it.
+    pub(crate) from_index: Option<usize>,
+    /// Mapping of the requested field's own type.
+    pub(crate) nested: FieldMapping,
+}
+
+impl FieldMapping {
+    /// Mapping of a list's element type. A `Leaf` list converts its elements 
by type alone,
+    /// as the adapter hands one to every column whose type holds no struct.
+    fn list_element(&self) -> DataFusionResult<&FieldMapping> {
+        match self {
+            FieldMapping::List(inner) => Ok(inner),
+            FieldMapping::Leaf => Ok(&FieldMapping::Leaf),
+            other => Err(DataFusionError::Internal(format!(
+                "list column resolved to a non-list field mapping: {other:?}"
+            ))),
+        }
+    }
+
+    /// Mappings of a map's key and value types; see 
[`FieldMapping::list_element`].
+    fn map_entries(&self) -> DataFusionResult<(&FieldMapping, &FieldMapping)> {
+        match self {
+            FieldMapping::Map(key, value) => Ok((key, value)),
+            FieldMapping::Leaf => Ok((&FieldMapping::Leaf, 
&FieldMapping::Leaf)),
+            other => Err(DataFusionError::Internal(format!(
+                "map column resolved to a non-map field mapping: {other:?}"
+            ))),
+        }
+    }
+
+    /// True when every requested field reads the file field at its own 
position, so a
+    /// metadata-only relabel of the file array already yields the requested 
layout.
+    pub(crate) fn is_positional(&self) -> bool {
+        match self {
+            FieldMapping::Struct(sources) => sources
+                .iter()
+                .enumerate()
+                .all(|(i, s)| s.from_index == Some(i) && 
s.nested.is_positional()),
+            FieldMapping::List(inner) => inner.is_positional(),
+            FieldMapping::Map(key, value) => key.is_positional() && 
value.is_positional(),
+            FieldMapping::Leaf => true,
+        }
+    }
+}
+
+/// True when a field of `schema`, at any nesting depth, carries a Parquet 
field id.
+pub(crate) fn schema_holds_field_ids(schema: &Schema) -> bool {
+    schema.fields().iter().any(|f| field_holds_id(f))
+}
+
+fn field_holds_id(field: &Field) -> bool {
+    field_id(field).is_some()
+        || match field.data_type() {
+            DataType::Struct(fields) => fields.iter().any(|f| 
field_holds_id(f)),
+            DataType::List(f) | DataType::LargeList(f) | DataType::Map(f, _) 
=> field_holds_id(f),
+            _ => false,
+        }
+}
+
+/// Resolve every requested root field against `file_schema` the way the 
expression adapter
+/// does, keeping only the ambiguity Spark reports. DataFusion's opener 
creates the adapter
+/// only when a predicate is pushed or the file schema differs from the 
requested one, so the

Review Comment:
   The reason the opener skips the adapter is spelled out here, on 
`FieldIdCheck`, and again in `init_datasource_exec`. Keeping it once on 
`FieldIdCheck` and having the other two point at it would mean it only has to 
be corrected in one place if DataFusion changes that condition.



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