peterxcli commented on code in PR #5177:
URL: https://github.com/apache/datafusion-comet/pull/5177#discussion_r3930271776
##########
native/core/src/parquet/parquet_support.rs:
##########
@@ -195,6 +217,28 @@ fn parquet_convert_array(
list_arr.nulls().cloned(),
)))
}
+ (
+ Timestamp(TimeUnit::Millisecond, _),
+ Timestamp(TimeUnit::Microsecond, target_tz),
+ ) if top_level && parquet_options.checked_timestamp_overflow => {
+ // Spark's Parquet reader calls the checked `millisToMicros`
conversion for both
+ // direct and dictionary values, independent of CAST evaluation
mode:
+ //
https://github.com/apache/spark/blob/v4.2.0/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java#L817-L833
+ // `millisToMicros` uses `Math.multiplyExact`:
+ //
https://github.com/apache/spark/blob/v4.2.0/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala#L103-L108
+ //
+ // The checked conversion is limited to TOP-LEVEL columns. Spark
only avoids the
+ // error for filtered-out values through row-group statistics
pruning, and
+ // DataFusion's PruningPredicate does not support nested fields
yet, so a checked
+ // conversion on a nested field would fail queries whose
predicates Spark prunes
+ // (e.g. `WHERE s.ts < X` over an all-overflowing file). Nested
fields keep the
+ // pre-existing safe-cast behavior below (overflow -> NULL).
+ let micros = array
+ .as_primitive::<TimestampMillisecondType>()
+ .try_unary::<_, TimestampMicrosecondType, _>(|value|
value.mul_checked(1_000))?
Review Comment:
Fixed in c0ad32ab9. Filtered scans now retain the existing safe timestamp
cast, so checked conversion cannot run before a Spark-only pruning path. The
regression alternates id values 1 and 3, verifies the enabled case has
dictionary-only data pages, and checks id = 2 across ANSI and row-filter
settings. The focused Spark suite passed.
##########
native/core/src/parquet/parquet_support.rs:
##########
@@ -195,6 +217,28 @@ fn parquet_convert_array(
list_arr.nulls().cloned(),
)))
}
+ (
+ Timestamp(TimeUnit::Millisecond, _),
+ Timestamp(TimeUnit::Microsecond, target_tz),
+ ) if top_level && parquet_options.checked_timestamp_overflow => {
Review Comment:
Fixed in c0ad32ab9. Every filtered scan now keeps the existing safe cast,
including filters whose physical column needs schema-evolution adaptation. The
regression reads INT32 id as LongType, filters id < 0, projects the overflowing
timestamp, and runs across the same configuration matrix when schema evolution
is enabled. The focused Spark suite passed.
##########
native/core/src/parquet/schema_adapter.rs:
##########
@@ -522,7 +534,220 @@ impl PhysicalExprAdapter for SparkPhysicalExprAdapter {
}
}
+/// The wrapped expression and the file column's timezone of a millis->micros
cast.
+type MillisCastParts = (Arc<dyn PhysicalExpr>, Option<Arc<str>>);
+
+/// If `expr` is a checked millis->micros [`CometCastColumnExpr`], return its
wrapped
+/// expression and the file column's timezone.
+fn as_millis_to_micros_cast(expr: &Arc<dyn PhysicalExpr>) ->
Option<MillisCastParts> {
+ let cast = expr.downcast_ref::<CometCastColumnExpr>()?;
+ match (
+ cast.input_physical_field().data_type(),
+ cast.target_field().data_type(),
+ ) {
+ (
+ DataType::Timestamp(TimeUnit::Millisecond, file_tz),
+ DataType::Timestamp(TimeUnit::Microsecond, _),
+ ) => Some((Arc::clone(cast.expr()), file_tz.clone())),
+ _ => None,
+ }
+}
+
+/// If `expr` is a microsecond timestamp literal (null or not), return its
value.
+fn as_micros_literal(expr: &Arc<dyn PhysicalExpr>) -> Option<Option<i64>> {
+ match expr.downcast_ref::<Literal>()?.value() {
+ ScalarValue::TimestampMicrosecond(micros, _) => Some(*micros),
+ _ => None,
+ }
+}
+
impl SparkPhysicalExprAdapter {
+ /// Rewrite predicate expressions over a `TIMESTAMP_MILLIS` file column
read as
+ /// microseconds into the millisecond domain — comparisons (including
null-safe
+ /// `<=>`), `IN` lists, and null checks — mirroring Spark's
`ParquetFilters`,
+ /// which pushes timestamp predicates down in the file's physical unit:
+ ///
https://github.com/apache/spark/blob/v4.2.0/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFilters.scala#L192-L196
+ ///
+ /// The wrapped `CometCastColumnExpr` is opaque to DataFusion's
pruning-predicate
+ /// analyzer, so leaving the conversion inside the predicate defeats
row-group
+ /// statistics pruning — and the checked conversion then raises overflow
errors for
+ /// values Spark never reads, because Spark prunes them from the
millisecond
+ /// statistics. Comparing raw millisecond values against a rescaled
literal is exact
+ /// (`m * 1000 OP lit` over the integers), never overflows, and DataFusion
can prune
+ /// with it. Values that survive the filter still go through the checked
conversion
+ /// when the scan output is adapted, so genuine reads of overflowing
values keep
+ /// failing like Spark's `millisToMicros`.
+ fn rewrite_millis_timestamp_comparison(
+ &self,
+ expr: Arc<dyn PhysicalExpr>,
+ ) -> DataFusionResult<Transformed<Arc<dyn PhysicalExpr>>> {
+ // `ts IS NULL` / `ts IS NOT NULL`: the checked conversion preserves
null-ness
+ // (it errors rather than producing nulls), so test the raw column
directly.
+ if let Some(is_null) = expr.downcast_ref::<IsNullExpr>() {
+ if let Some((inner, _)) = as_millis_to_micros_cast(is_null.arg()) {
+ return Ok(Transformed::yes(Arc::new(IsNullExpr::new(inner))));
+ }
+ }
+ if let Some(is_not_null) = expr.downcast_ref::<IsNotNullExpr>() {
+ if let Some((inner, _)) =
as_millis_to_micros_cast(is_not_null.arg()) {
+ return
Ok(Transformed::yes(Arc::new(IsNotNullExpr::new(inner))));
+ }
+ }
+
+ // `ts IN (...)` / `ts NOT IN (...)`: rescale every microsecond
literal in the
+ // list. DataFusion's pruning predicate understands `InListExpr` (up
to 20
+ // elements), so keeping the raw column visible restores row-group
pruning,
+ // mirroring Spark's `ParquetFilters` handling of `In`.
+ if let Some(in_list_expr) = expr.downcast_ref::<InListExpr>() {
+ if let Some((inner, file_tz)) =
as_millis_to_micros_cast(in_list_expr.expr()) {
+ return self.rewrite_millis_in_list(&expr, in_list_expr, inner,
file_tz);
+ }
+ }
+
+ let Some(binary) = expr.downcast_ref::<BinaryExpr>() else {
+ return Ok(Transformed::no(expr));
+ };
+ let matched = if let (Some(cast), Some(micros)) = (
+ as_millis_to_micros_cast(binary.left()),
+ as_micros_literal(binary.right()),
+ ) {
+ Some((cast, *binary.op(), micros))
+ } else if let (Some(micros), Some(cast)) = (
+ as_micros_literal(binary.left()),
+ as_millis_to_micros_cast(binary.right()),
+ ) {
+ binary.op().swap().map(|op| (cast, op, micros))
+ } else {
+ None
+ };
+ let Some(((inner, file_tz), op, micros)) = matched else {
+ return Ok(Transformed::no(expr));
+ };
+
+ let Some(micros) = micros else {
+ // NULL literal. The ordinary comparisons return NULL for every
row on both
+ // sides of the rewrite, so rescaling the literal to a NULL
millisecond
+ // literal is exact. The null-safe comparisons test null-ness of
the value,
+ // which the checked conversion preserves, so probe the raw column.
+ let rewritten: Arc<dyn PhysicalExpr> = match op {
+ Operator::IsNotDistinctFrom =>
Arc::new(IsNullExpr::new(inner)),
+ Operator::IsDistinctFrom =>
Arc::new(IsNotNullExpr::new(inner)),
+ Operator::Lt
+ | Operator::LtEq
+ | Operator::Gt
+ | Operator::GtEq
+ | Operator::Eq
+ | Operator::NotEq => Arc::new(BinaryExpr::new(
+ inner,
+ op,
+ Arc::new(Literal::new(ScalarValue::TimestampMillisecond(
+ None, file_tz,
+ ))),
+ )),
+ _ => return Ok(Transformed::no(expr)),
+ };
+ return Ok(Transformed::yes(rewritten));
+ };
+
+ // For a file value `m` (milliseconds) the logical value is exactly `m
* 1000`
+ // microseconds, so `m * 1000 OP L` rewrites to an exact comparison on
`m`.
+ let floor = micros.div_euclid(1_000);
+ let ceil = floor + i64::from(micros.rem_euclid(1_000) != 0);
+ let exact = micros % 1_000 == 0;
+ let (op, millis) = match op {
+ Operator::Lt => (Operator::Lt, ceil),
+ Operator::LtEq => (Operator::LtEq, floor),
+ Operator::Gt => (Operator::Gt, floor),
+ Operator::GtEq => (Operator::GtEq, ceil),
+ Operator::Eq if exact => (Operator::Eq, floor),
+ Operator::NotEq if exact => (Operator::NotEq, floor),
+ Operator::IsNotDistinctFrom if exact =>
(Operator::IsNotDistinctFrom, floor),
+ Operator::IsDistinctFrom if exact => (Operator::IsDistinctFrom,
floor),
+ // A sub-millisecond literal can never equal `m * 1000`. `col <
i64::MIN`
+ // (resp. `col >= i64::MIN`) is false (resp. true) for every
non-null value
+ // and NULL for nulls, matching `=` / `!=` null semantics while
remaining a
+ // plain, prunable comparison.
+ Operator::Eq => (Operator::Lt, i64::MIN),
+ Operator::NotEq => (Operator::GtEq, i64::MIN),
+ // The null-safe comparisons never return NULL, so an impossible
literal
+ // folds to a constant. DataFusion's pruning predicate evaluates
boolean
+ // literals, so `false` still prunes every row group.
+ Operator::IsNotDistinctFrom => {
+ return Ok(Transformed::yes(Arc::new(Literal::new(
+ ScalarValue::Boolean(Some(false)),
+ ))));
+ }
+ Operator::IsDistinctFrom => {
+ return Ok(Transformed::yes(Arc::new(Literal::new(
+ ScalarValue::Boolean(Some(true)),
+ ))));
+ }
+ _ => return Ok(Transformed::no(expr)),
+ };
+ let literal = Arc::new(Literal::new(ScalarValue::TimestampMillisecond(
+ Some(millis),
+ file_tz,
+ )));
+ Ok(Transformed::yes(Arc::new(BinaryExpr::new(
+ inner, op, literal,
+ ))))
+ }
+
+ /// Rewrite `ts IN (...)` / `ts NOT IN (...)` over a millis->micros
conversion into
+ /// the millisecond domain. Divisible literals rescale exactly;
sub-millisecond
+ /// literals can never equal `m * 1000` and drop out of the list, which
preserves
+ /// IN's three-valued logic (a dropped element contributes `false` to the
OR / `true`
+ /// to the AND for every non-null probe, and the null-probe result stays
NULL either
+ /// way). A list emptied this way degenerates to the same always-false /
+ /// always-true-with-null-semantics sentinels the `=` / `!=` rewrite uses.
+ fn rewrite_millis_in_list(
+ &self,
+ original: &Arc<dyn PhysicalExpr>,
+ in_list_expr: &InListExpr,
+ inner: Arc<dyn PhysicalExpr>,
+ file_tz: Option<Arc<str>>,
+ ) -> DataFusionResult<Transformed<Arc<dyn PhysicalExpr>>> {
+ let mut millis_list: Vec<Arc<dyn PhysicalExpr>> = Vec::new();
+ for item in in_list_expr.list() {
+ // Any non-literal or non-timestamp element: leave the expression
alone.
+ let Some(micros) = as_micros_literal(item) else {
+ return Ok(Transformed::no(Arc::clone(original)));
+ };
+ match micros {
+ None => millis_list.push(Arc::new(Literal::new(
+ ScalarValue::TimestampMillisecond(None, file_tz.clone()),
+ ))),
+ Some(v) if v % 1_000 == 0 =>
millis_list.push(Arc::new(Literal::new(
+ ScalarValue::TimestampMillisecond(Some(v / 1_000),
file_tz.clone()),
+ ))),
+ Some(_) => {}
+ }
+ }
+ let negated = in_list_expr.negated();
+ if millis_list.is_empty() {
+ let (op, sentinel) = if negated {
+ (Operator::GtEq, i64::MIN)
+ } else {
+ (Operator::Lt, i64::MIN)
+ };
+ return Ok(Transformed::yes(Arc::new(BinaryExpr::new(
+ inner,
+ op,
+ Arc::new(Literal::new(ScalarValue::TimestampMillisecond(
+ Some(sentinel),
+ file_tz,
+ ))),
+ ))));
+ }
+ let rewritten = in_list(
Review Comment:
Fixed in c0ad32ab9 by removing the finite predicate rewrite and using the
filtered-scan fallback before conversion. The regression covers both 20- and
21-value timestamp IN lists across plain/dictionary, ANSI, and row-filter
settings, with the overflowing timestamp projected. The focused Spark suite
passed.
##########
native/core/src/parquet/parquet_support.rs:
##########
@@ -195,6 +195,21 @@ fn parquet_convert_array(
list_arr.nulls().cloned(),
)))
}
+ (
+ Timestamp(TimeUnit::Millisecond, _),
+ Timestamp(TimeUnit::Microsecond, target_tz),
+ ) => {
+ // Spark's Parquet reader calls the checked `millisToMicros`
conversion for both
+ // direct and dictionary values, independent of CAST evaluation
mode:
+ //
https://github.com/apache/spark/blob/v4.2.0/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java#L817-L833
+ // `millisToMicros` uses `Math.multiplyExact`:
+ //
https://github.com/apache/spark/blob/v4.2.0/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala#L103-L108
+ let micros = array
+ .as_primitive::<TimestampMillisecondType>()
+ .try_unary::<_, TimestampMicrosecondType, _>(|value|
value.mul_checked(1_000))?
+ .with_timezone_opt(target_tz.clone());
Review Comment:
Superseded in c0ad32ab9 by the simpler filtered-scan fallback. Ordinary
comparison, short and long IN, null-safe equality, nested predicates,
dictionary exclusion, and widened filter columns no longer checked-convert
values before pruning; unfiltered reads remain checked. The focused Spark
regressions pass.
##########
native/core/src/parquet/schema_adapter.rs:
##########
@@ -522,7 +534,220 @@ impl PhysicalExprAdapter for SparkPhysicalExprAdapter {
}
}
+/// The wrapped expression and the file column's timezone of a millis->micros
cast.
+type MillisCastParts = (Arc<dyn PhysicalExpr>, Option<Arc<str>>);
+
+/// If `expr` is a checked millis->micros [`CometCastColumnExpr`], return its
wrapped
+/// expression and the file column's timezone.
+fn as_millis_to_micros_cast(expr: &Arc<dyn PhysicalExpr>) ->
Option<MillisCastParts> {
+ let cast = expr.downcast_ref::<CometCastColumnExpr>()?;
+ match (
+ cast.input_physical_field().data_type(),
+ cast.target_field().data_type(),
+ ) {
+ (
+ DataType::Timestamp(TimeUnit::Millisecond, file_tz),
+ DataType::Timestamp(TimeUnit::Microsecond, _),
+ ) => Some((Arc::clone(cast.expr()), file_tz.clone())),
+ _ => None,
+ }
+}
+
+/// If `expr` is a microsecond timestamp literal (null or not), return its
value.
+fn as_micros_literal(expr: &Arc<dyn PhysicalExpr>) -> Option<Option<i64>> {
+ match expr.downcast_ref::<Literal>()?.value() {
+ ScalarValue::TimestampMicrosecond(micros, _) => Some(*micros),
+ _ => None,
+ }
+}
+
impl SparkPhysicalExprAdapter {
+ /// Rewrite predicate expressions over a `TIMESTAMP_MILLIS` file column
read as
+ /// microseconds into the millisecond domain — comparisons (including
null-safe
+ /// `<=>`), `IN` lists, and null checks — mirroring Spark's
`ParquetFilters`,
+ /// which pushes timestamp predicates down in the file's physical unit:
+ ///
https://github.com/apache/spark/blob/v4.2.0/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFilters.scala#L192-L196
+ ///
+ /// The wrapped `CometCastColumnExpr` is opaque to DataFusion's
pruning-predicate
+ /// analyzer, so leaving the conversion inside the predicate defeats
row-group
+ /// statistics pruning — and the checked conversion then raises overflow
errors for
+ /// values Spark never reads, because Spark prunes them from the
millisecond
+ /// statistics. Comparing raw millisecond values against a rescaled
literal is exact
+ /// (`m * 1000 OP lit` over the integers), never overflows, and DataFusion
can prune
+ /// with it. Values that survive the filter still go through the checked
conversion
+ /// when the scan output is adapted, so genuine reads of overflowing
values keep
+ /// failing like Spark's `millisToMicros`.
+ fn rewrite_millis_timestamp_comparison(
+ &self,
+ expr: Arc<dyn PhysicalExpr>,
+ ) -> DataFusionResult<Transformed<Arc<dyn PhysicalExpr>>> {
+ // `ts IS NULL` / `ts IS NOT NULL`: the checked conversion preserves
null-ness
+ // (it errors rather than producing nulls), so test the raw column
directly.
+ if let Some(is_null) = expr.downcast_ref::<IsNullExpr>() {
+ if let Some((inner, _)) = as_millis_to_micros_cast(is_null.arg()) {
+ return Ok(Transformed::yes(Arc::new(IsNullExpr::new(inner))));
+ }
+ }
+ if let Some(is_not_null) = expr.downcast_ref::<IsNotNullExpr>() {
+ if let Some((inner, _)) =
as_millis_to_micros_cast(is_not_null.arg()) {
+ return
Ok(Transformed::yes(Arc::new(IsNotNullExpr::new(inner))));
+ }
+ }
+
+ // `ts IN (...)` / `ts NOT IN (...)`: rescale every microsecond
literal in the
+ // list. DataFusion's pruning predicate understands `InListExpr` (up
to 20
+ // elements), so keeping the raw column visible restores row-group
pruning,
+ // mirroring Spark's `ParquetFilters` handling of `In`.
+ if let Some(in_list_expr) = expr.downcast_ref::<InListExpr>() {
+ if let Some((inner, file_tz)) =
as_millis_to_micros_cast(in_list_expr.expr()) {
+ return self.rewrite_millis_in_list(&expr, in_list_expr, inner,
file_tz);
+ }
+ }
+
+ let Some(binary) = expr.downcast_ref::<BinaryExpr>() else {
+ return Ok(Transformed::no(expr));
+ };
+ let matched = if let (Some(cast), Some(micros)) = (
+ as_millis_to_micros_cast(binary.left()),
+ as_micros_literal(binary.right()),
+ ) {
+ Some((cast, *binary.op(), micros))
+ } else if let (Some(micros), Some(cast)) = (
+ as_micros_literal(binary.left()),
+ as_millis_to_micros_cast(binary.right()),
+ ) {
+ binary.op().swap().map(|op| (cast, op, micros))
+ } else {
+ None
+ };
+ let Some(((inner, file_tz), op, micros)) = matched else {
+ return Ok(Transformed::no(expr));
+ };
+
+ let Some(micros) = micros else {
+ // NULL literal. The ordinary comparisons return NULL for every
row on both
+ // sides of the rewrite, so rescaling the literal to a NULL
millisecond
+ // literal is exact. The null-safe comparisons test null-ness of
the value,
+ // which the checked conversion preserves, so probe the raw column.
+ let rewritten: Arc<dyn PhysicalExpr> = match op {
+ Operator::IsNotDistinctFrom =>
Arc::new(IsNullExpr::new(inner)),
+ Operator::IsDistinctFrom =>
Arc::new(IsNotNullExpr::new(inner)),
+ Operator::Lt
+ | Operator::LtEq
+ | Operator::Gt
+ | Operator::GtEq
+ | Operator::Eq
+ | Operator::NotEq => Arc::new(BinaryExpr::new(
+ inner,
+ op,
+ Arc::new(Literal::new(ScalarValue::TimestampMillisecond(
+ None, file_tz,
+ ))),
+ )),
+ _ => return Ok(Transformed::no(expr)),
+ };
+ return Ok(Transformed::yes(rewritten));
+ };
+
+ // For a file value `m` (milliseconds) the logical value is exactly `m
* 1000`
+ // microseconds, so `m * 1000 OP L` rewrites to an exact comparison on
`m`.
+ let floor = micros.div_euclid(1_000);
+ let ceil = floor + i64::from(micros.rem_euclid(1_000) != 0);
+ let exact = micros % 1_000 == 0;
+ let (op, millis) = match op {
+ Operator::Lt => (Operator::Lt, ceil),
+ Operator::LtEq => (Operator::LtEq, floor),
+ Operator::Gt => (Operator::Gt, floor),
+ Operator::GtEq => (Operator::GtEq, ceil),
+ Operator::Eq if exact => (Operator::Eq, floor),
+ Operator::NotEq if exact => (Operator::NotEq, floor),
+ Operator::IsNotDistinctFrom if exact =>
(Operator::IsNotDistinctFrom, floor),
+ Operator::IsDistinctFrom if exact => (Operator::IsDistinctFrom,
floor),
+ // A sub-millisecond literal can never equal `m * 1000`. `col <
i64::MIN`
+ // (resp. `col >= i64::MIN`) is false (resp. true) for every
non-null value
+ // and NULL for nulls, matching `=` / `!=` null semantics while
remaining a
+ // plain, prunable comparison.
+ Operator::Eq => (Operator::Lt, i64::MIN),
+ Operator::NotEq => (Operator::GtEq, i64::MIN),
+ // The null-safe comparisons never return NULL, so an impossible
literal
+ // folds to a constant. DataFusion's pruning predicate evaluates
boolean
+ // literals, so `false` still prunes every row group.
+ Operator::IsNotDistinctFrom => {
+ return Ok(Transformed::yes(Arc::new(Literal::new(
+ ScalarValue::Boolean(Some(false)),
+ ))));
+ }
+ Operator::IsDistinctFrom => {
+ return Ok(Transformed::yes(Arc::new(Literal::new(
+ ScalarValue::Boolean(Some(true)),
+ ))));
+ }
+ _ => return Ok(Transformed::no(expr)),
+ };
+ let literal = Arc::new(Literal::new(ScalarValue::TimestampMillisecond(
+ Some(millis),
+ file_tz,
+ )));
+ Ok(Transformed::yes(Arc::new(BinaryExpr::new(
+ inner, op, literal,
+ ))))
+ }
+
+ /// Rewrite `ts IN (...)` / `ts NOT IN (...)` over a millis->micros
conversion into
+ /// the millisecond domain. Divisible literals rescale exactly;
sub-millisecond
+ /// literals can never equal `m * 1000` and drop out of the list, which
preserves
+ /// IN's three-valued logic (a dropped element contributes `false` to the
OR / `true`
+ /// to the AND for every non-null probe, and the null-probe result stays
NULL either
+ /// way). A list emptied this way degenerates to the same always-false /
+ /// always-true-with-null-semantics sentinels the `=` / `!=` rewrite uses.
+ fn rewrite_millis_in_list(
+ &self,
+ original: &Arc<dyn PhysicalExpr>,
+ in_list_expr: &InListExpr,
+ inner: Arc<dyn PhysicalExpr>,
+ file_tz: Option<Arc<str>>,
+ ) -> DataFusionResult<Transformed<Arc<dyn PhysicalExpr>>> {
+ let mut millis_list: Vec<Arc<dyn PhysicalExpr>> = Vec::new();
+ for item in in_list_expr.list() {
+ // Any non-literal or non-timestamp element: leave the expression
alone.
+ let Some(micros) = as_micros_literal(item) else {
+ return Ok(Transformed::no(Arc::clone(original)));
+ };
+ match micros {
+ None => millis_list.push(Arc::new(Literal::new(
+ ScalarValue::TimestampMillisecond(None, file_tz.clone()),
+ ))),
+ Some(v) if v % 1_000 == 0 =>
millis_list.push(Arc::new(Literal::new(
+ ScalarValue::TimestampMillisecond(Some(v / 1_000),
file_tz.clone()),
+ ))),
+ Some(_) => {}
+ }
+ }
+ let negated = in_list_expr.negated();
+ if millis_list.is_empty() {
Review Comment:
Fixed in c0ad32ab9 by deleting the millis-domain predicate rewrite, so an
originally empty NOT IN keeps DataFusion and Spark semantics. The regression
uses NULL plus epoch, disables OptimizeIn and ConvertToLocalRelation, sets
nonlegacy empty-list behavior, and passes with row-filter pushdown both off and
on.
--
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]