@@ -96,27 +96,63 @@ object StatsColumn {
9696 * NOTE: It would be more accurate to call these "file keeping" predicates, because they specify the
9797 * set of files a query must examine, not the set of rows a query can safely skip.
9898 */
99- private [sql] case class DataSkippingPredicate (
100- expr : Column ,
101- referencedStats : Set [StatsColumn ]
102- )
99+ private [sql] class DataSkippingPredicate private (
100+ val expr : Column ,
101+ val referencedStats : Set [StatsColumn ],
102+ validatedExpr : (Set [StatsColumn ] => Column ) => Column ) {
103+ /**
104+ * Returns a file-level predicate that is safe to evaluate against stats.
105+ *
106+ * `expr` can only be trusted when all referenced stats are present for a file. This method adds
107+ * the required missing-stats checks while preserving the predicate's boolean shape, so one side
108+ * of an AND can still skip a file when the other side's stats are missing.
109+ */
110+ def withStatsValidation (verifyStatsForFilter : Set [StatsColumn ] => Column ): Column = {
111+ validatedExpr(verifyStatsForFilter)
112+ }
113+ }
103114
104- /**
105- * Overloads the constructor for `DataSkippingPredicate`, allowing callers to pass referenced stats
106- * as individual arguments, rather than wrapped up as a Set.
107- *
108- * For example, instead of this:
109- *
110- * DataSkippingPredicate(pred, Set(stat1, stat2))
111- *
112- * We can just do:
113- *
114- * DataSkippingPredicate(pred, stat1, stat2)
115- */
116115private [sql] object DataSkippingPredicate {
116+ def apply (filters : Column , referencedStats : Set [StatsColumn ]): DataSkippingPredicate = {
117+ new DataSkippingPredicate (
118+ filters,
119+ referencedStats,
120+ verifyStatsForFilter => filters || ! verifyStatsForFilter(referencedStats))
121+ }
122+
123+ /**
124+ * Overloads the constructor for `DataSkippingPredicate`, allowing callers to pass referenced
125+ * stats as individual arguments, rather than wrapped up as a Set.
126+ *
127+ * For example, instead of this:
128+ *
129+ * DataSkippingPredicate(pred, Set(stat1, stat2))
130+ *
131+ * We can just do:
132+ *
133+ * DataSkippingPredicate(pred, stat1, stat2)
134+ */
117135 def apply (filters : Column , referencedStats : StatsColumn * ): DataSkippingPredicate = {
118136 DataSkippingPredicate (filters, referencedStats.toSet)
119137 }
138+
139+ def and (left : DataSkippingPredicate , right : DataSkippingPredicate ): DataSkippingPredicate = {
140+ new DataSkippingPredicate (
141+ left.expr && right.expr,
142+ left.referencedStats ++ right.referencedStats,
143+ verifyStatsForFilter =>
144+ left.withStatsValidation(verifyStatsForFilter) &&
145+ right.withStatsValidation(verifyStatsForFilter))
146+ }
147+
148+ def or (left : DataSkippingPredicate , right : DataSkippingPredicate ): DataSkippingPredicate = {
149+ new DataSkippingPredicate (
150+ left.expr || right.expr,
151+ left.referencedStats ++ right.referencedStats,
152+ verifyStatsForFilter =>
153+ left.withStatsValidation(verifyStatsForFilter) ||
154+ right.withStatsValidation(verifyStatsForFilter))
155+ }
120156}
121157
122158/**
@@ -619,13 +655,14 @@ trait DataSkippingReaderBase
619655 val (partitionSize, partitionFilter) = buildSizeCollectorFilter()
620656 val (scanSize, scanFilter) = buildSizeCollectorFilter()
621657
622- // NOTE: If any stats are missing, the value of `dataFilters` is untrustworthy -- it could be
623- // NULL or even just plain incorrect. We rely on `verifyStatsForFilter` to be FALSE in that
624- // case, forcing the overall OR to evaluate as TRUE no matter what value `dataFilters` takes.
658+ // NOTE: If a filter's required stats are missing, that filter's value is untrustworthy -- it
659+ // could be NULL or even just plain incorrect. Missing stats force that filter's file-keeping
660+ // predicate to TRUE, but compound filters preserve their boolean shape. This lets one side of
661+ // an AND still prune a file even if the other side's stats are missing.
625662 val filteredFiles = withStats.where(
626663 totalFilter(trueLiteral) &&
627664 partitionFilter(partitionFilters) &&
628- scanFilter(dataFilters.expr || ! verifyStatsForFilter(dataFilters.referencedStats ))
665+ scanFilter(dataFilters.withStatsValidation(verifyStatsForFilter ))
629666 )
630667
631668 val statsColumn = if (keepNumRecords) {
@@ -796,9 +833,7 @@ trait DataSkippingReaderBase
796833
797834 val finalSkippingFilters = skippingFilters
798835 .map(_._2.get)
799- .reduceOption((skip1, skip2) => DataSkippingPredicate (
800- // Fold the filters into a conjunction, while unioning their referencedStats.
801- skip1.expr && skip2.expr, skip1.referencedStats ++ skip2.referencedStats))
836+ .reduceOption(DataSkippingPredicate .and)
802837 .getOrElse(DataSkippingPredicate (trueLiteral))
803838
804839 val (files, sizes) = {
0 commit comments