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


##########
spark/src/test/scala/org/apache/comet/CometIcebergNativeSuite.scala:
##########
@@ -4306,6 +4306,229 @@ class CometIcebergNativeSuite
     }
   }
 
+  /**
+   * Compares `SELECT id FROM table WHERE predicate` with Spark for each 
predicate, alone and
+   * after `l IS NOT NULL AND`. That array null check is a conjunct the serde 
never pushes, as in
+   * the queries that found these bugs, so each predicate is also checked in a 
residual left
+   * unpushed.
+   */
+  private def checkResidualPredicates(table: String, predicates: Seq[String]): 
Unit =
+    for (predicate <- predicates; prefix <- Seq("", "l IS NOT NULL AND ")) {

Review Comment:
   With the prefix, the residual never converts. The AND branch needs both 
sides, and the null check on the array column `l` is declined by 
`isComplexType`, so each prefixed query reruns the unprefixed one with nothing 
pushed. The docstring says as much. That doubles the query count of the three 
new tests without reaching the code this PR changes. The prefix starts to 
matter with partial conjuncts in #6847. Could the prefix and the `l` column 
move there, so this PR and its backports only run the cases they fix?



##########
spark/src/main/scala/org/apache/comet/serde/operator/CometIcebergNativeScan.scala:
##########
@@ -720,6 +738,24 @@ object CometIcebergNativeScan extends 
CometOperatorSerde[CometBatchScanExec] wit
               case IS_NULL => Some(unaryPredicate(columnName, 
IcebergPredicateOperator.IsNull))
               case IS_NOT_NULL | NOT_NULL =>
                 Some(unaryPredicate(columnName, 
IcebergPredicateOperator.NotNull))
+              // Files written before the promotion still hold the older type, 
which iceberg-rust
+              // compares with a literal incorrectly (see 
IcebergReflection.promotedColumns),
+              // except for equality outside NOT: pruning keeps every row 
group and page for it,
+              // and a literal the older type cannot hold matches no row there 
anyway.
+              case op if promotedColumns.contains(columnName) && (op != EQ || 
underNot) =>
+                None
+              // Iceberg converts Spark's null-safe `id <=> 1` to `id = 1`, 
which Spark evaluates
+              // to false for a NULL id and the native reader to NULL. Both 
reject the row, but
+              // for `NOT (id <=> 1)` Spark keeps it while the native reader 
still drops it. A
+              // NOT_NULL conjunct of an enclosing AND makes the column 
non-nullable here.
+              case EQ if underNot && attribute.nullable => None
+              // Spark sorts every NaN above all numbers. The native row 
filter sorts negative NaNs
+              // below them, and iceberg-rust prunes row groups on min/max 
statistics that can
+              // leave NaN out, or hold it as both when parquet-mr 1.9 or 
earlier starts a row
+              // group with NaN (apache/iceberg-rust#3494).

Review Comment:
   apache/iceberg-rust#3494 was closed on 2026-10-23 by 
apache/iceberg-rust#3502 ("treat NaN row group bounds as might-match for 
inequalities"), which is 54 commits past the pinned `1f3bc34`. Could the 
comment name #3502, so the next pin bump knows which part of this exclusion it 
can revisit? The fix is for row-group bounds, so the page index side would 
still need checking then.



##########
spark/src/main/scala/org/apache/comet/iceberg/IcebergReflection.scala:
##########
@@ -1216,6 +1216,48 @@ object IcebergReflection extends Logging {
     }
   }
 
+  /**
+   * Top-level column names of `schema` whose type differs in some schema of 
the table's history
+   * (`table.schemas()`), such as an int column promoted to long. Data files 
written before the
+   * promotion keep the older type, and iceberg-rust compares literals against 
it incorrectly: the
+   * page index skips every page for range and IN predicates, and the row 
filter casts the literal
+   * to the older type, so an out-of-range value becomes NULL 
(apache/iceberg-rust#3122).
+   *
+   * Expiring snapshots with `cleanExpiredMetadata` removes schemas that no 
retained snapshot
+   * uses, even though files written under them can remain. Iceberg numbers 
schemas consecutively
+   * from 0, so a gap means the history is incomplete, and every column of a 
type that a promotion
+   * can produce (long, double, decimal) is returned. The history cannot 
reveal a file whose type
+   * differs without a recorded schema change, such as one imported by 
`add_files`, `migrate` or
+   * `snapshot`, or one promoted under metadata from before Iceberg 0.12, 
which kept only the
+   * current schema. Called at serialization time, so it throws on reflection 
failure.
+   */
+  def promotedColumns(table: Any, schema: Any): Set[String] = {
+    import scala.jdk.CollectionConverters._
+    val findField = getMethod(schema.getClass, "findField", classOf[Int])
+    def typeOf(field: Any): String = getMethod(field.getClass, 
"type").invoke(field).toString

Review Comment:
   `fieldTypeOf` at line 1416 already does this lookup, so this can be 
`fieldTypeOf(field).toString`, the way this method already reuses `fieldIdOf`.



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