Skip to content

Core: Add content stats evaluator - #17413

Draft
nastra wants to merge 12 commits into
apache:mainfrom
nastra:inclusive-stats-evaluator
Draft

Core: Add content stats evaluator#17413
nastra wants to merge 12 commits into
apache:mainfrom
nastra:inclusive-stats-evaluator

Conversation

@nastra

@nastra nastra commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@nastra
nastra marked this pull request as draft July 29, 2026 12:34
@github-actions github-actions Bot added API core Specification Issues that may introduce spec changes. labels Jul 29, 2026
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.expressions;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this forced promotion of package private v4 class into public and result in the revapi breakage. I am wondering if we shall consider the alternative below ?

  1. increase the visibility of InclusiveEvalVisitor into public
  2. move InclusiveStatsEvaluator to org.apache.iceberg package with package private class.

This avoid the unnecessary change to ContentStats, FieldStats and TrackedFile and revapi. We can eventually move InclusiveStatsEvaluator to org.apache.iceberg.expressions package once it's v4 ready.

This also require expose VariantExpressionUtil.castTo() as a protected method in InclusiveEvalVisitor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that's probably a good alternative. I only added the revapi exceptions to make CI pass but I wasn't actually planning on merging it (unless we would decide to make all of those classes public)

/cc @rdblue @anoopj

Comment thread format/spec.md Outdated

Field-level structs in `content_stats` are based on the corresponding table field's type, requirement, and ID (`field-id`).

Stats are tracked for primitive and `variant` fields. Stats are not tracked for `struct`, `list`, and `map` fields, or for any field contained in a `list` or `map` because it may be repeated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe consider move the spec to a separate PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep that's the plan. I'm currently also looking at an approach that wouldn't require any spec changes

Comment thread format/spec.md Outdated
| _optional_ | 3 | `tight_bounds` | `boolean` | all except `geometry`, `geography`, `variant` | When true, `lower_bound` and `upper_bound` must be equal to the min and max values |
| _optional_ | 4 | `value_count` | `long` | all | Number of values in the column (including null and NaN values) |
| _optional_ | 5 | `null_value_count` | `long` | optional fields | Number of null values in the column |
| _optional_ | 5 | `null_value_count` | `long` | fields that may be null | Number of null values in the column |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like we now omit the fields which is definitely required, null count is guaranteed to be 0. Curious, is this intentional depart from v1-v3 null_count?

}

return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method is very similar to InclusiveStatsEvaluator::allAncestorFieldsAreRequired, since both are purely schema based and have nothing to do with stats, maybe consider move to the schema.isNullable(int fieldId) instead as it already have the fieldId of all schema cached lazily?

In reader, it can use referencedIds.stream().filter(id -> !schema.isNullable(id)) and in writer it can be fieldStatsStruct(tableSchema.isNullable(id), ...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I've unified the functionality already into TypeUtil.isNullable()

Comment thread core/src/test/java/org/apache/iceberg/TestInclusiveStatsEvaluator.java Outdated
nastra added 4 commits July 30, 2026 10:23
The single_value_nulls fixtures in testNotEqWithSingleValue and
testNotInWithSingleValue reported 2 nulls for field 3, the "required"
column. A required column cannot contain nulls, so no writer produces
those metrics. Move both to the optional "some_empty" column,
which keeps the case the tests cover: a file whose bounds hold a single
value still has to be read when the column also contains nulls, because
those nulls match != and NOT IN.

The NOT IN fixture also reported a NaN count for a string column, which
only float and double columns track.
The range_of_values and single_value fixtures reported a NaN count for
field 3, a string column. Only float and double columns track NaN counts,
which is why the single_value fixture in testNotEqWithSingleValue already
passed null for them.

Generated-by: Cursor
`FILE_6` in `TestInclusiveMetricsEvaluator` carried value and null counts for the
`required_address` and `optional_address` structs. Parquet produces metrics for leaf
columns only in `ParquetMetrics.MetricsVisitor.struct` and returns just the concatenation of
its children and never emits a `FieldMetrics` for the struct — so no Parquet file yields
those entries. They made `notNull("optional_address")` appear prunable when in practice it is not
…luatorWithExtract/TestInclusiveMetricsEvaluatorWithTransforms extendable
@nastra
nastra force-pushed the inclusive-stats-evaluator branch 2 times, most recently from a4e91ed to d505d89 Compare July 30, 2026 13:00
nastra added 7 commits July 30, 2026 15:05
Content stats omit null_value_count for a field that cannot be null, but
InclusiveStatsEvaluator read the missing count as unknown and stopped
pruning files for notEqual, notIn, and notStartsWith. The v3 metrics maps
carry an explicit zero from Parquet, so the same filters pruned there.
Detect the fields that cannot be null the same way expression binding
does, by requiring the field and all of its ancestors to be required, and
report no nulls for them.
The content stats schema omitted null_value_count whenever a field was
required, but a required field nested in an optional struct is null on
every row where its parent is null, and Parquet reports that count from
the leaf column's definition levels. Include the count for any field that
may be null, either because it or one of its ancestors is optional.
@nastra
nastra force-pushed the inclusive-stats-evaluator branch from d505d89 to 1f62852 Compare July 30, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API core Specification Issues that may introduce spec changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants