This is an automated email from the ASF dual-hosted git repository.

butterbright pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new af0a2379 Fix NPE in tagFamilyFilters.Eq when filter is nil (#874)
af0a2379 is described below

commit af0a237995a75bf425c993c6d03ccdd5c245ef06
Author: Gao Hongtao <[email protected]>
AuthorDate: Sun Nov 30 21:41:58 2025 +0800

    Fix NPE in tagFamilyFilters.Eq when filter is nil (#874)
    
    Add nil check in Eq() method to prevent panic when tags have min/max
    but no filter. Also improve unmarshal to skip useless tags and add
    comprehensive tests.
---
 banyand/stream/tag_filter.go      |  11 ++
 banyand/stream/tag_filter_test.go | 254 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 265 insertions(+)

diff --git a/banyand/stream/tag_filter.go b/banyand/stream/tag_filter.go
index c84eabf6..4f0ce3d1 100644
--- a/banyand/stream/tag_filter.go
+++ b/banyand/stream/tag_filter.go
@@ -141,9 +141,11 @@ func (tff tagFamilyFilter) 
unmarshal(tagFamilyMetadataBlock *dataBlock, metaRead
        }
        for _, tm := range tfm.tagMetadata {
                tf := generateTagFilter()
+               hasMinMax := false
                if tm.valueType == pbv1.ValueTypeInt64 {
                        tf.min = tm.min
                        tf.max = tm.max
+                       hasMinMax = true
                }
                if tm.filterBlock.size > 0 {
                        bb.Buf = pkgbytes.ResizeExact(bb.Buf[:0], 
int(tm.filterBlock.size))
@@ -168,6 +170,11 @@ func (tff tagFamilyFilter) 
unmarshal(tagFamilyMetadataBlock *dataBlock, metaRead
                                tf.filter = df
                        }
                }
+               // Only add to map if it has useful data (filter or min/max for 
range queries)
+               if tf.filter == nil && !hasMinMax {
+                       releaseTagFilter(tf)
+                       continue
+               }
                tff[tm.name] = tf
        }
 }
@@ -209,6 +216,10 @@ func (tfs *tagFamilyFilters) unmarshal(tagFamilies 
map[string]*dataBlock, metaRe
 func (tfs *tagFamilyFilters) Eq(tagName string, tagValue string) bool {
        for _, tff := range tfs.tagFamilyFilters {
                if tf, ok := (*tff)[tagName]; ok {
+                       if tf.filter == nil {
+                               // No filter available, conservatively return 
true (don't skip)
+                               return true
+                       }
                        return tf.filter.MightContain([]byte(tagValue))
                }
        }
diff --git a/banyand/stream/tag_filter_test.go 
b/banyand/stream/tag_filter_test.go
index 7ea3dfc2..ad5fafcc 100644
--- a/banyand/stream/tag_filter_test.go
+++ b/banyand/stream/tag_filter_test.go
@@ -25,8 +25,10 @@ import (
 
        "github.com/stretchr/testify/assert"
 
+       "github.com/apache/skywalking-banyandb/pkg/encoding"
        "github.com/apache/skywalking-banyandb/pkg/filter"
        "github.com/apache/skywalking-banyandb/pkg/fs"
+       "github.com/apache/skywalking-banyandb/pkg/index"
        pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
 )
 
@@ -468,3 +470,255 @@ func BenchmarkTagFamilyFiltersUnmarshal(b *testing.B) {
                })
        }
 }
+
+func generateMetaAndFilterWithScenarios(scenarios []struct {
+       name       string
+       valueType  pbv1.ValueType
+       hasFilter  bool
+       hasMinMax  bool
+       encodeType encoding.EncodeType
+},
+) ([]byte, []byte, []byte) {
+       tfm := generateTagFamilyMetadata()
+       defer releaseTagFamilyMetadata(tfm)
+       filterBuf := bytes.Buffer{}
+       tagValueBuf := bytes.Buffer{}
+
+       for _, scenario := range scenarios {
+               tm := &tagMetadata{
+                       name:      scenario.name,
+                       valueType: scenario.valueType,
+               }
+
+               if scenario.hasMinMax && scenario.valueType == 
pbv1.ValueTypeInt64 {
+                       tm.min = make([]byte, 8)
+                       tm.max = make([]byte, 8)
+                       binary.BigEndian.PutUint64(tm.min, 100)
+                       binary.BigEndian.PutUint64(tm.max, 200)
+               }
+
+               if scenario.hasFilter {
+                       // Create bloom filter
+                       bf := filter.NewBloomFilter(10)
+                       bf.Add([]byte("test-value"))
+                       buf := make([]byte, 0)
+                       buf = encodeBloomFilter(buf, bf)
+                       tm.filterBlock.offset = uint64(filterBuf.Len())
+                       tm.filterBlock.size = uint64(len(buf))
+                       filterBuf.Write(buf)
+               } else {
+                       // Set up tag value data with encode type
+                       tm.offset = uint64(tagValueBuf.Len())
+                       encodeTypeByte := byte(scenario.encodeType)
+                       tagValueBuf.WriteByte(encodeTypeByte)
+                       if scenario.encodeType == encoding.EncodeTypeDictionary 
{
+                               // Write dictionary data: encode type byte + 
dictionary values
+                               // Dictionary format: VarUint64(count) + 
EncodeBytesBlock(values)
+                               dictValues := [][]byte{
+                                       []byte("dict-value-1"),
+                                       []byte("dict-value-2"),
+                               }
+                               dictBuf := encoding.VarUint64ToBytes(nil, 
uint64(len(dictValues)))
+                               dictBuf = encoding.EncodeBytesBlock(dictBuf, 
dictValues)
+                               tagValueBuf.Write(dictBuf)
+                               tm.size = uint64(1 + len(dictBuf))
+                       } else {
+                               // Non-dictionary encoding, just the encode 
type byte
+                               tm.size = 1
+                       }
+               }
+
+               tfm.tagMetadata = append(tfm.tagMetadata, *tm)
+       }
+
+       metaBuf := make([]byte, 0)
+       metaBuf = tfm.marshal(metaBuf)
+       return metaBuf, filterBuf.Bytes(), tagValueBuf.Bytes()
+}
+
+func TestTagFamilyFiltersNPEAndUnmarshalBehavior(t *testing.T) {
+       // Test all scenarios together to verify NPE fix and unmarshal behavior
+       scenarios := []struct {
+               name       string
+               valueType  pbv1.ValueType
+               hasFilter  bool
+               hasMinMax  bool
+               encodeType encoding.EncodeType
+       }{
+               {
+                       name:       "bloom-tag",
+                       valueType:  pbv1.ValueTypeStr,
+                       hasFilter:  true,
+                       hasMinMax:  false,
+                       encodeType: encoding.EncodeTypePlain,
+               },
+               {
+                       name:       "dict-tag",
+                       valueType:  pbv1.ValueTypeStr,
+                       hasFilter:  false,
+                       hasMinMax:  false,
+                       encodeType: encoding.EncodeTypeDictionary,
+               },
+               {
+                       name:       "numeric-tag",
+                       valueType:  pbv1.ValueTypeInt64,
+                       hasFilter:  false,
+                       hasMinMax:  true,
+                       encodeType: encoding.EncodeTypePlain,
+               },
+               {
+                       name:       "useless-tag",
+                       valueType:  pbv1.ValueTypeStr,
+                       hasFilter:  false,
+                       hasMinMax:  false,
+                       encodeType: encoding.EncodeTypePlain,
+               },
+       }
+
+       metaBuf, filterBuf, tagValueBuf := 
generateMetaAndFilterWithScenarios(scenarios)
+
+       tagFamilies := map[string]*dataBlock{
+               "default": {
+                       offset: 0,
+                       size:   uint64(len(metaBuf)),
+               },
+       }
+       metaReaders := map[string]fs.Reader{
+               "default": &mockReader{data: metaBuf},
+       }
+       filterReaders := map[string]fs.Reader{
+               "default": &mockReader{data: filterBuf},
+       }
+       tagValueReaders := map[string]fs.Reader{
+               "default": &mockReader{data: tagValueBuf},
+       }
+
+       tfs := generateTagFamilyFilters()
+       defer releaseTagFamilyFilters(tfs)
+       tfs.unmarshal(tagFamilies, metaReaders, filterReaders, tagValueReaders)
+
+       tff := tfs.tagFamilyFilters[0]
+       assert.NotNil(t, tff)
+
+       tests := []struct {
+               rangeOpts     *index.RangeOpts
+               name          string
+               tagName       string
+               eqValue       string
+               eqDescription string
+               shouldBeInMap bool
+               hasFilter     bool
+               hasMinMax     bool
+               eqExpected    bool
+               testRange     bool
+               rangeExpected bool
+               rangeErr      bool
+       }{
+               {
+                       name:          "bloom filter tag",
+                       tagName:       "bloom-tag",
+                       shouldBeInMap: true,
+                       hasFilter:     true,
+                       hasMinMax:     false,
+                       eqValue:       "test-value",
+                       eqExpected:    true,
+                       eqDescription: "Eq should return true when value is in 
bloom filter",
+               },
+               {
+                       name:          "bloom filter tag - not found",
+                       tagName:       "bloom-tag",
+                       shouldBeInMap: true,
+                       hasFilter:     true,
+                       eqValue:       "definitely-not-there",
+                       eqExpected:    false,
+                       eqDescription: "Eq should return false when value is 
not in bloom filter",
+               },
+               {
+                       name:          "dictionary filter tag",
+                       tagName:       "dict-tag",
+                       shouldBeInMap: true,
+                       hasFilter:     true,
+                       hasMinMax:     false,
+                       eqValue:       "dict-value-1",
+                       eqExpected:    true,
+                       eqDescription: "Eq should return true when value is in 
dictionary",
+               },
+               {
+                       name:          "dictionary filter tag - not found",
+                       tagName:       "dict-tag",
+                       shouldBeInMap: true,
+                       hasFilter:     true,
+                       eqValue:       "not-in-dict",
+                       eqExpected:    false,
+                       eqDescription: "Eq should return false when value is 
not in dictionary",
+               },
+               {
+                       name:          "numeric tag with min/max",
+                       tagName:       "numeric-tag",
+                       shouldBeInMap: true,
+                       hasFilter:     false,
+                       hasMinMax:     true,
+                       eqValue:       "any-value",
+                       eqExpected:    true,
+                       eqDescription: "Eq should return true (conservative) 
when filter is nil",
+                       testRange:     true,
+                       rangeOpts: &index.RangeOpts{
+                               Lower:         &index.FloatTermValue{Value: 
150.0},
+                               Upper:         &index.FloatTermValue{Value: 
180.0},
+                               IncludesLower: true,
+                               IncludesUpper: true,
+                       },
+                       rangeExpected: false, // should not skip (value is 
within range)
+                       rangeErr:      false,
+               },
+               {
+                       name:          "useless tag (no filter, no min/max)",
+                       tagName:       "useless-tag",
+                       shouldBeInMap: false,
+                       hasFilter:     false,
+                       hasMinMax:     false,
+                       eqValue:       "any-value",
+                       eqExpected:    true,
+                       eqDescription: "Eq should return true (conservative) 
when tag is not in map",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert := assert.New(t)
+
+                       // Verify tag presence in map
+                       tf, ok := (*tff)[tt.tagName]
+                       if tt.shouldBeInMap {
+                               assert.True(ok, "tag %s should be in map", 
tt.tagName)
+                               assert.NotNil(tf, "tag filter should not be 
nil")
+                               if tt.hasFilter {
+                                       assert.NotNil(tf.filter, "tag should 
have filter")
+                               } else {
+                                       assert.Nil(tf.filter, "tag should not 
have filter")
+                               }
+                               if tt.hasMinMax {
+                                       assert.NotEmpty(tf.min, "tag should 
have min value")
+                                       assert.NotEmpty(tf.max, "tag should 
have max value")
+                               }
+                       } else {
+                               assert.False(ok, "tag %s should NOT be in map", 
tt.tagName)
+                       }
+
+                       // Test Eq - should not panic
+                       result := tfs.Eq(tt.tagName, tt.eqValue)
+                       assert.Equal(tt.eqExpected, result, tt.eqDescription)
+
+                       // Test Range if applicable
+                       if tt.testRange && tt.shouldBeInMap {
+                               shouldSkip, err := tfs.Range(tt.tagName, 
*tt.rangeOpts)
+                               if tt.rangeErr {
+                                       assert.Error(err)
+                               } else {
+                                       assert.NoError(err)
+                                       assert.Equal(tt.rangeExpected, 
shouldSkip, "Range should return expected value")
+                               }
+                       }
+               })
+       }
+}

Reply via email to