comphead commented on code in PR #7089:
URL: https://github.com/apache/datafusion-comet/pull/7089#discussion_r4329263189


##########
spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala:
##########
@@ -1824,6 +1824,70 @@ abstract class ParquetReadSuite extends CometTestBase {
     }
   }
 
+  test("native scan rejects TIMESTAMP(NANOS) read as timestamp like Spark") {
+    // Regression guard for 
https://github.com/apache/datafusion-comet/issues/7034. Spark's
+    // writer cannot produce TIMESTAMP(NANOS), so timestamp_nanos.parquet was 
written with
+    // PyArrow. Next to `id int`, it holds TIMESTAMP(NANOS) leaves that are 
UTC-adjusted (`t`,
+    // `s.t` and the elements of `a`) or not (`u` and the elements of `b`), 
all set to
+    // 2020-09-13 12:26:40.123456789 UTC in the row with id 1 and null or 
empty in the row with
+    // id 2. Spark has no updater that reads TIMESTAMP(NANOS) as timestamp or 
timestamp_ntz.
+    val path = getResourceParquetFilePath("test-data/timestamp_nanos.parquet")

Review Comment:
   I opened `timestamp_nanos.parquet` with pyarrow. Its leaves are `id`, `t`, 
`u`, `s.t`, `a.list.element` and `b.list.element`, so `id` comes first, no 
non-NANOS column follows a NANOS one, and there is no map. A miscount in 
`mark_parquet_leaves` that tags a later non-NANOS leaf would not fail this 
test, and sunchao's ListView case needs that shape too. If the resource is 
regenerated for it, could it also get a trailing `TIMESTAMP_MICROS` column that 
is read on its own, and a `map<string, timestamp(ns)>` column? That would pin 
the leaf counting for struct, list and map in one file.



##########
native/core/src/parquet/eager_page_index_reader_factory.rs:
##########
@@ -734,7 +758,7 @@ impl AsyncFileReader for EagerPageIndexReader {
                     DataFusionError::External(source) => 
ParquetError::External(source),
                     other => ParquetError::External(Box::new(other)),
                 })?;
-                if *row_filters {
+                if *row_filters || has_nanos {

Review Comment:
   The module doc (line 75) and the `with_conversion_check` doc (lines 253 and 
254) still say a deferred conversion failure is raised on the first data-page 
request only when row filters are enabled. With this change the same deferral 
also applies, with row filters off, to a file that has a `TIMESTAMP(NANOS)` 
leaf, as the new comment at line 735 says. Could both docs be updated to match?



##########
spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala:
##########
@@ -1824,6 +1824,70 @@ abstract class ParquetReadSuite extends CometTestBase {
     }
   }
 
+  test("native scan rejects TIMESTAMP(NANOS) read as timestamp like Spark") {
+    // Regression guard for 
https://github.com/apache/datafusion-comet/issues/7034. Spark's
+    // writer cannot produce TIMESTAMP(NANOS), so timestamp_nanos.parquet was 
written with
+    // PyArrow. Next to `id int`, it holds TIMESTAMP(NANOS) leaves that are 
UTC-adjusted (`t`,
+    // `s.t` and the elements of `a`) or not (`u` and the elements of `b`), 
all set to
+    // 2020-09-13 12:26:40.123456789 UTC in the row with id 1 and null or 
empty in the row with
+    // id 2. Spark has no updater that reads TIMESTAMP(NANOS) as timestamp or 
timestamp_ntz.
+    val path = getResourceParquetFilePath("test-data/timestamp_nanos.parquet")
+    val readTypes = Seq(
+      "t timestamp",
+      "u timestamp",
+      "s struct<t: timestamp>",
+      "a array<timestamp>",
+      "b array<timestamp>",
+      "t timestamp_ntz",
+      "u timestamp_ntz")
+    for (pushdown <- Seq(false, true); readType <- readTypes) {
+      withSQLConf(
+        SQLConf.USE_V1_SOURCE_LIST.key -> "parquet",
+        CometConf.COMET_PARQUET_ROW_FILTER_PUSHDOWN_ENABLED.key -> 
pushdown.toString) {
+        val df = spark.read.schema(s"id int, 
$readType").parquet(path).where("id > 0")
+        checkCometOperators(stripAQEPlan(df.queryExecution.executedPlan))
+        val (sparkError, cometError) = checkSparkAnswerMaybeThrows(df)
+        Seq("Spark" -> sparkError, "Comet" -> cometError).foreach { case 
(engine, error) =>
+          val chain = error.toSeq.flatMap(causeChain)
+          assert(
+            chain.exists(_.isInstanceOf[
+              
org.apache.spark.sql.execution.datasources.SchemaColumnConvertNotSupportedException]),
+            s"$engine, $readType, row filter pushdown $pushdown: 
${chain.mkString("\n")}")
+        }
+        // The TIMESTAMP(NANOS) leaves only fail the read when they are 
requested.
+        checkSparkAnswerAndOperator(spark.read.schema("id 
int").parquet(path).where("id > 0"))

Review Comment:
   This assertion does not depend on `readType`, so it runs 14 times (2 
pushdown settings times 7 read types) for 2 distinct inputs, and each run is a 
full Spark plus Comet read. Could it move out of the inner loop, for example 
into its own loop over `pushdown` after it?



##########
spark/src/test/scala/org/apache/comet/parquet/ParquetReadSuite.scala:
##########
@@ -1824,6 +1824,70 @@ abstract class ParquetReadSuite extends CometTestBase {
     }
   }
 
+  test("native scan rejects TIMESTAMP(NANOS) read as timestamp like Spark") {
+    // Regression guard for 
https://github.com/apache/datafusion-comet/issues/7034. Spark's
+    // writer cannot produce TIMESTAMP(NANOS), so timestamp_nanos.parquet was 
written with
+    // PyArrow. Next to `id int`, it holds TIMESTAMP(NANOS) leaves that are 
UTC-adjusted (`t`,
+    // `s.t` and the elements of `a`) or not (`u` and the elements of `b`), 
all set to
+    // 2020-09-13 12:26:40.123456789 UTC in the row with id 1 and null or 
empty in the row with
+    // id 2. Spark has no updater that reads TIMESTAMP(NANOS) as timestamp or 
timestamp_ntz.
+    val path = getResourceParquetFilePath("test-data/timestamp_nanos.parquet")
+    val readTypes = Seq(

Review Comment:
   Spark reads a `TIMESTAMP(NANOS)` column as `bigint`, and a file with such a 
leaf now goes through the footer check on every open. Could `t bigint` be 
checked here as a read that must succeed and match Spark, so the newly active 
check is shown not to touch the one read Spark allows? I did not run it, so I 
do not know whether Comet matches Spark for that read today. If it does not, a 
separate issue is the right place.



##########
native/core/src/parquet/schema_adapter.rs:
##########
@@ -2198,6 +2264,366 @@ pub(crate) mod test {
         Ok(())
     }
 
+    /// `TIMESTAMP(NANOS)` read as `timestamp` or `timestamp_ntz`: Spark reads 
nanosecond
+    /// timestamps only as `bigint` (`nanosAsLong`), so the cast to 
microseconds is rejected
+    /// whether or not the file column is UTC-adjusted. See #7034.
+    #[tokio::test]
+    async fn parquet_nanos_timestamp_read_as_timestamp_errors() -> Result<(), 
DataFusionError> {
+        for file_tz in [Some("UTC"), None] {
+            for read_tz in [Some("UTC"), None] {
+                let values = Arc::new(
+                    
TimestampNanosecondArray::from(vec![1_600_000_000_123_456_789i64])
+                        .with_timezone_opt(file_tz),
+                ) as Arc<dyn arrow::array::Array>;
+                let msg = assert_rejected_conversion(
+                    Field::new(
+                        "t",
+                        DataType::Timestamp(TimeUnit::Nanosecond, 
file_tz.map(Into::into)),
+                        false,
+                    ),
+                    values,
+                    DataType::Timestamp(TimeUnit::Microsecond, 
read_tz.map(Into::into)),
+                )
+                .await?;
+                assert!(
+                    msg.contains("Column: [[t]]")
+                        && msg.contains("Found: INT64")
+                        && msg.contains("Expected: timestamp"),
+                    "file tz {file_tz:?}, read tz {read_tz:?}: unexpected 
error: {msg}"
+                );
+            }
+        }
+        Ok(())
+    }
+
+    /// The `TIMESTAMP(NANOS)` rejection keys on the footer check's marker, 
not on the Arrow
+    /// type: a marked leaf is rejected at any depth and with or without a 
timezone, and an
+    /// unmarked nanosecond leaf, which may be INT96, is accepted. Reading 
nanoseconds as `bigint`
+    /// stays allowed.
+    #[test]
+    fn nanos_timestamp_rejection_keys_on_the_parquet_type() -> Result<(), 
DataFusionError> {
+        let options = default_options();
+        let nanos_field = |name: &str, tz: Option<&str>, marked: bool| {
+            let field = Field::new(
+                name,
+                DataType::Timestamp(TimeUnit::Nanosecond, tz.map(Into::into)),
+                true,
+            );
+            if marked {
+                field.with_metadata(std::collections::HashMap::from([(
+                    NANOS_TIMESTAMP_KEY.to_string(),
+                    "true".to_string(),
+                )]))
+            } else {
+                field
+            }
+        };
+        let micros_utc = DataType::Timestamp(TimeUnit::Microsecond, 
Some("UTC".into()));
+        let micros_ntz = DataType::Timestamp(TimeUnit::Microsecond, None);
+        for tz in [None, Some("UTC")] {
+            for marked in [true, false] {
+                let leaf = || Arc::new(nanos_field("t", tz, marked));
+                let element = || Arc::new(nanos_field("element", tz, marked));
+                let map_of = |value: Field| {
+                    DataType::Map(
+                        Arc::new(Field::new(
+                            "key_value",
+                            DataType::Struct(Fields::from(vec![
+                                Field::new("key", DataType::Utf8, false),
+                                value,
+                            ])),
+                            false,
+                        )),
+                        false,
+                    )
+                };
+                for (physical, target) in [
+                    (leaf().as_ref().clone(), micros_utc.clone()),
+                    (leaf().as_ref().clone(), micros_ntz.clone()),
+                    (
+                        Field::new("a", DataType::List(element()), true),
+                        list_type(micros_utc.clone()),
+                    ),
+                    (
+                        Field::new("a", 
DataType::Struct(Fields::from(vec![leaf()])), true),
+                        struct_type(vec![("t", micros_utc.clone())]),
+                    ),
+                    (
+                        Field::new(
+                            "a",
+                            DataType::List(Arc::new(Field::new(
+                                "element",
+                                DataType::Struct(Fields::from(vec![leaf()])),
+                                true,
+                            ))),
+                            true,
+                        ),
+                        list_type(struct_type(vec![("t", 
micros_utc.clone())])),
+                    ),
+                    (
+                        Field::new("a", map_of(nanos_field("value", tz, 
marked)), true),
+                        map_of(Field::new("value", micros_utc.clone(), true)),
+                    ),
+                ] {
+                    // Reading a UTC-adjusted timestamp as `timestamp_ntz` is 
rejected anyway
+                    // with the default (Spark 3.x) options; see SPARK-36182.
+                    let expected = marked || (tz.is_some() && target == 
micros_ntz);
+                    let check = check_field_conversion(&physical, &target, 
"a", &options)?;
+                    assert_eq!(
+                        matches!(check, ConversionCheck::RejectOnNonEmpty { .. 
}),
+                        expected,
+                        "{physical:?} (marked: {marked}) read as {target}"
+                    );
+                }
+                let check = check_field_conversion(
+                    &leaf().as_ref().clone(),
+                    &DataType::Int64,
+                    "t",
+                    &options,
+                )?;
+                assert!(matches!(check, ConversionCheck::Accept), "{tz:?} as 
bigint");
+            }
+        }
+        Ok(())
+    }
+
+    /// An INT96 array in the legacy `bag` list layout reaches the conversion 
check as a
+    /// timezone-free nanosecond leaf below a list (#7029), the same Arrow 
type as a
+    /// `TIMESTAMP(NANOS)` leaf, so the `TIMESTAMP(NANOS)` rejection must 
leave it alone. Write
+    /// such a file and read it through the scan.
+    #[tokio::test]
+    async fn legacy_int96_array_still_reads_as_timestamp() -> Result<(), 
DataFusionError> {
+        use parquet::data_type::{Int96, Int96Type};
+        use parquet::file::writer::SerializedFileWriter;
+        use parquet::schema::parser::parse_message_type;
+
+        let schema = Arc::new(parse_message_type(
+            "message spark_schema {
+                OPTIONAL GROUP a (LIST) {
+                    REPEATED GROUP bag { OPTIONAL INT96 array; }
+                }
+            }",
+        )?);
+        // 1970-01-02 00:00:00.001 and 1970-01-03 00:00:00: INT96 holds the 
nanoseconds of the
+        // day (low word first) and then the Julian day.
+        const UNIX_EPOCH_JULIAN_DAY: u32 = 2_440_588;
+        let mut first = Int96::new();
+        first.set_data(1_000_000, 0, UNIX_EPOCH_JULIAN_DAY + 1);
+        let mut second = Int96::new();
+        second.set_data(0, 0, UNIX_EPOCH_JULIAN_DAY + 2);
+
+        let filename = get_temp_filename().to_str().unwrap().to_string();
+        let mut writer =
+            SerializedFileWriter::new(File::create(&filename)?, schema, 
Default::default())?;
+        let mut row_group = writer.next_row_group()?;
+        let mut a = row_group.next_column()?.unwrap();
+        a.typed::<Int96Type>()
+            .write_batch(&[first, second], Some(&[3, 3]), Some(&[0, 1]))?;
+        a.close()?;
+        row_group.close()?;
+        writer.close()?;
+
+        let micros_utc = DataType::Timestamp(TimeUnit::Microsecond, 
Some("UTC".into()));
+        let required = Arc::new(Schema::new(vec![Field::new(
+            "a",
+            list_type(micros_utc),
+            true,
+        )]));
+        let mut stream = scan_parquet_file(filename, required, 
default_options(), None, false)?;
+        let batch = stream.next().await.unwrap()?;
+        assert!(stream.next().await.is_none());
+
+        let list = batch.column(0).as_list::<i32>();
+        assert_eq!(list.len(), 1);
+        let values = list.value(0);
+        let expected = TimestampMicrosecondArray::from(vec![86_400_001_000i64, 
172_800_000_000])
+            .with_timezone("UTC");
+        assert_eq!(
+            values
+                .as_any()
+                .downcast_ref::<TimestampMicrosecondArray>()
+                .expect("timestamp(us, UTC) values"),
+            &expected
+        );
+        Ok(())
+    }
+
+    /// Write `id INT32 = 1` and `t INT96 = 1970-01-02 00:00:00.001` with the 
low-level Parquet
+    /// writer, adding `arrow_schema` to the footer as the `ARROW:schema` hint 
when given.
+    fn write_int96_file(arrow_schema: Option<&Schema>) -> Result<String, 
DataFusionError> {
+        use parquet::arrow::{encode_arrow_schema, ARROW_SCHEMA_META_KEY};
+        use parquet::data_type::{Int32Type as ParquetInt32, Int96, Int96Type};
+        use parquet::file::metadata::KeyValue;
+        use parquet::file::properties::WriterProperties;
+        use parquet::file::writer::SerializedFileWriter;
+        use parquet::schema::parser::parse_message_type;
+
+        let schema = Arc::new(parse_message_type(
+            "message spark_schema { REQUIRED INT32 id; OPTIONAL INT96 t; }",
+        )?);
+        let properties = WriterProperties::builder()
+            .set_key_value_metadata(arrow_schema.map(|arrow_schema| {
+                vec![KeyValue::new(
+                    ARROW_SCHEMA_META_KEY.to_string(),
+                    encode_arrow_schema(arrow_schema),
+                )]
+            }))
+            .build();
+        let mut t = Int96::new();
+        t.set_data(1_000_000, 0, 2_440_589);
+
+        let filename = get_temp_filename().to_str().unwrap().to_string();
+        let mut writer =
+            SerializedFileWriter::new(File::create(&filename)?, schema, 
Arc::new(properties))?;
+        let mut row_group = writer.next_row_group()?;
+        let mut id = row_group.next_column()?.unwrap();
+        id.typed::<ParquetInt32>().write_batch(&[1], None, None)?;
+        id.close()?;
+        let mut column = row_group.next_column()?.unwrap();
+        column
+            .typed::<Int96Type>()
+            .write_batch(&[t], Some(&[1]), None)?;
+        column.close()?;
+        row_group.close()?;
+        writer.close()?;
+        Ok(filename)
+    }
+
+    /// Read `id` and `t` of [`write_int96_file`] as `int` and `timestamp` 
with the filter
+    /// `id > 0`, with or without row filters, and check that `t` comes back.
+    async fn assert_int96_reads_as_timestamp(filename: &str) -> Result<(), 
DataFusionError> {
+        let micros_utc = DataType::Timestamp(TimeUnit::Microsecond, 
Some("UTC".into()));
+        let required = Arc::new(Schema::new(vec![
+            Field::new("id", DataType::Int32, false),
+            Field::new("t", micros_utc, true),
+        ]));
+        for row_filters in [false, true] {
+            let id_positive: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
+                Arc::new(Column::new("id", 0)),
+                Operator::Gt,
+                Arc::new(Literal::new(ScalarValue::Int32(Some(0)))),
+            ));
+            let mut stream = scan_parquet_file(
+                filename.to_string(),
+                Arc::clone(&required),
+                default_options(),
+                Some(id_positive),
+                row_filters,
+            )?;
+            let batch = stream.next().await.unwrap()?;
+            assert!(stream.next().await.is_none());
+            assert_eq!(
+                batch
+                    .column(1)
+                    .as_any()
+                    .downcast_ref::<TimestampMicrosecondArray>()
+                    .expect("timestamp(us, UTC) values"),
+                
&TimestampMicrosecondArray::from(vec![86_400_001_000i64]).with_timezone("UTC"),
+                "row filters: {row_filters}"
+            );
+        }
+        Ok(())
+    }
+
+    /// A plain INT96 column reads as `timestamp`, also with row filters, 
where the footer
+    /// check sees it before INT96 coercion, as a timezone-free nanosecond 
timestamp.
+    #[tokio::test]
+    async fn int96_reads_as_timestamp_with_row_filters() -> Result<(), 
DataFusionError> {
+        assert_int96_reads_as_timestamp(&write_int96_file(None)?).await
+    }
+
+    /// An INT96 column whose `ARROW:schema` hint says `timestamp('ns', 
tz='UTC')`, as PyArrow
+    /// writes with `use_deprecated_int96_timestamps=True`, keeps that 
timezone through INT96
+    /// coercion, and still reads as `timestamp`.
+    #[tokio::test]
+    async fn int96_with_arrow_timezone_hint_reads_as_timestamp() -> Result<(), 
DataFusionError> {
+        let hint = Schema::new(vec![
+            Field::new("id", DataType::Int32, false),
+            Field::new(
+                "t",
+                DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
+                true,
+            ),
+        ]);
+        assert_int96_reads_as_timestamp(&write_int96_file(Some(&hint))?).await
+    }
+
+    /// `TIMESTAMP(NANOS)` is rejected below a list too, with or without row 
filters and
+    /// whether or not it is UTC-adjusted, since the rule no longer has to 
leave timezone-free
+    /// nanosecond leaves below a list to INT96.
+    #[tokio::test]
+    async fn nanos_timestamp_array_read_as_timestamp_errors() -> Result<(), 
DataFusionError> {

Review Comment:
   `nanos_timestamp_array_read_as_timestamp_errors` reads a `TIMESTAMP(NANOS)` 
array with and without UTC adjustment and with row filters on and off, which 
are the cases `a` and `b` cover in the JVM test. It also repeats the `id > 0` 
`BinaryExpr` block that `assert_int96_reads_as_timestamp` has. What does it 
cover that the JVM test and 
`nanos_timestamp_rejection_keys_on_the_parquet_type` do not? If it is only the 
column path in the error text, could that assertion move into an existing test 
and this one go? The doc comment also describes the previous design ("no longer 
has to leave ... below a list to INT96"), so it needs rewording if the test 
stays.



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