laskoviymishka commented on code in PR #2006:
URL: https://github.com/apache/iceberg-go/pull/2006#discussion_r4064217621


##########
table/internal/parquet_files.go:
##########
@@ -1990,6 +2038,23 @@ func (w wrapPqArrowReader) GetRecords(ctx 
context.Context, cols []int, tester an
                                }
                        }
 
+                       if use && dictionaryPruningActive {
+                               if rgMeta == nil {
+                                       rgMeta = fileMeta.RowGroup(rg)
+                               }
+                               use = checkRowGroupDictionaries(
+                                       dictionaryReader, fileMeta, rgMeta, rg,
+                                       dictionaryPredsByColumn, 
rowGroupTester.DictionaryPreds)
+                               if use {
+                                       dictionaryKeepStreak++
+                                       if dictionaryKeepStreak >= 
parquetDictionaryKeepStreakLimit {
+                                               dictionaryPruningActive = false

Review Comment:
   This is the streak breaker from last round, and it's still the thing I'd 
hold on. Once eight groups in a row survive, `dictionaryPruningActive` flips 
false and never comes back on for the rest of the file, because the `else` 
reset here only runs while pruning is still active so it can't re-arm it. For 
the common shape (a value clustered in the first groups, then absent down the 
long tail) we stop pruning exactly where the wins are, and for older files with 
no bloom filter this is the only sub-stats pass, so we lose it for most of the 
file. Java's `ParquetDictionaryRowGroupFilter` just decodes every row group 
unconditionally.
   
   Given the buffered `dictionarySource` already covers the per-page read cost, 
I'd drop the streak mechanism rather than tune the limit. If we want to keep a 
guard it needs to key on the prune fraction so a genuinely-pruning tail keeps 
getting checked, but I think removing it is the cleaner call. wdyt?



##########
table/internal/parquet_files.go:
##########
@@ -2019,6 +2084,396 @@ func (w wrapPqArrowReader) GetRecords(ctx 
context.Context, cols []int, tester an
        return w.GetRecordReader(ctx, cols, rgList)
 }
 
+// Once several consecutive row groups survive dictionary checks, further
+// checks are unlikely to pay for themselves. The limit is deliberately high
+// enough to keep checking after a short cluster of matching groups, since a
+// later group may still be prunable. Stopping is fail-open: it can only leave
+// extra row groups to the normal reader, never drop matching data.
+const parquetDictionaryKeepStreakLimit = 8
+
+// nonClosingReaderAtSeeker gives a temporary Parquet reader access to a shared
+// source without allowing that reader's Close method to close the source owned
+// by the main Arrow reader.
+type nonClosingReaderAtSeeker struct {
+       parquet.ReaderAtSeeker
+}
+
+func (nonClosingReaderAtSeeker) Close() error {
+       return nil
+}
+
+func newParquetDictionaryReader(
+       source parquet.ReaderAtSeeker, fileMeta *metadata.FileMetaData, mem 
memory.Allocator,
+) (*file.Reader, error) {
+       readProps := parquet.NewReaderProperties(mem)
+       readProps.BufferedStreamEnabled = true
+
+       return file.NewParquetReader(nonClosingReaderAtSeeker{ReaderAtSeeker: 
source},
+               file.WithMetadata(fileMeta), file.WithReadProps(readProps))
+}
+
+func groupRowGroupDictionaryPredicates(
+       fieldIDToColIdx map[int]int,
+       preds []RowGroupDictionaryPred,
+) map[int][]int {
+       predsByColumn := make(map[int][]int)
+       for i, pred := range preds {
+               if len(pred.PhysBytes) == 0 {
+                       continue
+               }
+
+               colIdx, ok := fieldIDToColIdx[pred.FieldID]
+               if ok {
+                       predsByColumn[colIdx] = append(predsByColumn[colIdx], i)
+               }
+       }
+
+       return predsByColumn
+}
+
+// checkRowGroupDictionaries checks each dictionary predicate against the
+// complete dictionary for its column in row group rg. It returns false only
+// when the column metadata proves that every data page is dictionary encoded
+// and none of the predicate values occur in the dictionary. Missing encoding
+// metadata, dictionary pages, unsupported physical types, malformed pages, or
+// reader errors keep the row group so this optimisation cannot drop data.
+func checkRowGroupDictionaries(
+       rdr *file.Reader,
+       fileMeta *metadata.FileMetaData,
+       rgMeta *metadata.RowGroupMetaData,
+       rg int,
+       predsByColumn map[int][]int,
+       preds []RowGroupDictionaryPred,
+) bool {
+       if len(preds) == 0 || len(predsByColumn) == 0 {
+               return true
+       }
+
+       rgReader := rdr.RowGroup(rg)
+       for colIdx, predIndexes := range predsByColumn {
+               chunk, err := rgMeta.ColumnChunk(colIdx)
+               if err != nil || !parquetColumnUsesOnlyDictionaryData(chunk) {

Review Comment:
   If `ColumnChunk` ever returns a nil chunk with a nil error, 
`parquetColumnUsesOnlyDictionaryData` dereferences it right away via 
`chunk.HasDictionaryPage()` and we panic in the pruning hot path instead of 
failing open. A `chunk == nil` in this guard keeps it conservative:
   
   ```go
   if err != nil || chunk == nil || !parquetColumnUsesOnlyDictionaryData(chunk) 
{
   ```



##########
table/evaluators.go:
##########
@@ -1901,3 +1901,40 @@ func newBloomFilterPredicatesFromRewritten(expr 
iceberg.BooleanExpression) ([]in
 
        return iceberg.VisitExpr(expr, &bloomPredicateCollector{})
 }
+
+// newDictionaryPredicates reuses the same conservative EqualTo/In collector
+// as Bloom filters. The physical literal bytes are also the representation
+// needed to compare values decoded from a PLAIN dictionary page.
+func newDictionaryPredicates(expr iceberg.BooleanExpression) 
([]internal.RowGroupDictionaryPred, error) {
+       if expr == nil {
+               return nil, nil
+       }
+
+       rewritten, err := iceberg.RewriteNotExpr(expr)
+       if err != nil {
+               return nil, err
+       }
+
+       return newDictionaryPredicatesFromRewritten(rewritten)
+}
+
+func newDictionaryPredicatesFromRewritten(expr iceberg.BooleanExpression) 
([]internal.RowGroupDictionaryPred, error) {
+       bloomPreds, err := newBloomFilterPredicatesFromRewritten(expr)
+       if err != nil {
+               return nil, err
+       }
+
+       if len(bloomPreds) == 0 {
+               return nil, nil
+       }
+
+       dictionaryPreds := make([]internal.RowGroupDictionaryPred, 
len(bloomPreds))
+       for i, pred := range bloomPreds {
+               dictionaryPreds[i] = internal.RowGroupDictionaryPred{

Review Comment:
   This is the red check now. Staticcheck S1016 wants a direct conversion 
instead of the field-by-field literal, since the two pred types are 
structurally identical:
   
   ```go
   for i, pred := range bloomPreds {
        dictionaryPreds[i] = internal.RowGroupDictionaryPred(pred)
   }
   ```
   
   That shares the `PhysBytes` slice between the bloom and dictionary preds, 
which is fine today since neither path writes it, but a `slices.Clone` there is 
cheap insurance if we ever sort or dedup in place. Green CI is the one hard 
blocker left before I approve.



##########
table/internal/parquet_files_test.go:
##########
@@ -2211,6 +2224,505 @@ func TestBloomFilterRowGroupPruning(t *testing.T) {
        })
 }
 
+// buildDictionaryTestParquet writes a Parquet file with two low-cardinality
+// dictionary-encoded row groups. The single required INT32 column "id" has
+// Iceberg field_id=1.
+func buildDictionaryTestParquet(t testing.TB, rowGroups ...[]int32) []byte {
+       t.Helper()
+
+       idNode := schema.NewInt32Node("id", parquet.Repetitions.Required, 1)
+       rootNode, err := schema.NewGroupNode("schema", 
parquet.Repetitions.Required,
+               schema.FieldList{idNode}, -1)
+       require.NoError(t, err)
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithStats(true),
+               parquet.WithDictionaryDefault(true),
+       )
+
+       var buf bytes.Buffer
+       pw := file.NewParquetWriter(&buf, rootNode, 
file.WithWriterProps(writerProps))
+       for _, values := range rowGroups {
+               rgw, werr := pw.AppendRowGroupChecked()
+               require.NoError(t, werr)
+               cw, werr := rgw.NextColumn()
+               require.NoError(t, werr)
+               _, werr = cw.(*file.Int32ColumnChunkWriter).WriteBatch(values, 
nil, nil)
+               require.NoError(t, werr)
+               require.NoError(t, cw.Close())
+               require.NoError(t, rgw.Close())
+       }
+       require.NoError(t, pw.Close())
+
+       return buf.Bytes()
+}
+
+type multiColumnDictionaryTestRowGroup struct {
+       ids        []int32
+       categories []string
+}
+
+func buildMultiColumnDictionaryTestParquet(
+       t testing.TB, rowGroups ...multiColumnDictionaryTestRowGroup,
+) []byte {
+       t.Helper()
+
+       idNode := schema.NewInt32Node("id", parquet.Repetitions.Required, 1)
+       categoryNode := schema.NewByteArrayNode("category", 
parquet.Repetitions.Required, 2)
+       rootNode, err := schema.NewGroupNode("schema", 
parquet.Repetitions.Required,
+               schema.FieldList{idNode, categoryNode}, -1)
+       require.NoError(t, err)
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithStats(true),
+               parquet.WithDictionaryDefault(true),
+       )
+
+       var buf bytes.Buffer
+       pw := file.NewParquetWriter(&buf, rootNode, 
file.WithWriterProps(writerProps))
+       for _, values := range rowGroups {
+               require.Len(t, values.categories, len(values.ids))
+
+               rgw, werr := pw.AppendRowGroupChecked()
+               require.NoError(t, werr)
+
+               idColumn, werr := rgw.NextColumn()
+               require.NoError(t, werr)
+               _, werr = 
idColumn.(*file.Int32ColumnChunkWriter).WriteBatch(values.ids, nil, nil)
+               require.NoError(t, werr)
+               require.NoError(t, idColumn.Close())
+
+               categoryColumn, werr := rgw.NextColumn()
+               require.NoError(t, werr)
+               categories := make([]parquet.ByteArray, len(values.categories))
+               for i, category := range values.categories {
+                       categories[i] = parquet.ByteArray(category)
+               }
+               _, werr = 
categoryColumn.(*file.ByteArrayColumnChunkWriter).WriteBatch(categories, nil, 
nil)
+               require.NoError(t, werr)
+               require.NoError(t, categoryColumn.Close())
+               require.NoError(t, rgw.Close())
+       }
+       require.NoError(t, pw.Close())
+
+       return buf.Bytes()
+}
+
+// TestDictionaryRowGroupPruning verifies that dictionary-only row groups are
+// skipped when an EqualTo/In predicate has no value in the complete 
dictionary.
+func TestDictionaryRowGroupPruning(t *testing.T) {
+       const rgSize = 1024
+       rg0 := make([]int32, rgSize)
+       rg1 := make([]int32, rgSize)
+       for i := range rg0 {
+               rg0[i] = 1 + int32(i%2)*2
+               rg1[i] = 5 + int32(i%2)*2
+       }
+
+       data := buildDictionaryTestParquet(t, rg0, rg1)
+       alwaysKeep := func(_ *metadata.RowGroupMetaData, _ []int) (bool, error) 
{
+               return true, nil
+       }
+
+       ctx := context.Background()
+       cols := []int{0}
+
+       t.Run("EqualTo absent from both dictionaries", func(t *testing.T) {
+               rdr := openBloomTestReader(t, data)
+               var survivors []internal.RowGroupSpan
+               tester := &internal.ParquetRowGroupTester{
+                       DictionaryPreds: []internal.RowGroupDictionaryPred{
+                               {FieldID: 1, PhysBytes: 
[][]byte{int32PhysBytes(2)}},
+                       },
+                       Survivors: &survivors,
+               }
+               rr, err := rdr.GetRecords(ctx, cols, tester)
+               require.NoError(t, err)
+
+               assert.Equal(t, int64(0), countRecords(t, rr))
+               assert.Empty(t, survivors)
+               require.NoError(t, rdr.Close())
+       })
+
+       t.Run("EqualTo keeps the matching dictionary", func(t *testing.T) {
+               rdr := openBloomTestReader(t, data)
+               var survivors []internal.RowGroupSpan
+               tester := &internal.ParquetRowGroupTester{
+                       DictionaryPreds: []internal.RowGroupDictionaryPred{
+                               {FieldID: 1, PhysBytes: 
[][]byte{int32PhysBytes(7)}},
+                       },
+                       StatsFn:   alwaysKeep,
+                       Survivors: &survivors,
+               }
+               rr, err := rdr.GetRecords(ctx, cols, tester)
+               require.NoError(t, err)
+
+               assert.Equal(t, int64(rgSize), countRecords(t, rr))
+               assert.Equal(t, []internal.RowGroupSpan{{FirstRowPos: rgSize, 
NumRows: rgSize}}, survivors)
+               require.NoError(t, rdr.Close())
+       })
+
+       t.Run("In keeps a row group when any value is present", func(t 
*testing.T) {
+               rdr := openBloomTestReader(t, data)
+               var survivors []internal.RowGroupSpan
+               tester := &internal.ParquetRowGroupTester{
+                       DictionaryPreds: []internal.RowGroupDictionaryPred{
+                               {FieldID: 1, PhysBytes: 
[][]byte{int32PhysBytes(2), int32PhysBytes(5)}},
+                       },
+                       StatsFn:   alwaysKeep,
+                       Survivors: &survivors,
+               }
+               rr, err := rdr.GetRecords(ctx, cols, tester)
+               require.NoError(t, err)
+
+               assert.Equal(t, int64(rgSize), countRecords(t, rr))
+               assert.Equal(t, []internal.RowGroupSpan{{FirstRowPos: rgSize, 
NumRows: rgSize}}, survivors)
+               require.NoError(t, rdr.Close())
+       })
+}
+
+func TestDictionaryRowGroupPruningMultiColumnAnd(t *testing.T) {
+       const rgSize = 1024
+
+       alwaysKeep := func(_ *metadata.RowGroupMetaData, _ []int) (bool, error) 
{
+               return true, nil
+       }
+       repeatedIDs := func(first, second int32) []int32 {
+               values := make([]int32, rgSize)
+               for i := range values {
+                       if i%2 == 0 {
+                               values[i] = first
+                       } else {
+                               values[i] = second
+                       }
+               }
+
+               return values
+       }
+       repeatedCategory := func(category string) []string {
+               values := make([]string, rgSize)
+               for i := range values {
+                       values[i] = category
+               }
+
+               return values
+       }
+
+       tests := []struct {
+               name      string
+               rowGroups []multiColumnDictionaryTestRowGroup
+               preds     []internal.RowGroupDictionaryPred
+               wantRows  int64
+               survivors []internal.RowGroupSpan
+       }{
+               {
+                       name: "id predicate prunes a group while category 
matches",
+                       rowGroups: []multiColumnDictionaryTestRowGroup{
+                               {ids: repeatedIDs(1, 3), categories: 
repeatedCategory("a")},
+                               {ids: repeatedIDs(5, 7), categories: 
repeatedCategory("a")},
+                       },
+                       preds: []internal.RowGroupDictionaryPred{
+                               {FieldID: 1, PhysBytes: 
[][]byte{int32PhysBytes(5)}},
+                               {FieldID: 2, PhysBytes: [][]byte{[]byte("a")}},
+                       },
+                       wantRows:  rgSize,
+                       survivors: []internal.RowGroupSpan{{FirstRowPos: 
rgSize, NumRows: rgSize}},
+               },
+               {
+                       name: "category predicate prunes a group while id 
matches",
+                       rowGroups: []multiColumnDictionaryTestRowGroup{
+                               {ids: repeatedIDs(1, 3), categories: 
repeatedCategory("a")},
+                               {ids: repeatedIDs(1, 3), categories: 
repeatedCategory("b")},
+                       },
+                       preds: []internal.RowGroupDictionaryPred{
+                               {FieldID: 1, PhysBytes: 
[][]byte{int32PhysBytes(1)}},
+                               {FieldID: 2, PhysBytes: [][]byte{[]byte("a")}},
+                       },
+                       wantRows:  rgSize,
+                       survivors: []internal.RowGroupSpan{{FirstRowPos: 0, 
NumRows: rgSize}},
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       data := buildMultiColumnDictionaryTestParquet(t, 
test.rowGroups...)
+                       rdr := openBloomTestReader(t, data)
+                       defer rdr.Close()
+
+                       var survivors []internal.RowGroupSpan
+                       tester := &internal.ParquetRowGroupTester{
+                               StatsFn:         alwaysKeep,
+                               DictionaryPreds: test.preds,
+                               Survivors:       &survivors,
+                       }
+                       rr, err := rdr.GetRecords(context.Background(), 
[]int{0, 1}, tester)
+                       require.NoError(t, err)
+
+                       assert.Equal(t, test.wantRows, countRecords(t, rr))
+                       assert.Equal(t, test.survivors, survivors)
+               })
+       }
+}
+
+func TestDictionaryRowGroupPruningKeepsCheckingAfterKeepStreak(t *testing.T) {

Review Comment:
   This test is named for the keep-streak, but with the limit at 8 and only 
four groups it never actually trips the breaker. `dictionaryPruningActive` 
stays true the whole way, so the disable path has no coverage, and that's how 
the permanent-disable behavior slipped through.
   
   If we keep any streak logic I'd want a case with exactly 
`parquetDictionaryKeepStreakLimit` matching groups followed by two prunable 
ones, asserting the tail groups survive so the fail-open trade-off is pinned. 
If we drop the streak, this test can just go.



##########
table/internal/parquet_files.go:
##########
@@ -2019,6 +2084,396 @@ func (w wrapPqArrowReader) GetRecords(ctx 
context.Context, cols []int, tester an
        return w.GetRecordReader(ctx, cols, rgList)
 }
 
+// Once several consecutive row groups survive dictionary checks, further
+// checks are unlikely to pay for themselves. The limit is deliberately high
+// enough to keep checking after a short cluster of matching groups, since a
+// later group may still be prunable. Stopping is fail-open: it can only leave
+// extra row groups to the normal reader, never drop matching data.
+const parquetDictionaryKeepStreakLimit = 8
+
+// nonClosingReaderAtSeeker gives a temporary Parquet reader access to a shared
+// source without allowing that reader's Close method to close the source owned
+// by the main Arrow reader.
+type nonClosingReaderAtSeeker struct {
+       parquet.ReaderAtSeeker
+}
+
+func (nonClosingReaderAtSeeker) Close() error {
+       return nil
+}
+
+func newParquetDictionaryReader(
+       source parquet.ReaderAtSeeker, fileMeta *metadata.FileMetaData, mem 
memory.Allocator,
+) (*file.Reader, error) {
+       readProps := parquet.NewReaderProperties(mem)
+       readProps.BufferedStreamEnabled = true
+
+       return file.NewParquetReader(nonClosingReaderAtSeeker{ReaderAtSeeker: 
source},
+               file.WithMetadata(fileMeta), file.WithReadProps(readProps))
+}
+
+func groupRowGroupDictionaryPredicates(
+       fieldIDToColIdx map[int]int,
+       preds []RowGroupDictionaryPred,
+) map[int][]int {
+       predsByColumn := make(map[int][]int)
+       for i, pred := range preds {
+               if len(pred.PhysBytes) == 0 {
+                       continue
+               }
+
+               colIdx, ok := fieldIDToColIdx[pred.FieldID]
+               if ok {
+                       predsByColumn[colIdx] = append(predsByColumn[colIdx], i)
+               }
+       }
+
+       return predsByColumn
+}
+
+// checkRowGroupDictionaries checks each dictionary predicate against the
+// complete dictionary for its column in row group rg. It returns false only
+// when the column metadata proves that every data page is dictionary encoded
+// and none of the predicate values occur in the dictionary. Missing encoding
+// metadata, dictionary pages, unsupported physical types, malformed pages, or
+// reader errors keep the row group so this optimisation cannot drop data.
+func checkRowGroupDictionaries(
+       rdr *file.Reader,
+       fileMeta *metadata.FileMetaData,
+       rgMeta *metadata.RowGroupMetaData,
+       rg int,
+       predsByColumn map[int][]int,
+       preds []RowGroupDictionaryPred,
+) bool {
+       if len(preds) == 0 || len(predsByColumn) == 0 {
+               return true
+       }
+
+       rgReader := rdr.RowGroup(rg)
+       for colIdx, predIndexes := range predsByColumn {
+               chunk, err := rgMeta.ColumnChunk(colIdx)
+               if err != nil || !parquetColumnUsesOnlyDictionaryData(chunk) {
+                       continue
+               }
+
+               column := fileMeta.Schema.Column(colIdx)
+               pageRdr, err := rgReader.GetColumnPageReader(colIdx)
+               if err != nil {
+                       continue
+               }
+
+               dictPage, dictErr := pageRdr.GetDictionaryPage()
+               if dictErr != nil || dictPage == nil {
+                       if dictPage != nil {
+                               dictPage.Release()
+                       }
+                       _ = pageRdr.Close()
+
+                       continue
+               }
+
+               matches, known := dictionaryMatchesPredicates(
+                       dictPage, column.PhysicalType(), column.TypeLength(), 
preds, predIndexes)
+               dictPage.Release()
+               closeErr := pageRdr.Close()
+               if !known {
+                       continue
+               }
+               if closeErr != nil {
+                       // A close error makes an otherwise conclusive 
dictionary result
+                       // unusable, so keep the row group conservatively.
+                       slog.Warn("dictionary row-group pruning skipped after 
page reader close error",
+                               "rowGroup", rg, "err", closeErr)
+
+                       continue
+               }
+
+               if !matches {
+                       return false
+               }
+       }
+
+       return true
+}
+
+// parquetColumnUsesOnlyDictionaryData verifies that the optional encoding
+// statistics identify a dictionary page and dictionary-encoded data pages,
+// with no PLAIN fallback pages. EncodingStats is deliberately required: the
+// column encoding list cannot distinguish the PLAIN dictionary page itself
+// from a later PLAIN data page in older files.
+func parquetColumnUsesOnlyDictionaryData(chunk *metadata.ColumnChunkMetaData) 
bool {
+       if !chunk.HasDictionaryPage() {
+               return false
+       }
+
+       stats := chunk.EncodingStats()
+       if len(stats) == 0 {
+               return false
+       }
+
+       var hasDictionaryPage, hasDictionaryData bool
+       for _, stat := range stats {
+               switch stat.PageType {
+               case file.PageTypeDictionaryPage:
+                       if stat.Encoding != parquet.Encodings.Plain && 
stat.Encoding != parquet.Encodings.PlainDict {
+                               return false
+                       }
+                       hasDictionaryPage = true
+               case file.PageTypeDataPage, file.PageTypeDataPageV2:
+                       if stat.Encoding != parquet.Encodings.RLEDict && 
stat.Encoding != parquet.Encodings.PlainDict {
+                               return false
+                       }
+                       hasDictionaryData = true
+               default:
+                       return false
+               }
+       }
+
+       return hasDictionaryPage && hasDictionaryData
+}
+
+// dictionaryMatchesPredicates decodes a PLAIN dictionary page and reports
+// whether every requested predicate has at least one matching value. The
+// returned known flag is false for unsupported or malformed input that
+// prevents a pruning decision, which means the caller must retain the row
+// group.
+func dictionaryMatchesPredicates(
+       page *file.DictionaryPage,
+       physicalType parquet.Type,
+       typeLen int,
+       preds []RowGroupDictionaryPred,
+       predIndexes []int,
+) (bool, bool) {
+       pageEncoding := parquet.Encoding(page.Encoding())
+       if pageEncoding != parquet.Encodings.Plain && pageEncoding != 
parquet.Encodings.PlainDict {
+               return false, false
+       }
+
+       width, variableWidth, ok := parquetDictionaryValueLayout(physicalType, 
typeLen)
+       if !ok {
+               return false, false
+       }
+
+       for _, predIndex := range predIndexes {
+               usable := false
+               for _, candidate := range preds[predIndex].PhysBytes {
+                       if variableWidth || len(candidate) == width {
+                               usable = true
+
+                               break
+                       }
+               }
+               if !usable {
+                       return false, false
+               }
+       }
+
+       data := page.Data()
+       offset := 0
+       numValues := page.NumValues()
+       if numValues < 0 {
+               return false, false
+       }
+       if numValues == 0 {

Review Comment:
   Last round this path pruned when it shouldn't; now it fails open, which is 
the safe direction, thanks. It's slightly over-corrected though: a zero-entry 
dictionary page means the column has no non-null values, so an EqualTo/In 
genuinely can't match, and this could return `false, true` (known, prunable) 
rather than handing the group to the reader. Minor, and only worth it if it's 
easy, since the safe answer is already correct.



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